From 0338f5bccffa652d886b507ff4d22a810eaa2d15 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Jan 2021 21:12:38 +0100 Subject: [PATCH 001/796] Bump version to 2021.3.0dev0 (#45617) --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 15ea6b7b00d..4406c8bdfc3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,6 +1,6 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 -MINOR_VERSION = 2 +MINOR_VERSION = 3 PATCH_VERSION = "0.dev0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" From 3f948e027a9304bd7927af1c3e3b61dd25b9c292 Mon Sep 17 00:00:00 2001 From: Julian Engelhardt Date: Wed, 27 Jan 2021 22:37:59 +0100 Subject: [PATCH 002/796] Clean tcp tests (#41673) Co-authored-by: Martin Hjelmare --- homeassistant/components/tcp/sensor.py | 5 +- tests/components/tcp/test_binary_sensor.py | 117 ++++---- tests/components/tcp/test_sensor.py | 302 +++++++-------------- 3 files changed, 170 insertions(+), 254 deletions(-) diff --git a/homeassistant/components/tcp/sensor.py b/homeassistant/components/tcp/sensor.py index 868cd9b8557..9b7e1539fb4 100644 --- a/homeassistant/components/tcp/sensor.py +++ b/homeassistant/components/tcp/sensor.py @@ -78,10 +78,7 @@ class TcpSensor(Entity): @property def name(self): """Return the name of this sensor.""" - name = self._config[CONF_NAME] - if name is not None: - return name - return super().name + return self._config[CONF_NAME] @property def state(self): diff --git a/tests/components/tcp/test_binary_sensor.py b/tests/components/tcp/test_binary_sensor.py index 2dc16ad79c7..21dd84b1892 100644 --- a/tests/components/tcp/test_binary_sensor.py +++ b/tests/components/tcp/test_binary_sensor.py @@ -1,62 +1,83 @@ """The tests for the TCP binary sensor platform.""" -import unittest -from unittest.mock import Mock, patch +from datetime import timedelta +from unittest.mock import call, patch -from homeassistant.components.tcp import binary_sensor as bin_tcp -import homeassistant.components.tcp.sensor as tcp -from homeassistant.setup import setup_component +import pytest -from tests.common import assert_setup_component, get_test_home_assistant +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow + +from tests.common import assert_setup_component, async_fire_time_changed import tests.components.tcp.test_sensor as test_tcp +BINARY_SENSOR_CONFIG = test_tcp.TEST_CONFIG["sensor"] +TEST_CONFIG = {"binary_sensor": BINARY_SENSOR_CONFIG} +TEST_ENTITY = "binary_sensor.test_name" -class TestTCPBinarySensor(unittest.TestCase): - """Test the TCP Binary Sensor.""" - def setup_method(self, method): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() +@pytest.fixture(name="mock_socket") +def mock_socket_fixture(): + """Mock the socket.""" + with patch( + "homeassistant.components.tcp.sensor.socket.socket" + ) as mock_socket, patch( + "homeassistant.components.tcp.sensor.select.select", + return_value=(True, False, False), + ): + # yield the return value of the socket context manager + yield mock_socket.return_value.__enter__.return_value - def teardown_method(self, method): - """Stop down everything that was started.""" - self.hass.stop() - def test_setup_platform_valid_config(self): - """Check a valid configuration.""" - with assert_setup_component(0, "binary_sensor"): - assert setup_component(self.hass, "binary_sensor", test_tcp.TEST_CONFIG) +@pytest.fixture +def now(): + """Return datetime UTC now.""" + return utcnow() - def test_setup_platform_invalid_config(self): - """Check the invalid configuration.""" - with assert_setup_component(0): - assert setup_component( - self.hass, - "binary_sensor", - {"binary_sensor": {"platform": "tcp", "porrt": 1234}}, - ) - @patch("homeassistant.components.tcp.sensor.TcpSensor.update") - def test_setup_platform_devices(self, mock_update): - """Check the supplied config and call add_entities with sensor.""" - add_entities = Mock() - ret = bin_tcp.setup_platform(None, test_tcp.TEST_CONFIG, add_entities) - assert ret is None - assert add_entities.called - assert isinstance(add_entities.call_args[0][0][0], bin_tcp.TcpBinarySensor) +async def test_setup_platform_valid_config(hass, mock_socket): + """Check a valid configuration.""" + with assert_setup_component(1, "binary_sensor"): + assert await async_setup_component(hass, "binary_sensor", TEST_CONFIG) + await hass.async_block_till_done() - @patch("homeassistant.components.tcp.sensor.TcpSensor.update") - def test_is_on_true(self, mock_update): - """Check the return that _state is value_on.""" - sensor = bin_tcp.TcpBinarySensor(self.hass, test_tcp.TEST_CONFIG["sensor"]) - sensor._state = test_tcp.TEST_CONFIG["sensor"][tcp.CONF_VALUE_ON] - print(sensor._state) - assert sensor.is_on - @patch("homeassistant.components.tcp.sensor.TcpSensor.update") - def test_is_on_false(self, mock_update): - """Check the return that _state is not the same as value_on.""" - sensor = bin_tcp.TcpBinarySensor(self.hass, test_tcp.TEST_CONFIG["sensor"]) - sensor._state = "{} abc".format( - test_tcp.TEST_CONFIG["sensor"][tcp.CONF_VALUE_ON] +async def test_setup_platform_invalid_config(hass, mock_socket): + """Check the invalid configuration.""" + with assert_setup_component(0): + assert await async_setup_component( + hass, + "binary_sensor", + {"binary_sensor": {"platform": "tcp", "porrt": 1234}}, ) - assert not sensor.is_on + await hass.async_block_till_done() + + +async def test_state(hass, mock_socket, now): + """Check the state and update of the binary sensor.""" + mock_socket.recv.return_value = b"off" + assert await async_setup_component(hass, "binary_sensor", TEST_CONFIG) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY) + + assert state + assert state.state == STATE_OFF + assert mock_socket.connect.called + assert mock_socket.connect.call_args == call( + (BINARY_SENSOR_CONFIG["host"], BINARY_SENSOR_CONFIG["port"]) + ) + assert mock_socket.send.called + assert mock_socket.send.call_args == call(BINARY_SENSOR_CONFIG["payload"].encode()) + assert mock_socket.recv.called + assert mock_socket.recv.call_args == call(BINARY_SENSOR_CONFIG["buffer_size"]) + + mock_socket.recv.return_value = b"on" + + async_fire_time_changed(hass, now + timedelta(seconds=45)) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY) + + assert state + assert state.state == STATE_ON diff --git a/tests/components/tcp/test_sensor.py b/tests/components/tcp/test_sensor.py index 8e79d4e514d..b1efef305bf 100644 --- a/tests/components/tcp/test_sensor.py +++ b/tests/components/tcp/test_sensor.py @@ -1,16 +1,13 @@ """The tests for the TCP sensor platform.""" from copy import copy -import socket -import unittest -from unittest.mock import Mock, patch -from uuid import uuid4 +from unittest.mock import call, patch + +import pytest import homeassistant.components.tcp.sensor as tcp -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.template import Template -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component -from tests.common import assert_setup_component, get_test_home_assistant +from tests.common import assert_setup_component TEST_CONFIG = { "sensor": { @@ -21,13 +18,16 @@ TEST_CONFIG = { tcp.CONF_TIMEOUT: tcp.DEFAULT_TIMEOUT + 1, tcp.CONF_PAYLOAD: "test_payload", tcp.CONF_UNIT_OF_MEASUREMENT: "test_unit", - tcp.CONF_VALUE_TEMPLATE: Template("test_template"), + tcp.CONF_VALUE_TEMPLATE: "{{ 'test_' + value }}", tcp.CONF_VALUE_ON: "test_on", tcp.CONF_BUFFER_SIZE: tcp.DEFAULT_BUFFER_SIZE + 1, } } +SENSOR_TEST_CONFIG = TEST_CONFIG["sensor"] +TEST_ENTITY = "sensor.test_name" KEYS_AND_DEFAULTS = { + tcp.CONF_NAME: tcp.DEFAULT_NAME, tcp.CONF_TIMEOUT: tcp.DEFAULT_TIMEOUT, tcp.CONF_UNIT_OF_MEASUREMENT: None, tcp.CONF_VALUE_TEMPLATE: None, @@ -35,229 +35,127 @@ KEYS_AND_DEFAULTS = { tcp.CONF_BUFFER_SIZE: tcp.DEFAULT_BUFFER_SIZE, } +socket_test_value = "value" -class TestTCPSensor(unittest.TestCase): - """Test the TCP Sensor.""" - def setup_method(self, method): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() +@pytest.fixture(name="mock_socket") +def mock_socket_fixture(mock_select): + """Mock socket.""" + with patch("homeassistant.components.tcp.sensor.socket.socket") as mock_socket: + socket_instance = mock_socket.return_value.__enter__.return_value + socket_instance.recv.return_value = socket_test_value.encode() + yield socket_instance - def teardown_method(self, method): - """Stop everything that was started.""" - self.hass.stop() - @patch("homeassistant.components.tcp.sensor.TcpSensor.update") - def test_setup_platform_valid_config(self, mock_update): - """Check a valid configuration and call add_entities with sensor.""" - with assert_setup_component(0, "sensor"): - assert setup_component(self.hass, "sensor", TEST_CONFIG) +@pytest.fixture(name="mock_select") +def mock_select_fixture(): + """Mock select.""" + with patch( + "homeassistant.components.tcp.sensor.select.select", + return_value=(True, False, False), + ) as mock_select: + yield mock_select - add_entities = Mock() - tcp.setup_platform(None, TEST_CONFIG["sensor"], add_entities) - assert add_entities.called - assert isinstance(add_entities.call_args[0][0][0], tcp.TcpSensor) - def test_setup_platform_invalid_config(self): - """Check an invalid configuration.""" - with assert_setup_component(0): - assert setup_component( - self.hass, "sensor", {"sensor": {"platform": "tcp", "porrt": 1234}} - ) +async def test_setup_platform_valid_config(hass, mock_socket): + """Check a valid configuration and call add_entities with sensor.""" + with assert_setup_component(1, "sensor"): + assert await async_setup_component(hass, "sensor", TEST_CONFIG) + await hass.async_block_till_done() - @patch("homeassistant.components.tcp.sensor.TcpSensor.update") - def test_name(self, mock_update): - """Return the name if set in the configuration.""" - sensor = tcp.TcpSensor(self.hass, TEST_CONFIG["sensor"]) - assert sensor.name == TEST_CONFIG["sensor"][tcp.CONF_NAME] - @patch("homeassistant.components.tcp.sensor.TcpSensor.update") - def test_name_not_set(self, mock_update): - """Return the superclass name property if not set in configuration.""" - config = copy(TEST_CONFIG["sensor"]) - del config[tcp.CONF_NAME] - entity = Entity() - sensor = tcp.TcpSensor(self.hass, config) - assert sensor.name == entity.name - - @patch("homeassistant.components.tcp.sensor.TcpSensor.update") - def test_state(self, mock_update): - """Return the contents of _state.""" - sensor = tcp.TcpSensor(self.hass, TEST_CONFIG["sensor"]) - uuid = str(uuid4()) - sensor._state = uuid - assert sensor.state == uuid - - @patch("homeassistant.components.tcp.sensor.TcpSensor.update") - def test_unit_of_measurement(self, mock_update): - """Return the configured unit of measurement.""" - sensor = tcp.TcpSensor(self.hass, TEST_CONFIG["sensor"]) - assert ( - sensor.unit_of_measurement - == TEST_CONFIG["sensor"][tcp.CONF_UNIT_OF_MEASUREMENT] +async def test_setup_platform_invalid_config(hass, mock_socket): + """Check an invalid configuration.""" + with assert_setup_component(0): + assert await async_setup_component( + hass, "sensor", {"sensor": {"platform": "tcp", "porrt": 1234}} ) + await hass.async_block_till_done() - @patch("homeassistant.components.tcp.sensor.TcpSensor.update") - def test_config_valid_keys(self, *args): - """Store valid keys in _config.""" - sensor = tcp.TcpSensor(self.hass, TEST_CONFIG["sensor"]) - del TEST_CONFIG["sensor"]["platform"] - for key in TEST_CONFIG["sensor"]: - assert key in sensor._config +async def test_state(hass, mock_socket, mock_select): + """Return the contents of _state.""" + assert await async_setup_component(hass, "sensor", TEST_CONFIG) + await hass.async_block_till_done() - def test_validate_config_valid_keys(self): - """Return True when provided with the correct keys.""" - with assert_setup_component(0, "sensor"): - assert setup_component(self.hass, "sensor", TEST_CONFIG) + state = hass.states.get(TEST_ENTITY) - @patch("homeassistant.components.tcp.sensor.TcpSensor.update") - def test_config_invalid_keys(self, mock_update): - """Shouldn't store invalid keys in _config.""" - config = copy(TEST_CONFIG["sensor"]) - config.update({"a": "test_a", "b": "test_b", "c": "test_c"}) - sensor = tcp.TcpSensor(self.hass, config) - for invalid_key in "abc": - assert invalid_key not in sensor._config + assert state + assert state.state == "test_value" + assert ( + state.attributes["unit_of_measurement"] + == SENSOR_TEST_CONFIG[tcp.CONF_UNIT_OF_MEASUREMENT] + ) + assert mock_socket.connect.called + assert mock_socket.connect.call_args == call( + (SENSOR_TEST_CONFIG["host"], SENSOR_TEST_CONFIG["port"]) + ) + assert mock_socket.send.called + assert mock_socket.send.call_args == call(SENSOR_TEST_CONFIG["payload"].encode()) + assert mock_select.call_args == call( + [mock_socket], [], [], SENSOR_TEST_CONFIG[tcp.CONF_TIMEOUT] + ) + assert mock_socket.recv.called + assert mock_socket.recv.call_args == call(SENSOR_TEST_CONFIG["buffer_size"]) - def test_validate_config_invalid_keys(self): - """Test with invalid keys plus some extra.""" - config = copy(TEST_CONFIG["sensor"]) - config.update({"a": "test_a", "b": "test_b", "c": "test_c"}) - with assert_setup_component(0, "sensor"): - assert setup_component(self.hass, "sensor", {"tcp": config}) - @patch("homeassistant.components.tcp.sensor.TcpSensor.update") - def test_config_uses_defaults(self, mock_update): - """Check if defaults were set.""" - config = copy(TEST_CONFIG["sensor"]) +async def test_config_uses_defaults(hass, mock_socket): + """Check if defaults were set.""" + config = copy(SENSOR_TEST_CONFIG) - for key in KEYS_AND_DEFAULTS: - del config[key] + for key in KEYS_AND_DEFAULTS: + del config[key] - with assert_setup_component(1) as result_config: - assert setup_component(self.hass, "sensor", {"sensor": config}) + with assert_setup_component(1) as result_config: + assert await async_setup_component(hass, "sensor", {"sensor": config}) + await hass.async_block_till_done() - sensor = tcp.TcpSensor(self.hass, result_config["sensor"][0]) + state = hass.states.get("sensor.tcp_sensor") - for key, default in KEYS_AND_DEFAULTS.items(): - assert sensor._config[key] == default + assert state + assert state.state == "value" - def test_validate_config_missing_defaults(self): - """Return True when defaulted keys are not provided.""" - config = copy(TEST_CONFIG["sensor"]) + for key, default in KEYS_AND_DEFAULTS.items(): + assert result_config["sensor"][0].get(key) == default - for key in KEYS_AND_DEFAULTS: - del config[key] - with assert_setup_component(0, "sensor"): - assert setup_component(self.hass, "sensor", {"tcp": config}) +@pytest.mark.parametrize("sock_attr", ["connect", "send"]) +async def test_update_socket_error(hass, mock_socket, sock_attr): + """Test socket errors during update.""" + socket_method = getattr(mock_socket, sock_attr) + socket_method.side_effect = OSError("Boom") - def test_validate_config_missing_required(self): - """Return False when required config items are missing.""" - for key in TEST_CONFIG["sensor"]: - if key in KEYS_AND_DEFAULTS: - continue - config = copy(TEST_CONFIG["sensor"]) - del config[key] - with assert_setup_component(0, "sensor"): - assert setup_component(self.hass, "sensor", {"tcp": config}) + assert await async_setup_component(hass, "sensor", TEST_CONFIG) + await hass.async_block_till_done() - @patch("homeassistant.components.tcp.sensor.TcpSensor.update") - def test_init_calls_update(self, mock_update): - """Call update() method during __init__().""" - tcp.TcpSensor(self.hass, TEST_CONFIG) - assert mock_update.called + state = hass.states.get(TEST_ENTITY) - @patch("socket.socket") - @patch("select.select", return_value=(True, False, False)) - def test_update_connects_to_host_and_port(self, mock_select, mock_socket): - """Connect to the configured host and port.""" - tcp.TcpSensor(self.hass, TEST_CONFIG["sensor"]) - mock_socket = mock_socket().__enter__() - assert mock_socket.connect.mock_calls[0][1] == ( - ( - TEST_CONFIG["sensor"][tcp.CONF_HOST], - TEST_CONFIG["sensor"][tcp.CONF_PORT], - ), - ) + assert state + assert state.state == "unknown" - @patch("socket.socket.connect", side_effect=socket.error()) - def test_update_returns_if_connecting_fails(self, *args): - """Return if connecting to host fails.""" - with patch("homeassistant.components.tcp.sensor.TcpSensor.update"): - sensor = tcp.TcpSensor(self.hass, TEST_CONFIG["sensor"]) - assert sensor.update() is None - @patch("socket.socket.connect") - @patch("socket.socket.send", side_effect=socket.error()) - def test_update_returns_if_sending_fails(self, *args): - """Return if sending fails.""" - with patch("homeassistant.components.tcp.sensor.TcpSensor.update"): - sensor = tcp.TcpSensor(self.hass, TEST_CONFIG["sensor"]) - assert sensor.update() is None +async def test_update_select_fails(hass, mock_socket, mock_select): + """Test select fails to return a socket for reading.""" + mock_select.return_value = (False, False, False) - @patch("socket.socket.connect") - @patch("socket.socket.send") - @patch("select.select", return_value=(False, False, False)) - def test_update_returns_if_select_fails(self, *args): - """Return if select fails to return a socket.""" - with patch("homeassistant.components.tcp.sensor.TcpSensor.update"): - sensor = tcp.TcpSensor(self.hass, TEST_CONFIG["sensor"]) - assert sensor.update() is None + assert await async_setup_component(hass, "sensor", TEST_CONFIG) + await hass.async_block_till_done() - @patch("socket.socket") - @patch("select.select", return_value=(True, False, False)) - def test_update_sends_payload(self, mock_select, mock_socket): - """Send the configured payload as bytes.""" - tcp.TcpSensor(self.hass, TEST_CONFIG["sensor"]) - mock_socket = mock_socket().__enter__() - mock_socket.send.assert_called_with( - TEST_CONFIG["sensor"][tcp.CONF_PAYLOAD].encode() - ) + state = hass.states.get(TEST_ENTITY) - @patch("socket.socket") - @patch("select.select", return_value=(True, False, False)) - def test_update_calls_select_with_timeout(self, mock_select, mock_socket): - """Provide the timeout argument to select.""" - tcp.TcpSensor(self.hass, TEST_CONFIG["sensor"]) - mock_socket = mock_socket().__enter__() - mock_select.assert_called_with( - [mock_socket], [], [], TEST_CONFIG["sensor"][tcp.CONF_TIMEOUT] - ) + assert state + assert state.state == "unknown" - @patch("socket.socket") - @patch("select.select", return_value=(True, False, False)) - def test_update_receives_packet_and_sets_as_state(self, mock_select, mock_socket): - """Test the response from the socket and set it as the state.""" - test_value = "test_value" - mock_socket = mock_socket().__enter__() - mock_socket.recv.return_value = test_value.encode() - config = copy(TEST_CONFIG["sensor"]) - del config[tcp.CONF_VALUE_TEMPLATE] - sensor = tcp.TcpSensor(self.hass, config) - assert sensor._state == test_value - @patch("socket.socket") - @patch("select.select", return_value=(True, False, False)) - def test_update_renders_value_in_template(self, mock_select, mock_socket): - """Render the value in the provided template.""" - test_value = "test_value" - mock_socket = mock_socket().__enter__() - mock_socket.recv.return_value = test_value.encode() - config = copy(TEST_CONFIG["sensor"]) - config[tcp.CONF_VALUE_TEMPLATE] = Template("{{ value }} {{ 1+1 }}") - sensor = tcp.TcpSensor(self.hass, config) - assert sensor._state == "%s 2" % test_value +async def test_update_returns_if_template_render_fails(hass, mock_socket): + """Return None if rendering the template fails.""" + config = copy(SENSOR_TEST_CONFIG) + config[tcp.CONF_VALUE_TEMPLATE] = "{{ value / 0 }}" - @patch("socket.socket") - @patch("select.select", return_value=(True, False, False)) - def test_update_returns_if_template_render_fails(self, mock_select, mock_socket): - """Return None if rendering the template fails.""" - test_value = "test_value" - mock_socket = mock_socket().__enter__() - mock_socket.recv.return_value = test_value.encode() - config = copy(TEST_CONFIG["sensor"]) - config[tcp.CONF_VALUE_TEMPLATE] = Template("{{ this won't work") - sensor = tcp.TcpSensor(self.hass, config) - assert sensor.update() is None + assert await async_setup_component(hass, "sensor", {"sensor": config}) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY) + + assert state + assert state.state == "unknown" From 068d1b5eb818fbbe0145377bf0de5ef79f12cb0e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 27 Jan 2021 17:44:36 -0600 Subject: [PATCH 003/796] Separate fan speeds into percentages and presets modes (#45407) Co-authored-by: Martin Hjelmare Co-authored-by: John Carr --- homeassistant/components/bond/fan.py | 15 +- homeassistant/components/comfoconnect/fan.py | 11 +- homeassistant/components/deconz/fan.py | 15 +- homeassistant/components/demo/fan.py | 240 +++++++++- homeassistant/components/dyson/fan.py | 30 +- homeassistant/components/esphome/fan.py | 15 +- homeassistant/components/fan/__init__.py | 412 +++++++++++++++++- .../components/fan/reproduce_state.py | 6 + homeassistant/components/fan/services.yaml | 26 ++ .../components/homekit_controller/fan.py | 12 +- homeassistant/components/insteon/fan.py | 15 +- homeassistant/components/isy994/fan.py | 30 +- homeassistant/components/lutron_caseta/fan.py | 15 +- homeassistant/components/mqtt/fan.py | 15 +- homeassistant/components/ozw/fan.py | 11 +- homeassistant/components/smartthings/fan.py | 15 +- homeassistant/components/smarty/fan.py | 9 +- homeassistant/components/tasmota/fan.py | 11 +- homeassistant/components/template/fan.py | 16 +- homeassistant/components/tuya/fan.py | 15 +- homeassistant/components/vallox/fan.py | 15 +- homeassistant/components/vesync/fan.py | 15 +- homeassistant/components/wemo/fan.py | 15 +- homeassistant/components/wilight/fan.py | 15 +- homeassistant/components/wink/fan.py | 15 +- homeassistant/components/xiaomi_miio/fan.py | 15 +- homeassistant/components/zha/fan.py | 11 +- homeassistant/components/zwave/fan.py | 9 +- homeassistant/components/zwave_js/fan.py | 15 +- homeassistant/util/percentage.py | 87 ++++ tests/components/demo/test_fan.py | 295 +++++++++++-- tests/components/emulated_hue/test_hue_api.py | 23 +- tests/components/fan/common.py | 45 +- tests/components/fan/test_init.py | 31 +- tests/components/google_assistant/__init__.py | 21 + tests/util/test_percentage.py | 158 +++++++ 36 files changed, 1607 insertions(+), 112 deletions(-) create mode 100644 homeassistant/util/percentage.py create mode 100644 tests/util/test_percentage.py diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index e59d0234beb..6e33aa6d161 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -118,7 +118,20 @@ class BondFan(BondEntity, FanEntity): self._device.device_id, Action.set_speed(bond_speed) ) - async def async_turn_on(self, speed: Optional[str] = None, **kwargs) -> None: + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + async def async_turn_on( + self, + speed: Optional[str] = None, + percentage: Optional[int] = None, + preset_mode: Optional[str] = None, + **kwargs, + ) -> None: """Turn on the fan.""" _LOGGER.debug("Fan async_turn_on called with speed %s", speed) diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index b5eac4f9afe..18549c52d35 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -102,7 +102,16 @@ class ComfoConnectFan(FanEntity): """List of available fan modes.""" return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - def turn_on(self, speed: str = None, **kwargs) -> None: + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + def turn_on( + self, speed: str = None, percentage=None, preset_mode=None, **kwargs + ) -> None: """Turn on the fan.""" if speed is None: speed = SPEED_LOW diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index d92addff5bd..1ca4c8ff9c2 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -107,7 +107,20 @@ class DeconzFan(DeconzDevice, FanEntity): await self._device.set_speed(SPEEDS[speed]) - async def async_turn_on(self, speed: str = None, **kwargs) -> None: + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + async def async_turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: """Turn on fan.""" if not speed: speed = convert_speed(self._default_on_speed) diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index ee49f0a2e99..bd6661b6c2b 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -1,14 +1,20 @@ """Demo fan platform that has a fake fan.""" +from typing import List, Optional + from homeassistant.components.fan import ( SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, + SPEED_OFF, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, + SUPPORT_PRESET_MODE, SUPPORT_SET_SPEED, FanEntity, ) -from homeassistant.const import STATE_OFF + +PRESET_MODE_AUTO = "auto" +PRESET_MODE_SMART = "smart" FULL_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION LIMITED_SUPPORT = SUPPORT_SET_SPEED @@ -18,8 +24,55 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Set up the demo fan platform.""" async_add_entities( [ - DemoFan(hass, "fan1", "Living Room Fan", FULL_SUPPORT), - DemoFan(hass, "fan2", "Ceiling Fan", LIMITED_SUPPORT), + # These fans implement the old model + DemoFan( + hass, + "fan1", + "Living Room Fan", + FULL_SUPPORT, + None, + [ + SPEED_OFF, + SPEED_LOW, + SPEED_MEDIUM, + SPEED_HIGH, + PRESET_MODE_AUTO, + PRESET_MODE_SMART, + ], + ), + DemoFan( + hass, + "fan2", + "Ceiling Fan", + LIMITED_SUPPORT, + None, + [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH], + ), + # These fans implement the newer model + AsyncDemoPercentageFan( + hass, + "fan3", + "Percentage Full Fan", + FULL_SUPPORT, + [PRESET_MODE_AUTO, PRESET_MODE_SMART], + None, + ), + DemoPercentageFan( + hass, + "fan4", + "Percentage Limited Fan", + LIMITED_SUPPORT, + [PRESET_MODE_AUTO, PRESET_MODE_SMART], + None, + ), + AsyncDemoPercentageFan( + hass, + "fan5", + "Preset Only Limited Fan", + SUPPORT_PRESET_MODE, + [PRESET_MODE_AUTO, PRESET_MODE_SMART], + [], + ), ] ) @@ -29,21 +82,30 @@ async def async_setup_entry(hass, config_entry, async_add_entities): await async_setup_platform(hass, {}, async_add_entities) -class DemoFan(FanEntity): - """A demonstration fan component.""" +class BaseDemoFan(FanEntity): + """A demonstration fan component that uses legacy fan speeds.""" def __init__( - self, hass, unique_id: str, name: str, supported_features: int + self, + hass, + unique_id: str, + name: str, + supported_features: int, + preset_modes: Optional[List[str]], + speed_list: Optional[List[str]], ) -> None: """Initialize the entity.""" self.hass = hass self._unique_id = unique_id self._supported_features = supported_features - self._speed = STATE_OFF + self._speed = SPEED_OFF + self._percentage = 0 + self._speed_list = speed_list + self._preset_modes = preset_modes + self._preset_mode = None self._oscillating = None self._direction = None self._name = name - if supported_features & SUPPORT_OSCILLATE: self._oscillating = False if supported_features & SUPPORT_DIRECTION: @@ -64,17 +126,42 @@ class DemoFan(FanEntity): """No polling needed for a demo fan.""" return False + @property + def current_direction(self) -> str: + """Fan direction.""" + return self._direction + + @property + def oscillating(self) -> bool: + """Oscillating.""" + return self._oscillating + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return self._supported_features + + +class DemoFan(BaseDemoFan, FanEntity): + """A demonstration fan component that uses legacy fan speeds.""" + @property def speed(self) -> str: """Return the current speed.""" return self._speed @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + def speed_list(self): + """Return the speed list.""" + return self._speed_list - def turn_on(self, speed: str = None, **kwargs) -> None: + def turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: """Turn on the entity.""" if speed is None: speed = SPEED_MEDIUM @@ -83,7 +170,7 @@ class DemoFan(FanEntity): def turn_off(self, **kwargs) -> None: """Turn off the entity.""" self.oscillate(False) - self.set_speed(STATE_OFF) + self.set_speed(SPEED_OFF) def set_speed(self, speed: str) -> None: """Set the speed of the fan.""" @@ -100,17 +187,124 @@ class DemoFan(FanEntity): self._oscillating = oscillating self.schedule_update_ha_state() - @property - def current_direction(self) -> str: - """Fan direction.""" - return self._direction + +class DemoPercentageFan(BaseDemoFan, FanEntity): + """A demonstration fan component that uses percentages.""" @property - def oscillating(self) -> bool: - """Oscillating.""" - return self._oscillating + def percentage(self) -> str: + """Return the current speed.""" + return self._percentage + + def set_percentage(self, percentage: int) -> None: + """Set the speed of the fan, as a percentage.""" + self._percentage = percentage + self._preset_mode = None + self.schedule_update_ha_state() @property - def supported_features(self) -> int: - """Flag supported features.""" - return self._supported_features + def preset_mode(self) -> Optional[str]: + """Return the current preset mode, e.g., auto, smart, interval, favorite.""" + return self._preset_mode + + @property + def preset_modes(self) -> Optional[List[str]]: + """Return a list of available preset modes.""" + return self._preset_modes + + def set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + if preset_mode in self.preset_modes: + self._preset_mode = preset_mode + self._percentage = None + self.schedule_update_ha_state() + else: + raise ValueError(f"Invalid preset mode: {preset_mode}") + + def turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: + """Turn on the entity.""" + if preset_mode: + self.set_preset_mode(preset_mode) + return + + if percentage is None: + percentage = 67 + + self.set_percentage(percentage) + + def turn_off(self, **kwargs) -> None: + """Turn off the entity.""" + self.set_percentage(0) + + +class AsyncDemoPercentageFan(BaseDemoFan, FanEntity): + """An async demonstration fan component that uses percentages.""" + + @property + def percentage(self) -> str: + """Return the current speed.""" + return self._percentage + + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed of the fan, as a percentage.""" + self._percentage = percentage + self._preset_mode = None + self.async_write_ha_state() + + @property + def preset_mode(self) -> Optional[str]: + """Return the current preset mode, e.g., auto, smart, interval, favorite.""" + return self._preset_mode + + @property + def preset_modes(self) -> Optional[List[str]]: + """Return a list of available preset modes.""" + return self._preset_modes + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + if preset_mode not in self.preset_modes: + raise ValueError( + "{preset_mode} is not a valid preset_mode: {self.preset_modes}" + ) + self._preset_mode = preset_mode + self._percentage = None + self.async_write_ha_state() + + async def async_turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: + """Turn on the entity.""" + if preset_mode: + await self.async_set_preset_mode(preset_mode) + return + + if percentage is None: + percentage = 67 + + await self.async_set_percentage(percentage) + + async def async_turn_off(self, **kwargs) -> None: + """Turn off the entity.""" + await self.async_oscillate(False) + await self.async_set_percentage(0) + + async def async_set_direction(self, direction: str) -> None: + """Set the direction of the fan.""" + self._direction = direction + self.async_write_ha_state() + + async def async_oscillate(self, oscillating: bool) -> None: + """Set oscillation.""" + self._oscillating = oscillating + self.async_write_ha_state() diff --git a/homeassistant/components/dyson/fan.py b/homeassistant/components/dyson/fan.py index 6690f77390d..7a57a75523e 100644 --- a/homeassistant/components/dyson/fan.py +++ b/homeassistant/components/dyson/fan.py @@ -233,7 +233,20 @@ class DysonPureCoolLinkEntity(DysonFanEntity): """Initialize the fan.""" super().__init__(device, DysonPureCoolState) - def turn_on(self, speed: Optional[str] = None, **kwargs) -> None: + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + def turn_on( + self, + speed: Optional[str] = None, + percentage: Optional[int] = None, + preset_mode: Optional[str] = None, + **kwargs, + ) -> None: """Turn on the fan.""" _LOGGER.debug("Turn on fan %s with speed %s", self.name, speed) if speed is not None: @@ -299,7 +312,20 @@ class DysonPureCoolEntity(DysonFanEntity): """Initialize the fan.""" super().__init__(device, DysonPureCoolV2State) - def turn_on(self, speed: Optional[str] = None, **kwargs) -> None: + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + def turn_on( + self, + speed: Optional[str] = None, + percentage: Optional[int] = None, + preset_mode: Optional[str] = None, + **kwargs, + ) -> None: """Turn on the fan.""" _LOGGER.debug("Turn on fan %s", self.name) diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 32f445d23a2..c7abac576e7 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -79,7 +79,20 @@ class EsphomeFan(EsphomeEntity, FanEntity): self._static_info.key, speed=_fan_speeds.from_hass(speed) ) - async def async_turn_on(self, speed: Optional[str] = None, **kwargs) -> None: + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + async def async_turn_on( + self, + speed: Optional[str] = None, + percentage: Optional[int] = None, + preset_mode: Optional[str] = None, + **kwargs, + ) -> None: """Turn on the fan.""" if speed == SPEED_OFF: await self.async_turn_off() diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 90a3d030703..7b6b083c964 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -2,7 +2,7 @@ from datetime import timedelta import functools as ft import logging -from typing import Optional +from typing import List, Optional import voluptuous as vol @@ -20,6 +20,10 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.loader import bind_hass +from homeassistant.util.percentage import ( + ordered_list_item_to_percentage, + percentage_to_ordered_list_item, +) _LOGGER = logging.getLogger(__name__) @@ -32,10 +36,13 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}" SUPPORT_SET_SPEED = 1 SUPPORT_OSCILLATE = 2 SUPPORT_DIRECTION = 4 +SUPPORT_PRESET_MODE = 8 SERVICE_SET_SPEED = "set_speed" SERVICE_OSCILLATE = "oscillate" SERVICE_SET_DIRECTION = "set_direction" +SERVICE_SET_PERCENTAGE = "set_percentage" +SERVICE_SET_PRESET_MODE = "set_preset_mode" SPEED_OFF = "off" SPEED_LOW = "low" @@ -46,9 +53,47 @@ DIRECTION_FORWARD = "forward" DIRECTION_REVERSE = "reverse" ATTR_SPEED = "speed" +ATTR_PERCENTAGE = "percentage" ATTR_SPEED_LIST = "speed_list" ATTR_OSCILLATING = "oscillating" ATTR_DIRECTION = "direction" +ATTR_PRESET_MODE = "preset_mode" +ATTR_PRESET_MODES = "preset_modes" + +# Invalid speeds do not conform to the entity model, but have crept +# into core integrations at some point so we are temporarily +# accommodating them in the transition to percentages. +_NOT_SPEED_OFF = "off" +_NOT_SPEED_AUTO = "auto" +_NOT_SPEED_SMART = "smart" +_NOT_SPEED_INTERVAL = "interval" +_NOT_SPEED_IDLE = "idle" +_NOT_SPEED_FAVORITE = "favorite" + +_NOT_SPEEDS_FILTER = { + _NOT_SPEED_OFF, + _NOT_SPEED_AUTO, + _NOT_SPEED_SMART, + _NOT_SPEED_INTERVAL, + _NOT_SPEED_IDLE, + _NOT_SPEED_FAVORITE, +} + +_FAN_NATIVE = "_fan_native" + +OFF_SPEED_VALUES = [SPEED_OFF, None] + + +class NoValidSpeedsError(ValueError): + """Exception class when there are no valid speeds.""" + + +class NotValidSpeedError(ValueError): + """Exception class when the speed in not in the speed list.""" + + +class NotValidPresetModeError(ValueError): + """Exception class when the preset_mode in not in the preset_modes list.""" @bind_hass @@ -56,7 +101,7 @@ def is_on(hass, entity_id: str) -> bool: """Return if the fans are on based on the statemachine.""" state = hass.states.get(entity_id) if ATTR_SPEED in state.attributes: - return state.attributes[ATTR_SPEED] not in [SPEED_OFF, None] + return state.attributes[ATTR_SPEED] not in OFF_SPEED_VALUES return state.state == STATE_ON @@ -68,15 +113,27 @@ async def async_setup(hass, config: dict): await component.async_setup(config) + # After the transition to percentage and preset_modes concludes, + # switch this back to async_turn_on and remove async_turn_on_compat component.async_register_entity_service( - SERVICE_TURN_ON, {vol.Optional(ATTR_SPEED): cv.string}, "async_turn_on" + SERVICE_TURN_ON, + { + vol.Optional(ATTR_SPEED): cv.string, + vol.Optional(ATTR_PERCENTAGE): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), + vol.Optional(ATTR_PRESET_MODE): cv.string, + }, + "async_turn_on_compat", ) component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") + # After the transition to percentage and preset_modes concludes, + # remove this service component.async_register_entity_service( SERVICE_SET_SPEED, {vol.Required(ATTR_SPEED): cv.string}, - "async_set_speed", + "async_set_speed_deprecated", [SUPPORT_SET_SPEED], ) component.async_register_entity_service( @@ -91,6 +148,22 @@ async def async_setup(hass, config: dict): "async_set_direction", [SUPPORT_DIRECTION], ) + component.async_register_entity_service( + SERVICE_SET_PERCENTAGE, + { + vol.Required(ATTR_PERCENTAGE): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ) + }, + "async_set_percentage", + [SUPPORT_SET_SPEED], + ) + component.async_register_entity_service( + SERVICE_SET_PRESET_MODE, + {vol.Required(ATTR_PRESET_MODE): cv.string}, + "async_set_preset_mode", + [SUPPORT_SET_SPEED, SUPPORT_PRESET_MODE], + ) return True @@ -105,19 +178,91 @@ async def async_unload_entry(hass, entry): return await hass.data[DOMAIN].async_unload_entry(entry) +def _fan_native(method): + """Native fan method not overridden.""" + setattr(method, _FAN_NATIVE, True) + return method + + class FanEntity(ToggleEntity): """Representation of a fan.""" + @_fan_native def set_speed(self, speed: str) -> None: """Set the speed of the fan.""" raise NotImplementedError() + async def async_set_speed_deprecated(self, speed: str): + """Set the speed of the fan.""" + _LOGGER.warning( + "fan.set_speed is deprecated, use fan.set_percentage or fan.set_preset_mode instead." + ) + await self.async_set_speed(speed) + + @_fan_native async def async_set_speed(self, speed: str): """Set the speed of the fan.""" if speed == SPEED_OFF: await self.async_turn_off() + return + + if speed in self.preset_modes: + if not hasattr(self.async_set_preset_mode, _FAN_NATIVE): + await self.async_set_preset_mode(speed) + return + if not hasattr(self.set_preset_mode, _FAN_NATIVE): + await self.hass.async_add_executor_job(self.set_preset_mode, speed) + return else: - await self.hass.async_add_executor_job(self.set_speed, speed) + if not hasattr(self.async_set_percentage, _FAN_NATIVE): + await self.async_set_percentage(self.speed_to_percentage(speed)) + return + if not hasattr(self.set_percentage, _FAN_NATIVE): + await self.hass.async_add_executor_job( + self.set_percentage, self.speed_to_percentage(speed) + ) + return + + await self.hass.async_add_executor_job(self.set_speed, speed) + + @_fan_native + def set_percentage(self, percentage: int) -> None: + """Set the speed of the fan, as a percentage.""" + raise NotImplementedError() + + @_fan_native + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed of the fan, as a percentage.""" + if percentage == 0: + await self.async_turn_off() + elif not hasattr(self.set_percentage, _FAN_NATIVE): + await self.hass.async_add_executor_job(self.set_percentage, percentage) + else: + await self.async_set_speed(self.percentage_to_speed(percentage)) + + @_fan_native + def set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + self._valid_preset_mode_or_raise(preset_mode) + self.set_speed(preset_mode) + + @_fan_native + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + if not hasattr(self.set_preset_mode, _FAN_NATIVE): + await self.hass.async_add_executor_job(self.set_preset_mode, preset_mode) + return + + self._valid_preset_mode_or_raise(preset_mode) + await self.async_set_speed(preset_mode) + + def _valid_preset_mode_or_raise(self, preset_mode): + """Raise NotValidPresetModeError on invalid preset_mode.""" + preset_modes = self.preset_modes + if preset_mode not in preset_modes: + raise NotValidPresetModeError( + f"The preset_mode {preset_mode} is not a valid preset_mode: {preset_modes}" + ) def set_direction(self, direction: str) -> None: """Set the direction of the fan.""" @@ -128,18 +273,75 @@ class FanEntity(ToggleEntity): await self.hass.async_add_executor_job(self.set_direction, direction) # pylint: disable=arguments-differ - def turn_on(self, speed: Optional[str] = None, **kwargs) -> None: + def turn_on( + self, + speed: Optional[str] = None, + percentage: Optional[int] = None, + preset_mode: Optional[str] = None, + **kwargs, + ) -> None: """Turn on the fan.""" raise NotImplementedError() # pylint: disable=arguments-differ - async def async_turn_on(self, speed: Optional[str] = None, **kwargs): + async def async_turn_on_compat( + self, + speed: Optional[str] = None, + percentage: Optional[int] = None, + preset_mode: Optional[str] = None, + **kwargs, + ) -> None: + """Turn on the fan. + + This _compat version wraps async_turn_on with + backwards and forward compatibility. + + After the transition to percentage and preset_modes concludes, it + should be removed. + """ + if preset_mode is not None: + self._valid_preset_mode_or_raise(preset_mode) + speed = preset_mode + percentage = None + elif speed is not None: + _LOGGER.warning( + "Calling fan.turn_on with the speed argument is deprecated, use percentage or preset_mode instead." + ) + if speed in self.preset_modes: + preset_mode = speed + percentage = None + else: + percentage = self.speed_to_percentage(speed) + elif percentage is not None: + speed = self.percentage_to_speed(percentage) + + await self.async_turn_on( + speed=speed, + percentage=percentage, + preset_mode=preset_mode, + **kwargs, + ) + + # pylint: disable=arguments-differ + async def async_turn_on( + self, + speed: Optional[str] = None, + percentage: Optional[int] = None, + preset_mode: Optional[str] = None, + **kwargs, + ) -> None: """Turn on the fan.""" if speed == SPEED_OFF: await self.async_turn_off() else: await self.hass.async_add_executor_job( - ft.partial(self.turn_on, speed, **kwargs) + ft.partial( + self.turn_on, + speed=speed, + percentage=percentage, + preset_mode=preset_mode, + **kwargs, + ) ) def oscillate(self, oscillating: bool) -> None: @@ -155,15 +357,57 @@ class FanEntity(ToggleEntity): """Return true if the entity is on.""" return self.speed not in [SPEED_OFF, None] + @property + def _implemented_percentage(self): + """Return true if percentage has been implemented.""" + return not hasattr(self.set_percentage, _FAN_NATIVE) or not hasattr( + self.async_set_percentage, _FAN_NATIVE + ) + + @property + def _implemented_preset_mode(self): + """Return true if preset_mode has been implemented.""" + return not hasattr(self.set_preset_mode, _FAN_NATIVE) or not hasattr( + self.async_set_preset_mode, _FAN_NATIVE + ) + + @property + def _implemented_speed(self): + """Return true if speed has been implemented.""" + return not hasattr(self.set_speed, _FAN_NATIVE) or not hasattr( + self.async_set_speed, _FAN_NATIVE + ) + @property def speed(self) -> Optional[str]: """Return the current speed.""" + if self._implemented_preset_mode: + preset_mode = self.preset_mode + if preset_mode: + return preset_mode + if self._implemented_percentage: + return self.percentage_to_speed(self.percentage) return None + @property + def percentage(self) -> Optional[int]: + """Return the current speed as a percentage.""" + if not self._implemented_preset_mode: + if self.speed in self.preset_modes: + return None + if not self._implemented_percentage: + return self.speed_to_percentage(self.speed) + return 0 + @property def speed_list(self) -> list: """Get the list of available speeds.""" - return [] + speeds = [] + if self._implemented_percentage: + speeds += [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + if self._implemented_preset_mode: + speeds += self.preset_modes + return speeds @property def current_direction(self) -> Optional[str]: @@ -178,9 +422,79 @@ class FanEntity(ToggleEntity): @property def capability_attributes(self): """Return capability attributes.""" + attrs = {} if self.supported_features & SUPPORT_SET_SPEED: - return {ATTR_SPEED_LIST: self.speed_list} - return {} + attrs[ATTR_SPEED_LIST] = self.speed_list + + if ( + self.supported_features & SUPPORT_SET_SPEED + or self.supported_features & SUPPORT_PRESET_MODE + ): + attrs[ATTR_PRESET_MODES] = self.preset_modes + + return attrs + + def speed_to_percentage(self, speed: str) -> int: + """ + Map a speed to a percentage. + + Officially this should only have to deal with the 4 pre-defined speeds: + + return { + SPEED_OFF: 0, + SPEED_LOW: 33, + SPEED_MEDIUM: 66, + SPEED_HIGH: 100, + }[speed] + + Unfortunately lots of fans make up their own speeds. So the default + mapping is more dynamic. + """ + if speed in OFF_SPEED_VALUES: + return 0 + + speed_list = speed_list_without_preset_modes(self.speed_list) + + if speed_list and speed not in speed_list: + raise NotValidSpeedError(f"The speed {speed} is not a valid speed.") + + try: + return ordered_list_item_to_percentage(speed_list, speed) + except ValueError as ex: + raise NoValidSpeedsError( + f"The speed_list {speed_list} does not contain any valid speeds." + ) from ex + + def percentage_to_speed(self, percentage: int) -> str: + """ + Map a percentage onto self.speed_list. + + Officially, this should only have to deal with 4 pre-defined speeds. + + if value == 0: + return SPEED_OFF + elif value <= 33: + return SPEED_LOW + elif value <= 66: + return SPEED_MEDIUM + else: + return SPEED_HIGH + + Unfortunately there is currently a high degree of non-conformancy. + Until fans have been corrected a more complicated and dynamic + mapping is used. + """ + if percentage == 0: + return SPEED_OFF + + speed_list = speed_list_without_preset_modes(self.speed_list) + + try: + return percentage_to_ordered_list_item(speed_list, percentage) + except ValueError as ex: + raise NoValidSpeedsError( + f"The speed_list {speed_list} does not contain any valid speeds." + ) from ex @property def state_attributes(self) -> dict: @@ -196,6 +510,13 @@ class FanEntity(ToggleEntity): if supported_features & SUPPORT_SET_SPEED: data[ATTR_SPEED] = self.speed + data[ATTR_PERCENTAGE] = self.percentage + + if ( + supported_features & SUPPORT_PRESET_MODE + or supported_features & SUPPORT_SET_SPEED + ): + data[ATTR_PRESET_MODE] = self.preset_mode return data @@ -203,3 +524,72 @@ class FanEntity(ToggleEntity): def supported_features(self) -> int: """Flag supported features.""" return 0 + + @property + def preset_mode(self) -> Optional[str]: + """Return the current preset mode, e.g., auto, smart, interval, favorite. + + Requires SUPPORT_SET_SPEED. + """ + speed = self.speed + if speed in self.preset_modes: + return speed + return None + + @property + def preset_modes(self) -> Optional[List[str]]: + """Return a list of available preset modes. + + Requires SUPPORT_SET_SPEED. + """ + return preset_modes_from_speed_list(self.speed_list) + + +def speed_list_without_preset_modes(speed_list: List): + """Filter out non-speeds from the speed list. + + The goal is to get the speeds in a list from lowest to + highest by removing speeds that are not valid or out of order + so we can map them to percentages. + + Examples: + input: ["off", "low", "low-medium", "medium", "medium-high", "high", "auto"] + output: ["low", "low-medium", "medium", "medium-high", "high"] + + input: ["off", "auto", "low", "medium", "high"] + output: ["low", "medium", "high"] + + input: ["off", "1", "2", "3", "4", "5", "6", "7", "smart"] + output: ["1", "2", "3", "4", "5", "6", "7"] + + input: ["Auto", "Silent", "Favorite", "Idle", "Medium", "High", "Strong"] + output: ["Silent", "Medium", "High", "Strong"] + """ + + return [speed for speed in speed_list if speed.lower() not in _NOT_SPEEDS_FILTER] + + +def preset_modes_from_speed_list(speed_list: List): + """Filter out non-preset modes from the speed list. + + The goal is to return only preset modes. + + Examples: + input: ["off", "low", "low-medium", "medium", "medium-high", "high", "auto"] + output: ["auto"] + + input: ["off", "auto", "low", "medium", "high"] + output: ["auto"] + + input: ["off", "1", "2", "3", "4", "5", "6", "7", "smart"] + output: ["smart"] + + input: ["Auto", "Silent", "Favorite", "Idle", "Medium", "High", "Strong"] + output: ["Auto", "Favorite", "Idle"] + """ + + return [ + speed + for speed in speed_list + if speed.lower() in _NOT_SPEEDS_FILTER and speed.lower() != SPEED_OFF + ] diff --git a/homeassistant/components/fan/reproduce_state.py b/homeassistant/components/fan/reproduce_state.py index 55ae78d90f6..b5f0fca47b7 100644 --- a/homeassistant/components/fan/reproduce_state.py +++ b/homeassistant/components/fan/reproduce_state.py @@ -17,10 +17,14 @@ from homeassistant.helpers.typing import HomeAssistantType from . import ( ATTR_DIRECTION, ATTR_OSCILLATING, + ATTR_PERCENTAGE, + ATTR_PRESET_MODE, ATTR_SPEED, DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, + SERVICE_SET_PERCENTAGE, + SERVICE_SET_PRESET_MODE, SERVICE_SET_SPEED, ) @@ -31,6 +35,8 @@ ATTRIBUTES = { # attribute: service ATTR_DIRECTION: SERVICE_SET_DIRECTION, ATTR_OSCILLATING: SERVICE_OSCILLATE, ATTR_SPEED: SERVICE_SET_SPEED, + ATTR_PERCENTAGE: SERVICE_SET_PERCENTAGE, + ATTR_PRESET_MODE: SERVICE_SET_PRESET_MODE, } diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index 1fb88a36d2c..760aaabcf4a 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -9,6 +9,26 @@ set_speed: description: Speed setting example: "low" +set_preset_mode: + description: Set preset mode for a fan device. + fields: + entity_id: + description: Name(s) of entities to change. + example: "fan.kitchen" + preset_mode: + description: New value of preset mode + example: "auto" + +set_percentage: + description: Sets fan speed percentage. + fields: + entity_id: + description: Name(s) of the entities to set + example: "fan.living_room" + percentage: + description: Percentage speed setting + example: 25 + turn_on: description: Turns fan on. fields: @@ -18,6 +38,12 @@ turn_on: speed: description: Speed setting example: "high" + percentage: + description: Percentage speed setting + example: 75 + preset_mode: + description: Preset mode setting + example: "auto" turn_off: description: Turns fan off. diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index 476d0f2c8e5..828347e4b89 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -130,9 +130,17 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): {CharacteristicsTypes.SWING_MODE: 1 if oscillating else 0} ) - async def async_turn_on(self, speed=None, **kwargs): + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + async def async_turn_on( + self, speed=None, percentage=None, preset_mode=None, **kwargs + ): """Turn the specified fan on.""" - characteristics = {} if not self.is_on: diff --git a/homeassistant/components/insteon/fan.py b/homeassistant/components/insteon/fan.py index fb741d7a4b0..f9d1c381f49 100644 --- a/homeassistant/components/insteon/fan.py +++ b/homeassistant/components/insteon/fan.py @@ -63,7 +63,20 @@ class InsteonFanEntity(InsteonEntity, FanEntity): """Flag supported features.""" return SUPPORT_SET_SPEED - async def async_turn_on(self, speed: str = None, **kwargs) -> None: + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + async def async_turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: """Turn on the fan.""" if speed is None: speed = SPEED_MEDIUM diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index 384ff22403a..c94c2f607bd 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -71,7 +71,20 @@ class ISYFanEntity(ISYNodeEntity, FanEntity): """Send the set speed command to the ISY994 fan device.""" self._node.turn_on(val=STATE_TO_VALUE.get(speed, 255)) - def turn_on(self, speed: str = None, **kwargs) -> None: + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + def turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: """Send the turn on command to the ISY994 fan device.""" self.set_speed(speed) @@ -108,7 +121,20 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity): if not self._actions.run_then(): _LOGGER.error("Unable to turn off the fan") - def turn_on(self, speed: str = None, **kwargs) -> None: + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + def turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: """Send the turn off command to ISY994 fan program.""" if not self._actions.run_else(): _LOGGER.error("Unable to turn on the fan") diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py index 045cd35cd17..80472535d51 100644 --- a/homeassistant/components/lutron_caseta/fan.py +++ b/homeassistant/components/lutron_caseta/fan.py @@ -75,7 +75,20 @@ class LutronCasetaFan(LutronCasetaDevice, FanEntity): """Flag supported features. Speed Only.""" return SUPPORT_SET_SPEED - async def async_turn_on(self, speed: str = None, **kwargs): + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + async def async_turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ): """Turn the fan on.""" if speed is None: speed = SPEED_MEDIUM diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index e5cebd43714..f96180b0982 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -317,7 +317,20 @@ class MqttFan(MqttEntity, FanEntity): """Return the oscillation state.""" return self._oscillation - async def async_turn_on(self, speed: str = None, **kwargs) -> None: + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + async def async_turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: """Turn on the entity. This method is a coroutine. diff --git a/homeassistant/components/ozw/fan.py b/homeassistant/components/ozw/fan.py index 818bd710496..5043379303f 100644 --- a/homeassistant/components/ozw/fan.py +++ b/homeassistant/components/ozw/fan.py @@ -57,7 +57,16 @@ class ZwaveFan(ZWaveDeviceEntity, FanEntity): self._previous_speed = speed self.values.primary.send_value(SPEED_TO_VALUE[speed]) - async def async_turn_on(self, speed=None, **kwargs): + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + async def async_turn_on( + self, speed=None, percentage=None, preset_mode=None, **kwargs + ): """Turn the device on.""" if speed is None: # Value 255 tells device to return to previous value diff --git a/homeassistant/components/smartthings/fan.py b/homeassistant/components/smartthings/fan.py index e366f6bb3e3..bff9862c8f6 100644 --- a/homeassistant/components/smartthings/fan.py +++ b/homeassistant/components/smartthings/fan.py @@ -50,7 +50,20 @@ class SmartThingsFan(SmartThingsEntity, FanEntity): # the entity state ahead of receiving the confirming push updates self.async_write_ha_state() - async def async_turn_on(self, speed: str = None, **kwargs) -> None: + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + async def async_turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: """Turn the fan on.""" if speed is not None: value = SPEED_TO_VALUE[speed] diff --git a/homeassistant/components/smarty/fan.py b/homeassistant/components/smarty/fan.py index e46198b051b..40c244944ce 100644 --- a/homeassistant/components/smarty/fan.py +++ b/homeassistant/components/smarty/fan.py @@ -86,7 +86,14 @@ class SmartyFan(FanEntity): self._speed = speed self._state = True - def turn_on(self, speed=None, **kwargs): + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + def turn_on(self, speed=None, percentage=None, preset_mode=None, **kwargs): """Turn on the fan.""" _LOGGER.debug("Turning on fan. Speed is %s", speed) if speed is None: diff --git a/homeassistant/components/tasmota/fan.py b/homeassistant/components/tasmota/fan.py index bdcc00dc764..1d7aa9f38cb 100644 --- a/homeassistant/components/tasmota/fan.py +++ b/homeassistant/components/tasmota/fan.py @@ -79,7 +79,16 @@ class TasmotaFan( else: self._tasmota_entity.set_speed(HA_TO_TASMOTA_SPEED_MAP[speed]) - async def async_turn_on(self, speed=None, **kwargs): + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + async def async_turn_on( + self, speed=None, percentage=None, preset_mode=None, **kwargs + ): """Turn the fan on.""" # Tasmota does not support turning a fan on with implicit speed await self.async_set_speed(speed or fan.SPEED_MEDIUM) diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index fa67f60dac0..fe32d095677 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -251,8 +251,20 @@ class TemplateFan(TemplateEntity, FanEntity): """Return the oscillation state.""" return self._direction - # pylint: disable=arguments-differ - async def async_turn_on(self, speed: str = None) -> None: + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + async def async_turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: """Turn on the fan.""" await self._on_script.async_run({ATTR_SPEED: speed}, context=self._context) self._state = STATE_ON diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index e88349cf795..a66e1ff92a4 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -75,7 +75,20 @@ class TuyaFanDevice(TuyaDevice, FanEntity): else: self._tuya.set_speed(speed) - def turn_on(self, speed: str = None, **kwargs) -> None: + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + def turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: """Turn on the fan.""" if speed is not None: self.set_speed(speed) diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py index c79ee15db59..525bf00f50e 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -137,7 +137,20 @@ class ValloxFan(FanEntity): self._available = False _LOGGER.error("Error updating fan: %s", err) - async def async_turn_on(self, speed: str = None, **kwargs) -> None: + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + async def async_turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: """Turn the device on.""" _LOGGER.debug("Turn on: %s", speed) diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index 7cc3f00e1a0..1e2cc08473c 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -107,7 +107,20 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): self.smartfan.manual_mode() self.smartfan.change_fan_speed(FAN_SPEEDS.index(speed)) - def turn_on(self, speed: str = None, **kwargs) -> None: + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + def turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: """Turn the device on.""" self.smartfan.turn_on() self.set_speed(speed) diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 0dca71a0d8d..cef51a1e3b1 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -185,7 +185,20 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity): self._available = False self.wemo.reconnect_with_device() - def turn_on(self, speed: str = None, **kwargs) -> None: + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + def turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: """Turn the switch on.""" if speed is None: try: diff --git a/homeassistant/components/wilight/fan.py b/homeassistant/components/wilight/fan.py index 6d8ad88d6c0..d59b9398d9e 100644 --- a/homeassistant/components/wilight/fan.py +++ b/homeassistant/components/wilight/fan.py @@ -94,7 +94,20 @@ class WiLightFan(WiLightDevice, FanEntity): self._direction = self._status["direction"] return self._direction - async def async_turn_on(self, speed: str = None, **kwargs): + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + async def async_turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: """Turn on the fan.""" if speed is None: await self._client.set_fan_direction(self._index, self._direction) diff --git a/homeassistant/components/wink/fan.py b/homeassistant/components/wink/fan.py index 535e7094fcd..3aab66e353d 100644 --- a/homeassistant/components/wink/fan.py +++ b/homeassistant/components/wink/fan.py @@ -40,7 +40,20 @@ class WinkFanDevice(WinkDevice, FanEntity): """Set the speed of the fan.""" self.wink.set_state(True, speed) - def turn_on(self, speed: str = None, **kwargs) -> None: + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + def turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: """Turn on the fan.""" self.wink.set_state(True, speed) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index e2a1b3b8143..0d07654e61b 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -718,7 +718,20 @@ class XiaomiGenericDevice(FanEntity): return False - async def async_turn_on(self, speed: str = None, **kwargs) -> None: + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + async def async_turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: """Turn the device on.""" if speed: # If operation mode was set the device must not be turned on. diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index b25d1c1aa39..bc5714ef08c 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -95,7 +95,16 @@ class BaseFan(FanEntity): """Flag supported features.""" return SUPPORT_SET_SPEED - async def async_turn_on(self, speed: str = None, **kwargs) -> None: + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + async def async_turn_on( + self, speed=None, percentage=None, preset_mode=None, **kwargs + ) -> None: """Turn the entity on.""" if speed is None: speed = SPEED_MEDIUM diff --git a/homeassistant/components/zwave/fan.py b/homeassistant/components/zwave/fan.py index df6f5d8b8f5..1827996241d 100644 --- a/homeassistant/components/zwave/fan.py +++ b/homeassistant/components/zwave/fan.py @@ -58,7 +58,14 @@ class ZwaveFan(ZWaveDeviceEntity, FanEntity): """Set the speed of the fan.""" self.node.set_dimmer(self.values.primary.value_id, SPEED_TO_VALUE[speed]) - def turn_on(self, speed=None, **kwargs): + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + def turn_on(self, speed=None, percentage=None, preset_mode=None, **kwargs): """Turn the device on.""" if speed is None: # Value 255 tells device to return to previous value diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index 7113272d2ea..29a93b38b8c 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -72,7 +72,20 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): target_value = self.get_zwave_value("targetValue") await self.info.node.async_set_value(target_value, SPEED_TO_VALUE[speed]) - async def async_turn_on(self, speed: Optional[str] = None, **kwargs: Any) -> None: + # + # The fan entity model has changed to use percentages and preset_modes + # instead of speeds. + # + # Please review + # https://developers.home-assistant.io/docs/core/entity/fan/ + # + async def async_turn_on( + self, + speed: Optional[str] = None, + percentage: Optional[int] = None, + preset_mode: Optional[str] = None, + **kwargs: Any, + ) -> None: """Turn the device on.""" if speed is None: # Value 255 tells device to return to previous value diff --git a/homeassistant/util/percentage.py b/homeassistant/util/percentage.py new file mode 100644 index 00000000000..fa4c9dcc252 --- /dev/null +++ b/homeassistant/util/percentage.py @@ -0,0 +1,87 @@ +"""Percentage util functions.""" + +from typing import List, Tuple + + +def ordered_list_item_to_percentage(ordered_list: List[str], item: str) -> int: + """Determine the percentage of an item in an ordered list. + + When using this utility for fan speeds, do not include "off" + + Given the list: ["low", "medium", "high", "very_high"], this + function will return the following when when the item is passed + in: + + low: 25 + medium: 50 + high: 75 + very_high: 100 + + """ + if item not in ordered_list: + raise ValueError + + list_len = len(ordered_list) + list_position = ordered_list.index(item) + 1 + return (list_position * 100) // list_len + + +def percentage_to_ordered_list_item(ordered_list: List[str], percentage: int) -> str: + """Find the item that most closely matches the percentage in an ordered list. + + When using this utility for fan speeds, do not include "off" + + Given the list: ["low", "medium", "high", "very_high"], this + function will return the following when when the item is passed + in: + + 1-25: low + 26-50: medium + 51-75: high + 76-100: very_high + """ + list_len = len(ordered_list) + if not list_len: + raise ValueError + + for offset, speed in enumerate(ordered_list): + list_position = offset + 1 + upper_bound = (list_position * 100) // list_len + if percentage <= upper_bound: + return speed + + return ordered_list[-1] + + +def ranged_value_to_percentage( + low_high_range: Tuple[float, float], value: float +) -> int: + """Given a range of low and high values convert a single value to a percentage. + + When using this utility for fan speeds, do not include 0 if it is off + + Given a low value of 1 and a high value of 255 this function + will return: + + (1,255), 255: 100 + (1,255), 127: 50 + (1,255), 10: 4 + """ + return int((value * 100) // (low_high_range[1] - low_high_range[0] + 1)) + + +def percentage_to_ranged_value( + low_high_range: Tuple[float, float], percentage: int +) -> float: + """Given a range of low and high values convert a percentage to a single value. + + When using this utility for fan speeds, do not include 0 if it is off + + Given a low value of 1 and a high value of 255 this function + will return: + + (1,255), 100: 255 + (1,255), 50: 127.5 + (1,255), 4: 10.2 + """ + return (low_high_range[1] - low_high_range[0] + 1) * percentage / 100 diff --git a/tests/components/demo/test_fan.py b/tests/components/demo/test_fan.py index 2dca57d3e6b..5297e64bda9 100644 --- a/tests/components/demo/test_fan.py +++ b/tests/components/demo/test_fan.py @@ -2,6 +2,7 @@ import pytest from homeassistant.components import fan +from homeassistant.components.demo.fan import PRESET_MODE_AUTO, PRESET_MODE_SMART from homeassistant.const import ( ATTR_ENTITY_ID, ENTITY_MATCH_ALL, @@ -12,7 +13,15 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -FAN_ENTITY_ID = "fan.living_room_fan" +FULL_FAN_ENTITY_IDS = ["fan.living_room_fan", "fan.percentage_full_fan"] +FANS_WITH_PRESET_MODE_ONLY = ["fan.preset_only_limited_fan"] +LIMITED_AND_FULL_FAN_ENTITY_IDS = FULL_FAN_ENTITY_IDS + [ + "fan.ceiling_fan", + "fan.percentage_limited_fan", +] +FANS_WITH_PRESET_MODES = FULL_FAN_ENTITY_IDS + [ + "fan.percentage_limited_fan", +] @pytest.fixture(autouse=True) @@ -22,124 +31,338 @@ async def setup_comp(hass): await hass.async_block_till_done() -async def test_turn_on(hass): +@pytest.mark.parametrize("fan_entity_id", LIMITED_AND_FULL_FAN_ENTITY_IDS) +async def test_turn_on(hass, fan_entity_id): """Test turning on the device.""" - state = hass.states.get(FAN_ENTITY_ID) + state = hass.states.get(fan_entity_id) assert state.state == STATE_OFF await hass.services.async_call( - fan.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: FAN_ENTITY_ID}, blocking=True + fan.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: fan_entity_id}, blocking=True ) - state = hass.states.get(FAN_ENTITY_ID) + state = hass.states.get(fan_entity_id) assert state.state == STATE_ON + +@pytest.mark.parametrize("fan_entity_id", FULL_FAN_ENTITY_IDS) +async def test_turn_on_with_speed_and_percentage(hass, fan_entity_id): + """Test turning on the device.""" + state = hass.states.get(fan_entity_id) + assert state.state == STATE_OFF + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_HIGH}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH + assert state.attributes[fan.ATTR_PERCENTAGE] == 100 + await hass.services.async_call( fan.DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: FAN_ENTITY_ID, fan.ATTR_SPEED: fan.SPEED_HIGH}, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 100}, blocking=True, ) - state = hass.states.get(FAN_ENTITY_ID) + state = hass.states.get(fan_entity_id) assert state.state == STATE_ON assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH + assert state.attributes[fan.ATTR_PERCENTAGE] == 100 -async def test_turn_off(hass): +@pytest.mark.parametrize("fan_entity_id", FANS_WITH_PRESET_MODE_ONLY) +async def test_turn_on_with_preset_mode_only(hass, fan_entity_id): + """Test turning on the device with a preset_mode and no speed setting.""" + state = hass.states.get(fan_entity_id) + assert state.state == STATE_OFF + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PRESET_MODE: PRESET_MODE_AUTO}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_PRESET_MODE] == PRESET_MODE_AUTO + assert state.attributes[fan.ATTR_PRESET_MODES] == [ + PRESET_MODE_AUTO, + PRESET_MODE_SMART, + ] + + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PRESET_MODE: PRESET_MODE_SMART}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_PRESET_MODE] == PRESET_MODE_SMART + + await hass.services.async_call( + fan.DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: fan_entity_id}, blocking=True + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_OFF + assert state.attributes[fan.ATTR_PRESET_MODE] is None + + with pytest.raises(ValueError): + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PRESET_MODE: "invalid"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(fan_entity_id) + assert state.state == STATE_OFF + assert state.attributes[fan.ATTR_PRESET_MODE] is None + + +@pytest.mark.parametrize("fan_entity_id", FANS_WITH_PRESET_MODES) +async def test_turn_on_with_preset_mode_and_speed(hass, fan_entity_id): + """Test turning on the device with a preset_mode and speed.""" + state = hass.states.get(fan_entity_id) + assert state.state == STATE_OFF + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PRESET_MODE: PRESET_MODE_AUTO}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_SPEED] == PRESET_MODE_AUTO + assert state.attributes[fan.ATTR_PERCENTAGE] is None + assert state.attributes[fan.ATTR_PRESET_MODE] == PRESET_MODE_AUTO + assert state.attributes[fan.ATTR_SPEED_LIST] == [ + fan.SPEED_OFF, + fan.SPEED_LOW, + fan.SPEED_MEDIUM, + fan.SPEED_HIGH, + PRESET_MODE_AUTO, + PRESET_MODE_SMART, + ] + assert state.attributes[fan.ATTR_PRESET_MODES] == [ + PRESET_MODE_AUTO, + PRESET_MODE_SMART, + ] + + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 100}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH + assert state.attributes[fan.ATTR_PERCENTAGE] == 100 + assert state.attributes[fan.ATTR_PRESET_MODE] is None + + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PRESET_MODE: PRESET_MODE_SMART}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_SPEED] == PRESET_MODE_SMART + assert state.attributes[fan.ATTR_PERCENTAGE] is None + assert state.attributes[fan.ATTR_PRESET_MODE] == PRESET_MODE_SMART + + await hass.services.async_call( + fan.DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: fan_entity_id}, blocking=True + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_OFF + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF + assert state.attributes[fan.ATTR_PERCENTAGE] == 0 + assert state.attributes[fan.ATTR_PRESET_MODE] is None + + with pytest.raises(ValueError): + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PRESET_MODE: "invalid"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(fan_entity_id) + assert state.state == STATE_OFF + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF + assert state.attributes[fan.ATTR_PERCENTAGE] == 0 + assert state.attributes[fan.ATTR_PRESET_MODE] is None + + +@pytest.mark.parametrize("fan_entity_id", LIMITED_AND_FULL_FAN_ENTITY_IDS) +async def test_turn_off(hass, fan_entity_id): """Test turning off the device.""" - state = hass.states.get(FAN_ENTITY_ID) + state = hass.states.get(fan_entity_id) assert state.state == STATE_OFF await hass.services.async_call( - fan.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: FAN_ENTITY_ID}, blocking=True + fan.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: fan_entity_id}, blocking=True ) - state = hass.states.get(FAN_ENTITY_ID) + state = hass.states.get(fan_entity_id) assert state.state == STATE_ON await hass.services.async_call( - fan.DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: FAN_ENTITY_ID}, blocking=True + fan.DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: fan_entity_id}, blocking=True ) - state = hass.states.get(FAN_ENTITY_ID) + state = hass.states.get(fan_entity_id) assert state.state == STATE_OFF -async def test_turn_off_without_entity_id(hass): +@pytest.mark.parametrize("fan_entity_id", LIMITED_AND_FULL_FAN_ENTITY_IDS) +async def test_turn_off_without_entity_id(hass, fan_entity_id): """Test turning off all fans.""" - state = hass.states.get(FAN_ENTITY_ID) + state = hass.states.get(fan_entity_id) assert state.state == STATE_OFF await hass.services.async_call( - fan.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: FAN_ENTITY_ID}, blocking=True + fan.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: fan_entity_id}, blocking=True ) - state = hass.states.get(FAN_ENTITY_ID) + state = hass.states.get(fan_entity_id) assert state.state == STATE_ON await hass.services.async_call( fan.DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True ) - state = hass.states.get(FAN_ENTITY_ID) + state = hass.states.get(fan_entity_id) assert state.state == STATE_OFF -async def test_set_direction(hass): +@pytest.mark.parametrize("fan_entity_id", FULL_FAN_ENTITY_IDS) +async def test_set_direction(hass, fan_entity_id): """Test setting the direction of the device.""" - state = hass.states.get(FAN_ENTITY_ID) + state = hass.states.get(fan_entity_id) assert state.state == STATE_OFF await hass.services.async_call( fan.DOMAIN, fan.SERVICE_SET_DIRECTION, - {ATTR_ENTITY_ID: FAN_ENTITY_ID, fan.ATTR_DIRECTION: fan.DIRECTION_REVERSE}, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_DIRECTION: fan.DIRECTION_REVERSE}, blocking=True, ) - state = hass.states.get(FAN_ENTITY_ID) + state = hass.states.get(fan_entity_id) assert state.attributes[fan.ATTR_DIRECTION] == fan.DIRECTION_REVERSE -async def test_set_speed(hass): +@pytest.mark.parametrize("fan_entity_id", FULL_FAN_ENTITY_IDS) +async def test_set_speed(hass, fan_entity_id): """Test setting the speed of the device.""" - state = hass.states.get(FAN_ENTITY_ID) + state = hass.states.get(fan_entity_id) assert state.state == STATE_OFF await hass.services.async_call( fan.DOMAIN, fan.SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: FAN_ENTITY_ID, fan.ATTR_SPEED: fan.SPEED_LOW}, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_LOW}, blocking=True, ) - state = hass.states.get(FAN_ENTITY_ID) + state = hass.states.get(fan_entity_id) assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW -async def test_oscillate(hass): +@pytest.mark.parametrize("fan_entity_id", FANS_WITH_PRESET_MODES) +async def test_set_preset_mode(hass, fan_entity_id): + """Test setting the preset mode of the device.""" + state = hass.states.get(fan_entity_id) + assert state.state == STATE_OFF + + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PRESET_MODE: PRESET_MODE_AUTO}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_SPEED] == PRESET_MODE_AUTO + assert state.attributes[fan.ATTR_PERCENTAGE] is None + assert state.attributes[fan.ATTR_PRESET_MODE] == PRESET_MODE_AUTO + + +@pytest.mark.parametrize("fan_entity_id", LIMITED_AND_FULL_FAN_ENTITY_IDS) +async def test_set_preset_mode_invalid(hass, fan_entity_id): + """Test setting a invalid preset mode for the device.""" + state = hass.states.get(fan_entity_id) + assert state.state == STATE_OFF + + with pytest.raises(ValueError): + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PRESET_MODE: "invalid"}, + blocking=True, + ) + await hass.async_block_till_done() + + with pytest.raises(ValueError): + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PRESET_MODE: "invalid"}, + blocking=True, + ) + await hass.async_block_till_done() + + +@pytest.mark.parametrize("fan_entity_id", FULL_FAN_ENTITY_IDS) +async def test_set_percentage(hass, fan_entity_id): + """Test setting the percentage speed of the device.""" + state = hass.states.get(fan_entity_id) + assert state.state == STATE_OFF + + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 33}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW + assert state.attributes[fan.ATTR_PERCENTAGE] == 33 + + +@pytest.mark.parametrize("fan_entity_id", FULL_FAN_ENTITY_IDS) +async def test_oscillate(hass, fan_entity_id): """Test oscillating the fan.""" - state = hass.states.get(FAN_ENTITY_ID) + state = hass.states.get(fan_entity_id) assert state.state == STATE_OFF assert not state.attributes.get(fan.ATTR_OSCILLATING) await hass.services.async_call( fan.DOMAIN, fan.SERVICE_OSCILLATE, - {ATTR_ENTITY_ID: FAN_ENTITY_ID, fan.ATTR_OSCILLATING: True}, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_OSCILLATING: True}, blocking=True, ) - state = hass.states.get(FAN_ENTITY_ID) + state = hass.states.get(fan_entity_id) assert state.attributes[fan.ATTR_OSCILLATING] is True await hass.services.async_call( fan.DOMAIN, fan.SERVICE_OSCILLATE, - {ATTR_ENTITY_ID: FAN_ENTITY_ID, fan.ATTR_OSCILLATING: False}, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_OSCILLATING: False}, blocking=True, ) - state = hass.states.get(FAN_ENTITY_ID) + state = hass.states.get(fan_entity_id) assert state.attributes[fan.ATTR_OSCILLATING] is False -async def test_is_on(hass): +@pytest.mark.parametrize("fan_entity_id", LIMITED_AND_FULL_FAN_ENTITY_IDS) +async def test_is_on(hass, fan_entity_id): """Test is on service call.""" - assert not fan.is_on(hass, FAN_ENTITY_ID) + assert not fan.is_on(hass, fan_entity_id) await hass.services.async_call( - fan.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: FAN_ENTITY_ID}, blocking=True + fan.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: fan_entity_id}, blocking=True ) - assert fan.is_on(hass, FAN_ENTITY_ID) + assert fan.is_on(hass, fan_entity_id) diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 4d8079b9db9..04832f4adc7 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -70,16 +70,19 @@ ENTITY_IDS_BY_NUMBER = { "8": "media_player.lounge_room", "9": "fan.living_room_fan", "10": "fan.ceiling_fan", - "11": "cover.living_room_window", - "12": "climate.hvac", - "13": "climate.heatpump", - "14": "climate.ecobee", - "15": "light.no_brightness", - "16": "humidifier.humidifier", - "17": "humidifier.dehumidifier", - "18": "humidifier.hygrostat", - "19": "scene.light_on", - "20": "scene.light_off", + "11": "fan.percentage_full_fan", + "12": "fan.percentage_limited_fan", + "13": "fan.preset_only_limited_fan", + "14": "cover.living_room_window", + "15": "climate.hvac", + "16": "climate.heatpump", + "17": "climate.ecobee", + "18": "light.no_brightness", + "19": "humidifier.humidifier", + "20": "humidifier.dehumidifier", + "21": "humidifier.hygrostat", + "22": "scene.light_on", + "23": "scene.light_off", } ENTITY_NUMBERS_BY_ID = {v: k for k, v in ENTITY_IDS_BY_NUMBER.items()} diff --git a/tests/components/fan/common.py b/tests/components/fan/common.py index 70a2c7e43d3..215849e6aab 100644 --- a/tests/components/fan/common.py +++ b/tests/components/fan/common.py @@ -6,10 +6,14 @@ components. Instead call the service directly. from homeassistant.components.fan import ( ATTR_DIRECTION, ATTR_OSCILLATING, + ATTR_PERCENTAGE, + ATTR_PRESET_MODE, ATTR_SPEED, DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, + SERVICE_SET_PERCENTAGE, + SERVICE_SET_PRESET_MODE, SERVICE_SET_SPEED, ) from homeassistant.const import ( @@ -20,11 +24,22 @@ from homeassistant.const import ( ) -async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL, speed: str = None) -> None: +async def async_turn_on( + hass, + entity_id=ENTITY_MATCH_ALL, + speed: str = None, + percentage: int = None, + preset_mode: str = None, +) -> None: """Turn all or specified fan on.""" data = { key: value - for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_SPEED, speed)] + for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_SPEED, speed), + (ATTR_PERCENTAGE, percentage), + (ATTR_PRESET_MODE, preset_mode), + ] if value is not None } @@ -65,6 +80,32 @@ async def async_set_speed(hass, entity_id=ENTITY_MATCH_ALL, speed: str = None) - await hass.services.async_call(DOMAIN, SERVICE_SET_SPEED, data, blocking=True) +async def async_set_preset_mode( + hass, entity_id=ENTITY_MATCH_ALL, preset_mode: str = None +) -> None: + """Set preset mode for all or specified fan.""" + data = { + key: value + for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_PRESET_MODE, preset_mode)] + if value is not None + } + + await hass.services.async_call(DOMAIN, SERVICE_SET_PRESET_MODE, data, blocking=True) + + +async def async_set_percentage( + hass, entity_id=ENTITY_MATCH_ALL, percentage: int = None +) -> None: + """Set percentage for all or specified fan.""" + data = { + key: value + for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_PERCENTAGE, percentage)] + if value is not None + } + + await hass.services.async_call(DOMAIN, SERVICE_SET_PERCENTAGE, data, blocking=True) + + async def async_set_direction( hass, entity_id=ENTITY_MATCH_ALL, direction: str = None ) -> None: diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index a8beed73a07..f5c303bd416 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -2,7 +2,7 @@ import pytest -from homeassistant.components.fan import FanEntity +from homeassistant.components.fan import FanEntity, NotValidPresetModeError class BaseFan(FanEntity): @@ -17,6 +17,7 @@ def test_fanentity(): fan = BaseFan() assert fan.state == "off" assert len(fan.speed_list) == 0 + assert len(fan.preset_modes) == 0 assert fan.supported_features == 0 assert fan.capability_attributes == {} # Test set_speed not required @@ -24,7 +25,35 @@ def test_fanentity(): fan.oscillate(True) with pytest.raises(NotImplementedError): fan.set_speed("slow") + with pytest.raises(NotImplementedError): + fan.set_percentage(0) + with pytest.raises(NotValidPresetModeError): + fan.set_preset_mode("auto") with pytest.raises(NotImplementedError): fan.turn_on() with pytest.raises(NotImplementedError): fan.turn_off() + + +async def test_async_fanentity(hass): + """Test async fan entity methods.""" + fan = BaseFan() + fan.hass = hass + assert fan.state == "off" + assert len(fan.speed_list) == 0 + assert len(fan.preset_modes) == 0 + assert fan.supported_features == 0 + assert fan.capability_attributes == {} + # Test set_speed not required + with pytest.raises(NotImplementedError): + await fan.async_oscillate(True) + with pytest.raises(NotImplementedError): + await fan.async_set_speed("slow") + with pytest.raises(NotImplementedError): + await fan.async_set_percentage(0) + with pytest.raises(NotValidPresetModeError): + await fan.async_set_preset_mode("auto") + with pytest.raises(NotImplementedError): + await fan.async_turn_on() + with pytest.raises(NotImplementedError): + await fan.async_turn_off() diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index cb11f1ceaac..4bef45cf0ee 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -245,6 +245,27 @@ DEMO_DEVICES = [ "type": "action.devices.types.FAN", "willReportState": False, }, + { + "id": "fan.percentage_full_fan", + "name": {"name": "Percentage Full Fan"}, + "traits": ["action.devices.traits.FanSpeed", "action.devices.traits.OnOff"], + "type": "action.devices.types.FAN", + "willReportState": False, + }, + { + "id": "fan.percentage_limited_fan", + "name": {"name": "Percentage Limited Fan"}, + "traits": ["action.devices.traits.FanSpeed", "action.devices.traits.OnOff"], + "type": "action.devices.types.FAN", + "willReportState": False, + }, + { + "id": "fan.preset_only_limited_fan", + "name": {"name": "Preset Only Limited Fan"}, + "traits": ["action.devices.traits.OnOff"], + "type": "action.devices.types.FAN", + "willReportState": False, + }, { "id": "climate.hvac", "name": {"name": "Hvac"}, diff --git a/tests/util/test_percentage.py b/tests/util/test_percentage.py new file mode 100644 index 00000000000..4ad28f8567c --- /dev/null +++ b/tests/util/test_percentage.py @@ -0,0 +1,158 @@ +"""Test Home Assistant percentage conversions.""" + +import math + +import pytest + +from homeassistant.util.percentage import ( + ordered_list_item_to_percentage, + percentage_to_ordered_list_item, + percentage_to_ranged_value, + ranged_value_to_percentage, +) + +SPEED_LOW = "low" +SPEED_MEDIUM = "medium" +SPEED_HIGH = "high" + +SPEED_1 = SPEED_LOW +SPEED_2 = SPEED_MEDIUM +SPEED_3 = SPEED_HIGH +SPEED_4 = "very_high" +SPEED_5 = "storm" +SPEED_6 = "hurricane" +SPEED_7 = "solar_wind" + +LEGACY_ORDERED_LIST = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] +SMALL_ORDERED_LIST = [SPEED_1, SPEED_2, SPEED_3, SPEED_4] +LARGE_ORDERED_LIST = [SPEED_1, SPEED_2, SPEED_3, SPEED_4, SPEED_5, SPEED_6, SPEED_7] + + +async def test_ordered_list_percentage_round_trip(): + """Test we can round trip.""" + for ordered_list in (SMALL_ORDERED_LIST, LARGE_ORDERED_LIST): + for i in range(1, 100): + ordered_list_item_to_percentage( + ordered_list, percentage_to_ordered_list_item(ordered_list, i) + ) == i + + +async def test_ordered_list_item_to_percentage(): + """Test percentage of an item in an ordered list.""" + + assert ordered_list_item_to_percentage(LEGACY_ORDERED_LIST, SPEED_LOW) == 33 + assert ordered_list_item_to_percentage(LEGACY_ORDERED_LIST, SPEED_MEDIUM) == 66 + assert ordered_list_item_to_percentage(LEGACY_ORDERED_LIST, SPEED_HIGH) == 100 + + assert ordered_list_item_to_percentage(SMALL_ORDERED_LIST, SPEED_1) == 25 + assert ordered_list_item_to_percentage(SMALL_ORDERED_LIST, SPEED_2) == 50 + assert ordered_list_item_to_percentage(SMALL_ORDERED_LIST, SPEED_3) == 75 + assert ordered_list_item_to_percentage(SMALL_ORDERED_LIST, SPEED_4) == 100 + + assert ordered_list_item_to_percentage(LARGE_ORDERED_LIST, SPEED_1) == 14 + assert ordered_list_item_to_percentage(LARGE_ORDERED_LIST, SPEED_2) == 28 + assert ordered_list_item_to_percentage(LARGE_ORDERED_LIST, SPEED_3) == 42 + assert ordered_list_item_to_percentage(LARGE_ORDERED_LIST, SPEED_4) == 57 + assert ordered_list_item_to_percentage(LARGE_ORDERED_LIST, SPEED_5) == 71 + assert ordered_list_item_to_percentage(LARGE_ORDERED_LIST, SPEED_6) == 85 + assert ordered_list_item_to_percentage(LARGE_ORDERED_LIST, SPEED_7) == 100 + + with pytest.raises(ValueError): + assert ordered_list_item_to_percentage([], SPEED_1) + + +async def test_percentage_to_ordered_list_item(): + """Test item that most closely matches the percentage in an ordered list.""" + + assert percentage_to_ordered_list_item(SMALL_ORDERED_LIST, 1) == SPEED_1 + assert percentage_to_ordered_list_item(SMALL_ORDERED_LIST, 25) == SPEED_1 + assert percentage_to_ordered_list_item(SMALL_ORDERED_LIST, 26) == SPEED_2 + assert percentage_to_ordered_list_item(SMALL_ORDERED_LIST, 50) == SPEED_2 + assert percentage_to_ordered_list_item(SMALL_ORDERED_LIST, 51) == SPEED_3 + assert percentage_to_ordered_list_item(SMALL_ORDERED_LIST, 75) == SPEED_3 + assert percentage_to_ordered_list_item(SMALL_ORDERED_LIST, 76) == SPEED_4 + assert percentage_to_ordered_list_item(SMALL_ORDERED_LIST, 100) == SPEED_4 + + assert percentage_to_ordered_list_item(LEGACY_ORDERED_LIST, 17) == SPEED_LOW + assert percentage_to_ordered_list_item(LEGACY_ORDERED_LIST, 33) == SPEED_LOW + assert percentage_to_ordered_list_item(LEGACY_ORDERED_LIST, 50) == SPEED_MEDIUM + assert percentage_to_ordered_list_item(LEGACY_ORDERED_LIST, 66) == SPEED_MEDIUM + assert percentage_to_ordered_list_item(LEGACY_ORDERED_LIST, 84) == SPEED_HIGH + assert percentage_to_ordered_list_item(LEGACY_ORDERED_LIST, 100) == SPEED_HIGH + + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 1) == SPEED_1 + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 14) == SPEED_1 + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 25) == SPEED_2 + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 26) == SPEED_2 + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 28) == SPEED_2 + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 29) == SPEED_3 + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 41) == SPEED_3 + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 42) == SPEED_3 + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 43) == SPEED_4 + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 56) == SPEED_4 + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 50) == SPEED_4 + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 51) == SPEED_4 + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 75) == SPEED_6 + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 76) == SPEED_6 + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 100) == SPEED_7 + + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 1) == SPEED_1 + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 25) == SPEED_2 + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 26) == SPEED_2 + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 50) == SPEED_4 + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 51) == SPEED_4 + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 75) == SPEED_6 + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 76) == SPEED_6 + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 100) == SPEED_7 + + assert percentage_to_ordered_list_item(LARGE_ORDERED_LIST, 100.1) == SPEED_7 + + with pytest.raises(ValueError): + assert percentage_to_ordered_list_item([], 100) + + +async def test_ranged_value_to_percentage_large(): + """Test a large range of low and high values convert a single value to a percentage.""" + range = (1, 255) + + assert ranged_value_to_percentage(range, 255) == 100 + assert ranged_value_to_percentage(range, 127) == 49 + assert ranged_value_to_percentage(range, 10) == 3 + assert ranged_value_to_percentage(range, 1) == 0 + + +async def test_percentage_to_ranged_value_large(): + """Test a large range of low and high values convert a percentage to a single value.""" + range = (1, 255) + + assert percentage_to_ranged_value(range, 100) == 255 + assert percentage_to_ranged_value(range, 50) == 127.5 + assert percentage_to_ranged_value(range, 4) == 10.2 + + assert math.ceil(percentage_to_ranged_value(range, 100)) == 255 + assert math.ceil(percentage_to_ranged_value(range, 50)) == 128 + assert math.ceil(percentage_to_ranged_value(range, 4)) == 11 + + +async def test_ranged_value_to_percentage_small(): + """Test a small range of low and high values convert a single value to a percentage.""" + range = (1, 6) + + assert ranged_value_to_percentage(range, 1) == 16 + assert ranged_value_to_percentage(range, 2) == 33 + assert ranged_value_to_percentage(range, 3) == 50 + assert ranged_value_to_percentage(range, 4) == 66 + assert ranged_value_to_percentage(range, 5) == 83 + assert ranged_value_to_percentage(range, 6) == 100 + + +async def test_percentage_to_ranged_value_small(): + """Test a small range of low and high values convert a percentage to a single value.""" + range = (1, 6) + + assert math.ceil(percentage_to_ranged_value(range, 16)) == 1 + assert math.ceil(percentage_to_ranged_value(range, 33)) == 2 + assert math.ceil(percentage_to_ranged_value(range, 50)) == 3 + assert math.ceil(percentage_to_ranged_value(range, 66)) == 4 + assert math.ceil(percentage_to_ranged_value(range, 83)) == 5 + assert math.ceil(percentage_to_ranged_value(range, 100)) == 6 From 5711d61b38f2d294ad957bf7f5d9650915541feb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Jan 2021 08:55:22 +0100 Subject: [PATCH 004/796] Bump hatasmota to 0.2.7 (#45625) --- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_light.py | 56 ++++++++++++++----- 4 files changed, 45 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index a8e17815181..bd48cae8e59 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.2.6"], + "requirements": ["hatasmota==0.2.7"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"] diff --git a/requirements_all.txt b/requirements_all.txt index ec9556d433d..f90acacbe33 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ hass-nabucasa==0.41.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.2.6 +hatasmota==0.2.7 # homeassistant.components.jewish_calendar hdate==0.9.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d3f7350928b..661192640a4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -384,7 +384,7 @@ hangups==0.4.11 hass-nabucasa==0.41.0 # homeassistant.components.tasmota -hatasmota==0.2.6 +hatasmota==0.2.7 # 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 c7968df32f2..d64e39aacf0 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -789,7 +789,7 @@ async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): # Turn the light on and verify MQTT message is sent await common.async_turn_on(hass, "light.test") mqtt_mock.async_publish.assert_called_once_with( - "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade 0;NoDelay;Power1 ON", 0, False + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON", 0, False ) mqtt_mock.async_publish.reset_mock() @@ -800,21 +800,21 @@ async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): # Turn the light off and verify MQTT message is sent await common.async_turn_off(hass, "light.test") mqtt_mock.async_publish.assert_called_once_with( - "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade 0;NoDelay;Power1 OFF", 0, False + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 OFF", 0, False ) mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT messages are sent await common.async_turn_on(hass, "light.test", brightness=192) mqtt_mock.async_publish.assert_called_once_with( - "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade 0;NoDelay;Dimmer 75", 0, False + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Dimmer 75", 0, False ) mqtt_mock.async_publish.reset_mock() await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 0;NoDelay;Power1 ON;NoDelay;Color2 255,128,0", + "NoDelay;Power1 ON;NoDelay;Color2 255,128,0", 0, False, ) @@ -823,7 +823,7 @@ async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): await common.async_turn_on(hass, "light.test", color_temp=200) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 0;NoDelay;Power1 ON;NoDelay;CT 200", + "NoDelay;Power1 ON;NoDelay;CT 200", 0, False, ) @@ -832,7 +832,7 @@ async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): await common.async_turn_on(hass, "light.test", white_value=128) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 0;NoDelay;Power1 ON;NoDelay;White 50", + "NoDelay;Power1 ON;NoDelay;White 50", 0, False, ) @@ -841,7 +841,7 @@ async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): await common.async_turn_on(hass, "light.test", effect="Random") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 0;NoDelay;Power1 ON;NoDelay;Scheme 4", + "NoDelay;Power1 ON;NoDelay;Scheme 4", 0, False, ) @@ -873,7 +873,7 @@ async def test_sending_mqtt_commands_power_unlinked(hass, mqtt_mock, setup_tasmo # Turn the light on and verify MQTT message is sent await common.async_turn_on(hass, "light.test") mqtt_mock.async_publish.assert_called_once_with( - "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade 0;NoDelay;Power1 ON", 0, False + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON", 0, False ) mqtt_mock.async_publish.reset_mock() @@ -884,7 +884,7 @@ async def test_sending_mqtt_commands_power_unlinked(hass, mqtt_mock, setup_tasmo # Turn the light off and verify MQTT message is sent await common.async_turn_off(hass, "light.test") mqtt_mock.async_publish.assert_called_once_with( - "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade 0;NoDelay;Power1 OFF", 0, False + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 OFF", 0, False ) mqtt_mock.async_publish.reset_mock() @@ -892,7 +892,7 @@ async def test_sending_mqtt_commands_power_unlinked(hass, mqtt_mock, setup_tasmo await common.async_turn_on(hass, "light.test", brightness=192) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 0;NoDelay;Dimmer 75;NoDelay;Power1 ON", + "NoDelay;Dimmer 75;NoDelay;Power1 ON", 0, False, ) @@ -978,6 +978,24 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): ) mqtt_mock.async_publish.reset_mock() + # Fake state update from the light + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":100}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 255 + + # Dim the light from 100->0: Speed should be 0 + await common.async_turn_off(hass, "light.test", transition=0) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Fade 0;NoDelay;Power1 OFF", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + # Fake state update from the light async_fire_mqtt_message( hass, @@ -1121,6 +1139,16 @@ async def test_transition_fixed(hass, mqtt_mock, setup_tasmota): ) mqtt_mock.async_publish.reset_mock() + # Dim the light from 0->50: Speed should be 0 + await common.async_turn_on(hass, "light.test", brightness=128, transition=0) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Fade 0;NoDelay;Dimmer 50", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + async def test_relay_as_light(hass, mqtt_mock, setup_tasmota): """Test relay show up as light in light mode.""" @@ -1167,7 +1195,7 @@ async def _test_split_light(hass, mqtt_mock, config, num_lights, num_switches): await common.async_turn_on(hass, entity) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - f"NoDelay;Fade 0;NoDelay;Power{idx+num_switches+1} ON", + f"NoDelay;Power{idx+num_switches+1} ON", 0, False, ) @@ -1177,7 +1205,7 @@ async def _test_split_light(hass, mqtt_mock, config, num_lights, num_switches): await common.async_turn_on(hass, entity, brightness=(idx + 1) * 25.5) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - f"NoDelay;Fade 0;NoDelay;Channel{idx+num_switches+1} {(idx+1)*10}", + f"NoDelay;Channel{idx+num_switches+1} {(idx+1)*10}", 0, False, ) @@ -1239,7 +1267,7 @@ async def _test_unlinked_light(hass, mqtt_mock, config, num_switches): await common.async_turn_on(hass, entity) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - f"NoDelay;Fade 0;NoDelay;Power{idx+num_switches+1} ON", + f"NoDelay;Power{idx+num_switches+1} ON", 0, False, ) @@ -1249,7 +1277,7 @@ async def _test_unlinked_light(hass, mqtt_mock, config, num_switches): await common.async_turn_on(hass, entity, brightness=(idx + 1) * 25.5) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - f"NoDelay;Fade 0;NoDelay;Dimmer{idx+1} {(idx+1)*10}", + f"NoDelay;Dimmer{idx+1} {(idx+1)*10}", 0, False, ) From 7673f572486d281804de8226c23264cbeee9c9f9 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 28 Jan 2021 09:26:41 +0100 Subject: [PATCH 005/796] Add additional error handling for automation script run (#45613) --- homeassistant/components/automation/__init__.py | 6 ++++++ homeassistant/helpers/script.py | 3 +++ tests/components/automation/test_init.py | 1 + 3 files changed, 10 insertions(+) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index f1b6df48bde..201eeb5c456 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -404,6 +404,12 @@ class AutomationEntity(ToggleEntity, RestoreEntity): await self.action_script.async_run( variables, trigger_context, started_action ) + except (vol.Invalid, HomeAssistantError) as err: + self._logger.error( + "Error while executing automation %s: %s", + self.entity_id, + err, + ) except Exception: # pylint: disable=broad-except self._logger.exception("While executing automation %s", self.entity_id) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index ff87312dbc2..f197664f7e6 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -291,6 +291,9 @@ class _ScriptRun: elif isinstance(exception, exceptions.ServiceNotFound): error_desc = "Service not found" + elif isinstance(exception, exceptions.HomeAssistantError): + error_desc = "Error" + else: error_desc = "Unexpected error" level = _LOG_EXCEPTION diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 244f37ecb9c..c31af555e32 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -947,6 +947,7 @@ async def test_automation_with_error_in_script(hass, caplog): hass.bus.async_fire("test_event") await hass.async_block_till_done() assert "Service not found" in caplog.text + assert "Traceback" not in caplog.text async def test_automation_with_error_in_script_2(hass, caplog): From 92e084cee1eef3f694a9545455d081e02b81a3ea Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 28 Jan 2021 09:33:18 +0100 Subject: [PATCH 006/796] Include relative path in tts get url (#45623) * Include relative path in tts get url * Always cal get_url when requested --- homeassistant/components/tts/__init__.py | 26 ++++++++++++++---------- tests/components/tts/test_init.py | 7 ++++--- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 719f8c52e7a..d278283baaf 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -27,7 +27,6 @@ from homeassistant.const import ( CONF_PLATFORM, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, - HTTP_OK, ) from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError @@ -117,7 +116,7 @@ async def async_setup(hass, config): use_cache = conf.get(CONF_CACHE, DEFAULT_CACHE) cache_dir = conf.get(CONF_CACHE_DIR, DEFAULT_CACHE_DIR) time_memory = conf.get(CONF_TIME_MEMORY, DEFAULT_TIME_MEMORY) - base_url = conf.get(CONF_BASE_URL) or get_url(hass) + base_url = conf.get(CONF_BASE_URL) hass.data[BASE_URL_KEY] = base_url await tts.async_init_cache(use_cache, cache_dir, time_memory, base_url) @@ -165,13 +164,16 @@ async def async_setup(hass, config): options = service.data.get(ATTR_OPTIONS) try: - url = await tts.async_get_url( + url = await tts.async_get_url_path( p_type, message, cache=cache, language=language, options=options ) except HomeAssistantError as err: _LOGGER.error("Error on init TTS: %s", err) return + base = tts.base_url or get_url(hass) + url = base + url + data = { ATTR_MEDIA_CONTENT_ID: url, ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, @@ -290,7 +292,7 @@ class SpeechManager: provider.name = engine self.providers[engine] = provider - async def async_get_url( + async def async_get_url_path( self, engine, message, cache=None, language=None, options=None ): """Get URL for play message. @@ -342,7 +344,7 @@ class SpeechManager: engine, key, message, use_cache, language, options ) - return f"{self.base_url}/api/tts_proxy/{filename}" + return f"/api/tts_proxy/{filename}" async def async_get_tts_audio(self, engine, key, message, cache, language, options): """Receive TTS and store for view in cache. @@ -579,15 +581,17 @@ class TextToSpeechUrlView(HomeAssistantView): options = data.get(ATTR_OPTIONS) try: - url = await self.tts.async_get_url( + path = await self.tts.async_get_url_path( p_type, message, cache=cache, language=language, options=options ) - resp = self.json({"url": url}, HTTP_OK) except HomeAssistantError as err: _LOGGER.error("Error on init tts: %s", err) - resp = self.json({"error": err}, HTTP_BAD_REQUEST) + return self.json({"error": err}, HTTP_BAD_REQUEST) - return resp + base = self.tts.base_url or get_url(self.tts.hass) + url = base + path + + return self.json({"url": url, "path": path}) class TextToSpeechView(HomeAssistantView): @@ -595,7 +599,7 @@ class TextToSpeechView(HomeAssistantView): requires_auth = False url = "/api/tts_proxy/{filename}" - name = "api:tts:speech" + name = "api:tts_speech" def __init__(self, tts): """Initialize a tts view.""" @@ -614,4 +618,4 @@ class TextToSpeechView(HomeAssistantView): def get_base_url(hass): """Get base URL.""" - return hass.data[BASE_URL_KEY] + return hass.data[BASE_URL_KEY] or get_url(hass) diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 61d77b6c8e2..77fbd3f7170 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -699,9 +699,10 @@ async def test_setup_component_and_web_get_url(hass, hass_client): req = await client.post(url, json=data) assert req.status == 200 response = await req.json() - assert response.get("url") == ( - "http://example.local:8123/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" - ) + assert response == { + "url": "http://example.local:8123/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", + "path": "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", + } async def test_setup_component_and_web_get_url_bad_config(hass, hass_client): From e43d8651124dc89dd0af6314052c504008ae24cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jan 2021 02:40:10 -0600 Subject: [PATCH 007/796] Update ozw to use new fan entity model (#45577) --- homeassistant/components/ozw/fan.py | 59 +++++++++-------------------- tests/components/ozw/test_fan.py | 44 ++++++++++----------- 2 files changed, 38 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/ozw/fan.py b/homeassistant/components/ozw/fan.py index 5043379303f..b4054207d0f 100644 --- a/homeassistant/components/ozw/fan.py +++ b/homeassistant/components/ozw/fan.py @@ -4,15 +4,15 @@ import math from homeassistant.components.fan import ( DOMAIN as FAN_DOMAIN, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, SUPPORT_SET_SPEED, FanEntity, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) from .const import DATA_UNSUBSCRIBE, DOMAIN from .entity import ZWaveDeviceEntity @@ -20,11 +20,7 @@ from .entity import ZWaveDeviceEntity _LOGGER = logging.getLogger(__name__) SUPPORTED_FEATURES = SUPPORT_SET_SPEED - -# Value will first be divided to an integer -VALUE_TO_SPEED = {0: SPEED_OFF, 1: SPEED_LOW, 2: SPEED_MEDIUM, 3: SPEED_HIGH} -SPEED_TO_VALUE = {SPEED_OFF: 0, SPEED_LOW: 1, SPEED_MEDIUM: 50, SPEED_HIGH: 99} -SPEED_LIST = [*SPEED_TO_VALUE] +SPEED_RANGE = (1, 99) # off is not included async def async_setup_entry(hass, config_entry, async_add_entities): @@ -44,35 +40,22 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class ZwaveFan(ZWaveDeviceEntity, FanEntity): """Representation of a Z-Wave fan.""" - def __init__(self, values): - """Initialize the fan.""" - super().__init__(values) - self._previous_speed = None + async def async_set_percentage(self, percentage): + """Set the speed percentage of the fan.""" + if percentage is None: + # Value 255 tells device to return to previous value + zwave_speed = 255 + elif percentage == 0: + zwave_speed = 0 + else: + zwave_speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) + self.values.primary.send_value(zwave_speed) - async def async_set_speed(self, speed): - """Set the speed of the fan.""" - if speed not in SPEED_TO_VALUE: - _LOGGER.warning("Invalid speed received: %s", speed) - return - self._previous_speed = speed - self.values.primary.send_value(SPEED_TO_VALUE[speed]) - - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # async def async_turn_on( self, speed=None, percentage=None, preset_mode=None, **kwargs ): """Turn the device on.""" - if speed is None: - # Value 255 tells device to return to previous value - self.values.primary.send_value(255) - else: - await self.async_set_speed(speed) + await self.async_set_percentage(percentage) async def async_turn_off(self, **kwargs): """Turn the device off.""" @@ -84,19 +67,13 @@ class ZwaveFan(ZWaveDeviceEntity, FanEntity): return self.values.primary.value > 0 @property - def speed(self): + def percentage(self): """Return the current speed. The Z-Wave speed value is a byte 0-255. 255 means previous value. The normal range of the speed is 0-99. 0 means off. """ - value = math.ceil(self.values.primary.value * 3 / 100) - return VALUE_TO_SPEED.get(value, self._previous_speed) - - @property - def speed_list(self): - """Get the list of available speeds.""" - return SPEED_LIST + return ranged_value_to_percentage(SPEED_RANGE, self.values.primary.value) @property def supported_features(self): diff --git a/tests/components/ozw/test_fan.py b/tests/components/ozw/test_fan.py index cca1143c44b..5556b663f6f 100644 --- a/tests/components/ozw/test_fan.py +++ b/tests/components/ozw/test_fan.py @@ -1,5 +1,5 @@ -"""Test Z-Wave Lights.""" -from homeassistant.components.ozw.fan import SPEED_TO_VALUE +"""Test Z-Wave Fans.""" +import pytest from .common import setup_ozw @@ -38,11 +38,10 @@ async def test_fan(hass, fan_data, fan_msg, sent_messages, caplog): assert state.state == "off" # Test turning on - new_speed = "medium" await hass.services.async_call( "fan", "turn_on", - {"entity_id": "fan.in_wall_smart_fan_control_level", "speed": new_speed}, + {"entity_id": "fan.in_wall_smart_fan_control_level", "percentage": 66}, blocking=True, ) @@ -50,13 +49,13 @@ async def test_fan(hass, fan_data, fan_msg, sent_messages, caplog): msg = sent_messages[-1] assert msg["topic"] == "OpenZWave/1/command/setvalue/" assert msg["payload"] == { - "Value": SPEED_TO_VALUE[new_speed], + "Value": 66, "ValueIDKey": 172589073, } # Feedback on state fan_msg.decode() - fan_msg.payload["Value"] = SPEED_TO_VALUE[new_speed] + fan_msg.payload["Value"] = 66 fan_msg.encode() receive_message(fan_msg) await hass.async_block_till_done() @@ -64,7 +63,7 @@ async def test_fan(hass, fan_data, fan_msg, sent_messages, caplog): state = hass.states.get("fan.in_wall_smart_fan_control_level") assert state is not None assert state.state == "on" - assert state.attributes["speed"] == new_speed + assert state.attributes["percentage"] == 66 # Test turn on without speed await hass.services.async_call( @@ -84,7 +83,7 @@ async def test_fan(hass, fan_data, fan_msg, sent_messages, caplog): # Feedback on state fan_msg.decode() - fan_msg.payload["Value"] = SPEED_TO_VALUE[new_speed] + fan_msg.payload["Value"] = 99 fan_msg.encode() receive_message(fan_msg) await hass.async_block_till_done() @@ -92,14 +91,13 @@ async def test_fan(hass, fan_data, fan_msg, sent_messages, caplog): state = hass.states.get("fan.in_wall_smart_fan_control_level") assert state is not None assert state.state == "on" - assert state.attributes["speed"] == new_speed + assert state.attributes["percentage"] == 100 - # Test set speed to off - new_speed = "off" + # Test set percentage to 0 await hass.services.async_call( "fan", - "set_speed", - {"entity_id": "fan.in_wall_smart_fan_control_level", "speed": new_speed}, + "set_percentage", + {"entity_id": "fan.in_wall_smart_fan_control_level", "percentage": 0}, blocking=True, ) @@ -107,13 +105,13 @@ async def test_fan(hass, fan_data, fan_msg, sent_messages, caplog): msg = sent_messages[-1] assert msg["topic"] == "OpenZWave/1/command/setvalue/" assert msg["payload"] == { - "Value": SPEED_TO_VALUE[new_speed], + "Value": 0, "ValueIDKey": 172589073, } # Feedback on state fan_msg.decode() - fan_msg.payload["Value"] = SPEED_TO_VALUE[new_speed] + fan_msg.payload["Value"] = 0 fan_msg.encode() receive_message(fan_msg) await hass.async_block_till_done() @@ -124,12 +122,10 @@ async def test_fan(hass, fan_data, fan_msg, sent_messages, caplog): # Test invalid speed new_speed = "invalid" - await hass.services.async_call( - "fan", - "set_speed", - {"entity_id": "fan.in_wall_smart_fan_control_level", "speed": new_speed}, - blocking=True, - ) - - assert len(sent_messages) == 4 - assert "Invalid speed received: invalid" in caplog.text + with pytest.raises(ValueError): + await hass.services.async_call( + "fan", + "set_speed", + {"entity_id": "fan.in_wall_smart_fan_control_level", "speed": new_speed}, + blocking=True, + ) From 0ec068667f34165d7aefae5c41375bc35dfe681b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jan 2021 02:40:48 -0600 Subject: [PATCH 008/796] Update lutron_caseta manufacturer string (#45637) Use the string that Lutron uses so it can be automatched to the Lutron app via homekit --- homeassistant/components/lutron_caseta/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lutron_caseta/const.py b/homeassistant/components/lutron_caseta/const.py index 5f6032ba6dc..4226be36f05 100644 --- a/homeassistant/components/lutron_caseta/const.py +++ b/homeassistant/components/lutron_caseta/const.py @@ -19,7 +19,7 @@ LUTRON_CASETA_BUTTON_EVENT = "lutron_caseta_button_event" BRIDGE_DEVICE_ID = "1" -MANUFACTURER = "Lutron" +MANUFACTURER = "Lutron Electronics Co., Inc" ATTR_SERIAL = "serial" ATTR_TYPE = "type" From 0441960ffdf023e41ca29ab67df1945125d90473 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jan 2021 03:06:18 -0600 Subject: [PATCH 009/796] Update wemo to use new fan entity model (#45582) --- homeassistant/components/wemo/fan.py | 90 +++++++++------------------- 1 file changed, 28 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index cef51a1e3b1..cdbcc89fae6 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -2,20 +2,18 @@ import asyncio from datetime import timedelta import logging +import math from pywemo.ouimeaux_device.api.service import ActionException import voluptuous as vol -from homeassistant.components.fan import ( - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, - SUPPORT_SET_SPEED, - FanEntity, -) +from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) from .const import ( DOMAIN as WEMO_DOMAIN, @@ -48,37 +46,17 @@ WEMO_HUMIDITY_100 = 4 WEMO_FAN_OFF = 0 WEMO_FAN_MINIMUM = 1 -WEMO_FAN_LOW = 2 # Not used due to limitations of the base fan implementation -WEMO_FAN_MEDIUM = 3 -WEMO_FAN_HIGH = 4 # Not used due to limitations of the base fan implementation +WEMO_FAN_MEDIUM = 4 WEMO_FAN_MAXIMUM = 5 +SPEED_RANGE = (WEMO_FAN_MINIMUM, WEMO_FAN_MAXIMUM) # off is not included + WEMO_WATER_EMPTY = 0 WEMO_WATER_LOW = 1 WEMO_WATER_GOOD = 2 -SUPPORTED_SPEEDS = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - SUPPORTED_FEATURES = SUPPORT_SET_SPEED -# Since the base fan object supports a set list of fan speeds, -# we have to reuse some of them when mapping to the 5 WeMo speeds -WEMO_FAN_SPEED_TO_HASS = { - WEMO_FAN_OFF: SPEED_OFF, - WEMO_FAN_MINIMUM: SPEED_LOW, - WEMO_FAN_LOW: SPEED_LOW, # Reusing SPEED_LOW - WEMO_FAN_MEDIUM: SPEED_MEDIUM, - WEMO_FAN_HIGH: SPEED_HIGH, # Reusing SPEED_HIGH - WEMO_FAN_MAXIMUM: SPEED_HIGH, -} - -# Because we reused mappings in the previous dict, we have to filter them -# back out in this dict, or else we would have duplicate keys -HASS_FAN_SPEED_TO_WEMO = { - v: k - for (k, v) in WEMO_FAN_SPEED_TO_HASS.items() - if k not in [WEMO_FAN_LOW, WEMO_FAN_HIGH] -} SET_HUMIDITY_SCHEMA = { vol.Required(ATTR_TARGET_HUMIDITY): vol.All( @@ -122,7 +100,8 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity): def __init__(self, device): """Initialize the WeMo switch.""" super().__init__(device) - self._fan_mode = None + self._fan_mode = WEMO_FAN_OFF + self._fan_mode_string = None self._target_humidity = None self._current_humidity = None self._water_level = None @@ -141,21 +120,16 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity): return { ATTR_CURRENT_HUMIDITY: self._current_humidity, ATTR_TARGET_HUMIDITY: self._target_humidity, - ATTR_FAN_MODE: self._fan_mode, + ATTR_FAN_MODE: self._fan_mode_string, ATTR_WATER_LEVEL: self._water_level, ATTR_FILTER_LIFE: self._filter_life, ATTR_FILTER_EXPIRED: self._filter_expired, } @property - def speed(self) -> str: - """Return the current speed.""" - return WEMO_FAN_SPEED_TO_HASS.get(self._fan_mode) - - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return SUPPORTED_SPEEDS + def percentage(self) -> str: + """Return the current speed percentage.""" + return ranged_value_to_percentage(SPEED_RANGE, self._fan_mode) @property def supported_features(self) -> int: @@ -167,7 +141,8 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity): try: self._state = self.wemo.get_state(force_update) - self._fan_mode = self.wemo.fan_mode_string + self._fan_mode = self.wemo.fan_mode + self._fan_mode_string = self.wemo.fan_mode_string self._target_humidity = self.wemo.desired_humidity_percent self._current_humidity = self.wemo.current_humidity_percent self._water_level = self.wemo.water_level_string @@ -185,13 +160,6 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity): self._available = False self.wemo.reconnect_with_device() - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # def turn_on( self, speed: str = None, @@ -199,17 +167,8 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity): preset_mode: str = None, **kwargs, ) -> None: - """Turn the switch on.""" - if speed is None: - try: - self.wemo.set_state(self._last_fan_on_mode) - except ActionException as err: - _LOGGER.warning("Error while turning on device %s (%s)", self.name, err) - self._available = False - else: - self.set_speed(speed) - - self.schedule_update_ha_state() + """Turn the fan on.""" + self.set_percentage(percentage) def turn_off(self, **kwargs) -> None: """Turn the switch off.""" @@ -221,10 +180,17 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity): self.schedule_update_ha_state() - def set_speed(self, speed: str) -> None: + def set_percentage(self, percentage: int) -> None: """Set the fan_mode of the Humidifier.""" + if percentage is None: + named_speed = self._last_fan_on_mode + elif percentage == 0: + named_speed = WEMO_FAN_OFF + else: + named_speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) + try: - self.wemo.set_state(HASS_FAN_SPEED_TO_WEMO.get(speed)) + self.wemo.set_state(named_speed) except ActionException as err: _LOGGER.warning( "Error while setting speed of device %s (%s)", self.name, err From 3896e81db747a7ed71bc0a14954fd5b9678aa61f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jan 2021 03:09:16 -0600 Subject: [PATCH 010/796] Update smartthings to use new fan entity model (#45592) --- homeassistant/components/smartthings/fan.py | 55 +++++++-------------- 1 file changed, 19 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/smartthings/fan.py b/homeassistant/components/smartthings/fan.py index bff9862c8f6..b09bfe0ad46 100644 --- a/homeassistant/components/smartthings/fan.py +++ b/homeassistant/components/smartthings/fan.py @@ -1,22 +1,19 @@ """Support for fans through the SmartThings cloud API.""" +import math from typing import Optional, Sequence from pysmartthings import Capability -from homeassistant.components.fan import ( - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, - SUPPORT_SET_SPEED, - FanEntity, +from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, ) from . import SmartThingsEntity from .const import DATA_BROKERS, DOMAIN -VALUE_TO_SPEED = {0: SPEED_OFF, 1: SPEED_LOW, 2: SPEED_MEDIUM, 3: SPEED_HIGH} -SPEED_TO_VALUE = {v: k for k, v in VALUE_TO_SPEED.items()} +SPEED_RANGE = (1, 3) # off is not included async def async_setup_entry(hass, config_entry, async_add_entities): @@ -42,21 +39,19 @@ def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: class SmartThingsFan(SmartThingsEntity, FanEntity): """Define a SmartThings Fan.""" - async def async_set_speed(self, speed: str): - """Set the speed of the fan.""" - value = SPEED_TO_VALUE[speed] - await self._device.set_fan_speed(value, set_status=True) + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed percentage of the fan.""" + if percentage is None: + await self._device.switch_on(set_status=True) + elif percentage == 0: + await self._device.switch_off(set_status=True) + else: + value = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) + await self._device.set_fan_speed(value, set_status=True) # State is set optimistically in the command above, therefore update # the entity state ahead of receiving the confirming push updates self.async_write_ha_state() - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # async def async_turn_on( self, speed: str = None, @@ -65,14 +60,7 @@ class SmartThingsFan(SmartThingsEntity, FanEntity): **kwargs, ) -> None: """Turn the fan on.""" - if speed is not None: - value = SPEED_TO_VALUE[speed] - await self._device.set_fan_speed(value, set_status=True) - else: - await self._device.switch_on(set_status=True) - # State is set optimistically in the commands above, therefore update - # the entity state ahead of receiving the confirming push updates - self.async_write_ha_state() + await self.async_set_percentage(percentage) async def async_turn_off(self, **kwargs) -> None: """Turn the fan off.""" @@ -87,14 +75,9 @@ class SmartThingsFan(SmartThingsEntity, FanEntity): return self._device.status.switch @property - def speed(self) -> str: - """Return the current speed.""" - return VALUE_TO_SPEED[self._device.status.fan_speed] - - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + def percentage(self) -> str: + """Return the current speed percentage.""" + return ranged_value_to_percentage(SPEED_RANGE, self._device.status.fan_speed) @property def supported_features(self) -> int: From 0693d8a064633135a19c21138fd81c7364e2b2fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jan 2021 03:15:24 -0600 Subject: [PATCH 011/796] Update zwave_js to use new fan entity model (#45543) --- homeassistant/components/zwave_js/fan.py | 68 ++++++++---------------- tests/components/zwave_js/test_fan.py | 2 +- 2 files changed, 22 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index 29a93b38b8c..6e62869f749 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -7,16 +7,16 @@ from zwave_js_server.client import Client as ZwaveClient from homeassistant.components.fan import ( DOMAIN as FAN_DOMAIN, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, SUPPORT_SET_SPEED, FanEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN from .discovery import ZwaveDiscoveryInfo @@ -26,10 +26,7 @@ _LOGGER = logging.getLogger(__name__) SUPPORTED_FEATURES = SUPPORT_SET_SPEED -# Value will first be divided to an integer -VALUE_TO_SPEED = {0: SPEED_OFF, 1: SPEED_LOW, 2: SPEED_MEDIUM, 3: SPEED_HIGH} -SPEED_TO_VALUE = {SPEED_OFF: 0, SPEED_LOW: 1, SPEED_MEDIUM: 50, SPEED_HIGH: 99} -SPEED_LIST = [*SPEED_TO_VALUE] +SPEED_RANGE = (1, 99) # off is not included async def async_setup_entry( @@ -57,28 +54,20 @@ async def async_setup_entry( class ZwaveFan(ZWaveBaseEntity, FanEntity): """Representation of a Z-Wave fan.""" - def __init__( - self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo - ) -> None: - """Initialize the fan.""" - super().__init__(config_entry, client, info) - self._previous_speed: Optional[str] = None - - async def async_set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - if speed not in SPEED_TO_VALUE: - raise ValueError(f"Invalid speed received: {speed}") - self._previous_speed = speed + async def async_set_percentage(self, percentage: Optional[int]) -> None: + """Set the speed percentage of the fan.""" target_value = self.get_zwave_value("targetValue") - await self.info.node.async_set_value(target_value, SPEED_TO_VALUE[speed]) - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # + if percentage is None: + # Value 255 tells device to return to previous value + zwave_speed = 255 + elif percentage == 0: + zwave_speed = 0 + else: + zwave_speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) + + await self.info.node.async_set_value(target_value, zwave_speed) + async def async_turn_on( self, speed: Optional[str] = None, @@ -87,12 +76,7 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): **kwargs: Any, ) -> None: """Turn the device on.""" - if speed is None: - # Value 255 tells device to return to previous value - target_value = self.get_zwave_value("targetValue") - await self.info.node.async_set_value(target_value, 255) - else: - await self.async_set_speed(speed) + await self.async_set_percentage(percentage) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" @@ -105,19 +89,9 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): return bool(self.info.primary_value.value > 0) @property - def speed(self) -> Optional[str]: - """Return the current speed. - - The Z-Wave speed value is a byte 0-255. 255 means previous value. - The normal range of the speed is 0-99. 0 means off. - """ - value = math.ceil(self.info.primary_value.value * 3 / 100) - return VALUE_TO_SPEED.get(value, self._previous_speed) - - @property - def speed_list(self) -> List[str]: - """Get the list of available speeds.""" - return SPEED_LIST + def percentage(self) -> int: + """Return the current speed percentage.""" + return ranged_value_to_percentage(SPEED_RANGE, self.info.primary_value.value) @property def supported_features(self) -> int: diff --git a/tests/components/zwave_js/test_fan.py b/tests/components/zwave_js/test_fan.py index 5b726179ac9..a817a551f9b 100644 --- a/tests/components/zwave_js/test_fan.py +++ b/tests/components/zwave_js/test_fan.py @@ -43,7 +43,7 @@ async def test_fan(hass, client, in_wall_smart_fan_control, integration): "label": "Target value", }, } - assert args["value"] == 50 + assert args["value"] == 66 client.async_send_command.reset_mock() From 85e463d507cd7de5df391863de5c479aaa6719fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jan 2021 03:23:12 -0600 Subject: [PATCH 012/796] Update bond to use new fan entity model (#45534) --- homeassistant/components/bond/fan.py | 69 +++++++++++----------------- tests/components/bond/test_fan.py | 63 +++++++++++++++++++++++-- 2 files changed, 87 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index 6e33aa6d161..18eeb912ed8 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -1,17 +1,13 @@ """Support for Bond fans.""" import logging import math -from typing import Any, Callable, List, Optional +from typing import Any, Callable, List, Optional, Tuple from bond_api import Action, DeviceType, Direction from homeassistant.components.fan import ( DIRECTION_FORWARD, DIRECTION_REVERSE, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, SUPPORT_DIRECTION, SUPPORT_SET_SPEED, FanEntity, @@ -19,6 +15,10 @@ from homeassistant.components.fan import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) from .const import DOMAIN from .entity import BondEntity @@ -70,22 +70,16 @@ class BondFan(BondEntity, FanEntity): return features @property - def speed(self) -> Optional[str]: - """Return the current speed.""" - if self._power == 0: - return SPEED_OFF - if not self._power or not self._speed: - return None - - # map 1..max_speed Bond speed to 1..3 HA speed - max_speed = max(self._device.props.get("max_speed", 3), self._speed) - ha_speed = math.ceil(self._speed * (len(self.speed_list) - 1) / max_speed) - return self.speed_list[ha_speed] + def _speed_range(self) -> Tuple[int, int]: + """Return the range of speeds.""" + return (1, self._device.props.get("max_speed", 3)) @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + def percentage(self) -> Optional[str]: + """Return the current speed percentage for the fan.""" + if not self._speed or not self._power: + return 0 + return ranged_value_to_percentage(self._speed_range, self._speed) @property def current_direction(self) -> Optional[str]: @@ -98,33 +92,27 @@ class BondFan(BondEntity, FanEntity): return direction - async def async_set_speed(self, speed: str) -> None: + async def async_set_percentage(self, percentage: int) -> None: """Set the desired speed for the fan.""" - _LOGGER.debug("async_set_speed called with speed %s", speed) + _LOGGER.debug("async_set_percentage called with percentage %s", percentage) - if speed == SPEED_OFF: + if percentage == 0: await self.async_turn_off() return - max_speed = self._device.props.get("max_speed", 3) - if speed == SPEED_LOW: - bond_speed = 1 - elif speed == SPEED_HIGH: - bond_speed = max_speed - else: - bond_speed = math.ceil(max_speed / 2) + bond_speed = math.ceil( + percentage_to_ranged_value(self._speed_range, percentage) + ) + _LOGGER.debug( + "async_set_percentage converted percentage %s to bond speed %s", + percentage, + bond_speed, + ) await self._hub.bond.action( self._device.device_id, Action.set_speed(bond_speed) ) - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # async def async_turn_on( self, speed: Optional[str] = None, @@ -133,13 +121,10 @@ class BondFan(BondEntity, FanEntity): **kwargs, ) -> None: """Turn on the fan.""" - _LOGGER.debug("Fan async_turn_on called with speed %s", speed) + _LOGGER.debug("Fan async_turn_on called with percentage %s", percentage) - if speed is not None: - if speed == SPEED_OFF: - await self.async_turn_off() - else: - await self.async_set_speed(speed) + if percentage is not None: + await self.async_set_percentage(percentage) else: await self._hub.bond.action(self._device.device_id, Action.turn_on()) diff --git a/tests/components/bond/test_fan.py b/tests/components/bond/test_fan.py index 1adea282a30..49a6e4a5b68 100644 --- a/tests/components/bond/test_fan.py +++ b/tests/components/bond/test_fan.py @@ -41,12 +41,17 @@ def ceiling_fan(name: str): async def turn_fan_on( - hass: core.HomeAssistant, fan_id: str, speed: Optional[str] = None + hass: core.HomeAssistant, + fan_id: str, + speed: Optional[str] = None, + percentage: Optional[int] = None, ) -> None: """Turn the fan on at the specified speed.""" service_data = {ATTR_ENTITY_ID: fan_id} if speed: service_data[fan.ATTR_SPEED] = speed + if percentage: + service_data[fan.ATTR_PERCENTAGE] = percentage await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, @@ -93,13 +98,13 @@ async def test_non_standard_speed_list(hass: core.HomeAssistant): with patch_bond_action() as mock_set_speed_low: await turn_fan_on(hass, "fan.name_1", fan.SPEED_LOW) mock_set_speed_low.assert_called_once_with( - "test-device-id", Action.set_speed(1) + "test-device-id", Action.set_speed(2) ) with patch_bond_action() as mock_set_speed_medium: await turn_fan_on(hass, "fan.name_1", fan.SPEED_MEDIUM) mock_set_speed_medium.assert_called_once_with( - "test-device-id", Action.set_speed(3) + "test-device-id", Action.set_speed(4) ) with patch_bond_action() as mock_set_speed_high: @@ -135,6 +140,58 @@ async def test_turn_on_fan_with_speed(hass: core.HomeAssistant): mock_set_speed.assert_called_with("test-device-id", Action.set_speed(1)) +async def test_turn_on_fan_with_percentage_3_speeds(hass: core.HomeAssistant): + """Tests that turn on command delegates to set speed API.""" + await setup_platform( + hass, FAN_DOMAIN, ceiling_fan("name-1"), bond_device_id="test-device-id" + ) + + with patch_bond_action() as mock_set_speed, patch_bond_device_state(): + await turn_fan_on(hass, "fan.name_1", percentage=10) + + mock_set_speed.assert_called_with("test-device-id", Action.set_speed(1)) + + mock_set_speed.reset_mock() + with patch_bond_action() as mock_set_speed, patch_bond_device_state(): + await turn_fan_on(hass, "fan.name_1", percentage=50) + + mock_set_speed.assert_called_with("test-device-id", Action.set_speed(2)) + + mock_set_speed.reset_mock() + with patch_bond_action() as mock_set_speed, patch_bond_device_state(): + await turn_fan_on(hass, "fan.name_1", percentage=100) + + mock_set_speed.assert_called_with("test-device-id", Action.set_speed(3)) + + +async def test_turn_on_fan_with_percentage_6_speeds(hass: core.HomeAssistant): + """Tests that turn on command delegates to set speed API.""" + await setup_platform( + hass, + FAN_DOMAIN, + ceiling_fan("name-1"), + bond_device_id="test-device-id", + props={"max_speed": 6}, + ) + + with patch_bond_action() as mock_set_speed, patch_bond_device_state(): + await turn_fan_on(hass, "fan.name_1", percentage=10) + + mock_set_speed.assert_called_with("test-device-id", Action.set_speed(1)) + + mock_set_speed.reset_mock() + with patch_bond_action() as mock_set_speed, patch_bond_device_state(): + await turn_fan_on(hass, "fan.name_1", percentage=50) + + mock_set_speed.assert_called_with("test-device-id", Action.set_speed(3)) + + mock_set_speed.reset_mock() + with patch_bond_action() as mock_set_speed, patch_bond_device_state(): + await turn_fan_on(hass, "fan.name_1", percentage=100) + + mock_set_speed.assert_called_with("test-device-id", Action.set_speed(6)) + + async def test_turn_on_fan_without_speed(hass: core.HomeAssistant): """Tests that turn on command delegates to turn on API.""" await setup_platform( From ee592350b3b1af736a460a9400fffc97dc6eaf49 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jan 2021 03:23:55 -0600 Subject: [PATCH 013/796] Update isy994 to use new fan entity model (#45536) --- homeassistant/components/isy994/fan.py | 77 +++++++++----------------- 1 file changed, 27 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index c94c2f607bd..74ed477d3a7 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -1,36 +1,22 @@ """Support for ISY994 fans.""" +import math from typing import Callable from pyisy.constants import ISY_VALUE_UNKNOWN -from homeassistant.components.fan import ( - DOMAIN as FAN, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, - SUPPORT_SET_SPEED, - FanEntity, -) +from homeassistant.components.fan import DOMAIN as FAN, SUPPORT_SET_SPEED, FanEntity from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS from .entity import ISYNodeEntity, ISYProgramEntity from .helpers import migrate_old_unique_ids -VALUE_TO_STATE = { - 0: SPEED_OFF, - 63: SPEED_LOW, - 64: SPEED_LOW, - 190: SPEED_MEDIUM, - 191: SPEED_MEDIUM, - 255: SPEED_HIGH, -} - -STATE_TO_VALUE = {} -for key in VALUE_TO_STATE: - STATE_TO_VALUE[VALUE_TO_STATE[key]] = key +SPEED_RANGE = (1, 255) # off is not included async def async_setup_entry( @@ -56,9 +42,11 @@ class ISYFanEntity(ISYNodeEntity, FanEntity): """Representation of an ISY994 fan device.""" @property - def speed(self) -> str: - """Return the current speed.""" - return VALUE_TO_STATE.get(self._node.status) + def percentage(self) -> str: + """Return the current speed percentage.""" + if self._node.status == ISY_VALUE_UNKNOWN: + return None + return ranged_value_to_percentage(SPEED_RANGE, self._node.status) @property def is_on(self) -> bool: @@ -67,17 +55,16 @@ class ISYFanEntity(ISYNodeEntity, FanEntity): return None return self._node.status != 0 - def set_speed(self, speed: str) -> None: - """Send the set speed command to the ISY994 fan device.""" - self._node.turn_on(val=STATE_TO_VALUE.get(speed, 255)) + def set_percentage(self, percentage: int) -> None: + """Set node to speed percentage for the ISY994 fan device.""" + if percentage == 0: + self._node.turn_off() + return + + isy_speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) + + self._node.turn_on(val=isy_speed) - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # def turn_on( self, speed: str = None, @@ -86,17 +73,12 @@ class ISYFanEntity(ISYNodeEntity, FanEntity): **kwargs, ) -> None: """Send the turn on command to the ISY994 fan device.""" - self.set_speed(speed) + self.set_percentage(percentage) def turn_off(self, **kwargs) -> None: """Send the turn off command to the ISY994 fan device.""" self._node.turn_off() - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - @property def supported_features(self) -> int: """Flag supported features.""" @@ -107,9 +89,11 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity): """Representation of an ISY994 fan program.""" @property - def speed(self) -> str: - """Return the current speed.""" - return VALUE_TO_STATE.get(self._node.status) + def percentage(self) -> str: + """Return the current speed percentage.""" + if self._node.status == ISY_VALUE_UNKNOWN: + return None + return ranged_value_to_percentage(SPEED_RANGE, self._node.status) @property def is_on(self) -> bool: @@ -121,13 +105,6 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity): if not self._actions.run_then(): _LOGGER.error("Unable to turn off the fan") - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # def turn_on( self, speed: str = None, From 22e44e4ba4a8d0baae193aa166c3414a0f5cc770 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jan 2021 03:35:01 -0600 Subject: [PATCH 014/796] Update zwave to use new fan entity model (#45541) Co-authored-by: Paulus Schoutsen --- homeassistant/components/zwave/fan.py | 61 +++++++++------------------ tests/components/zwave/test_fan.py | 8 ++-- 2 files changed, 25 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/zwave/fan.py b/homeassistant/components/zwave/fan.py index 1827996241d..ea529ccd90b 100644 --- a/homeassistant/components/zwave/fan.py +++ b/homeassistant/components/zwave/fan.py @@ -1,28 +1,19 @@ """Support for Z-Wave fans.""" import math -from homeassistant.components.fan import ( - DOMAIN, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, - SUPPORT_SET_SPEED, - FanEntity, -) +from homeassistant.components.fan import DOMAIN, SUPPORT_SET_SPEED, FanEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) from . import ZWaveDeviceEntity -SPEED_LIST = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - SUPPORTED_FEATURES = SUPPORT_SET_SPEED -# Value will first be divided to an integer -VALUE_TO_SPEED = {0: SPEED_OFF, 1: SPEED_LOW, 2: SPEED_MEDIUM, 3: SPEED_HIGH} - -SPEED_TO_VALUE = {SPEED_OFF: 0, SPEED_LOW: 1, SPEED_MEDIUM: 50, SPEED_HIGH: 99} +SPEED_RANGE = (1, 99) # off is not included async def async_setup_entry(hass, config_entry, async_add_entities): @@ -51,41 +42,31 @@ class ZwaveFan(ZWaveDeviceEntity, FanEntity): def update_properties(self): """Handle data changes for node values.""" - value = math.ceil(self.values.primary.data * 3 / 100) - self._state = VALUE_TO_SPEED[value] + self._state = self.values.primary.data - def set_speed(self, speed): - """Set the speed of the fan.""" - self.node.set_dimmer(self.values.primary.value_id, SPEED_TO_VALUE[speed]) + def set_percentage(self, percentage): + """Set the speed percentage of the fan.""" + if percentage is None: + # Value 255 tells device to return to previous value + zwave_speed = 255 + elif percentage == 0: + zwave_speed = 0 + else: + zwave_speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) + self.node.set_dimmer(self.values.primary.value_id, zwave_speed) - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # def turn_on(self, speed=None, percentage=None, preset_mode=None, **kwargs): """Turn the device on.""" - if speed is None: - # Value 255 tells device to return to previous value - self.node.set_dimmer(self.values.primary.value_id, 255) - else: - self.set_speed(speed) + self.set_percentage(percentage) def turn_off(self, **kwargs): """Turn the device off.""" self.node.set_dimmer(self.values.primary.value_id, 0) @property - def speed(self): - """Return the current speed.""" - return self._state - - @property - def speed_list(self): - """Get the list of available speeds.""" - return SPEED_LIST + def percentage(self): + """Return the current speed percentage.""" + return ranged_value_to_percentage(SPEED_RANGE, self._state) @property def supported_features(self): diff --git a/tests/components/zwave/test_fan.py b/tests/components/zwave/test_fan.py index e5dac881ba2..18188cefcd6 100644 --- a/tests/components/zwave/test_fan.py +++ b/tests/components/zwave/test_fan.py @@ -39,7 +39,7 @@ def test_fan_turn_on(mock_openzwave): node.reset_mock() - device.turn_on(speed=SPEED_OFF) + device.turn_on(percentage=0) assert node.set_dimmer.called value_id, brightness = node.set_dimmer.mock_calls[0][1] @@ -49,7 +49,7 @@ def test_fan_turn_on(mock_openzwave): node.reset_mock() - device.turn_on(speed=SPEED_LOW) + device.turn_on(percentage=1) assert node.set_dimmer.called value_id, brightness = node.set_dimmer.mock_calls[0][1] @@ -59,7 +59,7 @@ def test_fan_turn_on(mock_openzwave): node.reset_mock() - device.turn_on(speed=SPEED_MEDIUM) + device.turn_on(percentage=50) assert node.set_dimmer.called value_id, brightness = node.set_dimmer.mock_calls[0][1] @@ -69,7 +69,7 @@ def test_fan_turn_on(mock_openzwave): node.reset_mock() - device.turn_on(speed=SPEED_HIGH) + device.turn_on(percentage=100) assert node.set_dimmer.called value_id, brightness = node.set_dimmer.mock_calls[0][1] From babfef829d866acf2c7e81e0d855feb2f106d1ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jan 2021 03:44:36 -0600 Subject: [PATCH 015/796] Add support for percentage speeds and preset modes to template fan (#45478) --- homeassistant/components/template/fan.py | 218 +++++++++++++-- tests/components/template/test_fan.py | 323 ++++++++++++++++++++--- 2 files changed, 493 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index fe32d095677..5d01790f21a 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -6,6 +6,8 @@ import voluptuous as vol from homeassistant.components.fan import ( ATTR_DIRECTION, ATTR_OSCILLATING, + ATTR_PERCENTAGE, + ATTR_PRESET_MODE, ATTR_SPEED, DIRECTION_FORWARD, DIRECTION_REVERSE, @@ -13,10 +15,12 @@ from homeassistant.components.fan import ( SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, + SPEED_OFF, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity, + preset_modes_from_speed_list, ) from homeassistant.const import ( CONF_ENTITY_ID, @@ -42,14 +46,19 @@ _LOGGER = logging.getLogger(__name__) CONF_FANS = "fans" CONF_SPEED_LIST = "speeds" +CONF_PRESET_MODES = "preset_modes" CONF_SPEED_TEMPLATE = "speed_template" +CONF_PERCENTAGE_TEMPLATE = "percentage_template" +CONF_PRESET_MODE_TEMPLATE = "preset_mode_template" CONF_OSCILLATING_TEMPLATE = "oscillating_template" CONF_DIRECTION_TEMPLATE = "direction_template" CONF_ON_ACTION = "turn_on" CONF_OFF_ACTION = "turn_off" +CONF_SET_PERCENTAGE_ACTION = "set_percentage" CONF_SET_SPEED_ACTION = "set_speed" CONF_SET_OSCILLATING_ACTION = "set_oscillating" CONF_SET_DIRECTION_ACTION = "set_direction" +CONF_SET_PRESET_MODE_ACTION = "set_preset_mode" _VALID_STATES = [STATE_ON, STATE_OFF] _VALID_OSC = [True, False] @@ -57,22 +66,31 @@ _VALID_DIRECTIONS = [DIRECTION_FORWARD, DIRECTION_REVERSE] FAN_SCHEMA = vol.All( cv.deprecated(CONF_ENTITY_ID), + cv.deprecated(CONF_SPEED_LIST), + cv.deprecated(CONF_SPEED_TEMPLATE), + cv.deprecated(CONF_SET_SPEED_ACTION), vol.Schema( { vol.Optional(CONF_FRIENDLY_NAME): cv.string, vol.Required(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_SPEED_TEMPLATE): cv.template, + vol.Optional(CONF_PERCENTAGE_TEMPLATE): cv.template, + vol.Optional(CONF_PRESET_MODE_TEMPLATE): cv.template, vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template, vol.Optional(CONF_DIRECTION_TEMPLATE): cv.template, vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_SPEED_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_SET_PERCENTAGE_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_SET_PRESET_MODE_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_DIRECTION_ACTION): cv.SCRIPT_SCHEMA, vol.Optional( - CONF_SPEED_LIST, default=[SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + CONF_SPEED_LIST, + default=[SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH], ): cv.ensure_list, + vol.Optional(CONF_PRESET_MODES): cv.ensure_list, vol.Optional(CONF_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_UNIQUE_ID): cv.string, } @@ -93,6 +111,8 @@ async def _async_create_entities(hass, config): state_template = device_config[CONF_VALUE_TEMPLATE] speed_template = device_config.get(CONF_SPEED_TEMPLATE) + percentage_template = device_config.get(CONF_PERCENTAGE_TEMPLATE) + preset_mode_template = device_config.get(CONF_PRESET_MODE_TEMPLATE) oscillating_template = device_config.get(CONF_OSCILLATING_TEMPLATE) direction_template = device_config.get(CONF_DIRECTION_TEMPLATE) availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) @@ -100,10 +120,13 @@ async def _async_create_entities(hass, config): on_action = device_config[CONF_ON_ACTION] off_action = device_config[CONF_OFF_ACTION] set_speed_action = device_config.get(CONF_SET_SPEED_ACTION) + set_percentage_action = device_config.get(CONF_SET_PERCENTAGE_ACTION) + set_preset_mode_action = device_config.get(CONF_SET_PRESET_MODE_ACTION) set_oscillating_action = device_config.get(CONF_SET_OSCILLATING_ACTION) set_direction_action = device_config.get(CONF_SET_DIRECTION_ACTION) speed_list = device_config[CONF_SPEED_LIST] + preset_modes = device_config.get(CONF_PRESET_MODES) unique_id = device_config.get(CONF_UNIQUE_ID) fans.append( @@ -113,15 +136,20 @@ async def _async_create_entities(hass, config): friendly_name, state_template, speed_template, + percentage_template, + preset_mode_template, oscillating_template, direction_template, availability_template, on_action, off_action, set_speed_action, + set_percentage_action, + set_preset_mode_action, set_oscillating_action, set_direction_action, speed_list, + preset_modes, unique_id, ) ) @@ -146,15 +174,20 @@ class TemplateFan(TemplateEntity, FanEntity): friendly_name, state_template, speed_template, + percentage_template, + preset_mode_template, oscillating_template, direction_template, availability_template, on_action, off_action, set_speed_action, + set_percentage_action, + set_preset_mode_action, set_oscillating_action, set_direction_action, speed_list, + preset_modes, unique_id, ): """Initialize the fan.""" @@ -167,6 +200,8 @@ class TemplateFan(TemplateEntity, FanEntity): self._template = state_template self._speed_template = speed_template + self._percentage_template = percentage_template + self._preset_mode_template = preset_mode_template self._oscillating_template = oscillating_template self._direction_template = direction_template self._supported_features = 0 @@ -182,6 +217,18 @@ class TemplateFan(TemplateEntity, FanEntity): hass, set_speed_action, friendly_name, domain ) + self._set_percentage_script = None + if set_percentage_action: + self._set_percentage_script = Script( + hass, set_percentage_action, friendly_name, domain + ) + + self._set_preset_mode_script = None + if set_preset_mode_action: + self._set_preset_mode_script = Script( + hass, set_preset_mode_action, friendly_name, domain + ) + self._set_oscillating_script = None if set_oscillating_action: self._set_oscillating_script = Script( @@ -196,10 +243,16 @@ class TemplateFan(TemplateEntity, FanEntity): self._state = STATE_OFF self._speed = None + self._percentage = None + self._preset_mode = None self._oscillating = None self._direction = None - if self._speed_template: + if ( + self._speed_template + or self._percentage_template + or self._preset_mode_template + ): self._supported_features |= SUPPORT_SET_SPEED if self._oscillating_template: self._supported_features |= SUPPORT_OSCILLATE @@ -211,6 +264,9 @@ class TemplateFan(TemplateEntity, FanEntity): # List of valid speeds self._speed_list = speed_list + # List of valid preset modes + self._preset_modes = preset_modes + @property def name(self): """Return the display name of this fan.""" @@ -231,6 +287,13 @@ class TemplateFan(TemplateEntity, FanEntity): """Get the list of available speeds.""" return self._speed_list + @property + def preset_modes(self) -> list: + """Get the list of available preset modes.""" + if self._preset_modes is not None: + return self._preset_modes + return preset_modes_from_speed_list(self._speed_list) + @property def is_on(self): """Return true if device is on.""" @@ -241,6 +304,16 @@ class TemplateFan(TemplateEntity, FanEntity): """Return the current speed.""" return self._speed + @property + def preset_mode(self): + """Return the current preset mode.""" + return self._preset_mode + + @property + def percentage(self): + """Return the current speed percentage.""" + return self._percentage + @property def oscillating(self): """Return the oscillation state.""" @@ -251,13 +324,6 @@ class TemplateFan(TemplateEntity, FanEntity): """Return the oscillation state.""" return self._direction - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # async def async_turn_on( self, speed: str = None, @@ -266,10 +332,21 @@ class TemplateFan(TemplateEntity, FanEntity): **kwargs, ) -> None: """Turn on the fan.""" - await self._on_script.async_run({ATTR_SPEED: speed}, context=self._context) + await self._on_script.async_run( + { + ATTR_SPEED: speed, + ATTR_PERCENTAGE: percentage, + ATTR_PRESET_MODE: preset_mode, + }, + context=self._context, + ) self._state = STATE_ON - if speed is not None: + if preset_mode is not None: + await self.async_set_preset_mode(preset_mode) + elif percentage is not None: + await self.async_set_percentage(percentage) + elif speed is not None: await self.async_set_speed(speed) # pylint: disable=arguments-differ @@ -280,17 +357,53 @@ class TemplateFan(TemplateEntity, FanEntity): async def async_set_speed(self, speed: str) -> None: """Set the speed of the fan.""" - if self._set_speed_script is None: + if speed not in self.speed_list: + _LOGGER.error( + "Received invalid speed: %s. Expected: %s", speed, self.speed_list + ) return - if speed in self._speed_list: - self._speed = speed + self._state = STATE_OFF if speed == SPEED_OFF else STATE_ON + self._speed = speed + self._preset_mode = None + self._percentage = self.speed_to_percentage(speed) + + if self._set_speed_script: await self._set_speed_script.async_run( - {ATTR_SPEED: speed}, context=self._context + {ATTR_SPEED: self._speed}, context=self._context ) - else: + + async def async_set_percentage(self, percentage: int) -> None: + """Set the percentage speed of the fan.""" + speed_list = self.speed_list + self._state = STATE_OFF if percentage == 0 else STATE_ON + self._speed = self.percentage_to_speed(percentage) if speed_list else None + self._percentage = percentage + self._preset_mode = None + + if self._set_percentage_script: + await self._set_percentage_script.async_run( + {ATTR_PERCENTAGE: self._percentage}, context=self._context + ) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset_mode of the fan.""" + if preset_mode not in self.preset_modes: _LOGGER.error( - "Received invalid speed: %s. Expected: %s", speed, self._speed_list + "Received invalid preset_mode: %s. Expected: %s", + preset_mode, + self.preset_modes, + ) + return + + self._state = STATE_ON + self._preset_mode = preset_mode + self._speed = preset_mode + self._percentage = None + + if self._set_preset_mode_script: + await self._set_preset_mode_script.async_run( + {ATTR_PRESET_MODE: self._preset_mode}, context=self._context ) async def async_oscillate(self, oscillating: bool) -> None: @@ -350,6 +463,22 @@ class TemplateFan(TemplateEntity, FanEntity): async def async_added_to_hass(self): """Register callbacks.""" self.add_template_attribute("_state", self._template, None, self._update_state) + if self._preset_mode_template is not None: + self.add_template_attribute( + "_preset_mode", + self._preset_mode_template, + None, + self._update_preset_mode, + none_on_template_error=True, + ) + if self._percentage_template is not None: + self.add_template_attribute( + "_percentage", + self._percentage_template, + None, + self._update_percentage, + none_on_template_error=True, + ) if self._speed_template is not None: self.add_template_attribute( "_speed", @@ -382,14 +511,69 @@ class TemplateFan(TemplateEntity, FanEntity): speed = str(speed) if speed in self._speed_list: + self._state = STATE_OFF if speed == SPEED_OFF else STATE_ON self._speed = speed + self._percentage = self.speed_to_percentage(speed) + self._preset_mode = speed if speed in self.preset_modes else None elif speed in [STATE_UNAVAILABLE, STATE_UNKNOWN]: self._speed = None + self._percentage = 0 + self._preset_mode = None else: _LOGGER.error( "Received invalid speed: %s. Expected: %s", speed, self._speed_list ) self._speed = None + self._percentage = 0 + self._preset_mode = None + + @callback + def _update_percentage(self, percentage): + # Validate percentage + try: + percentage = int(float(percentage)) + except ValueError: + _LOGGER.error("Received invalid percentage: %s", percentage) + self._speed = None + self._percentage = 0 + self._preset_mode = None + return + + if 0 <= percentage <= 100: + self._state = STATE_OFF if percentage == 0 else STATE_ON + self._percentage = percentage + if self._speed_list: + self._speed = self.percentage_to_speed(percentage) + self._preset_mode = None + else: + _LOGGER.error("Received invalid percentage: %s", percentage) + self._speed = None + self._percentage = 0 + self._preset_mode = None + + @callback + def _update_preset_mode(self, preset_mode): + # Validate preset mode + preset_mode = str(preset_mode) + + if preset_mode in self.preset_modes: + self._state = STATE_ON + self._speed = preset_mode + self._percentage = None + self._preset_mode = preset_mode + elif preset_mode in [STATE_UNAVAILABLE, STATE_UNKNOWN]: + self._speed = None + self._percentage = None + self._preset_mode = None + else: + _LOGGER.error( + "Received invalid preset_mode: %s. Expected: %s", + preset_mode, + self.preset_mode, + ) + self._speed = None + self._percentage = None + self._preset_mode = None @callback def _update_oscillating(self, oscillating): diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index 8e7c519def9..b3927ad3118 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -6,12 +6,15 @@ from homeassistant import setup from homeassistant.components.fan import ( ATTR_DIRECTION, ATTR_OSCILLATING, + ATTR_PERCENTAGE, + ATTR_PRESET_MODE, ATTR_SPEED, DIRECTION_FORWARD, DIRECTION_REVERSE, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, + SPEED_OFF, ) from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE @@ -25,6 +28,10 @@ _STATE_INPUT_BOOLEAN = "input_boolean.state" _STATE_AVAILABILITY_BOOLEAN = "availability_boolean.state" # Represent for fan's speed _SPEED_INPUT_SELECT = "input_select.speed" +# Represent for fan's preset mode +_PRESET_MODE_INPUT_SELECT = "input_select.preset_mode" +# Represent for fan's speed percentage +_PERCENTAGE_INPUT_NUMBER = "input_number.percentage" # Represent for fan's oscillating _OSC_INPUT = "input_select.osc" # Represent for fan's direction @@ -62,7 +69,7 @@ async def test_missing_optional_config(hass, calls): await hass.async_start() await hass.async_block_till_done() - _verify(hass, STATE_ON, None, None, None) + _verify(hass, STATE_ON, None, None, None, None, None) async def test_missing_value_template_config(hass, calls): @@ -191,9 +198,15 @@ async def test_templates_with_entities(hass, calls): "fans": { "test_fan": { "value_template": value_template, + "percentage_template": "{{ states('input_number.percentage') }}", "speed_template": "{{ states('input_select.speed') }}", + "preset_mode_template": "{{ states('input_select.preset_mode') }}", "oscillating_template": "{{ states('input_select.osc') }}", "direction_template": "{{ states('input_select.direction') }}", + "set_percentage": { + "service": "script.fans_set_speed", + "data_template": {"percentage": "{{ percentage }}"}, + }, "turn_on": {"service": "script.fan_on"}, "turn_off": {"service": "script.fan_off"}, } @@ -206,7 +219,7 @@ async def test_templates_with_entities(hass, calls): await hass.async_start() await hass.async_block_till_done() - _verify(hass, STATE_OFF, None, None, None) + _verify(hass, STATE_OFF, None, 0, None, None, None) hass.states.async_set(_STATE_INPUT_BOOLEAN, True) hass.states.async_set(_SPEED_INPUT_SELECT, SPEED_MEDIUM) @@ -214,7 +227,128 @@ async def test_templates_with_entities(hass, calls): hass.states.async_set(_DIRECTION_INPUT_SELECT, DIRECTION_FORWARD) await hass.async_block_till_done() - _verify(hass, STATE_ON, SPEED_MEDIUM, True, DIRECTION_FORWARD) + _verify(hass, STATE_ON, SPEED_MEDIUM, 66, True, DIRECTION_FORWARD, None) + + hass.states.async_set(_PERCENTAGE_INPUT_NUMBER, 33) + await hass.async_block_till_done() + _verify(hass, STATE_ON, SPEED_LOW, 33, True, DIRECTION_FORWARD, None) + + hass.states.async_set(_PERCENTAGE_INPUT_NUMBER, 66) + await hass.async_block_till_done() + _verify(hass, STATE_ON, SPEED_MEDIUM, 66, True, DIRECTION_FORWARD, None) + + hass.states.async_set(_PERCENTAGE_INPUT_NUMBER, 100) + await hass.async_block_till_done() + _verify(hass, STATE_ON, SPEED_HIGH, 100, True, DIRECTION_FORWARD, None) + + hass.states.async_set(_PERCENTAGE_INPUT_NUMBER, "dog") + await hass.async_block_till_done() + _verify(hass, STATE_ON, None, 0, True, DIRECTION_FORWARD, None) + + +async def test_templates_with_entities_and_invalid_percentage(hass, calls): + """Test templates with values from other entities.""" + hass.states.async_set("sensor.percentage", "0") + + with assert_setup_component(1, "fan"): + assert await setup.async_setup_component( + hass, + "fan", + { + "fan": { + "platform": "template", + "fans": { + "test_fan": { + "value_template": "{{ 'on' }}", + "percentage_template": "{{ states('sensor.percentage') }}", + "turn_on": {"service": "script.fan_on"}, + "turn_off": {"service": "script.fan_off"}, + }, + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + _verify(hass, STATE_OFF, SPEED_OFF, 0, None, None, None) + + hass.states.async_set("sensor.percentage", "33") + await hass.async_block_till_done() + + _verify(hass, STATE_ON, SPEED_LOW, 33, None, None, None) + + hass.states.async_set("sensor.percentage", "invalid") + await hass.async_block_till_done() + + _verify(hass, STATE_ON, None, 0, None, None, None) + + hass.states.async_set("sensor.percentage", "5000") + await hass.async_block_till_done() + + _verify(hass, STATE_ON, None, 0, None, None, None) + + hass.states.async_set("sensor.percentage", "100") + await hass.async_block_till_done() + + _verify(hass, STATE_ON, SPEED_HIGH, 100, None, None, None) + + hass.states.async_set("sensor.percentage", "0") + await hass.async_block_till_done() + + _verify(hass, STATE_OFF, SPEED_OFF, 0, None, None, None) + + +async def test_templates_with_entities_and_preset_modes(hass, calls): + """Test templates with values from other entities.""" + hass.states.async_set("sensor.preset_mode", "0") + + with assert_setup_component(1, "fan"): + assert await setup.async_setup_component( + hass, + "fan", + { + "fan": { + "platform": "template", + "fans": { + "test_fan": { + "value_template": "{{ 'on' }}", + "preset_modes": ["auto", "smart"], + "preset_mode_template": "{{ states('sensor.preset_mode') }}", + "turn_on": {"service": "script.fan_on"}, + "turn_off": {"service": "script.fan_off"}, + }, + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + _verify(hass, STATE_ON, None, None, None, None, None) + + hass.states.async_set("sensor.preset_mode", "invalid") + await hass.async_block_till_done() + + _verify(hass, STATE_ON, None, None, None, None, None) + + hass.states.async_set("sensor.preset_mode", "auto") + await hass.async_block_till_done() + + _verify(hass, STATE_ON, "auto", None, None, None, "auto") + + hass.states.async_set("sensor.preset_mode", "smart") + await hass.async_block_till_done() + + _verify(hass, STATE_ON, "smart", None, None, None, "smart") + + hass.states.async_set("sensor.preset_mode", "invalid") + await hass.async_block_till_done() + _verify(hass, STATE_ON, None, None, None, None, None) async def test_template_with_unavailable_entities(hass, calls): @@ -272,7 +406,7 @@ async def test_template_with_unavailable_parameters(hass, calls): await hass.async_start() await hass.async_block_till_done() - _verify(hass, STATE_ON, None, None, None) + _verify(hass, STATE_ON, None, 0, None, None, None) async def test_availability_template_with_entities(hass, calls): @@ -346,7 +480,7 @@ async def test_templates_with_valid_values(hass, calls): await hass.async_start() await hass.async_block_till_done() - _verify(hass, STATE_ON, SPEED_MEDIUM, True, DIRECTION_FORWARD) + _verify(hass, STATE_ON, SPEED_MEDIUM, 66, True, DIRECTION_FORWARD, None) async def test_templates_invalid_values(hass, calls): @@ -376,7 +510,7 @@ async def test_templates_invalid_values(hass, calls): await hass.async_start() await hass.async_block_till_done() - _verify(hass, STATE_OFF, None, None, None) + _verify(hass, STATE_OFF, None, 0, None, None, None) async def test_invalid_availability_template_keeps_component_available(hass, caplog): @@ -394,6 +528,7 @@ async def test_invalid_availability_template_keeps_component_available(hass, cap "value_template": "{{ 'on' }}", "availability_template": "{{ x - 12 }}", "speed_template": "{{ states('input_select.speed') }}", + "preset_mode_template": "{{ states('input_select.preset_mode') }}", "oscillating_template": "{{ states('input_select.osc') }}", "direction_template": "{{ states('input_select.direction') }}", "turn_on": {"service": "script.fan_on"}, @@ -427,14 +562,14 @@ async def test_on_off(hass, calls): # verify assert hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_ON - _verify(hass, STATE_ON, None, None, None) + _verify(hass, STATE_ON, None, 0, None, None, None) # Turn off fan await common.async_turn_off(hass, _TEST_FAN) # verify assert hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_OFF - _verify(hass, STATE_OFF, None, None, None) + _verify(hass, STATE_OFF, None, 0, None, None, None) async def test_on_with_speed(hass, calls): @@ -446,13 +581,13 @@ async def test_on_with_speed(hass, calls): # verify assert hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_ON - assert hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH - _verify(hass, STATE_ON, SPEED_HIGH, None, None) + assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 100 + _verify(hass, STATE_ON, SPEED_HIGH, 100, None, None, None) async def test_set_speed(hass, calls): """Test set valid speed.""" - await _register_components(hass) + await _register_components(hass, preset_modes=["auto", "smart"]) # Turn on fan await common.async_turn_on(hass, _TEST_FAN) @@ -462,14 +597,55 @@ async def test_set_speed(hass, calls): # verify assert hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH - _verify(hass, STATE_ON, SPEED_HIGH, None, None) + _verify(hass, STATE_ON, SPEED_HIGH, 100, None, None, None) # Set fan's speed to medium await common.async_set_speed(hass, _TEST_FAN, SPEED_MEDIUM) # verify assert hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_MEDIUM - _verify(hass, STATE_ON, SPEED_MEDIUM, None, None) + _verify(hass, STATE_ON, SPEED_MEDIUM, 66, None, None, None) + + # Set fan's speed to off + await common.async_set_speed(hass, _TEST_FAN, SPEED_OFF) + + # verify + assert hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_OFF + _verify(hass, STATE_OFF, SPEED_OFF, 0, None, None, None) + + +async def test_set_percentage(hass, calls): + """Test set valid speed percentage.""" + await _register_components(hass) + + # Turn on fan + await common.async_turn_on(hass, _TEST_FAN) + + # Set fan's percentage speed to 100 + await common.async_set_percentage(hass, _TEST_FAN, 100) + + # verify + assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 100 + + _verify(hass, STATE_ON, SPEED_HIGH, 100, None, None, None) + + # Set fan's percentage speed to 66 + await common.async_set_percentage(hass, _TEST_FAN, 66) + assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 66 + + _verify(hass, STATE_ON, SPEED_MEDIUM, 66, None, None, None) + + # Set fan's percentage speed to 0 + await common.async_set_percentage(hass, _TEST_FAN, 0) + assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 0 + + _verify(hass, STATE_OFF, SPEED_OFF, 0, None, None, None) + + # Set fan's percentage speed to 50 + await common.async_turn_on(hass, _TEST_FAN, percentage=50) + assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 50 + + _verify(hass, STATE_ON, SPEED_MEDIUM, 50, None, None, None) async def test_set_invalid_speed_from_initial_stage(hass, calls): @@ -484,7 +660,7 @@ async def test_set_invalid_speed_from_initial_stage(hass, calls): # verify speed is unchanged assert hass.states.get(_SPEED_INPUT_SELECT).state == "" - _verify(hass, STATE_ON, None, None, None) + _verify(hass, STATE_ON, None, 0, None, None, None) async def test_set_invalid_speed(hass, calls): @@ -499,14 +675,14 @@ async def test_set_invalid_speed(hass, calls): # verify assert hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH - _verify(hass, STATE_ON, SPEED_HIGH, None, None) + _verify(hass, STATE_ON, SPEED_HIGH, 100, None, None, None) # Set fan's speed to 'invalid' await common.async_set_speed(hass, _TEST_FAN, "invalid") # verify speed is unchanged assert hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH - _verify(hass, STATE_ON, SPEED_HIGH, None, None) + _verify(hass, STATE_ON, SPEED_HIGH, 100, None, None, None) async def test_custom_speed_list(hass, calls): @@ -521,14 +697,48 @@ async def test_custom_speed_list(hass, calls): # verify assert hass.states.get(_SPEED_INPUT_SELECT).state == "1" - _verify(hass, STATE_ON, "1", None, None) + _verify(hass, STATE_ON, "1", 33, None, None, None) # Set fan's speed to 'medium' which is invalid await common.async_set_speed(hass, _TEST_FAN, SPEED_MEDIUM) # verify that speed is unchanged assert hass.states.get(_SPEED_INPUT_SELECT).state == "1" - _verify(hass, STATE_ON, "1", None, None) + _verify(hass, STATE_ON, "1", 33, None, None, None) + + +async def test_preset_modes(hass, calls): + """Test preset_modes.""" + await _register_components( + hass, ["off", "low", "medium", "high", "auto", "smart"], ["auto", "smart"] + ) + + # Turn on fan + await common.async_turn_on(hass, _TEST_FAN) + + # Set fan's preset_mode to "auto" + await common.async_set_preset_mode(hass, _TEST_FAN, "auto") + + # verify + assert hass.states.get(_PRESET_MODE_INPUT_SELECT).state == "auto" + + # Set fan's preset_mode to "smart" + await common.async_set_preset_mode(hass, _TEST_FAN, "smart") + + # Verify fan's preset_mode is "smart" + assert hass.states.get(_PRESET_MODE_INPUT_SELECT).state == "smart" + + # Set fan's preset_mode to "invalid" + await common.async_set_preset_mode(hass, _TEST_FAN, "invalid") + + # Verify fan's preset_mode is still "smart" + assert hass.states.get(_PRESET_MODE_INPUT_SELECT).state == "smart" + + # Set fan's preset_mode to "auto" + await common.async_turn_on(hass, _TEST_FAN, preset_mode="auto") + + # verify + assert hass.states.get(_PRESET_MODE_INPUT_SELECT).state == "auto" async def test_set_osc(hass, calls): @@ -543,14 +753,14 @@ async def test_set_osc(hass, calls): # verify assert hass.states.get(_OSC_INPUT).state == "True" - _verify(hass, STATE_ON, None, True, None) + _verify(hass, STATE_ON, None, 0, True, None, None) # Set fan's osc to False await common.async_oscillate(hass, _TEST_FAN, False) # verify assert hass.states.get(_OSC_INPUT).state == "False" - _verify(hass, STATE_ON, None, False, None) + _verify(hass, STATE_ON, None, 0, False, None, None) async def test_set_invalid_osc_from_initial_state(hass, calls): @@ -566,7 +776,7 @@ async def test_set_invalid_osc_from_initial_state(hass, calls): # verify assert hass.states.get(_OSC_INPUT).state == "" - _verify(hass, STATE_ON, None, None, None) + _verify(hass, STATE_ON, None, 0, None, None, None) async def test_set_invalid_osc(hass, calls): @@ -581,7 +791,7 @@ async def test_set_invalid_osc(hass, calls): # verify assert hass.states.get(_OSC_INPUT).state == "True" - _verify(hass, STATE_ON, None, True, None) + _verify(hass, STATE_ON, None, 0, True, None, None) # Set fan's osc to None with pytest.raises(vol.Invalid): @@ -589,7 +799,7 @@ async def test_set_invalid_osc(hass, calls): # verify osc is unchanged assert hass.states.get(_OSC_INPUT).state == "True" - _verify(hass, STATE_ON, None, True, None) + _verify(hass, STATE_ON, None, 0, True, None, None) async def test_set_direction(hass, calls): @@ -604,14 +814,14 @@ async def test_set_direction(hass, calls): # verify assert hass.states.get(_DIRECTION_INPUT_SELECT).state == DIRECTION_FORWARD - _verify(hass, STATE_ON, None, None, DIRECTION_FORWARD) + _verify(hass, STATE_ON, None, 0, None, DIRECTION_FORWARD, None) # Set fan's direction to reverse await common.async_set_direction(hass, _TEST_FAN, DIRECTION_REVERSE) # verify assert hass.states.get(_DIRECTION_INPUT_SELECT).state == DIRECTION_REVERSE - _verify(hass, STATE_ON, None, None, DIRECTION_REVERSE) + _verify(hass, STATE_ON, None, 0, None, DIRECTION_REVERSE, None) async def test_set_invalid_direction_from_initial_stage(hass, calls): @@ -626,7 +836,7 @@ async def test_set_invalid_direction_from_initial_stage(hass, calls): # verify direction is unchanged assert hass.states.get(_DIRECTION_INPUT_SELECT).state == "" - _verify(hass, STATE_ON, None, None, None) + _verify(hass, STATE_ON, None, 0, None, None, None) async def test_set_invalid_direction(hass, calls): @@ -641,36 +851,61 @@ async def test_set_invalid_direction(hass, calls): # verify assert hass.states.get(_DIRECTION_INPUT_SELECT).state == DIRECTION_FORWARD - _verify(hass, STATE_ON, None, None, DIRECTION_FORWARD) + _verify(hass, STATE_ON, None, 0, None, DIRECTION_FORWARD, None) # Set fan's direction to 'invalid' await common.async_set_direction(hass, _TEST_FAN, "invalid") # verify direction is unchanged assert hass.states.get(_DIRECTION_INPUT_SELECT).state == DIRECTION_FORWARD - _verify(hass, STATE_ON, None, None, DIRECTION_FORWARD) + _verify(hass, STATE_ON, None, 0, None, DIRECTION_FORWARD, None) def _verify( - hass, expected_state, expected_speed, expected_oscillating, expected_direction + hass, + expected_state, + expected_speed, + expected_percentage, + expected_oscillating, + expected_direction, + expected_preset_mode, ): """Verify fan's state, speed and osc.""" state = hass.states.get(_TEST_FAN) attributes = state.attributes assert state.state == str(expected_state) assert attributes.get(ATTR_SPEED) == expected_speed + assert attributes.get(ATTR_PERCENTAGE) == expected_percentage assert attributes.get(ATTR_OSCILLATING) == expected_oscillating assert attributes.get(ATTR_DIRECTION) == expected_direction + assert attributes.get(ATTR_PRESET_MODE) == expected_preset_mode -async def _register_components(hass, speed_list=None): +async def _register_components(hass, speed_list=None, preset_modes=None): """Register basic components for testing.""" with assert_setup_component(1, "input_boolean"): assert await setup.async_setup_component( hass, "input_boolean", {"input_boolean": {"state": None}} ) - with assert_setup_component(3, "input_select"): + with assert_setup_component(1, "input_number"): + assert await setup.async_setup_component( + hass, + "input_number", + { + "input_number": { + "percentage": { + "min": 0.0, + "max": 100.0, + "name": "Percentage", + "step": 1.0, + "mode": "slider", + } + } + }, + ) + + with assert_setup_component(4, "input_select"): assert await setup.async_setup_component( hass, "input_select", @@ -680,14 +915,21 @@ async def _register_components(hass, speed_list=None): "name": "Speed", "options": [ "", + SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, "1", "2", "3", + "auto", + "smart", ], }, + "preset_mode": { + "name": "Preset Mode", + "options": ["auto", "smart"], + }, "osc": {"name": "oscillating", "options": ["", "True", "False"]}, "direction": { "name": "Direction", @@ -709,6 +951,8 @@ async def _register_components(hass, speed_list=None): test_fan_config = { "value_template": value_template, "speed_template": "{{ states('input_select.speed') }}", + "preset_mode_template": "{{ states('input_select.preset_mode') }}", + "percentage_template": "{{ states('input_number.percentage') }}", "oscillating_template": "{{ states('input_select.osc') }}", "direction_template": "{{ states('input_select.direction') }}", "turn_on": { @@ -726,6 +970,20 @@ async def _register_components(hass, speed_list=None): "option": "{{ speed }}", }, }, + "set_preset_mode": { + "service": "input_select.select_option", + "data_template": { + "entity_id": _PRESET_MODE_INPUT_SELECT, + "option": "{{ preset_mode }}", + }, + }, + "set_percentage": { + "service": "input_number.set_value", + "data_template": { + "entity_id": _PERCENTAGE_INPUT_NUMBER, + "value": "{{ percentage }}", + }, + }, "set_oscillating": { "service": "input_select.select_option", "data_template": { @@ -745,6 +1003,9 @@ async def _register_components(hass, speed_list=None): if speed_list: test_fan_config["speeds"] = speed_list + if preset_modes: + test_fan_config["preset_modes"] = preset_modes + assert await setup.async_setup_component( hass, "fan", From ab1d42950a522abc098c3adc7024066b11cf4471 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jan 2021 04:43:43 -0600 Subject: [PATCH 016/796] Update homekit_controller to use new fan entity model (#45547) --- .../components/homekit_controller/fan.py | 54 +++----------- .../components/homekit_controller/test_fan.py | 70 ++++++++++++++++--- 2 files changed, 71 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index 828347e4b89..e2cdf7b3cfd 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -5,10 +5,6 @@ from aiohomekit.model.services import ServicesTypes from homeassistant.components.fan import ( DIRECTION_FORWARD, DIRECTION_REVERSE, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, @@ -26,13 +22,6 @@ DIRECTION_TO_HK = { } HK_DIRECTION_TO_HA = {v: k for (k, v) in DIRECTION_TO_HK.items()} -SPEED_TO_PCNT = { - SPEED_HIGH: 100, - SPEED_MEDIUM: 50, - SPEED_LOW: 25, - SPEED_OFF: 0, -} - class BaseHomeKitFan(HomeKitEntity, FanEntity): """Representation of a Homekit fan.""" @@ -56,30 +45,12 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): return self.service.value(self.on_characteristic) == 1 @property - def speed(self): - """Return the current speed.""" + def percentage(self): + """Return the current speed percentage.""" if not self.is_on: - return SPEED_OFF + return 0 - rotation_speed = self.service.value(CharacteristicsTypes.ROTATION_SPEED) - - if rotation_speed > SPEED_TO_PCNT[SPEED_MEDIUM]: - return SPEED_HIGH - - if rotation_speed > SPEED_TO_PCNT[SPEED_LOW]: - return SPEED_MEDIUM - - if rotation_speed > SPEED_TO_PCNT[SPEED_OFF]: - return SPEED_LOW - - return SPEED_OFF - - @property - def speed_list(self): - """Get the list of available speeds.""" - if self.supported_features & SUPPORT_SET_SPEED: - return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - return [] + return self.service.value(CharacteristicsTypes.ROTATION_SPEED) @property def current_direction(self): @@ -115,13 +86,13 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): {CharacteristicsTypes.ROTATION_DIRECTION: DIRECTION_TO_HK[direction]} ) - async def async_set_speed(self, speed): + async def async_set_percentage(self, percentage): """Set the speed of the fan.""" - if speed == SPEED_OFF: + if percentage == 0: return await self.async_turn_off() await self.async_put_characteristics( - {CharacteristicsTypes.ROTATION_SPEED: SPEED_TO_PCNT[speed]} + {CharacteristicsTypes.ROTATION_SPEED: percentage} ) async def async_oscillate(self, oscillating: bool): @@ -130,13 +101,6 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): {CharacteristicsTypes.SWING_MODE: 1 if oscillating else 0} ) - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # async def async_turn_on( self, speed=None, percentage=None, preset_mode=None, **kwargs ): @@ -146,8 +110,8 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): if not self.is_on: characteristics[self.on_characteristic] = True - if self.supported_features & SUPPORT_SET_SPEED and speed: - characteristics[CharacteristicsTypes.ROTATION_SPEED] = SPEED_TO_PCNT[speed] + if self.supported_features & SUPPORT_SET_SPEED: + characteristics[CharacteristicsTypes.ROTATION_SPEED] = percentage if characteristics: await self.async_put_characteristics(characteristics) diff --git a/tests/components/homekit_controller/test_fan.py b/tests/components/homekit_controller/test_fan.py index e9ebce4045b..b8d42b21643 100644 --- a/tests/components/homekit_controller/test_fan.py +++ b/tests/components/homekit_controller/test_fan.py @@ -83,7 +83,7 @@ async def test_turn_on(hass, utcnow): blocking=True, ) assert helper.characteristics[V1_ON].value == 1 - assert helper.characteristics[V1_ROTATION_SPEED].value == 50 + assert helper.characteristics[V1_ROTATION_SPEED].value == 66.0 await hass.services.async_call( "fan", @@ -92,7 +92,7 @@ async def test_turn_on(hass, utcnow): blocking=True, ) assert helper.characteristics[V1_ON].value == 1 - assert helper.characteristics[V1_ROTATION_SPEED].value == 25 + assert helper.characteristics[V1_ROTATION_SPEED].value == 33.0 async def test_turn_off(hass, utcnow): @@ -130,7 +130,7 @@ async def test_set_speed(hass, utcnow): {"entity_id": "fan.testdevice", "speed": "medium"}, blocking=True, ) - assert helper.characteristics[V1_ROTATION_SPEED].value == 50 + assert helper.characteristics[V1_ROTATION_SPEED].value == 66.0 await hass.services.async_call( "fan", @@ -138,7 +138,7 @@ async def test_set_speed(hass, utcnow): {"entity_id": "fan.testdevice", "speed": "low"}, blocking=True, ) - assert helper.characteristics[V1_ROTATION_SPEED].value == 25 + assert helper.characteristics[V1_ROTATION_SPEED].value == 33.0 await hass.services.async_call( "fan", @@ -149,6 +149,29 @@ async def test_set_speed(hass, utcnow): assert helper.characteristics[V1_ON].value == 0 +async def test_set_percentage(hass, utcnow): + """Test that we set fan speed by percentage.""" + helper = await setup_test_component(hass, create_fan_service) + + helper.characteristics[V1_ON].value = 1 + + await hass.services.async_call( + "fan", + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 66}, + blocking=True, + ) + assert helper.characteristics[V1_ROTATION_SPEED].value == 66 + + await hass.services.async_call( + "fan", + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 0}, + blocking=True, + ) + assert helper.characteristics[V1_ON].value == 0 + + async def test_speed_read(hass, utcnow): """Test that we can read a fans oscillation.""" helper = await setup_test_component(hass, create_fan_service) @@ -157,19 +180,23 @@ async def test_speed_read(hass, utcnow): helper.characteristics[V1_ROTATION_SPEED].value = 100 state = await helper.poll_and_get_state() assert state.attributes["speed"] == "high" + assert state.attributes["percentage"] == 100 helper.characteristics[V1_ROTATION_SPEED].value = 50 state = await helper.poll_and_get_state() assert state.attributes["speed"] == "medium" + assert state.attributes["percentage"] == 50 helper.characteristics[V1_ROTATION_SPEED].value = 25 state = await helper.poll_and_get_state() assert state.attributes["speed"] == "low" + assert state.attributes["percentage"] == 25 helper.characteristics[V1_ON].value = 0 helper.characteristics[V1_ROTATION_SPEED].value = 0 state = await helper.poll_and_get_state() assert state.attributes["speed"] == "off" + assert state.attributes["percentage"] == 0 async def test_set_direction(hass, utcnow): @@ -239,7 +266,7 @@ async def test_v2_turn_on(hass, utcnow): blocking=True, ) assert helper.characteristics[V2_ACTIVE].value == 1 - assert helper.characteristics[V2_ROTATION_SPEED].value == 50 + assert helper.characteristics[V2_ROTATION_SPEED].value == 66.0 await hass.services.async_call( "fan", @@ -248,7 +275,7 @@ async def test_v2_turn_on(hass, utcnow): blocking=True, ) assert helper.characteristics[V2_ACTIVE].value == 1 - assert helper.characteristics[V2_ROTATION_SPEED].value == 25 + assert helper.characteristics[V2_ROTATION_SPEED].value == 33.0 async def test_v2_turn_off(hass, utcnow): @@ -286,7 +313,7 @@ async def test_v2_set_speed(hass, utcnow): {"entity_id": "fan.testdevice", "speed": "medium"}, blocking=True, ) - assert helper.characteristics[V2_ROTATION_SPEED].value == 50 + assert helper.characteristics[V2_ROTATION_SPEED].value == 66 await hass.services.async_call( "fan", @@ -294,7 +321,7 @@ async def test_v2_set_speed(hass, utcnow): {"entity_id": "fan.testdevice", "speed": "low"}, blocking=True, ) - assert helper.characteristics[V2_ROTATION_SPEED].value == 25 + assert helper.characteristics[V2_ROTATION_SPEED].value == 33 await hass.services.async_call( "fan", @@ -305,6 +332,29 @@ async def test_v2_set_speed(hass, utcnow): assert helper.characteristics[V2_ACTIVE].value == 0 +async def test_v2_set_percentage(hass, utcnow): + """Test that we set fan speed by percentage.""" + helper = await setup_test_component(hass, create_fanv2_service) + + helper.characteristics[V2_ACTIVE].value = 1 + + await hass.services.async_call( + "fan", + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 66}, + blocking=True, + ) + assert helper.characteristics[V2_ROTATION_SPEED].value == 66 + + await hass.services.async_call( + "fan", + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 0}, + blocking=True, + ) + assert helper.characteristics[V2_ACTIVE].value == 0 + + async def test_v2_speed_read(hass, utcnow): """Test that we can read a fans oscillation.""" helper = await setup_test_component(hass, create_fanv2_service) @@ -313,19 +363,23 @@ async def test_v2_speed_read(hass, utcnow): helper.characteristics[V2_ROTATION_SPEED].value = 100 state = await helper.poll_and_get_state() assert state.attributes["speed"] == "high" + assert state.attributes["percentage"] == 100 helper.characteristics[V2_ROTATION_SPEED].value = 50 state = await helper.poll_and_get_state() assert state.attributes["speed"] == "medium" + assert state.attributes["percentage"] == 50 helper.characteristics[V2_ROTATION_SPEED].value = 25 state = await helper.poll_and_get_state() assert state.attributes["speed"] == "low" + assert state.attributes["percentage"] == 25 helper.characteristics[V2_ACTIVE].value = 0 helper.characteristics[V2_ROTATION_SPEED].value = 0 state = await helper.poll_and_get_state() assert state.attributes["speed"] == "off" + assert state.attributes["percentage"] == 0 async def test_v2_set_direction(hass, utcnow): From e4a7692610f67c00a039aa158fc861564b7f19e2 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 28 Jan 2021 12:05:09 +0100 Subject: [PATCH 017/796] Upgrade pyyaml to 5.4.1 (CVE-2020-14343) (#45624) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c7e181cac19..f51b0e00e14 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ pillow==8.1.0 pip>=8.0.3,<20.3 python-slugify==4.0.1 pytz>=2020.5 -pyyaml==5.3.1 +pyyaml==5.4.1 requests==2.25.1 ruamel.yaml==0.15.100 scapy==2.4.4 diff --git a/requirements.txt b/requirements.txt index a4e32888047..c973f4e4030 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ cryptography==3.2 pip>=8.0.3,<20.3 python-slugify==4.0.1 pytz>=2020.5 -pyyaml==5.3.1 +pyyaml==5.4.1 requests==2.25.1 ruamel.yaml==0.15.100 voluptuous==0.12.1 diff --git a/setup.py b/setup.py index 2b05d2ebb2e..7f77e3795b4 100755 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ REQUIRES = [ "pip>=8.0.3,<20.3", "python-slugify==4.0.1", "pytz>=2020.5", - "pyyaml==5.3.1", + "pyyaml==5.4.1", "requests==2.25.1", "ruamel.yaml==0.15.100", "voluptuous==0.12.1", From 38d2cacf7a8d5e7507a452e3d9adc825ea8a69fc Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 28 Jan 2021 12:06:20 +0100 Subject: [PATCH 018/796] Support blocking trusted network from new ip (#44630) Co-authored-by: Paulus Schoutsen --- homeassistant/auth/__init__.py | 53 ++++++++++- homeassistant/auth/auth_store.py | 25 +++-- homeassistant/auth/models.py | 5 + homeassistant/auth/providers/__init__.py | 16 +++- .../auth/providers/legacy_api_password.py | 22 +---- .../auth/providers/trusted_networks.py | 27 ++++-- homeassistant/components/auth/__init__.py | 50 +++++++--- homeassistant/components/http/auth.py | 4 +- homeassistant/components/http/const.py | 1 + homeassistant/components/onboarding/views.py | 17 +++- .../auth/mfa_modules/test_insecure_example.py | 2 +- tests/auth/mfa_modules/test_notify.py | 2 +- tests/auth/mfa_modules/test_totp.py | 2 +- tests/auth/providers/test_trusted_networks.py | 11 +++ tests/auth/test_auth_store.py | 7 ++ tests/auth/test_init.py | 93 ++++++++++++++----- tests/components/auth/test_init.py | 58 ++++++++++-- tests/components/config/test_auth.py | 8 +- .../test_auth_provider_homeassistant.py | 52 ++++------- tests/components/onboarding/test_views.py | 24 ++++- tests/conftest.py | 33 ++++++- 21 files changed, 381 insertions(+), 131 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index e36eb6800fa..531e36ff0b3 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -24,6 +24,14 @@ _ProviderKey = Tuple[str, Optional[str]] _ProviderDict = Dict[_ProviderKey, AuthProvider] +class InvalidAuthError(Exception): + """Raised when a authentication error occurs.""" + + +class InvalidProvider(Exception): + """Authentication provider not found.""" + + async def auth_manager_from_config( hass: HomeAssistant, provider_configs: List[Dict[str, Any]], @@ -96,7 +104,7 @@ class AuthManagerFlowManager(data_entry_flow.FlowManager): return result # we got final result - if isinstance(result["data"], models.User): + if isinstance(result["data"], models.Credentials): result["result"] = result["data"] return result @@ -120,11 +128,12 @@ class AuthManagerFlowManager(data_entry_flow.FlowManager): modules = await self.auth_manager.async_get_enabled_mfa(user) if modules: + flow.credential = credentials flow.user = user flow.available_mfa_modules = modules return await flow.async_step_select_mfa_module() - result["result"] = await self.auth_manager.async_get_or_create_user(credentials) + result["result"] = credentials return result @@ -156,7 +165,7 @@ class AuthManager: return list(self._mfa_modules.values()) def get_auth_provider( - self, provider_type: str, provider_id: str + self, provider_type: str, provider_id: Optional[str] ) -> Optional[AuthProvider]: """Return an auth provider, None if not found.""" return self._providers.get((provider_type, provider_id)) @@ -367,6 +376,7 @@ class AuthManager: client_icon: Optional[str] = None, token_type: Optional[str] = None, access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION, + credential: Optional[models.Credentials] = None, ) -> models.RefreshToken: """Create a new refresh token for a user.""" if not user.is_active: @@ -415,6 +425,7 @@ class AuthManager: client_icon, token_type, access_token_expiration, + credential, ) async def async_get_refresh_token( @@ -440,6 +451,8 @@ class AuthManager: self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None ) -> str: """Create a new access token.""" + self.async_validate_refresh_token(refresh_token, remote_ip) + self._store.async_log_refresh_token_usage(refresh_token, remote_ip) now = dt_util.utcnow() @@ -453,6 +466,40 @@ class AuthManager: algorithm="HS256", ).decode() + @callback + def _async_resolve_provider( + self, refresh_token: models.RefreshToken + ) -> Optional[AuthProvider]: + """Get the auth provider for the given refresh token. + + Raises an exception if the expected provider is no longer available or return + None if no provider was expected for this refresh token. + """ + if refresh_token.credential is None: + return None + + provider = self.get_auth_provider( + refresh_token.credential.auth_provider_type, + refresh_token.credential.auth_provider_id, + ) + if provider is None: + raise InvalidProvider( + f"Auth provider {refresh_token.credential.auth_provider_type}, {refresh_token.credential.auth_provider_id} not available" + ) + return provider + + @callback + def async_validate_refresh_token( + self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None + ) -> None: + """Validate that a refresh token is usable. + + Will raise InvalidAuthError on errors. + """ + provider = self._async_resolve_provider(refresh_token) + if provider: + provider.async_validate_refresh_token(refresh_token, remote_ip) + async def async_validate_access_token( self, token: str ) -> Optional[models.RefreshToken]: diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 57ec9ee63dc..724f1c86722 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -208,6 +208,7 @@ class AuthStore: client_icon: Optional[str] = None, token_type: str = models.TOKEN_TYPE_NORMAL, access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION, + credential: Optional[models.Credentials] = None, ) -> models.RefreshToken: """Create a new token for a user.""" kwargs: Dict[str, Any] = { @@ -215,6 +216,7 @@ class AuthStore: "client_id": client_id, "token_type": token_type, "access_token_expiration": access_token_expiration, + "credential": credential, } if client_name: kwargs["client_name"] = client_name @@ -309,6 +311,7 @@ class AuthStore: users: Dict[str, models.User] = OrderedDict() groups: Dict[str, models.Group] = OrderedDict() + credentials: Dict[str, models.Credentials] = OrderedDict() # Soft-migrating data as we load. We are going to make sure we have a # read only group and an admin group. There are two states that we can @@ -415,15 +418,15 @@ class AuthStore: ) for cred_dict in data["credentials"]: - users[cred_dict["user_id"]].credentials.append( - models.Credentials( - id=cred_dict["id"], - is_new=False, - auth_provider_type=cred_dict["auth_provider_type"], - auth_provider_id=cred_dict["auth_provider_id"], - data=cred_dict["data"], - ) + credential = models.Credentials( + id=cred_dict["id"], + is_new=False, + auth_provider_type=cred_dict["auth_provider_type"], + auth_provider_id=cred_dict["auth_provider_id"], + data=cred_dict["data"], ) + credentials[cred_dict["id"]] = credential + users[cred_dict["user_id"]].credentials.append(credential) for rt_dict in data["refresh_tokens"]: # Filter out the old keys that don't have jwt_key (pre-0.76) @@ -469,6 +472,8 @@ class AuthStore: jwt_key=rt_dict["jwt_key"], last_used_at=last_used_at, last_used_ip=rt_dict.get("last_used_ip"), + credential=credentials.get(rt_dict.get("credential_id")), + version=rt_dict.get("version"), ) users[rt_dict["user_id"]].refresh_tokens[token.id] = token @@ -542,6 +547,10 @@ class AuthStore: if refresh_token.last_used_at else None, "last_used_ip": refresh_token.last_used_ip, + "credential_id": refresh_token.credential.id + if refresh_token.credential + else None, + "version": refresh_token.version, } for user in self._users.values() for refresh_token in user.refresh_tokens.values() diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index 5a838cfc805..4cc67b2ebd4 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -6,6 +6,7 @@ import uuid import attr +from homeassistant.const import __version__ from homeassistant.util import dt as dt_util from . import permissions as perm_mdl @@ -106,6 +107,10 @@ class RefreshToken: last_used_at: Optional[datetime] = attr.ib(default=None) last_used_ip: Optional[str] = attr.ib(default=None) + credential: Optional["Credentials"] = attr.ib(default=None) + + version: Optional[str] = attr.ib(default=__version__) + @attr.s(slots=True) class Credentials: diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index 1fe59346b00..e766083edc3 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -16,7 +16,7 @@ from homeassistant.util.decorator import Registry from ..auth_store import AuthStore from ..const import MFA_SESSION_EXPIRATION -from ..models import Credentials, User, UserMeta +from ..models import Credentials, RefreshToken, User, UserMeta _LOGGER = logging.getLogger(__name__) DATA_REQS = "auth_prov_reqs_processed" @@ -117,6 +117,16 @@ class AuthProvider: async def async_initialize(self) -> None: """Initialize the auth provider.""" + @callback + def async_validate_refresh_token( + self, refresh_token: RefreshToken, remote_ip: Optional[str] = None + ) -> None: + """Verify a refresh token is still valid. + + Optional hook for an auth provider to verify validity of a refresh token. + Should raise InvalidAuthError on errors. + """ + async def auth_provider_from_config( hass: HomeAssistant, store: AuthStore, config: Dict[str, Any] @@ -182,6 +192,7 @@ class LoginFlow(data_entry_flow.FlowHandler): self.created_at = dt_util.utcnow() self.invalid_mfa_times = 0 self.user: Optional[User] = None + self.credential: Optional[Credentials] = None async def async_step_init( self, user_input: Optional[Dict[str, str]] = None @@ -222,6 +233,7 @@ class LoginFlow(data_entry_flow.FlowHandler): self, user_input: Optional[Dict[str, str]] = None ) -> Dict[str, Any]: """Handle the step of mfa validation.""" + assert self.credential assert self.user errors = {} @@ -257,7 +269,7 @@ class LoginFlow(data_entry_flow.FlowHandler): return self.async_abort(reason="too_many_retry") if not errors: - return await self.async_finish(self.user) + return await self.async_finish(self.credential) description_placeholders: Dict[str, Optional[str]] = { "mfa_module_name": auth_module.name, diff --git a/homeassistant/auth/providers/legacy_api_password.py b/homeassistant/auth/providers/legacy_api_password.py index 15ba1dfc14c..ba96fa285f1 100644 --- a/homeassistant/auth/providers/legacy_api_password.py +++ b/homeassistant/auth/providers/legacy_api_password.py @@ -8,13 +8,12 @@ from typing import Any, Dict, Optional, cast import voluptuous as vol -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow -from .. import AuthManager -from ..models import Credentials, User, UserMeta +from ..models import Credentials, UserMeta AUTH_PROVIDER_TYPE = "legacy_api_password" CONF_API_PASSWORD = "api_password" @@ -30,23 +29,6 @@ class InvalidAuthError(HomeAssistantError): """Raised when submitting invalid authentication.""" -async def async_validate_password(hass: HomeAssistant, password: str) -> Optional[User]: - """Return a user if password is valid. None if not.""" - auth = cast(AuthManager, hass.auth) # type: ignore - providers = auth.get_auth_providers(AUTH_PROVIDER_TYPE) - if not providers: - raise ValueError("Legacy API password provider not found") - - try: - provider = cast(LegacyApiPasswordAuthProvider, providers[0]) - provider.async_validate_login(password) - return await auth.async_get_or_create_user( - await provider.async_get_or_create_credentials({}) - ) - except InvalidAuthError: - return None - - @AUTH_PROVIDERS.register(AUTH_PROVIDER_TYPE) class LegacyApiPasswordAuthProvider(AuthProvider): """An auth provider support legacy api_password.""" diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index 0cf79c3cc95..2afdbf98196 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -3,7 +3,14 @@ It shows list of users if access from trusted network. Abort login flow if not access from trusted network. """ -from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network, ip_network +from ipaddress import ( + IPv4Address, + IPv4Network, + IPv6Address, + IPv6Network, + ip_address, + ip_network, +) from typing import Any, Dict, List, Optional, Union, cast import voluptuous as vol @@ -13,7 +20,8 @@ from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow -from ..models import Credentials, UserMeta +from .. import InvalidAuthError +from ..models import Credentials, RefreshToken, UserMeta IPAddress = Union[IPv4Address, IPv6Address] IPNetwork = Union[IPv4Network, IPv6Network] @@ -46,10 +54,6 @@ CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend( ) -class InvalidAuthError(HomeAssistantError): - """Raised when try to access from untrusted networks.""" - - class InvalidUserError(HomeAssistantError): """Raised when try to login as invalid user.""" @@ -163,6 +167,17 @@ class TrustedNetworksAuthProvider(AuthProvider): ): raise InvalidAuthError("Not in trusted_networks") + @callback + def async_validate_refresh_token( + self, refresh_token: RefreshToken, remote_ip: Optional[str] = None + ) -> None: + """Verify a refresh token is still valid.""" + if remote_ip is None: + raise InvalidAuthError( + "Unknown remote ip can't be used for trusted network provider." + ) + self.async_validate_access(ip_address(remote_ip)) + class TrustedNetworksLoginFlow(LoginFlow): """Handler for the login flow.""" diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 43451632f38..4ddf82cc022 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -115,11 +115,13 @@ Result will be a long-lived access token: """ from datetime import timedelta +from typing import Union import uuid from aiohttp import web import voluptuous as vol +from homeassistant.auth import InvalidAuthError from homeassistant.auth.models import ( TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, Credentials, @@ -180,9 +182,11 @@ RESULT_TYPE_USER = "user" @bind_hass -def create_auth_code(hass, client_id: str, user: User) -> str: +def create_auth_code( + hass, client_id: str, credential_or_user: Union[Credentials, User] +) -> str: """Create an authorization code to fetch tokens.""" - return hass.data[DOMAIN](client_id, user) + return hass.data[DOMAIN](client_id, credential_or_user) async def async_setup(hass, config): @@ -228,9 +232,9 @@ class TokenView(HomeAssistantView): requires_auth = False cors_allowed = True - def __init__(self, retrieve_user): + def __init__(self, retrieve_auth): """Initialize the token view.""" - self._retrieve_user = retrieve_user + self._retrieve_auth = retrieve_auth @log_invalid_auth async def post(self, request): @@ -293,16 +297,15 @@ class TokenView(HomeAssistantView): status_code=HTTP_BAD_REQUEST, ) - user = self._retrieve_user(client_id, RESULT_TYPE_USER, code) + credential = self._retrieve_auth(client_id, RESULT_TYPE_CREDENTIALS, code) - if user is None or not isinstance(user, User): + if credential is None or not isinstance(credential, Credentials): return self.json( {"error": "invalid_request", "error_description": "Invalid code"}, status_code=HTTP_BAD_REQUEST, ) - # refresh user - user = await hass.auth.async_get_user(user.id) + user = await hass.auth.async_get_or_create_user(credential) if not user.is_active: return self.json( @@ -310,8 +313,18 @@ class TokenView(HomeAssistantView): status_code=HTTP_FORBIDDEN, ) - refresh_token = await hass.auth.async_create_refresh_token(user, client_id) - access_token = hass.auth.async_create_access_token(refresh_token, remote_addr) + refresh_token = await hass.auth.async_create_refresh_token( + user, client_id, credential=credential + ) + try: + access_token = hass.auth.async_create_access_token( + refresh_token, remote_addr + ) + except InvalidAuthError as exc: + return self.json( + {"error": "access_denied", "error_description": str(exc)}, + status_code=HTTP_FORBIDDEN, + ) return self.json( { @@ -346,7 +359,15 @@ class TokenView(HomeAssistantView): if refresh_token.client_id != client_id: return self.json({"error": "invalid_request"}, status_code=HTTP_BAD_REQUEST) - access_token = hass.auth.async_create_access_token(refresh_token, remote_addr) + try: + access_token = hass.auth.async_create_access_token( + refresh_token, remote_addr + ) + except InvalidAuthError as exc: + return self.json( + {"error": "access_denied", "error_description": str(exc)}, + status_code=HTTP_FORBIDDEN, + ) return self.json( { @@ -482,7 +503,12 @@ async def websocket_create_long_lived_access_token( access_token_expiration=timedelta(days=msg["lifespan"]), ) - access_token = hass.auth.async_create_access_token(refresh_token) + try: + access_token = hass.auth.async_create_access_token(refresh_token) + except InvalidAuthError as exc: + return websocket_api.error_message( + msg["id"], websocket_api.const.ERR_UNAUTHORIZED, str(exc) + ) connection.send_message(websocket_api.result_message(msg["id"], access_token)) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index f9e6df94489..3267c9cc70e 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -9,7 +9,7 @@ import jwt from homeassistant.core import callback from homeassistant.util import dt as dt_util -from .const import KEY_AUTHENTICATED, KEY_HASS_USER +from .const import KEY_AUTHENTICATED, KEY_HASS_REFRESH_TOKEN_ID, KEY_HASS_USER # mypy: allow-untyped-defs, no-check-untyped-defs @@ -62,6 +62,7 @@ def setup_auth(hass, app): return False request[KEY_HASS_USER] = refresh_token.user + request[KEY_HASS_REFRESH_TOKEN_ID] = refresh_token.id return True async def async_validate_signed_request(request): @@ -92,6 +93,7 @@ def setup_auth(hass, app): return False request[KEY_HASS_USER] = refresh_token.user + request[KEY_HASS_REFRESH_TOKEN_ID] = refresh_token.id return True @middleware diff --git a/homeassistant/components/http/const.py b/homeassistant/components/http/const.py index ebbc6cb9b81..3a32635bb27 100644 --- a/homeassistant/components/http/const.py +++ b/homeassistant/components/http/const.py @@ -2,3 +2,4 @@ KEY_AUTHENTICATED = "ha_authenticated" KEY_HASS = "hass" KEY_HASS_USER = "hass_user" +KEY_HASS_REFRESH_TOKEN_ID = "hass_refresh_token_id" diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index 0faf099b9bf..1d5528688dd 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components.auth import indieauth +from homeassistant.components.http.const import KEY_HASS_REFRESH_TOKEN_ID from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView from homeassistant.const import HTTP_BAD_REQUEST, HTTP_FORBIDDEN @@ -132,7 +133,9 @@ class UserOnboardingView(_BaseOnboardingView): # Return authorization code for fetching tokens and connect # during onboarding. - auth_code = hass.components.auth.create_auth_code(data["client_id"], user) + auth_code = hass.components.auth.create_auth_code( + data["client_id"], credentials + ) return self.json({"auth_code": auth_code}) @@ -183,7 +186,7 @@ class IntegrationOnboardingView(_BaseOnboardingView): async def post(self, request, data): """Handle token creation.""" hass = request.app["hass"] - user = request["hass_user"] + refresh_token_id = request[KEY_HASS_REFRESH_TOKEN_ID] async with self._lock: if self._async_is_done(): @@ -201,8 +204,16 @@ class IntegrationOnboardingView(_BaseOnboardingView): "invalid client id or redirect uri", HTTP_BAD_REQUEST ) + refresh_token = await hass.auth.async_get_refresh_token(refresh_token_id) + if refresh_token is None or refresh_token.credential is None: + return self.json_message( + "Credentials for user not available", HTTP_FORBIDDEN + ) + # Return authorization code so we can redirect user and log them in - auth_code = hass.components.auth.create_auth_code(data["client_id"], user) + auth_code = hass.components.auth.create_auth_code( + data["client_id"], refresh_token.credential + ) return self.json({"auth_code": auth_code}) diff --git a/tests/auth/mfa_modules/test_insecure_example.py b/tests/auth/mfa_modules/test_insecure_example.py index 5384ebee4bd..035433986d4 100644 --- a/tests/auth/mfa_modules/test_insecure_example.py +++ b/tests/auth/mfa_modules/test_insecure_example.py @@ -131,7 +131,7 @@ async def test_login(hass): result["flow_id"], {"pin": "123456"} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"].id == "mock-user" + assert result["data"].id == "mock-id" async def test_setup_flow(hass): diff --git a/tests/auth/mfa_modules/test_notify.py b/tests/auth/mfa_modules/test_notify.py index c79d76baf4f..1d08ad70cc8 100644 --- a/tests/auth/mfa_modules/test_notify.py +++ b/tests/auth/mfa_modules/test_notify.py @@ -229,7 +229,7 @@ async def test_login_flow_validates_mfa(hass): result["flow_id"], {"code": MOCK_CODE} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"].id == "mock-user" + assert result["data"].id == "mock-id" async def test_setup_user_notify_service(hass): diff --git a/tests/auth/mfa_modules/test_totp.py b/tests/auth/mfa_modules/test_totp.py index d0a4f3cf3ac..2e4aad98066 100644 --- a/tests/auth/mfa_modules/test_totp.py +++ b/tests/auth/mfa_modules/test_totp.py @@ -127,7 +127,7 @@ async def test_login_flow_validates_mfa(hass): result["flow_id"], {"code": MOCK_CODE} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"].id == "mock-user" + assert result["data"].id == "mock-id" async def test_race_condition_in_data_loading(hass): diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index 3156c40f876..4ece4875ba4 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -1,5 +1,6 @@ """Test the Trusted Networks auth provider.""" from ipaddress import ip_address, ip_network +from unittest.mock import Mock, patch import pytest import voluptuous as vol @@ -142,6 +143,16 @@ async def test_validate_access(provider): provider.async_validate_access(ip_address("2001:db8::ff00:42:8329")) +async def test_validate_refresh_token(provider): + """Verify re-validation of refresh token.""" + with patch.object(provider, "async_validate_access") as mock: + with pytest.raises(tn_auth.InvalidAuthError): + provider.async_validate_refresh_token(Mock(), None) + + provider.async_validate_refresh_token(Mock(), "127.0.0.1") + mock.assert_called_once_with(ip_address("127.0.0.1")) + + async def test_login_flow(manager, provider): """Test login flow.""" owner = await manager.async_create_user("test-owner") diff --git a/tests/auth/test_auth_store.py b/tests/auth/test_auth_store.py index 4ab0fc4a360..0c650adba3c 100644 --- a/tests/auth/test_auth_store.py +++ b/tests/auth/test_auth_store.py @@ -37,6 +37,7 @@ async def test_loading_no_group_data_format(hass, hass_storage): "last_used_at": "2018-10-03T13:43:19.774712+00:00", "token": "some-token", "user_id": "user-id", + "version": "1.2.3", }, { "access_token_expiration": 1800.0, @@ -87,12 +88,14 @@ async def test_loading_no_group_data_format(hass, hass_storage): assert len(owner.refresh_tokens) == 1 owner_token = list(owner.refresh_tokens.values())[0] assert owner_token.id == "user-token-id" + assert owner_token.version == "1.2.3" assert system.system_generated is True assert system.groups == [] assert len(system.refresh_tokens) == 1 system_token = list(system.refresh_tokens.values())[0] assert system_token.id == "system-token-id" + assert system_token.version is None async def test_loading_all_access_group_data_format(hass, hass_storage): @@ -129,6 +132,7 @@ async def test_loading_all_access_group_data_format(hass, hass_storage): "last_used_at": "2018-10-03T13:43:19.774712+00:00", "token": "some-token", "user_id": "user-id", + "version": "1.2.3", }, { "access_token_expiration": 1800.0, @@ -139,6 +143,7 @@ async def test_loading_all_access_group_data_format(hass, hass_storage): "last_used_at": "2018-10-03T13:43:19.774712+00:00", "token": "some-token", "user_id": "system-id", + "version": None, }, { "access_token_expiration": 1800.0, @@ -179,12 +184,14 @@ async def test_loading_all_access_group_data_format(hass, hass_storage): assert len(owner.refresh_tokens) == 1 owner_token = list(owner.refresh_tokens.values())[0] assert owner_token.id == "user-token-id" + assert owner_token.version == "1.2.3" assert system.system_generated is True assert system.groups == [] assert len(system.refresh_tokens) == 1 system_token = list(system.refresh_tokens.values())[0] assert system_token.id == "system-token-id" + assert system_token.version is None async def test_loading_empty_data(hass, hass_storage): diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index edcd01d51e1..4f34ce1d595 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -7,7 +7,12 @@ import pytest import voluptuous as vol from homeassistant import auth, data_entry_flow -from homeassistant.auth import auth_store, const as auth_const, models as auth_models +from homeassistant.auth import ( + InvalidAuthError, + auth_store, + const as auth_const, + models as auth_models, +) from homeassistant.auth.const import MFA_SESSION_EXPIRATION from homeassistant.core import callback from homeassistant.util import dt as dt_util @@ -162,7 +167,10 @@ async def test_create_new_user(hass): step["flow_id"], {"username": "test-user", "password": "test-pass"} ) assert step["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - user = step["result"] + credential = step["result"] + assert credential is not None + + user = await manager.async_get_or_create_user(credential) assert user is not None assert user.is_owner is False assert user.name == "Test Name" @@ -229,7 +237,8 @@ async def test_login_as_existing_user(mock_hass): ) assert step["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - user = step["result"] + credential = step["result"] + user = await manager.async_get_user_by_credentials(credential) assert user is not None assert user.id == "mock-user" assert user.is_owner is False @@ -259,7 +268,8 @@ async def test_linking_user_to_two_auth_providers(hass, hass_storage): step = await manager.login_flow.async_configure( step["flow_id"], {"username": "test-user", "password": "test-pass"} ) - user = step["result"] + credential = step["result"] + user = await manager.async_get_or_create_user(credential) assert user is not None step = await manager.login_flow.async_init( @@ -293,13 +303,19 @@ async def test_saving_loading(hass, hass_storage): step = await manager.login_flow.async_configure( step["flow_id"], {"username": "test-user", "password": "test-pass"} ) - user = step["result"] + credential = step["result"] + user = await manager.async_get_or_create_user(credential) + await manager.async_activate_user(user) # the first refresh token will be used to create access token - refresh_token = await manager.async_create_refresh_token(user, CLIENT_ID) + refresh_token = await manager.async_create_refresh_token( + user, CLIENT_ID, credential=credential + ) manager.async_create_access_token(refresh_token, "192.168.0.1") # the second refresh token will not be used - await manager.async_create_refresh_token(user, "dummy-client") + await manager.async_create_refresh_token( + user, "dummy-client", credential=credential + ) await flush_store(manager._store._store) @@ -452,6 +468,46 @@ async def test_refresh_token_type_long_lived_access_token(hass): assert token.token_type == auth_models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN +async def test_refresh_token_provider_validation(mock_hass): + """Test that creating access token from refresh token checks with provider.""" + manager = await auth.auth_manager_from_config( + mock_hass, + [ + { + "type": "insecure_example", + "users": [{"username": "test-user", "password": "test-pass"}], + } + ], + [], + ) + + credential = auth_models.Credentials( + id="mock-credential-id", + auth_provider_type="insecure_example", + auth_provider_id=None, + data={"username": "test-user"}, + is_new=False, + ) + + user = MockUser().add_to_auth_manager(manager) + user.credentials.append(credential) + refresh_token = await manager.async_create_refresh_token( + user, CLIENT_ID, credential=credential + ) + ip = "127.0.0.1" + + assert manager.async_create_access_token(refresh_token, ip) is not None + + with patch( + "homeassistant.auth.providers.insecure_example.ExampleAuthProvider.async_validate_refresh_token", + side_effect=InvalidAuthError("Invalid access"), + ) as call: + with pytest.raises(InvalidAuthError): + manager.async_create_access_token(refresh_token, ip) + + call.assert_called_with(refresh_token, ip) + + async def test_cannot_deactive_owner(mock_hass): """Test that we cannot deactivate the owner.""" manager = await auth.auth_manager_from_config(mock_hass, [], []) @@ -626,14 +682,10 @@ async def test_login_with_auth_module(mock_hass): step["flow_id"], {"pin": "test-pin"} ) - # Finally passed, get user + # Finally passed, get credential assert step["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - user = step["result"] - assert user is not None - assert user.id == "mock-user" - assert user.is_owner is False - assert user.is_active is False - assert user.name == "Paulus" + assert step["result"] + assert step["result"].id == "mock-id" async def test_login_with_multi_auth_module(mock_hass): @@ -703,14 +755,10 @@ async def test_login_with_multi_auth_module(mock_hass): step["flow_id"], {"pin": "test-pin2"} ) - # Finally passed, get user + # Finally passed, get credential assert step["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - user = step["result"] - assert user is not None - assert user.id == "mock-user" - assert user.is_owner is False - assert user.is_active is False - assert user.name == "Paulus" + assert step["result"] + assert step["result"].id == "mock-id" async def test_auth_module_expired_session(mock_hass): @@ -792,7 +840,8 @@ async def test_enable_mfa_for_user(hass, hass_storage): step = await manager.login_flow.async_configure( step["flow_id"], {"username": "test-user", "password": "test-pass"} ) - user = step["result"] + credential = step["result"] + user = await manager.async_get_or_create_user(credential) assert user is not None # new user don't have mfa enabled diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 2c9a39c6fb6..207667fc26d 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -2,6 +2,7 @@ from datetime import timedelta from unittest.mock import patch +from homeassistant.auth import InvalidAuthError from homeassistant.auth.models import Credentials from homeassistant.components import auth from homeassistant.components.auth import RESULT_TYPE_USER @@ -13,6 +14,24 @@ from . import async_setup_auth from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI, MockUser +async def async_setup_user_refresh_token(hass): + """Create a testing user with a connected credential.""" + user = await hass.auth.async_create_user("Test User") + + credential = Credentials( + id="mock-credential-id", + auth_provider_type="insecure_example", + auth_provider_id=None, + data={"username": "test-user"}, + is_new=False, + ) + user.credentials.append(credential) + + return await hass.auth.async_create_refresh_token( + user, CLIENT_ID, credential=credential + ) + + async def test_login_new_user_and_trying_refresh_token(hass, aiohttp_client): """Test logging in with new user and refreshing tokens.""" client = await async_setup_auth(hass, aiohttp_client, setup_api=True) @@ -107,12 +126,6 @@ async def test_ws_current_user(hass, hass_ws_client, hass_access_token): refresh_token = await hass.auth.async_validate_access_token(hass_access_token) user = refresh_token.user - credential = Credentials( - auth_provider_type="homeassistant", auth_provider_id=None, data={}, id="test-id" - ) - user.credentials.append(credential) - assert len(user.credentials) == 1 - client = await hass_ws_client(hass, hass_access_token) await client.send_json({"id": 5, "type": auth.WS_TYPE_CURRENT_USER}) @@ -185,8 +198,7 @@ async def test_refresh_token_system_generated(hass, aiohttp_client): async def test_refresh_token_different_client_id(hass, aiohttp_client): """Test that we verify client ID.""" client = await async_setup_auth(hass, aiohttp_client) - user = await hass.auth.async_create_user("Test User") - refresh_token = await hass.auth.async_create_refresh_token(user, CLIENT_ID) + refresh_token = await async_setup_user_refresh_token(hass) # No client ID resp = await client.post( @@ -229,11 +241,37 @@ async def test_refresh_token_different_client_id(hass, aiohttp_client): ) +async def test_refresh_token_provider_rejected( + hass, aiohttp_client, hass_admin_user, hass_admin_credential +): + """Test that we verify client ID.""" + client = await async_setup_auth(hass, aiohttp_client) + refresh_token = await async_setup_user_refresh_token(hass) + + # Rejected by provider + with patch( + "homeassistant.auth.providers.insecure_example.ExampleAuthProvider.async_validate_refresh_token", + side_effect=InvalidAuthError("Invalid access"), + ): + resp = await client.post( + "/auth/token", + data={ + "client_id": CLIENT_ID, + "grant_type": "refresh_token", + "refresh_token": refresh_token.token, + }, + ) + + assert resp.status == 403 + result = await resp.json() + assert result["error"] == "access_denied" + assert result["error_description"] == "Invalid access" + + async def test_revoking_refresh_token(hass, aiohttp_client): """Test that we can revoke refresh tokens.""" client = await async_setup_auth(hass, aiohttp_client) - user = await hass.auth.async_create_user("Test User") - refresh_token = await hass.auth.async_create_refresh_token(user, CLIENT_ID) + refresh_token = await async_setup_user_refresh_token(hass) # Test that we can create an access token resp = await client.post( diff --git a/tests/components/config/test_auth.py b/tests/components/config/test_auth.py index 2d3cfe54f5a..363910ffd72 100644 --- a/tests/components/config/test_auth.py +++ b/tests/components/config/test_auth.py @@ -48,7 +48,9 @@ async def test_list(hass, hass_ws_client, hass_admin_user): id="hij", name="Inactive User", is_active=False, groups=[group] ).add_to_hass(hass) - refresh_token = await hass.auth.async_create_refresh_token(owner, CLIENT_ID) + refresh_token = await hass.auth.async_create_refresh_token( + owner, CLIENT_ID, credential=owner.credentials[0] + ) access_token = hass.auth.async_create_access_token(refresh_token) client = await hass_ws_client(hass, access_token) @@ -60,13 +62,13 @@ async def test_list(hass, hass_ws_client, hass_admin_user): assert len(data) == 4 assert data[0] == { "id": hass_admin_user.id, - "username": None, + "username": "admin", "name": "Mock User", "is_owner": False, "is_active": True, "system_generated": False, "group_ids": [group.id for group in hass_admin_user.groups], - "credentials": [], + "credentials": [{"type": "homeassistant"}], } assert data[1] == { "id": owner.id, diff --git a/tests/components/config/test_auth_provider_homeassistant.py b/tests/components/config/test_auth_provider_homeassistant.py index 6af3e6507d5..0aafa93e635 100644 --- a/tests/components/config/test_auth_provider_homeassistant.py +++ b/tests/components/config/test_auth_provider_homeassistant.py @@ -4,24 +4,19 @@ import pytest from homeassistant.auth.providers import homeassistant as prov_ha from homeassistant.components.config import auth_provider_homeassistant as auth_ha -from tests.common import CLIENT_ID, MockUser, register_auth_provider +from tests.common import CLIENT_ID, MockUser @pytest.fixture(autouse=True) -def setup_config(hass): - """Fixture that sets up the auth provider homeassistant module.""" - hass.loop.run_until_complete( - register_auth_provider(hass, {"type": "homeassistant"}) - ) - hass.loop.run_until_complete(auth_ha.async_setup(hass)) +async def setup_config(hass, local_auth): + """Fixture that sets up the auth provider .""" + await auth_ha.async_setup(hass) @pytest.fixture -async def auth_provider(hass): +async def auth_provider(local_auth): """Hass auth provider.""" - provider = hass.auth.auth_providers[0] - await provider.async_initialize() - return provider + return local_auth @pytest.fixture @@ -34,8 +29,8 @@ async def owner_access_token(hass, hass_owner_user): @pytest.fixture -async def test_user_credential(hass, auth_provider): - """Add a test user.""" +async def hass_admin_credential(hass, auth_provider): + """Overload credentials to admin user.""" await hass.async_add_executor_job( auth_provider.data.add_auth, "test-user", "test-pass" ) @@ -124,7 +119,7 @@ async def test_create_auth(hass, hass_ws_client, hass_storage): "id": 5, "type": "config/auth_provider/homeassistant/create", "user_id": user.id, - "username": "test-user", + "username": "test-user2", "password": "test-pass", } ) @@ -135,10 +130,10 @@ async def test_create_auth(hass, hass_ws_client, hass_storage): creds = user.credentials[0] assert creds.auth_provider_type == "homeassistant" assert creds.auth_provider_id is None - assert creds.data == {"username": "test-user"} + assert creds.data == {"username": "test-user2"} assert prov_ha.STORAGE_KEY in hass_storage - entry = hass_storage[prov_ha.STORAGE_KEY]["data"]["users"][0] - assert entry["username"] == "test-user" + entry = hass_storage[prov_ha.STORAGE_KEY]["data"]["users"][1] + assert entry["username"] == "test-user2" async def test_create_auth_duplicate_username(hass, hass_ws_client, hass_storage): @@ -242,7 +237,7 @@ async def test_delete_unknown_auth(hass, hass_ws_client): { "id": 5, "type": "config/auth_provider/homeassistant/delete", - "username": "test-user", + "username": "test-user2", } ) @@ -251,12 +246,8 @@ async def test_delete_unknown_auth(hass, hass_ws_client): assert result["error"]["code"] == "auth_not_found" -async def test_change_password( - hass, hass_ws_client, hass_admin_user, auth_provider, test_user_credential -): +async def test_change_password(hass, hass_ws_client, auth_provider): """Test that change password succeeds with valid password.""" - await hass.auth.async_link_user(hass_admin_user, test_user_credential) - client = await hass_ws_client(hass) await client.send_json( { @@ -273,10 +264,9 @@ async def test_change_password( async def test_change_password_wrong_pw( - hass, hass_ws_client, hass_admin_user, auth_provider, test_user_credential + hass, hass_ws_client, hass_admin_user, auth_provider ): """Test that change password fails with invalid password.""" - await hass.auth.async_link_user(hass_admin_user, test_user_credential) client = await hass_ws_client(hass) await client.send_json( @@ -295,8 +285,9 @@ async def test_change_password_wrong_pw( await auth_provider.async_validate_login("test-user", "new-pass") -async def test_change_password_no_creds(hass, hass_ws_client): +async def test_change_password_no_creds(hass, hass_ws_client, hass_admin_user): """Test that change password fails with no credentials.""" + hass_admin_user.credentials.clear() client = await hass_ws_client(hass) await client.send_json( @@ -313,9 +304,7 @@ async def test_change_password_no_creds(hass, hass_ws_client): assert result["error"]["code"] == "credentials_not_found" -async def test_admin_change_password_not_owner( - hass, hass_ws_client, auth_provider, test_user_credential -): +async def test_admin_change_password_not_owner(hass, hass_ws_client, auth_provider): """Test that change password fails when not owner.""" client = await hass_ws_client(hass) @@ -358,6 +347,8 @@ async def test_admin_change_password_no_cred( hass, hass_ws_client, owner_access_token, hass_admin_user ): """Test that change password fails with unknown credential.""" + + hass_admin_user.credentials.clear() client = await hass_ws_client(hass, owner_access_token) await client.send_json( @@ -379,12 +370,9 @@ async def test_admin_change_password( hass_ws_client, owner_access_token, auth_provider, - test_user_credential, hass_admin_user, ): """Test that owners can change any password.""" - await hass.auth.async_link_user(hass_admin_user, test_user_credential) - client = await hass_ws_client(hass, owner_access_token) await client.send_json( diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 4fa6b8da78a..fe956b2ac0a 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -247,7 +247,7 @@ async def test_onboarding_user_race(hass, hass_storage, aiohttp_client): assert sorted([res1.status, res2.status]) == [200, HTTP_FORBIDDEN] -async def test_onboarding_integration(hass, hass_storage, hass_client): +async def test_onboarding_integration(hass, hass_storage, hass_client, hass_admin_user): """Test finishing integration step.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) @@ -288,6 +288,28 @@ async def test_onboarding_integration(hass, hass_storage, hass_client): assert len(user.refresh_tokens) == 2, user +async def test_onboarding_integration_missing_credential( + hass, hass_storage, hass_client, hass_access_token +): + """Test that we fail integration step if user is missing credentials.""" + mock_storage(hass_storage, {"done": [const.STEP_USER]}) + + assert await async_setup_component(hass, "onboarding", {}) + await hass.async_block_till_done() + + refresh_token = await hass.auth.async_validate_access_token(hass_access_token) + refresh_token.credential = None + + client = await hass_client() + + resp = await client.post( + "/api/onboarding/integration", + json={"client_id": CLIENT_ID, "redirect_uri": CLIENT_REDIRECT_URI}, + ) + + assert resp.status == 403 + + async def test_onboarding_integration_invalid_redirect_uri( hass, hass_storage, hass_client ): diff --git a/tests/conftest.py b/tests/conftest.py index 55249a58fc9..27559e9659d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,7 @@ import requests_mock as _requests_mock from homeassistant import core as ha, loader, runner, util from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY +from homeassistant.auth.models import Credentials from homeassistant.auth.providers import homeassistant, legacy_api_password from homeassistant.components import mqtt from homeassistant.components.websocket_api.auth import ( @@ -201,10 +202,20 @@ def mock_device_tracker_conf(): @pytest.fixture -def hass_access_token(hass, hass_admin_user): +async def hass_admin_credential(hass, local_auth): + """Provide credentials for admin user.""" + await hass.async_add_executor_job(local_auth.data.add_auth, "admin", "admin-pass") + + return await local_auth.async_get_or_create_credentials({"username": "admin"}) + + +@pytest.fixture +async def hass_access_token(hass, hass_admin_user, hass_admin_credential): """Return an access token to access Home Assistant.""" - refresh_token = hass.loop.run_until_complete( - hass.auth.async_create_refresh_token(hass_admin_user, CLIENT_ID) + await hass.auth.async_link_user(hass_admin_user, hass_admin_credential) + + refresh_token = await hass.auth.async_create_refresh_token( + hass_admin_user, CLIENT_ID, credential=hass_admin_credential ) return hass.auth.async_create_access_token(refresh_token) @@ -234,10 +245,21 @@ def hass_read_only_user(hass, local_auth): @pytest.fixture -def hass_read_only_access_token(hass, hass_read_only_user): +def hass_read_only_access_token(hass, hass_read_only_user, local_auth): """Return a Home Assistant read only user.""" + credential = Credentials( + id="mock-readonly-credential-id", + auth_provider_type="homeassistant", + auth_provider_id=None, + data={"username": "readonly"}, + is_new=False, + ) + hass_read_only_user.credentials.append(credential) + refresh_token = hass.loop.run_until_complete( - hass.auth.async_create_refresh_token(hass_read_only_user, CLIENT_ID) + hass.auth.async_create_refresh_token( + hass_read_only_user, CLIENT_ID, credential=credential + ) ) return hass.auth.async_create_access_token(refresh_token) @@ -260,6 +282,7 @@ def local_auth(hass): prv = homeassistant.HassAuthProvider( hass, hass.auth._store, {"type": "homeassistant"} ) + hass.loop.run_until_complete(prv.async_initialize()) hass.auth._providers[(prv.type, prv.id)] = prv return prv From f1c24939f38fb265e4570a9de3aab306fd3f95eb Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 28 Jan 2021 12:06:36 +0100 Subject: [PATCH 019/796] Upgrade beautifulsoup4 to 4.9.3 (#45619) --- homeassistant/components/scrape/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index dcb3a8fdff0..daa5a269dcf 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -2,7 +2,7 @@ "domain": "scrape", "name": "Scrape", "documentation": "https://www.home-assistant.io/integrations/scrape", - "requirements": ["beautifulsoup4==4.9.1"], + "requirements": ["beautifulsoup4==4.9.3"], "after_dependencies": ["rest"], "codeowners": ["@fabaff"] } diff --git a/requirements_all.txt b/requirements_all.txt index f90acacbe33..57e4961d3d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -333,7 +333,7 @@ batinfo==0.4.2 # beacontools[scan]==1.2.3 # homeassistant.components.scrape -beautifulsoup4==4.9.1 +beautifulsoup4==4.9.3 # homeassistant.components.beewi_smartclim # beewi_smartclim==0.0.10 From 3ff75eee5382cd8179b397e513b67f758886ea40 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jan 2021 05:38:18 -0600 Subject: [PATCH 020/796] Update homekit to use new fan entity model (#45549) --- homeassistant/components/homekit/type_fans.py | 53 ++++++------- homeassistant/components/homekit/util.py | 53 +------------ tests/components/homekit/test_type_fans.py | 76 ++++++------------- tests/components/homekit/test_util.py | 59 -------------- 4 files changed, 46 insertions(+), 195 deletions(-) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index 1142a476bc5..7ed7256d48c 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -6,14 +6,13 @@ from pyhap.const import CATEGORY_FAN from homeassistant.components.fan import ( ATTR_DIRECTION, ATTR_OSCILLATING, - ATTR_SPEED, - ATTR_SPEED_LIST, + ATTR_PERCENTAGE, DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, - SERVICE_SET_SPEED, + SERVICE_SET_PERCENTAGE, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, @@ -36,7 +35,6 @@ from .const import ( CHAR_SWING_MODE, SERV_FANV2, ) -from .util import HomeKitSpeedMapping _LOGGER = logging.getLogger(__name__) @@ -61,10 +59,6 @@ class Fan(HomeAccessory): if features & SUPPORT_OSCILLATE: chars.append(CHAR_SWING_MODE) if features & SUPPORT_SET_SPEED: - speed_list = self.hass.states.get(self.entity_id).attributes.get( - ATTR_SPEED_LIST - ) - self.speed_mapping = HomeKitSpeedMapping(speed_list) chars.append(CHAR_ROTATION_SPEED) serv_fan = self.add_preload_service(SERV_FANV2, chars) @@ -117,7 +111,7 @@ class Fan(HomeAccessory): # We always do this LAST to ensure they # get the speed they asked for if CHAR_ROTATION_SPEED in char_values: - self.set_speed(char_values[CHAR_ROTATION_SPEED]) + self.set_percentage(char_values[CHAR_ROTATION_SPEED]) def set_state(self, value): """Set state if call came from HomeKit.""" @@ -140,12 +134,11 @@ class Fan(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id, ATTR_OSCILLATING: oscillating} self.call_service(DOMAIN, SERVICE_OSCILLATE, params, oscillating) - def set_speed(self, value): + def set_percentage(self, value): """Set state if call came from HomeKit.""" _LOGGER.debug("%s: Set speed to %d", self.entity_id, value) - speed = self.speed_mapping.speed_to_states(value) - params = {ATTR_ENTITY_ID: self.entity_id, ATTR_SPEED: speed} - self.call_service(DOMAIN, SERVICE_SET_SPEED, params, speed) + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_PERCENTAGE: value} + self.call_service(DOMAIN, SERVICE_SET_PERCENTAGE, params, value) @callback def async_update_state(self, new_state): @@ -169,24 +162,22 @@ class Fan(HomeAccessory): if self.char_speed is not None and state != STATE_OFF: # We do not change the homekit speed when turning off # as it will clear the restore state - speed = new_state.attributes.get(ATTR_SPEED) - hk_speed_value = self.speed_mapping.speed_to_homekit(speed) - if hk_speed_value is not None and self.char_speed.value != hk_speed_value: - # If the homeassistant component reports its speed as the first entry - # in its speed list but is not off, the hk_speed_value is 0. But 0 - # is a special value in homekit. When you turn on a homekit accessory - # it will try to restore the last rotation speed state which will be - # the last value saved by char_speed.set_value. But if it is set to - # 0, HomeKit will update the rotation speed to 100 as it thinks 0 is - # off. - # - # Therefore, if the hk_speed_value is 0 and the device is still on, - # the rotation speed is mapped to 1 otherwise the update is ignored - # in order to avoid this incorrect behavior. - if hk_speed_value == 0 and state == STATE_ON: - hk_speed_value = 1 - if self.char_speed.value != hk_speed_value: - self.char_speed.set_value(hk_speed_value) + percentage = new_state.attributes.get(ATTR_PERCENTAGE) + # If the homeassistant component reports its speed as the first entry + # in its speed list but is not off, the hk_speed_value is 0. But 0 + # is a special value in homekit. When you turn on a homekit accessory + # it will try to restore the last rotation speed state which will be + # the last value saved by char_speed.set_value. But if it is set to + # 0, HomeKit will update the rotation speed to 100 as it thinks 0 is + # off. + # + # Therefore, if the hk_speed_value is 0 and the device is still on, + # the rotation speed is mapped to 1 otherwise the update is ignored + # in order to avoid this incorrect behavior. + if percentage == 0 and state == STATE_ON: + percentage = 1 + if percentage is not None and self.char_speed.value != percentage: + self.char_speed.set_value(percentage) # Handle Oscillating if self.char_swing is not None: diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 453ae13d846..98374b73f40 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -1,5 +1,4 @@ """Collection of useful functions for the HomeKit component.""" -from collections import OrderedDict, namedtuple import io import ipaddress import logging @@ -11,7 +10,7 @@ import socket import pyqrcode import voluptuous as vol -from homeassistant.components import binary_sensor, fan, media_player, sensor +from homeassistant.components import binary_sensor, media_player, sensor from homeassistant.const import ( ATTR_CODE, ATTR_SUPPORTED_FEATURES, @@ -310,56 +309,6 @@ def validate_media_player_features(state, feature_list): return True -SpeedRange = namedtuple("SpeedRange", ("start", "target")) -SpeedRange.__doc__ += """ Maps Home Assistant speed \ -values to percentage based HomeKit speeds. -start: Start of the range (inclusive). -target: Percentage to use to determine HomeKit percentages \ -from HomeAssistant speed. -""" - - -class HomeKitSpeedMapping: - """Supports conversion between Home Assistant and HomeKit fan speeds.""" - - def __init__(self, speed_list): - """Initialize a new SpeedMapping object.""" - if speed_list[0] != fan.SPEED_OFF: - _LOGGER.warning( - "%s does not contain the speed setting " - "%s as its first element. " - "Assuming that %s is equivalent to 'off'", - speed_list, - fan.SPEED_OFF, - speed_list[0], - ) - self.speed_ranges = OrderedDict() - list_size = len(speed_list) - for index, speed in enumerate(speed_list): - # By dividing by list_size -1 the following - # desired attributes hold true: - # * index = 0 => 0%, equal to "off" - # * index = len(speed_list) - 1 => 100 % - # * all other indices are equally distributed - target = index * 100 / (list_size - 1) - start = index * 100 / list_size - self.speed_ranges[speed] = SpeedRange(start, target) - - def speed_to_homekit(self, speed): - """Map Home Assistant speed state to HomeKit speed.""" - if speed is None: - return None - speed_range = self.speed_ranges[speed] - return round(speed_range.target) - - def speed_to_states(self, speed): - """Map HomeKit speed to Home Assistant speed state.""" - for state, speed_range in reversed(self.speed_ranges.items()): - if speed_range.start <= speed: - return state - return list(self.speed_ranges)[0] - - def show_setup_message(hass, entry_id, bridge_name, pincode, uri): """Display persistent notification with setup information.""" pin = pincode.decode() diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index bc1bac11844..fc5ac4344ad 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -1,6 +1,5 @@ """Test different accessory types: Fans.""" from collections import namedtuple -from unittest.mock import Mock from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE import pytest @@ -8,20 +7,15 @@ import pytest from homeassistant.components.fan import ( ATTR_DIRECTION, ATTR_OSCILLATING, - ATTR_SPEED, - ATTR_SPEED_LIST, + ATTR_PERCENTAGE, DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, - SPEED_HIGH, - SPEED_LOW, - SPEED_OFF, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, ) from homeassistant.components.homekit.const import ATTR_VALUE -from homeassistant.components.homekit.util import HomeKitSpeedMapping from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -266,15 +260,13 @@ async def test_fan_oscillate(hass, hk_driver, cls, events): async def test_fan_speed(hass, hk_driver, cls, events): """Test fan with speed.""" entity_id = "fan.demo" - speed_list = [SPEED_OFF, SPEED_LOW, SPEED_HIGH] hass.states.async_set( entity_id, STATE_ON, { ATTR_SUPPORTED_FEATURES: SUPPORT_SET_SPEED, - ATTR_SPEED: SPEED_OFF, - ATTR_SPEED_LIST: speed_list, + ATTR_PERCENTAGE: 0, }, ) await hass.async_block_till_done() @@ -288,20 +280,12 @@ async def test_fan_speed(hass, hk_driver, cls, events): await acc.run_handler() await hass.async_block_till_done() - assert ( - acc.speed_mapping.speed_ranges == HomeKitSpeedMapping(speed_list).speed_ranges - ) - - acc.speed_mapping.speed_to_homekit = Mock(return_value=42) - acc.speed_mapping.speed_to_states = Mock(return_value="ludicrous") - - hass.states.async_set(entity_id, STATE_ON, {ATTR_SPEED: SPEED_HIGH}) + hass.states.async_set(entity_id, STATE_ON, {ATTR_PERCENTAGE: 100}) await hass.async_block_till_done() - acc.speed_mapping.speed_to_homekit.assert_called_with(SPEED_HIGH) - assert acc.char_speed.value == 42 + assert acc.char_speed.value == 100 # Set from HomeKit - call_set_speed = async_mock_service(hass, DOMAIN, "set_speed") + call_set_percentage = async_mock_service(hass, DOMAIN, "set_percentage") char_speed_iid = acc.char_speed.to_HAP()[HAP_REPR_IID] char_active_iid = acc.char_active.to_HAP()[HAP_REPR_IID] @@ -320,18 +304,17 @@ async def test_fan_speed(hass, hk_driver, cls, events): ) await hass.async_add_executor_job(acc.char_speed.client_update_value, 42) await hass.async_block_till_done() - acc.speed_mapping.speed_to_states.assert_called_with(42) assert acc.char_speed.value == 42 assert acc.char_active.value == 1 - assert call_set_speed[0] - assert call_set_speed[0].data[ATTR_ENTITY_ID] == entity_id - assert call_set_speed[0].data[ATTR_SPEED] == "ludicrous" + assert call_set_percentage[0] + assert call_set_percentage[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_percentage[0].data[ATTR_PERCENTAGE] == 42 assert len(events) == 1 - assert events[-1].data[ATTR_VALUE] == "ludicrous" + assert events[-1].data[ATTR_VALUE] == 42 # Verify speed is preserved from off to on - hass.states.async_set(entity_id, STATE_OFF, {ATTR_SPEED: SPEED_OFF}) + hass.states.async_set(entity_id, STATE_OFF, {ATTR_PERCENTAGE: 42}) await hass.async_block_till_done() assert acc.char_speed.value == 42 assert acc.char_active.value == 0 @@ -356,7 +339,6 @@ async def test_fan_speed(hass, hk_driver, cls, events): async def test_fan_set_all_one_shot(hass, hk_driver, cls, events): """Test fan with speed.""" entity_id = "fan.demo" - speed_list = [SPEED_OFF, SPEED_LOW, SPEED_HIGH] hass.states.async_set( entity_id, @@ -365,10 +347,9 @@ async def test_fan_set_all_one_shot(hass, hk_driver, cls, events): ATTR_SUPPORTED_FEATURES: SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION, - ATTR_SPEED: SPEED_OFF, + ATTR_PERCENTAGE: 0, ATTR_OSCILLATING: False, ATTR_DIRECTION: DIRECTION_FORWARD, - ATTR_SPEED_LIST: speed_list, }, ) await hass.async_block_till_done() @@ -381,13 +362,6 @@ async def test_fan_set_all_one_shot(hass, hk_driver, cls, events): await acc.run_handler() await hass.async_block_till_done() - assert ( - acc.speed_mapping.speed_ranges == HomeKitSpeedMapping(speed_list).speed_ranges - ) - - acc.speed_mapping.speed_to_homekit = Mock(return_value=42) - acc.speed_mapping.speed_to_states = Mock(return_value="ludicrous") - hass.states.async_set( entity_id, STATE_OFF, @@ -395,17 +369,16 @@ async def test_fan_set_all_one_shot(hass, hk_driver, cls, events): ATTR_SUPPORTED_FEATURES: SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION, - ATTR_SPEED: SPEED_OFF, + ATTR_PERCENTAGE: 0, ATTR_OSCILLATING: False, ATTR_DIRECTION: DIRECTION_FORWARD, - ATTR_SPEED_LIST: speed_list, }, ) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF # Set from HomeKit - call_set_speed = async_mock_service(hass, DOMAIN, "set_speed") + call_set_percentage = async_mock_service(hass, DOMAIN, "set_percentage") call_oscillate = async_mock_service(hass, DOMAIN, "oscillate") call_set_direction = async_mock_service(hass, DOMAIN, "set_direction") call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") @@ -444,11 +417,10 @@ async def test_fan_set_all_one_shot(hass, hk_driver, cls, events): "mock_addr", ) await hass.async_block_till_done() - acc.speed_mapping.speed_to_states.assert_called_with(42) assert not call_turn_on - assert call_set_speed[0] - assert call_set_speed[0].data[ATTR_ENTITY_ID] == entity_id - assert call_set_speed[0].data[ATTR_SPEED] == "ludicrous" + assert call_set_percentage[0] + assert call_set_percentage[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_percentage[0].data[ATTR_PERCENTAGE] == 42 assert call_oscillate[0] assert call_oscillate[0].data[ATTR_ENTITY_ID] == entity_id assert call_oscillate[0].data[ATTR_OSCILLATING] is True @@ -459,7 +431,7 @@ async def test_fan_set_all_one_shot(hass, hk_driver, cls, events): assert events[0].data[ATTR_VALUE] is True assert events[1].data[ATTR_VALUE] == DIRECTION_REVERSE - assert events[2].data[ATTR_VALUE] == "ludicrous" + assert events[2].data[ATTR_VALUE] == 42 hass.states.async_set( entity_id, @@ -468,10 +440,9 @@ async def test_fan_set_all_one_shot(hass, hk_driver, cls, events): ATTR_SUPPORTED_FEATURES: SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION, - ATTR_SPEED: SPEED_OFF, + ATTR_PERCENTAGE: 0, ATTR_OSCILLATING: False, ATTR_DIRECTION: DIRECTION_FORWARD, - ATTR_SPEED_LIST: speed_list, }, ) await hass.async_block_till_done() @@ -506,11 +477,10 @@ async def test_fan_set_all_one_shot(hass, hk_driver, cls, events): # Turn on should not be called if its already on # and we set a fan speed await hass.async_block_till_done() - acc.speed_mapping.speed_to_states.assert_called_with(42) assert len(events) == 6 - assert call_set_speed[1] - assert call_set_speed[1].data[ATTR_ENTITY_ID] == entity_id - assert call_set_speed[1].data[ATTR_SPEED] == "ludicrous" + assert call_set_percentage[1] + assert call_set_percentage[1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_percentage[1].data[ATTR_PERCENTAGE] == 42 assert call_oscillate[1] assert call_oscillate[1].data[ATTR_ENTITY_ID] == entity_id assert call_oscillate[1].data[ATTR_OSCILLATING] is True @@ -520,7 +490,7 @@ async def test_fan_set_all_one_shot(hass, hk_driver, cls, events): assert events[-3].data[ATTR_VALUE] is True assert events[-2].data[ATTR_VALUE] == DIRECTION_REVERSE - assert events[-1].data[ATTR_VALUE] == "ludicrous" + assert events[-1].data[ATTR_VALUE] == 42 hk_driver.set_characteristics( { @@ -554,7 +524,7 @@ async def test_fan_set_all_one_shot(hass, hk_driver, cls, events): assert len(events) == 7 assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id - assert len(call_set_speed) == 2 + assert len(call_set_percentage) == 2 assert len(call_oscillate) == 2 assert len(call_set_direction) == 2 diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index c6845779313..e0f10a94d69 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -21,8 +21,6 @@ from homeassistant.components.homekit.const import ( TYPE_VALVE, ) from homeassistant.components.homekit.util import ( - HomeKitSpeedMapping, - SpeedRange, cleanup_name_for_homekit, convert_to_float, density_to_air_quality, @@ -251,63 +249,6 @@ async def test_dismiss_setup_msg(hass): assert call_dismiss_notification[0].data[ATTR_NOTIFICATION_ID] == "entry_id" -def test_homekit_speed_mapping(): - """Test if the SpeedRanges from a speed_list are as expected.""" - # A standard 2-speed fan - speed_mapping = HomeKitSpeedMapping(["off", "low", "high"]) - assert speed_mapping.speed_ranges == { - "off": SpeedRange(0, 0), - "low": SpeedRange(100 / 3, 50), - "high": SpeedRange(200 / 3, 100), - } - - # A standard 3-speed fan - speed_mapping = HomeKitSpeedMapping(["off", "low", "medium", "high"]) - assert speed_mapping.speed_ranges == { - "off": SpeedRange(0, 0), - "low": SpeedRange(100 / 4, 100 / 3), - "medium": SpeedRange(200 / 4, 200 / 3), - "high": SpeedRange(300 / 4, 100), - } - - # a Dyson-like fan with 10 speeds - speed_mapping = HomeKitSpeedMapping([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) - assert speed_mapping.speed_ranges == { - 0: SpeedRange(0, 0), - 1: SpeedRange(10, 100 / 9), - 2: SpeedRange(20, 200 / 9), - 3: SpeedRange(30, 300 / 9), - 4: SpeedRange(40, 400 / 9), - 5: SpeedRange(50, 500 / 9), - 6: SpeedRange(60, 600 / 9), - 7: SpeedRange(70, 700 / 9), - 8: SpeedRange(80, 800 / 9), - 9: SpeedRange(90, 100), - } - - -def test_speed_to_homekit(): - """Test speed conversion from HA to Homekit.""" - speed_mapping = HomeKitSpeedMapping(["off", "low", "high"]) - assert speed_mapping.speed_to_homekit(None) is None - assert speed_mapping.speed_to_homekit("off") == 0 - assert speed_mapping.speed_to_homekit("low") == 50 - assert speed_mapping.speed_to_homekit("high") == 100 - - -def test_speed_to_states(): - """Test speed conversion from Homekit to HA.""" - speed_mapping = HomeKitSpeedMapping(["off", "low", "high"]) - assert speed_mapping.speed_to_states(-1) == "off" - assert speed_mapping.speed_to_states(0) == "off" - assert speed_mapping.speed_to_states(33) == "off" - assert speed_mapping.speed_to_states(34) == "low" - assert speed_mapping.speed_to_states(50) == "low" - assert speed_mapping.speed_to_states(66) == "low" - assert speed_mapping.speed_to_states(67) == "high" - assert speed_mapping.speed_to_states(100) == "high" - - async def test_port_is_available(hass): """Test we can get an available port and it is actually available.""" next_port = await hass.async_add_executor_job( From 92efe4f4918f04ba1adb448969afe6d3de66c824 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 28 Jan 2021 13:28:39 +0100 Subject: [PATCH 021/796] Bump gios library (#45639) --- homeassistant/components/gios/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index 99e54edfeaf..468e22260b5 100644 --- a/homeassistant/components/gios/manifest.json +++ b/homeassistant/components/gios/manifest.json @@ -3,7 +3,7 @@ "name": "GIOŚ", "documentation": "https://www.home-assistant.io/integrations/gios", "codeowners": ["@bieniu"], - "requirements": ["gios==0.1.4"], + "requirements": ["gios==0.1.5"], "config_flow": true, "quality_scale": "platinum" } diff --git a/requirements_all.txt b/requirements_all.txt index 57e4961d3d8..7f4b49b1577 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -657,7 +657,7 @@ georss_qld_bushfire_alert_client==0.3 getmac==0.8.2 # homeassistant.components.gios -gios==0.1.4 +gios==0.1.5 # homeassistant.components.gitter gitterpy==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 661192640a4..3abd55baec2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -345,7 +345,7 @@ georss_qld_bushfire_alert_client==0.3 getmac==0.8.2 # homeassistant.components.gios -gios==0.1.4 +gios==0.1.5 # homeassistant.components.glances glances_api==0.2.0 From f0e451021382f6bafb0775adf8910ec1d5c08ca4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jan 2021 08:17:16 -0600 Subject: [PATCH 022/796] Update lutron_caseta to use new fan entity model (#45540) --- homeassistant/components/lutron_caseta/fan.py | 77 +++++++------------ 1 file changed, 26 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py index 80472535d51..935c8827c84 100644 --- a/homeassistant/components/lutron_caseta/fan.py +++ b/homeassistant/components/lutron_caseta/fan.py @@ -3,14 +3,10 @@ import logging from pylutron_caseta import FAN_HIGH, FAN_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_OFF -from homeassistant.components.fan import ( - DOMAIN, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, - SUPPORT_SET_SPEED, - FanEntity, +from homeassistant.components.fan import DOMAIN, SUPPORT_SET_SPEED, FanEntity +from homeassistant.util.percentage import ( + ordered_list_item_to_percentage, + percentage_to_ordered_list_item, ) from . import LutronCasetaDevice @@ -18,23 +14,8 @@ from .const import BRIDGE_DEVICE, BRIDGE_LEAP, DOMAIN as CASETA_DOMAIN _LOGGER = logging.getLogger(__name__) -VALUE_TO_SPEED = { - None: SPEED_OFF, - FAN_OFF: SPEED_OFF, - FAN_LOW: SPEED_LOW, - FAN_MEDIUM: SPEED_MEDIUM, - FAN_MEDIUM_HIGH: SPEED_MEDIUM, - FAN_HIGH: SPEED_HIGH, -} - -SPEED_TO_VALUE = { - SPEED_OFF: FAN_OFF, - SPEED_LOW: FAN_LOW, - SPEED_MEDIUM: FAN_MEDIUM, - SPEED_HIGH: FAN_HIGH, -} - -FAN_SPEEDS = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] +DEFAULT_ON_PERCENTAGE = 50 +ORDERED_NAMED_FAN_SPEEDS = [FAN_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH] async def async_setup_entry(hass, config_entry, async_add_entities): @@ -61,27 +42,17 @@ class LutronCasetaFan(LutronCasetaDevice, FanEntity): """Representation of a Lutron Caseta fan. Including Fan Speed.""" @property - def speed(self) -> str: - """Return the current speed.""" - return VALUE_TO_SPEED[self._device["fan_speed"]] - - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return FAN_SPEEDS + def percentage(self) -> str: + """Return the current speed percentage.""" + return ordered_list_item_to_percentage( + ORDERED_NAMED_FAN_SPEEDS, self._device["fan_speed"] + ) @property def supported_features(self) -> int: """Flag supported features. Speed Only.""" return SUPPORT_SET_SPEED - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # async def async_turn_on( self, speed: str = None, @@ -90,26 +61,30 @@ class LutronCasetaFan(LutronCasetaDevice, FanEntity): **kwargs, ): """Turn the fan on.""" - if speed is None: - speed = SPEED_MEDIUM - await self.async_set_speed(speed) + if percentage is None: + percentage = DEFAULT_ON_PERCENTAGE + + await self.async_set_percentage(percentage) async def async_turn_off(self, **kwargs): """Turn the fan off.""" - await self.async_set_speed(SPEED_OFF) + await self.async_set_percentage(0) - async def async_set_speed(self, speed: str) -> None: + async def async_set_percentage(self, percentage: int) -> None: """Set the speed of the fan.""" - await self._smartbridge.set_fan(self.device_id, SPEED_TO_VALUE[speed]) + if percentage == 0: + named_speed = FAN_OFF + else: + named_speed = percentage_to_ordered_list_item( + ORDERED_NAMED_FAN_SPEEDS, percentage + ) + + await self._smartbridge.set_fan(self.device_id, named_speed) @property def is_on(self): """Return true if device is on.""" - return VALUE_TO_SPEED[self._device["fan_speed"]] in [ - SPEED_LOW, - SPEED_MEDIUM, - SPEED_HIGH, - ] + return self.percentage and self.percentage > 0 async def async_update(self): """Update when forcing a refresh of the device.""" From a4c8cb6f8486855903bdf8cfac11408dbd637048 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 28 Jan 2021 15:30:10 +0100 Subject: [PATCH 023/796] Unregister webhook if it can't be established successfully (#42791) --- homeassistant/components/netatmo/__init__.py | 50 +++++++++++++++---- .../components/netatmo/data_handler.py | 5 ++ homeassistant/components/netatmo/light.py | 8 +-- 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 67e83189fc5..cdbd34991f2 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -21,6 +21,11 @@ from homeassistant.const import ( ) from homeassistant.core import CoreState, HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.event import async_call_later from . import api, config_flow from .const import ( @@ -54,18 +59,19 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = ["camera", "climate", "sensor"] +PLATFORMS = ["camera", "climate", "light", "sensor"] async def async_setup(hass: HomeAssistant, config: dict): """Set up the Netatmo component.""" - hass.data[DOMAIN] = {} - hass.data[DOMAIN][DATA_PERSONS] = {} - hass.data[DOMAIN][DATA_DEVICE_IDS] = {} - hass.data[DOMAIN][DATA_SCHEDULES] = {} - hass.data[DOMAIN][DATA_HOMES] = {} - hass.data[DOMAIN][DATA_EVENTS] = {} - hass.data[DOMAIN][DATA_CAMERAS] = {} + hass.data[DOMAIN] = { + DATA_PERSONS: {}, + DATA_DEVICE_IDS: {}, + DATA_SCHEDULES: {}, + DATA_HOMES: {}, + DATA_EVENTS: {}, + DATA_CAMERAS: {}, + } if DOMAIN not in config: return True @@ -114,6 +120,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if CONF_WEBHOOK_ID not in entry.data: return _LOGGER.debug("Unregister Netatmo webhook (%s)", entry.data[CONF_WEBHOOK_ID]) + async_dispatcher_send( + hass, + f"signal-{DOMAIN}-webhook-None", + {"type": "None", "data": {"push_type": "webhook_deactivation"}}, + ) webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) async def register_webhook(event): @@ -148,13 +159,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): webhook_register( hass, DOMAIN, "Netatmo", entry.data[CONF_WEBHOOK_ID], handle_webhook ) + + async def handle_event(event): + """Handle webhook events.""" + if event["data"]["push_type"] == "webhook_activation": + if activation_listener is not None: + _LOGGER.debug("sub called") + activation_listener() + + if activation_timeout is not None: + _LOGGER.debug("Unsub called") + activation_timeout() + + activation_listener = async_dispatcher_connect( + hass, + f"signal-{DOMAIN}-webhook-None", + handle_event, + ) + + activation_timeout = async_call_later(hass, 10, unregister_webhook) + await hass.async_add_executor_job( hass.data[DOMAIN][entry.entry_id][AUTH].addwebhook, webhook_url ) _LOGGER.info("Register Netatmo webhook: %s", webhook_url) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "light") - ) except pyatmo.ApiError as err: _LOGGER.error("Error during webhook registration - %s", err) diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index ae0995639c7..9bc4b197f1b 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -107,6 +107,10 @@ class NetatmoDataHandler: _LOGGER.info("%s webhook successfully registered", MANUFACTURER) self._webhook = True + elif event["data"]["push_type"] == "webhook_deactivation": + _LOGGER.info("%s webhook unregistered", MANUFACTURER) + self._webhook = False + elif event["data"]["push_type"] == "NACamera-connection": _LOGGER.debug("%s camera reconnected", MANUFACTURER) self._data_classes[CAMERA_DATA_CLASS_NAME][NEXT_SCAN] = time() @@ -118,6 +122,7 @@ class NetatmoDataHandler: partial(data_class, **kwargs), self._auth, ) + for update_callback in self._data_classes[data_class_entry][ "subscriptions" ]: diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 0c9e0c53176..dc8bf3f1fc8 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -53,9 +53,6 @@ async def async_setup_entry(hass, entry, async_add_entities): for camera in all_cameras: if camera["type"] == "NOC": - if not data_handler.webhook: - raise PlatformNotReady - _LOGGER.debug("Adding camera light %s %s", camera["id"], camera["name"]) entities.append( NetatmoLight( @@ -126,6 +123,11 @@ class NetatmoLight(NetatmoBase, LightEntity): self.async_write_ha_state() return + @property + def available(self) -> bool: + """If the webhook is not established, mark as unavailable.""" + return bool(self.data_handler.webhook) + @property def is_on(self): """Return true if light is on.""" From fdcf1fccf8b875b3e0b73b4b411f906a4f115472 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jan 2021 09:02:38 -0600 Subject: [PATCH 024/796] Update vesync to use new fan entity model (#45585) --- homeassistant/components/vesync/fan.py | 75 ++++++++++++++++---------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index 1e2cc08473c..10754007ce6 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -1,16 +1,14 @@ """Support for VeSync fans.""" import logging +import math -from homeassistant.components.fan import ( - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, - SUPPORT_SET_SPEED, - FanEntity, -) +from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) from .common import VeSyncDevice from .const import DOMAIN, VS_DISCOVERY, VS_DISPATCHERS, VS_FANS @@ -21,8 +19,11 @@ DEV_TYPE_TO_HA = { "LV-PUR131S": "fan", } -FAN_SPEEDS = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] FAN_MODE_AUTO = "auto" +FAN_MODE_SLEEP = "sleep" + +PRESET_MODES = [FAN_MODE_AUTO, FAN_MODE_SLEEP] +SPEED_RANGE = (1, 3) # off is not included async def async_setup_entry(hass, config_entry, async_add_entities): @@ -68,20 +69,25 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): return SUPPORT_SET_SPEED @property - def speed(self): + def percentage(self): """Return the current speed.""" - if self.smartfan.mode == FAN_MODE_AUTO: - return None if self.smartfan.mode == "manual": current_level = self.smartfan.fan_level if current_level is not None: - return FAN_SPEEDS[current_level] + return ranged_value_to_percentage(SPEED_RANGE, current_level) return None @property - def speed_list(self): - """Get the list of available speeds.""" - return FAN_SPEEDS + def preset_modes(self): + """Get the list of available preset modes.""" + return PRESET_MODES + + @property + def preset_mode(self): + """Get the current preset mode.""" + if self.smartfan.mode == FAN_MODE_AUTO: + return FAN_MODE_AUTO + return None @property def unique_info(self): @@ -99,21 +105,34 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): "screen_status": self.smartfan.screen_status, } - def set_speed(self, speed): + def set_percentage(self, percentage): """Set the speed of the device.""" + if percentage == 0: + self.smartfan.turn_off() + return + if not self.smartfan.is_on: self.smartfan.turn_on() self.smartfan.manual_mode() - self.smartfan.change_fan_speed(FAN_SPEEDS.index(speed)) + self.smartfan.change_fan_speed( + math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) + ) + self.schedule_update_ha_state() + + def set_preset_mode(self, preset_mode): + """Set the preset mode of device.""" + if preset_mode not in self.preset_modes: + raise ValueError( + "{preset_mode} is not one of the valid preset modes: {self.preset_modes}" + ) + + if not self.smartfan.is_on: + self.smartfan.turn_on() + + self.smartfan.auto_mode() + self.schedule_update_ha_state() - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # def turn_on( self, speed: str = None, @@ -122,5 +141,7 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): **kwargs, ) -> None: """Turn the device on.""" - self.smartfan.turn_on() - self.set_speed(speed) + if preset_mode: + self.set_preset_mode(preset_mode) + return + self.set_percentage(percentage) From 0da4034179bcca7a3ba259532ce3f4e17859f9ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jan 2021 09:05:02 -0600 Subject: [PATCH 025/796] Ensure history LazyState state value is always a string (#45644) Co-authored-by: Paulus Schoutsen --- homeassistant/components/history/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 894c2b15e47..1e22e45a892 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -699,7 +699,7 @@ class LazyState(State): """Init the lazy state.""" self._row = row self.entity_id = self._row.entity_id - self.state = self._row.state + self.state = self._row.state or "" self._attributes = None self._last_changed = None self._last_updated = None From 8065ece0bd61c0ef6030442a9994253fcb4b57f0 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Thu, 28 Jan 2021 17:14:33 +0100 Subject: [PATCH 026/796] Add first set of tests to devolo Home Control integration (#42527) * Add first two testcases * Remove repetition * Add first two testcases * Remove repetition * Add connection error test case * add test_setup_entry_credentials_valid * First attempt to use fixtures * Use markers * Optimize patch * Optimize marker use * Always patch mydevolo * Add first two testcases * Remove repetition * Add first two testcases * Remove repetition * Add connection error test case * add test_setup_entry_credentials_valid * First attempt to use fixtures * Use markers * Optimize patch * Optimize marker use * Always patch mydevolo * Add unload entry test case * Catch up with reality * Use unittest patch * Use core interface to start tests * Use entry state * Consistently assert entry state * Patch class instead of init Co-authored-by: Markus Bong <2Fake1987@gmail.com> --- .coveragerc | 1 - .../devolo_home_control/__init__.py | 18 +++++ .../devolo_home_control/conftest.py | 33 +++++++++ .../devolo_home_control/test_init.py | 71 +++++++++++++++++++ 4 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 tests/components/devolo_home_control/conftest.py create mode 100644 tests/components/devolo_home_control/test_init.py diff --git a/.coveragerc b/.coveragerc index 2c768060108..30ea684740d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -172,7 +172,6 @@ omit = homeassistant/components/denonavr/media_player.py homeassistant/components/denonavr/receiver.py homeassistant/components/deutsche_bahn/sensor.py - homeassistant/components/devolo_home_control/__init__.py homeassistant/components/devolo_home_control/binary_sensor.py homeassistant/components/devolo_home_control/climate.py homeassistant/components/devolo_home_control/const.py diff --git a/tests/components/devolo_home_control/__init__.py b/tests/components/devolo_home_control/__init__.py index 5e1e323cad8..5ffc0781c84 100644 --- a/tests/components/devolo_home_control/__init__.py +++ b/tests/components/devolo_home_control/__init__.py @@ -1 +1,19 @@ """Tests for the devolo_home_control integration.""" + +from homeassistant.components.devolo_home_control.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +def configure_integration(hass: HomeAssistant) -> MockConfigEntry: + """Configure the integration.""" + config = { + "username": "test-username", + "password": "test-password", + "mydevolo_url": "https://test_mydevolo_url.test", + } + entry = MockConfigEntry(domain=DOMAIN, data=config, unique_id="123456") + entry.add_to_hass(hass) + + return entry diff --git a/tests/components/devolo_home_control/conftest.py b/tests/components/devolo_home_control/conftest.py new file mode 100644 index 00000000000..487831b0fa4 --- /dev/null +++ b/tests/components/devolo_home_control/conftest.py @@ -0,0 +1,33 @@ +"""Fixtures for tests.""" + +from unittest.mock import patch + +import pytest + + +def pytest_configure(config): + """Define custom markers.""" + config.addinivalue_line( + "markers", + "credentials_invalid: Treat credentials as invalid.", + ) + config.addinivalue_line( + "markers", + "maintenance: Set maintenance mode to on.", + ) + + +@pytest.fixture(autouse=True) +def patch_mydevolo(request): + """Fixture to patch mydevolo into a desired state.""" + with patch( + "homeassistant.components.devolo_home_control.Mydevolo.credentials_valid", + return_value=not bool(request.node.get_closest_marker("credentials_invalid")), + ), patch( + "homeassistant.components.devolo_home_control.Mydevolo.maintenance", + return_value=bool(request.node.get_closest_marker("maintenance")), + ), patch( + "homeassistant.components.devolo_home_control.Mydevolo.get_gateway_ids", + return_value=["1400000000000001", "1400000000000002"], + ): + yield diff --git a/tests/components/devolo_home_control/test_init.py b/tests/components/devolo_home_control/test_init.py new file mode 100644 index 00000000000..f45400716f8 --- /dev/null +++ b/tests/components/devolo_home_control/test_init.py @@ -0,0 +1,71 @@ +"""Tests for the devolo Home Control integration.""" +from unittest.mock import patch + +from devolo_home_control_api.exceptions.gateway import GatewayOfflineError +import pytest + +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, + ENTRY_STATE_SETUP_ERROR, + ENTRY_STATE_SETUP_RETRY, +) +from homeassistant.core import HomeAssistant + +from tests.components.devolo_home_control import configure_integration + + +async def test_setup_entry(hass: HomeAssistant): + """Test setup entry.""" + entry = configure_integration(hass) + with patch("homeassistant.components.devolo_home_control.HomeControl"): + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ENTRY_STATE_LOADED + + +@pytest.mark.credentials_invalid +async def test_setup_entry_credentials_invalid(hass: HomeAssistant): + """Test setup entry fails if credentials are invalid.""" + entry = configure_integration(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ENTRY_STATE_SETUP_ERROR + + +@pytest.mark.maintenance +async def test_setup_entry_maintenance(hass: HomeAssistant): + """Test setup entry fails if mydevolo is in maintenance mode.""" + entry = configure_integration(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_setup_connection_error(hass: HomeAssistant): + """Test setup entry fails on connection error.""" + entry = configure_integration(hass) + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=ConnectionError, + ): + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_setup_gateway_offline(hass: HomeAssistant): + """Test setup entry fails on gateway offline.""" + entry = configure_integration(hass) + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=GatewayOfflineError, + ): + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_unload_entry(hass: HomeAssistant): + """Test unload entry.""" + entry = configure_integration(hass) + with patch("homeassistant.components.devolo_home_control.HomeControl"): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + await hass.config_entries.async_unload(entry.entry_id) + assert entry.state == ENTRY_STATE_NOT_LOADED From d7e0391e03a2aeddd6c66900cd57e8f721622d1b Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 28 Jan 2021 11:21:31 -0500 Subject: [PATCH 027/796] Allow Plex playback using provided playqueue ID (#45580) --- homeassistant/components/plex/media_player.py | 25 +++++++++++++------ homeassistant/components/plex/server.py | 4 +++ homeassistant/components/plex/services.py | 22 +++++++++++----- tests/components/plex/conftest.py | 6 +++++ tests/components/plex/test_services.py | 17 +++++++++++++ tests/fixtures/plex/media_100.xml | 8 +++++- tests/fixtures/plex/playqueue_1234.xml | 7 ++++++ 7 files changed, 75 insertions(+), 14 deletions(-) create mode 100644 tests/fixtures/plex/playqueue_1234.xml diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 24e37216b70..1a57186bd9b 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -22,6 +22,7 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -495,16 +496,26 @@ class PlexMediaPlayer(MediaPlayerEntity): if isinstance(src, int): src = {"plex_key": src} - shuffle = src.pop("shuffle", 0) - media = self.plex_server.lookup_media(media_type, **src) + playqueue_id = src.pop("playqueue_id", None) - if media is None: - _LOGGER.error("Media could not be found: %s", media_id) - return + if playqueue_id: + try: + playqueue = self.plex_server.get_playqueue(playqueue_id) + except plexapi.exceptions.NotFound as err: + raise HomeAssistantError( + f"PlayQueue '{playqueue_id}' could not be found" + ) from err + else: + shuffle = src.pop("shuffle", 0) + media = self.plex_server.lookup_media(media_type, **src) - _LOGGER.debug("Attempting to play %s on %s", media, self.name) + if media is None: + _LOGGER.error("Media could not be found: %s", media_id) + return + + _LOGGER.debug("Attempting to play %s on %s", media, self.name) + playqueue = self.plex_server.create_playqueue(media, shuffle=shuffle) - playqueue = self.plex_server.create_playqueue(media, shuffle=shuffle) try: self.device.playMedia(playqueue) except requests.exceptions.ConnectTimeout: diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index f8d55c71fc4..1baceb78ff1 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -593,6 +593,10 @@ class PlexServer: """Create playqueue on Plex server.""" return plexapi.playqueue.PlayQueue.create(self._plex_server, media, **kwargs) + def get_playqueue(self, playqueue_id): + """Retrieve existing playqueue from Plex server.""" + return plexapi.playqueue.PlayQueue.get(self._plex_server, playqueue_id) + def fetch_item(self, item): """Fetch item from Plex server.""" return self._plex_server.fetchItem(item) diff --git a/homeassistant/components/plex/services.py b/homeassistant/components/plex/services.py index 2e4057b890a..a5faa56a8bb 100644 --- a/homeassistant/components/plex/services.py +++ b/homeassistant/components/plex/services.py @@ -105,15 +105,25 @@ def lookup_plex_media(hass, content_type, content_id): content_type = DOMAIN plex_server_name = content.pop("plex_server", None) - shuffle = content.pop("shuffle", 0) - plex_server = get_plex_server(hass, plex_server_name) - media = plex_server.lookup_media(content_type, **content) - if media is None: - raise HomeAssistantError(f"Plex media not found using payload: '{content_id}'") + playqueue_id = content.pop("playqueue_id", None) + if playqueue_id: + try: + playqueue = plex_server.get_playqueue(playqueue_id) + except NotFound as err: + raise HomeAssistantError( + f"PlayQueue '{playqueue_id}' could not be found" + ) from err + else: + shuffle = content.pop("shuffle", 0) + media = plex_server.lookup_media(content_type, **content) + if media is None: + raise HomeAssistantError( + f"Plex media not found using payload: '{content_id}'" + ) + playqueue = plex_server.create_playqueue(media, shuffle=shuffle) - playqueue = plex_server.create_playqueue(media, shuffle=shuffle) return (playqueue, plex_server) diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py index 8fc25a819e8..d3e66cc4989 100644 --- a/tests/components/plex/conftest.py +++ b/tests/components/plex/conftest.py @@ -168,6 +168,12 @@ def playqueue_created_fixture(): return load_fixture("plex/playqueue_created.xml") +@pytest.fixture(name="playqueue_1234", scope="session") +def playqueue_1234_fixture(): + """Load payload for playqueue 1234 and return it.""" + return load_fixture("plex/playqueue_1234.xml") + + @pytest.fixture(name="plex_server_accounts", scope="session") def plex_server_accounts_fixture(): """Load payload accounts on the Plex server and return it.""" diff --git a/tests/components/plex/test_services.py b/tests/components/plex/test_services.py index 9d1715aa72b..cf8bc63c5da 100644 --- a/tests/components/plex/test_services.py +++ b/tests/components/plex/test_services.py @@ -113,6 +113,7 @@ async def test_sonos_play_media( setup_plex_server, requests_mock, empty_payload, + playqueue_1234, playqueue_created, plextv_account, sonos_resources, @@ -178,3 +179,19 @@ async def test_sonos_play_media( play_on_sonos(hass, MEDIA_TYPE_MUSIC, content_id_bad_media, sonos_speaker_name) assert "Plex media not found" in str(excinfo.value) assert playback_mock.call_count == 3 + + # Test with speakers available and playqueue + requests_mock.get("https://1.2.3.4:32400/playQueues/1234", text=playqueue_1234) + content_id_with_playqueue = '{"playqueue_id": 1234}' + play_on_sonos(hass, MEDIA_TYPE_MUSIC, content_id_with_playqueue, sonos_speaker_name) + assert playback_mock.call_count == 4 + + # Test with speakers available and invalid playqueue + requests_mock.get("https://1.2.3.4:32400/playQueues/1235", status_code=404) + content_id_with_playqueue = '{"playqueue_id": 1235}' + with pytest.raises(HomeAssistantError) as excinfo: + play_on_sonos( + hass, MEDIA_TYPE_MUSIC, content_id_with_playqueue, sonos_speaker_name + ) + assert "PlayQueue '1235' could not be found" in str(excinfo.value) + assert playback_mock.call_count == 4 diff --git a/tests/fixtures/plex/media_100.xml b/tests/fixtures/plex/media_100.xml index e1326a4c862..88ad7048fc0 100644 --- a/tests/fixtures/plex/media_100.xml +++ b/tests/fixtures/plex/media_100.xml @@ -1 +1,7 @@ - + + + + + + + diff --git a/tests/fixtures/plex/playqueue_1234.xml b/tests/fixtures/plex/playqueue_1234.xml new file mode 100644 index 00000000000..837c2ffbc3c --- /dev/null +++ b/tests/fixtures/plex/playqueue_1234.xml @@ -0,0 +1,7 @@ + + + + + + + From d148f9aa85f75751a2c4b8b6bb1cbe7d479a6f8f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jan 2021 10:23:10 -0600 Subject: [PATCH 028/796] Update comfoconnect to use new fan entity model (#45593) --- homeassistant/components/comfoconnect/fan.py | 75 +++++++++----------- 1 file changed, 33 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index 18549c52d35..1d7b9c1c0af 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -1,5 +1,6 @@ """Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit.""" import logging +import math from pycomfoconnect import ( CMD_FAN_MODE_AWAY, @@ -9,21 +10,25 @@ from pycomfoconnect import ( SENSOR_FAN_SPEED_MODE, ) -from homeassistant.components.fan import ( - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, - SUPPORT_SET_SPEED, - FanEntity, -) +from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) from . import DOMAIN, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, ComfoConnectBridge _LOGGER = logging.getLogger(__name__) -SPEED_MAPPING = {0: SPEED_OFF, 1: SPEED_LOW, 2: SPEED_MEDIUM, 3: SPEED_HIGH} +CMD_MAPPING = { + 0: CMD_FAN_MODE_AWAY, + 1: CMD_FAN_MODE_LOW, + 2: CMD_FAN_MODE_MEDIUM, + 3: CMD_FAN_MODE_HIGH, +} + +SPEED_RANGE = (1, 3) # away is not included in speeds and instead mapped to off def setup_platform(hass, config, add_entities, discovery_info=None): @@ -89,50 +94,36 @@ class ComfoConnectFan(FanEntity): return SUPPORT_SET_SPEED @property - def speed(self): - """Return the current fan mode.""" - try: - speed = self._ccb.data[SENSOR_FAN_SPEED_MODE] - return SPEED_MAPPING[speed] - except KeyError: + def percentage(self) -> str: + """Return the current speed percentage.""" + speed = self._ccb.data[SENSOR_FAN_SPEED_MODE] + if speed is None: return None + return ranged_value_to_percentage(SPEED_RANGE, speed) - @property - def speed_list(self): - """List of available fan modes.""" - return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # def turn_on( self, speed: str = None, percentage=None, preset_mode=None, **kwargs ) -> None: """Turn on the fan.""" - if speed is None: - speed = SPEED_LOW - self.set_speed(speed) + self.set_percentage(percentage) def turn_off(self, **kwargs) -> None: """Turn off the fan (to away).""" - self.set_speed(SPEED_OFF) + self.set_percentage(0) - def set_speed(self, speed: str): - """Set fan speed.""" - _LOGGER.debug("Changing fan speed to %s", speed) + def set_percentage(self, percentage: int): + """Set fan speed percentage.""" + _LOGGER.debug("Changing fan speed percentage to %s", percentage) - if speed == SPEED_OFF: - self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_AWAY) - elif speed == SPEED_LOW: - self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_LOW) - elif speed == SPEED_MEDIUM: - self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_MEDIUM) - elif speed == SPEED_HIGH: - self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_HIGH) + if percentage is None: + cmd = CMD_FAN_MODE_LOW + elif percentage == 0: + cmd = CMD_FAN_MODE_AWAY + else: + speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) + cmd = CMD_MAPPING[speed] + + self._ccb.comfoconnect.cmd_rmi_request(cmd) # Update current mode self.schedule_update_ha_state() From e7ddaec4684162be366c0e79702d9ceb532a11d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jan 2021 10:25:08 -0600 Subject: [PATCH 029/796] Update esphome to use new fan entity model (#45590) --- homeassistant/components/esphome/fan.py | 67 +++++++++---------------- 1 file changed, 23 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index c7abac576e7..8da52b8d584 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -1,15 +1,11 @@ """Support for ESPHome fans.""" -from typing import List, Optional +from typing import Optional from aioesphomeapi import FanDirection, FanInfo, FanSpeed, FanState from homeassistant.components.fan import ( DIRECTION_FORWARD, DIRECTION_REVERSE, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, @@ -17,6 +13,10 @@ from homeassistant.components.fan import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util.percentage import ( + ordered_list_item_to_percentage, + percentage_to_ordered_list_item, +) from . import ( EsphomeEntity, @@ -25,6 +25,8 @@ from . import ( platform_async_setup_entry, ) +ORDERED_NAMED_FAN_SPEEDS = [FanSpeed.LOW, FanSpeed.MEDIUM, FanSpeed.HIGH] + async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities @@ -41,15 +43,6 @@ async def async_setup_entry( ) -@esphome_map_enum -def _fan_speeds(): - return { - FanSpeed.LOW: SPEED_LOW, - FanSpeed.MEDIUM: SPEED_MEDIUM, - FanSpeed.HIGH: SPEED_HIGH, - } - - @esphome_map_enum def _fan_directions(): return { @@ -69,23 +62,20 @@ class EsphomeFan(EsphomeEntity, FanEntity): def _state(self) -> Optional[FanState]: return super()._state - async def async_set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - if speed == SPEED_OFF: + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed percentage of the fan.""" + if percentage == 0: await self.async_turn_off() return - await self._client.fan_command( - self._static_info.key, speed=_fan_speeds.from_hass(speed) - ) + data = {"key": self._static_info.key, "state": True} + if percentage is not None: + named_speed = percentage_to_ordered_list_item( + ORDERED_NAMED_FAN_SPEEDS, percentage + ) + data["speed"] = named_speed + await self._client.fan_command(**data) - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # async def async_turn_on( self, speed: Optional[str] = None, @@ -94,13 +84,7 @@ class EsphomeFan(EsphomeEntity, FanEntity): **kwargs, ) -> None: """Turn on the fan.""" - if speed == SPEED_OFF: - await self.async_turn_off() - return - data = {"key": self._static_info.key, "state": True} - if speed is not None: - data["speed"] = _fan_speeds.from_hass(speed) - await self._client.fan_command(**data) + await self.async_set_percentage(percentage) async def async_turn_off(self, **kwargs) -> None: """Turn off the fan.""" @@ -127,11 +111,13 @@ class EsphomeFan(EsphomeEntity, FanEntity): return self._state.state @esphome_state_property - def speed(self) -> Optional[str]: - """Return the current speed.""" + def percentage(self) -> Optional[str]: + """Return the current speed percentage.""" if not self._static_info.supports_speed: return None - return _fan_speeds.from_esphome(self._state.speed) + return ordered_list_item_to_percentage( + ORDERED_NAMED_FAN_SPEEDS, self._state.speed + ) @esphome_state_property def oscillating(self) -> None: @@ -147,13 +133,6 @@ class EsphomeFan(EsphomeEntity, FanEntity): return None return _fan_directions.from_esphome(self._state.direction) - @property - def speed_list(self) -> Optional[List[str]]: - """Get the list of available speeds.""" - if not self._static_info.supports_speed: - return None - return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - @property def supported_features(self) -> int: """Flag supported features.""" From 8bcb4092df5d93930216af36e77d5ad0a27f0a91 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 28 Jan 2021 23:45:36 +0100 Subject: [PATCH 030/796] Fix removing nodes in zwave_js integration (#45676) --- homeassistant/components/zwave_js/__init__.py | 23 ++++++++++++++++++- homeassistant/components/zwave_js/api.py | 20 ---------------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 4cf5a50460e..c995749f924 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -1,6 +1,7 @@ """The Z-Wave JS integration.""" import asyncio import logging +from typing import Tuple from async_timeout import timeout from zwave_js_server.client import Client as ZwaveClient @@ -36,6 +37,12 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: return True +@callback +def get_device_id(client: ZwaveClient, node: ZwaveNode) -> Tuple[str, str]: + """Get device registry identifier for Z-Wave node.""" + return (DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}") + + @callback def register_node_in_dev_reg( hass: HomeAssistant, @@ -47,7 +54,7 @@ def register_node_in_dev_reg( """Register node in dev reg.""" device = dev_reg.async_get_or_create( config_entry_id=entry.entry_id, - identifiers={(DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}")}, + identifiers={get_device_id(client, node)}, sw_version=node.firmware_version, name=node.name or node.device_config.description or f"Node {node.node_id}", model=node.device_config.label, @@ -118,6 +125,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # some visual feedback that something is (in the process of) being added register_node_in_dev_reg(hass, entry, dev_reg, client, node) + @callback + def async_on_node_removed(node: ZwaveNode) -> None: + """Handle node removed event.""" + # grab device in device registry attached to this node + dev_id = get_device_id(client, node) + device = dev_reg.async_get_device({dev_id}) + # note: removal of entity registry is handled by core + dev_reg.async_remove_device(device.id) + async def handle_ha_shutdown(event: Event) -> None: """Handle HA shutdown.""" await client.disconnect() @@ -171,6 +187,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: client.driver.controller.on( "node added", lambda event: async_on_node_added(event["node"]) ) + # listen for nodes being removed from the mesh + # NOTE: This will not remove nodes that were removed when HA was not running + client.driver.controller.on( + "node removed", lambda event: async_on_node_removed(event["node"]) + ) hass.async_create_task(start_platforms()) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 2027200d8b7..1a8a197571b 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -5,15 +5,12 @@ import logging from aiohttp import hdrs, web, web_exceptions import voluptuous as vol from zwave_js_server import dump -from zwave_js_server.client import Client as ZwaveClient -from zwave_js_server.model.node import Node as ZwaveNode from homeassistant.components import websocket_api from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import device_registry from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -248,9 +245,6 @@ async def websocket_remove_node( "node_id": node.node_id, } - # Remove from device registry - hass.async_create_task(remove_from_device_registry(hass, client, node)) - connection.send_message( websocket_api.event_message( msg[ID], {"event": "node removed", "node": node_details} @@ -272,20 +266,6 @@ async def websocket_remove_node( ) -async def remove_from_device_registry( - hass: HomeAssistant, client: ZwaveClient, node: ZwaveNode -) -> None: - """Remove a node from the device registry.""" - registry = await device_registry.async_get_registry(hass) - device = registry.async_get_device( - {(DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}")} - ) - if device is None: - return - - registry.async_remove_device(device.id) - - class DumpView(HomeAssistantView): """View to dump the state of the Z-Wave JS server.""" From 7bc8060122ef26afebc7608d7fd4c085e8bdd8fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20L=C3=B6hr?= Date: Fri, 29 Jan 2021 00:08:59 +0100 Subject: [PATCH 031/796] Add reauthentication flow to fritzbox integration (#45587) --- homeassistant/components/fritzbox/__init__.py | 18 +- .../components/fritzbox/config_flow.py | 50 ++++++ .../components/fritzbox/strings.json | 12 +- tests/components/fritzbox/test_config_flow.py | 162 +++++++++++++----- 4 files changed, 198 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 7297f514f96..3c01657da4e 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -2,9 +2,10 @@ import asyncio import socket -from pyfritzhome import Fritzhome +from pyfritzhome import Fritzhome, LoginError import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH from homeassistant.const import ( CONF_DEVICES, CONF_HOST, @@ -62,7 +63,7 @@ async def async_setup(hass, config): for entry_config in config[DOMAIN][CONF_DEVICES]: hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"}, data=entry_config + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_config ) ) @@ -76,7 +77,18 @@ async def async_setup_entry(hass, entry): user=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], ) - await hass.async_add_executor_job(fritz.login) + + try: + await hass.async_add_executor_job(fritz.login) + except LoginError: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data=entry, + ) + ) + return False hass.data.setdefault(DOMAIN, {CONF_CONNECTIONS: {}, CONF_DEVICES: set()}) hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] = fritz diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index ee1b3aff241..f54211aa8a2 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -47,6 +47,7 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize flow.""" + self._entry = None self._host = None self._name = None self._password = None @@ -62,6 +63,17 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): }, ) + async def _update_entry(self): + self.hass.config_entries.async_update_entry( + self._entry, + data={ + CONF_HOST: self._host, + CONF_PASSWORD: self._password, + CONF_USERNAME: self._username, + }, + ) + await self.hass.config_entries.async_reload(self._entry.entry_id) + def _try_connect(self): """Try to connect and check auth.""" fritzbox = Fritzhome( @@ -160,3 +172,41 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders={"name": self._name}, errors=errors, ) + + async def async_step_reauth(self, entry): + """Trigger a reauthentication flow.""" + self._entry = entry + self._host = entry.data[CONF_HOST] + self._name = entry.data[CONF_HOST] + self._username = entry.data[CONF_USERNAME] + + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Handle reauthorization flow.""" + errors = {} + + if user_input is not None: + self._password = user_input[CONF_PASSWORD] + self._username = user_input[CONF_USERNAME] + + result = await self.hass.async_add_executor_job(self._try_connect) + + if result == RESULT_SUCCESS: + await self._update_entry() + return self.async_abort(reason="reauth_successful") + if result != RESULT_INVALID_AUTH: + return self.async_abort(reason=result) + errors["base"] = result + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_USERNAME, default=self._username): str, + vol.Required(CONF_PASSWORD): str, + } + ), + description_placeholders={"name": self._name}, + errors=errors, + ) diff --git a/homeassistant/components/fritzbox/strings.json b/homeassistant/components/fritzbox/strings.json index 141348583f4..6de6b6d9d9a 100644 --- a/homeassistant/components/fritzbox/strings.json +++ b/homeassistant/components/fritzbox/strings.json @@ -16,16 +16,24 @@ "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth_confirm": { + "description": "Update your login information for {name}.", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "abort": { "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", - "not_supported": "Connected to AVM FRITZ!Box but it's unable to control Smart Home devices." + "not_supported": "Connected to AVM FRITZ!Box but it's unable to control Smart Home devices.", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" } } -} +} \ No newline at end of file diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index 31a9f89ce48..f07a78e30de 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -12,11 +12,19 @@ from homeassistant.components.ssdp import ( ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_UDN, ) +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) from homeassistant.helpers.typing import HomeAssistantType from . import MOCK_CONFIG +from tests.common import MockConfigEntry + MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] MOCK_SSDP_DATA = { ATTR_SSDP_LOCATION: "https://fake_host:12345/test", @@ -35,15 +43,15 @@ def fritz_fixture() -> Mock: async def test_user(hass: HomeAssistantType, fritz: Mock): """Test starting a flow by user.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == "create_entry" + assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == "fake_host" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_PASSWORD] == "fake_pass" @@ -56,9 +64,9 @@ async def test_user_auth_failed(hass: HomeAssistantType, fritz: Mock): fritz().login.side_effect = [LoginError("Boom"), mock.DEFAULT] result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "form" + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -68,33 +76,109 @@ async def test_user_not_successful(hass: HomeAssistantType, fritz: Mock): fritz().login.side_effect = OSError("Boom") result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "abort" + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "no_devices_found" async def test_user_already_configured(hass: HomeAssistantType, fritz: Mock): """Test starting a flow by user when already configured.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "create_entry" + assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert not result["result"].unique_id result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "abort" + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" +async def test_reauth_success(hass: HomeAssistantType, fritz: Mock): + """Test starting a reauthentication flow.""" + mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_REAUTH}, data=mock_config + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "other_fake_user", + CONF_PASSWORD: "other_fake_password", + }, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + assert mock_config.data[CONF_USERNAME] == "other_fake_user" + assert mock_config.data[CONF_PASSWORD] == "other_fake_password" + + +async def test_reauth_auth_failed(hass: HomeAssistantType, fritz: Mock): + """Test starting a reauthentication flow with authentication failure.""" + fritz().login.side_effect = LoginError("Boom") + + mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_REAUTH}, data=mock_config + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "other_fake_user", + CONF_PASSWORD: "other_fake_password", + }, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"]["base"] == "invalid_auth" + + +async def test_reauth_not_successful(hass: HomeAssistantType, fritz: Mock): + """Test starting a reauthentication flow but no connection found.""" + fritz().login.side_effect = OSError("Boom") + + mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_REAUTH}, data=mock_config + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "other_fake_user", + CONF_PASSWORD: "other_fake_password", + }, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "no_devices_found" + + async def test_import(hass: HomeAssistantType, fritz: Mock): """Test starting a flow by import.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "import"}, data=MOCK_USER_DATA ) - assert result["type"] == "create_entry" + assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == "fake_host" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_PASSWORD] == "fake_pass" @@ -105,16 +189,16 @@ async def test_import(hass: HomeAssistantType, fritz: Mock): async def test_ssdp(hass: HomeAssistantType, fritz: Mock): """Test starting a flow from discovery.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "form" + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "fake_pass", CONF_USERNAME: "fake_user"}, ) - assert result["type"] == "create_entry" + assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == "fake_name" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_PASSWORD] == "fake_pass" @@ -127,16 +211,16 @@ async def test_ssdp_no_friendly_name(hass: HomeAssistantType, fritz: Mock): MOCK_NO_NAME = MOCK_SSDP_DATA.copy() del MOCK_NO_NAME[ATTR_UPNP_FRIENDLY_NAME] result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "ssdp"}, data=MOCK_NO_NAME + DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_NO_NAME ) - assert result["type"] == "form" + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "fake_pass", CONF_USERNAME: "fake_user"}, ) - assert result["type"] == "create_entry" + assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == "fake_host" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_PASSWORD] == "fake_pass" @@ -149,9 +233,9 @@ async def test_ssdp_auth_failed(hass: HomeAssistantType, fritz: Mock): fritz().login.side_effect = LoginError("Boom") result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "form" + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -159,7 +243,7 @@ async def test_ssdp_auth_failed(hass: HomeAssistantType, fritz: Mock): result["flow_id"], user_input={CONF_PASSWORD: "whatever", CONF_USERNAME: "whatever"}, ) - assert result["type"] == "form" + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "confirm" assert result["errors"]["base"] == "invalid_auth" @@ -169,16 +253,16 @@ async def test_ssdp_not_successful(hass: HomeAssistantType, fritz: Mock): fritz().login.side_effect = OSError("Boom") result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "form" + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "whatever", CONF_USERNAME: "whatever"}, ) - assert result["type"] == "abort" + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "no_devices_found" @@ -187,62 +271,62 @@ async def test_ssdp_not_supported(hass: HomeAssistantType, fritz: Mock): fritz().get_device_elements.side_effect = HTTPError("Boom") result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "form" + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "whatever", CONF_USERNAME: "whatever"}, ) - assert result["type"] == "abort" + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "not_supported" async def test_ssdp_already_in_progress_unique_id(hass: HomeAssistantType, fritz: Mock): """Test starting a flow from discovery twice.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "form" + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "abort" + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_in_progress" async def test_ssdp_already_in_progress_host(hass: HomeAssistantType, fritz: Mock): """Test starting a flow from discovery twice.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "form" + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "confirm" MOCK_NO_UNIQUE_ID = MOCK_SSDP_DATA.copy() del MOCK_NO_UNIQUE_ID[ATTR_UPNP_UDN] result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "ssdp"}, data=MOCK_NO_UNIQUE_ID + DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_NO_UNIQUE_ID ) - assert result["type"] == "abort" + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_in_progress" async def test_ssdp_already_configured(hass: HomeAssistantType, fritz: Mock): """Test starting a flow from discovery when already configured.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "create_entry" + assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert not result["result"].unique_id result2 = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result2["type"] == "abort" + assert result2["type"] == RESULT_TYPE_ABORT assert result2["reason"] == "already_configured" assert result["result"].unique_id == "only-a-test" From 73cce8e8e226f5a911ee7e747196d7b4f3674ca7 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 28 Jan 2021 16:32:21 -0700 Subject: [PATCH 032/796] Replace strange "dict logic" in AirVisual pollutant level sensors (2 of 2) (#44903) * Replace strange "dict logic" in AirVisual main pollutant sensor * Move methods outside of class * Cleanup --- homeassistant/components/airvisual/sensor.py | 47 +++++++++++++++----- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 49a05272488..ae9995f36c3 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -58,14 +58,23 @@ NODE_PRO_SENSORS = [ (SENSOR_KIND_TEMPERATURE, "Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS), ] -POLLUTANT_MAPPING = { - "co": {"label": "Carbon Monoxide", "unit": CONCENTRATION_PARTS_PER_MILLION}, - "n2": {"label": "Nitrogen Dioxide", "unit": CONCENTRATION_PARTS_PER_BILLION}, - "o3": {"label": "Ozone", "unit": CONCENTRATION_PARTS_PER_BILLION}, - "p1": {"label": "PM10", "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, - "p2": {"label": "PM2.5", "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, - "s2": {"label": "Sulfur Dioxide", "unit": CONCENTRATION_PARTS_PER_BILLION}, -} + +@callback +def async_get_pollutant_label(symbol): + """Get a pollutant's label based on its symbol.""" + if symbol == "co": + return "Carbon Monoxide" + if symbol == "n2": + return "Nitrogen Dioxide" + if symbol == "o3": + return "Ozone" + if symbol == "p1": + return "PM10" + if symbol == "p2": + return "PM2.5" + if symbol == "s2": + return "Sulfur Dioxide" + return symbol @callback @@ -84,6 +93,24 @@ def async_get_pollutant_level_info(value): return ("Hazardous", "mdi:biohazard") +@callback +def async_get_pollutant_unit(symbol): + """Get a pollutant's unit based on its symbol.""" + if symbol == "co": + return CONCENTRATION_PARTS_PER_MILLION + if symbol == "n2": + return CONCENTRATION_PARTS_PER_BILLION + if symbol == "o3": + return CONCENTRATION_PARTS_PER_BILLION + if symbol == "p1": + return CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + if symbol == "p2": + return CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + if symbol == "s2": + return CONCENTRATION_PARTS_PER_BILLION + return None + + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up AirVisual sensors based on a config entry.""" coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] @@ -173,11 +200,11 @@ class AirVisualGeographySensor(AirVisualEntity): self._state = data[f"aqi{self._locale}"] elif self._kind == SENSOR_KIND_POLLUTANT: symbol = data[f"main{self._locale}"] - self._state = POLLUTANT_MAPPING[symbol]["label"] + self._state = async_get_pollutant_label(symbol) self._attrs.update( { ATTR_POLLUTANT_SYMBOL: symbol, - ATTR_POLLUTANT_UNIT: POLLUTANT_MAPPING[symbol]["unit"], + ATTR_POLLUTANT_UNIT: async_get_pollutant_unit(symbol), } ) From 1edae8cd48799b92fadae3ec9632bdc133f7fdd3 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 28 Jan 2021 16:39:06 -0700 Subject: [PATCH 033/796] Add last_lost_timestamp attribute to Tile (#45681) --- homeassistant/components/tile/device_tracker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index ae3852a2b07..f7cc4e1736e 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -16,9 +16,10 @@ ATTR_ALTITUDE = "altitude" ATTR_CONNECTION_STATE = "connection_state" ATTR_IS_DEAD = "is_dead" ATTR_IS_LOST = "is_lost" +ATTR_LAST_LOST_TIMESTAMP = "last_lost_timestamp" ATTR_RING_STATE = "ring_state" -ATTR_VOIP_STATE = "voip_state" ATTR_TILE_NAME = "tile_name" +ATTR_VOIP_STATE = "voip_state" DEFAULT_ATTRIBUTION = "Data provided by Tile" DEFAULT_ICON = "mdi:view-grid" @@ -135,6 +136,7 @@ class TileDeviceTracker(CoordinatorEntity, TrackerEntity): { ATTR_ALTITUDE: self._tile.altitude, ATTR_IS_LOST: self._tile.lost, + ATTR_LAST_LOST_TIMESTAMP: self._tile.lost_timestamp, ATTR_RING_STATE: self._tile.ring_state, ATTR_VOIP_STATE: self._tile.voip_state, } From eb370e9494a4eceea64eccd49e7620f2eb5366a8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 29 Jan 2021 00:46:28 +0100 Subject: [PATCH 034/796] Update frontend to 20210127.3 (#45679) --- 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 65ddab7f947..6f05ab04e6f 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==20210127.1"], + "requirements": ["home-assistant-frontend==20210127.3"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f51b0e00e14..30927b9cd32 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.41.0 -home-assistant-frontend==20210127.1 +home-assistant-frontend==20210127.3 httpx==0.16.1 jinja2>=2.11.2 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 7f4b49b1577..0b2e3895cb4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.10.4 # homeassistant.components.frontend -home-assistant-frontend==20210127.1 +home-assistant-frontend==20210127.3 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3abd55baec2..257ba4c58f8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -402,7 +402,7 @@ hole==0.5.1 holidays==0.10.4 # homeassistant.components.frontend -home-assistant-frontend==20210127.1 +home-assistant-frontend==20210127.3 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 8b3156ea82bd38f753a060de2984479f39b4aaa5 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Fri, 29 Jan 2021 03:14:39 +0100 Subject: [PATCH 035/796] Use new fixtures in devolo Home Control tests (#45669) --- .../devolo_home_control/test_config_flow.py | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py index dd856d2e6b5..7d2c9ce40f6 100644 --- a/tests/components/devolo_home_control/test_config_flow.py +++ b/tests/components/devolo_home_control/test_config_flow.py @@ -1,6 +1,8 @@ """Test the devolo_home_control config flow.""" from unittest.mock import patch +import pytest + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.devolo_home_control.const import DOMAIN from homeassistant.config_entries import SOURCE_USER @@ -24,9 +26,6 @@ async def test_form(hass): "homeassistant.components.devolo_home_control.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "homeassistant.components.devolo_home_control.config_flow.Mydevolo.credentials_valid", - return_value=True, - ), patch( "homeassistant.components.devolo_home_control.config_flow.Mydevolo.uuid", return_value="123456", ): @@ -48,6 +47,7 @@ async def test_form(hass): assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.credentials_invalid async def test_form_invalid_credentials(hass): """Test if we get the error message on invalid credentials.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -57,16 +57,12 @@ async def test_form_invalid_credentials(hass): assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.devolo_home_control.config_flow.Mydevolo.credentials_valid", - return_value=False, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"username": "test-username", "password": "test-password"}, - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "test-username", "password": "test-password"}, + ) - assert result["errors"] == {"base": "invalid_auth"} + assert result["errors"] == {"base": "invalid_auth"} async def test_form_already_configured(hass): @@ -74,9 +70,6 @@ async def test_form_already_configured(hass): with patch( "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="123456", data={}).add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -103,9 +96,6 @@ async def test_form_advanced_options(hass): "homeassistant.components.devolo_home_control.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "homeassistant.components.devolo_home_control.config_flow.Mydevolo.credentials_valid", - return_value=True, - ), patch( "homeassistant.components.devolo_home_control.config_flow.Mydevolo.uuid", return_value="123456", ): From c7db2c35b75344c89472e68dd6d11d8f6d6657b6 Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 29 Jan 2021 08:55:51 +0100 Subject: [PATCH 036/796] Add vicare heat pump sensors (#41413) --- .../components/vicare/binary_sensor.py | 36 ++++++++++++- homeassistant/components/vicare/sensor.py | 52 +++++++++++++++++-- 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vicare/binary_sensor.py b/homeassistant/components/vicare/binary_sensor.py index 9ae615a6367..b7e926b2379 100644 --- a/homeassistant/components/vicare/binary_sensor.py +++ b/homeassistant/components/vicare/binary_sensor.py @@ -23,8 +23,16 @@ _LOGGER = logging.getLogger(__name__) CONF_GETTER = "getter" SENSOR_CIRCULATION_PUMP_ACTIVE = "circulationpump_active" + +# gas sensors SENSOR_BURNER_ACTIVE = "burner_active" + +# heatpump sensors SENSOR_COMPRESSOR_ACTIVE = "compressor_active" +SENSOR_HEATINGROD_OVERALL = "heatingrod_overall" +SENSOR_HEATINGROD_LEVEL1 = "heatingrod_level1" +SENSOR_HEATINGROD_LEVEL2 = "heatingrod_level2" +SENSOR_HEATINGROD_LEVEL3 = "heatingrod_level3" SENSOR_TYPES = { SENSOR_CIRCULATION_PUMP_ACTIVE: { @@ -44,13 +52,39 @@ SENSOR_TYPES = { CONF_DEVICE_CLASS: DEVICE_CLASS_POWER, CONF_GETTER: lambda api: api.getCompressorActive(), }, + SENSOR_HEATINGROD_OVERALL: { + CONF_NAME: "Heating rod overall", + CONF_DEVICE_CLASS: DEVICE_CLASS_POWER, + CONF_GETTER: lambda api: api.getHeatingRodStatusOverall(), + }, + SENSOR_HEATINGROD_LEVEL1: { + CONF_NAME: "Heating rod level 1", + CONF_DEVICE_CLASS: DEVICE_CLASS_POWER, + CONF_GETTER: lambda api: api.getHeatingRodStatusLevel1(), + }, + SENSOR_HEATINGROD_LEVEL2: { + CONF_NAME: "Heating rod level 2", + CONF_DEVICE_CLASS: DEVICE_CLASS_POWER, + CONF_GETTER: lambda api: api.getHeatingRodStatusLevel2(), + }, + SENSOR_HEATINGROD_LEVEL3: { + CONF_NAME: "Heating rod level 3", + CONF_DEVICE_CLASS: DEVICE_CLASS_POWER, + CONF_GETTER: lambda api: api.getHeatingRodStatusLevel3(), + }, } SENSORS_GENERIC = [SENSOR_CIRCULATION_PUMP_ACTIVE] SENSORS_BY_HEATINGTYPE = { HeatingType.gas: [SENSOR_BURNER_ACTIVE], - HeatingType.heatpump: [SENSOR_COMPRESSOR_ACTIVE], + HeatingType.heatpump: [ + SENSOR_COMPRESSOR_ACTIVE, + SENSOR_HEATINGROD_OVERALL, + SENSOR_HEATINGROD_LEVEL1, + SENSOR_HEATINGROD_LEVEL2, + SENSOR_HEATINGROD_LEVEL3, + ], } diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 5e14795d540..a14e00923c2 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, PERCENTAGE, TEMP_CELSIUS, + TIME_HOURS, ) from homeassistant.helpers.entity import Entity @@ -52,6 +53,11 @@ SENSOR_GAS_CONSUMPTION_THIS_YEAR = "gas_consumption_heating_this_year" # heatpump sensors SENSOR_COMPRESSOR_STARTS = "compressor_starts" SENSOR_COMPRESSOR_HOURS = "compressor_hours" +SENSOR_COMPRESSOR_HOURS_LOADCLASS1 = "compressor_hours_loadclass1" +SENSOR_COMPRESSOR_HOURS_LOADCLASS2 = "compressor_hours_loadclass2" +SENSOR_COMPRESSOR_HOURS_LOADCLASS3 = "compressor_hours_loadclass3" +SENSOR_COMPRESSOR_HOURS_LOADCLASS4 = "compressor_hours_loadclass4" +SENSOR_COMPRESSOR_HOURS_LOADCLASS5 = "compressor_hours_loadclass5" SENSOR_TYPES = { SENSOR_OUTSIDE_TEMPERATURE: { @@ -149,7 +155,7 @@ SENSOR_TYPES = { SENSOR_BURNER_HOURS: { CONF_NAME: "Burner Hours", CONF_ICON: "mdi:counter", - CONF_UNIT_OF_MEASUREMENT: None, + CONF_UNIT_OF_MEASUREMENT: TIME_HOURS, CONF_GETTER: lambda api: api.getBurnerHours(), CONF_DEVICE_CLASS: None, }, @@ -164,10 +170,45 @@ SENSOR_TYPES = { SENSOR_COMPRESSOR_HOURS: { CONF_NAME: "Compressor Hours", CONF_ICON: "mdi:counter", - CONF_UNIT_OF_MEASUREMENT: None, + CONF_UNIT_OF_MEASUREMENT: TIME_HOURS, CONF_GETTER: lambda api: api.getCompressorHours(), CONF_DEVICE_CLASS: None, }, + SENSOR_COMPRESSOR_HOURS_LOADCLASS1: { + CONF_NAME: "Compressor Hours Load Class 1", + CONF_ICON: "mdi:counter", + CONF_UNIT_OF_MEASUREMENT: TIME_HOURS, + CONF_GETTER: lambda api: api.getCompressorHoursLoadClass1(), + CONF_DEVICE_CLASS: None, + }, + SENSOR_COMPRESSOR_HOURS_LOADCLASS2: { + CONF_NAME: "Compressor Hours Load Class 2", + CONF_ICON: "mdi:counter", + CONF_UNIT_OF_MEASUREMENT: TIME_HOURS, + CONF_GETTER: lambda api: api.getCompressorHoursLoadClass2(), + CONF_DEVICE_CLASS: None, + }, + SENSOR_COMPRESSOR_HOURS_LOADCLASS3: { + CONF_NAME: "Compressor Hours Load Class 3", + CONF_ICON: "mdi:counter", + CONF_UNIT_OF_MEASUREMENT: TIME_HOURS, + CONF_GETTER: lambda api: api.getCompressorHoursLoadClass3(), + CONF_DEVICE_CLASS: None, + }, + SENSOR_COMPRESSOR_HOURS_LOADCLASS4: { + CONF_NAME: "Compressor Hours Load Class 4", + CONF_ICON: "mdi:counter", + CONF_UNIT_OF_MEASUREMENT: TIME_HOURS, + CONF_GETTER: lambda api: api.getCompressorHoursLoadClass4(), + CONF_DEVICE_CLASS: None, + }, + SENSOR_COMPRESSOR_HOURS_LOADCLASS5: { + CONF_NAME: "Compressor Hours Load Class 5", + CONF_ICON: "mdi:counter", + CONF_UNIT_OF_MEASUREMENT: TIME_HOURS, + CONF_GETTER: lambda api: api.getCompressorHoursLoadClass5(), + CONF_DEVICE_CLASS: None, + }, SENSOR_RETURN_TEMPERATURE: { CONF_NAME: "Return Temperature", CONF_ICON: None, @@ -195,8 +236,13 @@ SENSORS_BY_HEATINGTYPE = { SENSOR_GAS_CONSUMPTION_THIS_YEAR, ], HeatingType.heatpump: [ - SENSOR_COMPRESSOR_HOURS, SENSOR_COMPRESSOR_STARTS, + SENSOR_COMPRESSOR_HOURS, + SENSOR_COMPRESSOR_HOURS_LOADCLASS1, + SENSOR_COMPRESSOR_HOURS_LOADCLASS2, + SENSOR_COMPRESSOR_HOURS_LOADCLASS3, + SENSOR_COMPRESSOR_HOURS_LOADCLASS4, + SENSOR_COMPRESSOR_HOURS_LOADCLASS5, SENSOR_RETURN_TEMPERATURE, ], } From 4f3b10d66110ba327cdf41a665a2bdb7e6ae1a0c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 29 Jan 2021 00:57:36 -0700 Subject: [PATCH 037/796] Stop Tile setup on invalid auth (#45683) --- homeassistant/components/tile/__init__.py | 5 ++++- homeassistant/components/tile/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index aceed9aa7ee..205742017d3 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -4,7 +4,7 @@ from datetime import timedelta from functools import partial from pytile import async_login -from pytile.errors import SessionExpiredError, TileError +from pytile.errors import InvalidAuthError, SessionExpiredError, TileError from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.exceptions import ConfigEntryNotReady @@ -43,6 +43,9 @@ async def async_setup_entry(hass, entry): session=websession, ) hass.data[DOMAIN][DATA_TILE][entry.entry_id] = await client.async_get_tiles() + except InvalidAuthError: + LOGGER.error("Invalid credentials provided") + return False except TileError as err: raise ConfigEntryNotReady("Error during integration setup") from err diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index 854fc663ba2..194fc49418a 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -3,6 +3,6 @@ "name": "Tile", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tile", - "requirements": ["pytile==5.1.0"], + "requirements": ["pytile==5.2.0"], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0b2e3895cb4..25edf3e7bfb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1849,7 +1849,7 @@ python_opendata_transport==0.2.1 pythonegardia==1.0.40 # homeassistant.components.tile -pytile==5.1.0 +pytile==5.2.0 # homeassistant.components.touchline pytouchline==0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 257ba4c58f8..e0d99bc8574 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -929,7 +929,7 @@ python-velbus==2.1.2 python_awair==0.2.1 # homeassistant.components.tile -pytile==5.1.0 +pytile==5.2.0 # homeassistant.components.traccar pytraccar==0.9.0 From c66a892233042b5c1a721fd932008e6327e95f76 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Fri, 29 Jan 2021 00:05:00 -0800 Subject: [PATCH 038/796] Remove YAML support from Hyperion integration (#45690) --- homeassistant/components/hyperion/__init__.py | 4 - .../components/hyperion/config_flow.py | 8 - homeassistant/components/hyperion/light.py | 158 +---------------- tests/components/hyperion/__init__.py | 3 - tests/components/hyperion/test_config_flow.py | 57 +------ tests/components/hyperion/test_light.py | 159 +----------------- 6 files changed, 6 insertions(+), 383 deletions(-) diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py index aeac922826d..b3606880f8c 100644 --- a/homeassistant/components/hyperion/__init__.py +++ b/homeassistant/components/hyperion/__init__.py @@ -278,10 +278,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b } ) - # Must only listen for option updates after the setup is complete, as otherwise - # the YAML->ConfigEntry migration code triggers an options update, which causes a - # reload -- which clashes with the initial load (causing entity_id / unique_id - # clashes). async def setup_then_listen() -> None: await asyncio.gather( *[ diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py index 11ab3289d14..f4528b0efbe 100644 --- a/homeassistant/components/hyperion/config_flow.py +++ b/homeassistant/components/hyperion/config_flow.py @@ -140,14 +140,6 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_auth() return await self.async_step_confirm() - async def async_step_import(self, import_data: ConfigType) -> Dict[str, Any]: - """Handle a flow initiated by a YAML config import.""" - self._data.update(import_data) - async with self._create_client(raw_connection=True) as hyperion_client: - if not hyperion_client: - return self.async_abort(reason="cannot_connect") - return await self._advance_to_auth_step_if_necessary(hyperion_client) - async def async_step_reauth( self, config_data: ConfigType, diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index a329ee5c20e..ce672194b9a 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -2,47 +2,30 @@ from __future__ import annotations import logging -import re from types import MappingProxyType from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple from hyperion import client, const -import voluptuous as vol -from homeassistant import data_entry_flow from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_HS_COLOR, - DOMAIN as LIGHT_DOMAIN, - PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_EFFECT, LightEntity, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback -from homeassistant.exceptions import PlatformNotReady -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity_registry import async_get_registry -from homeassistant.helpers.typing import ( - ConfigType, - DiscoveryInfoType, - HomeAssistantType, -) +from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.color as color_util -from . import ( - create_hyperion_client, - get_hyperion_unique_id, - listen_for_instance_updates, -) +from . import get_hyperion_unique_id, listen_for_instance_updates from .const import ( CONF_INSTANCE_CLIENTS, CONF_PRIORITY, @@ -73,8 +56,6 @@ CONF_EFFECT_LIST = "effect_list" # showing a solid color. This is the same method used by WLED. KEY_EFFECT_SOLID = "Solid" -KEY_ENTRY_ID_YAML = "YAML" - DEFAULT_COLOR = [255, 255, 255] DEFAULT_BRIGHTNESS = 255 DEFAULT_EFFECT = KEY_EFFECT_SOLID @@ -85,144 +66,11 @@ DEFAULT_EFFECT_LIST: List[str] = [] SUPPORT_HYPERION = SUPPORT_COLOR | SUPPORT_BRIGHTNESS | SUPPORT_EFFECT -# Usage of YAML for configuration of the Hyperion component is deprecated. -PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_HDMI_PRIORITY), - cv.deprecated(CONF_HOST), - cv.deprecated(CONF_PORT), - cv.deprecated(CONF_DEFAULT_COLOR), - cv.deprecated(CONF_NAME), - cv.deprecated(CONF_PRIORITY), - cv.deprecated(CONF_EFFECT_LIST), - PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_DEFAULT_COLOR, default=DEFAULT_COLOR): vol.All( - list, - vol.Length(min=3, max=3), - [vol.All(vol.Coerce(int), vol.Range(min=0, max=255))], - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PRIORITY, default=DEFAULT_PRIORITY): cv.positive_int, - vol.Optional( - CONF_HDMI_PRIORITY, default=DEFAULT_HDMI_PRIORITY - ): cv.positive_int, - vol.Optional(CONF_EFFECT_LIST, default=DEFAULT_EFFECT_LIST): vol.All( - cv.ensure_list, [cv.string] - ), - } - ), -) - ICON_LIGHTBULB = "mdi:lightbulb" ICON_EFFECT = "mdi:lava-lamp" ICON_EXTERNAL_SOURCE = "mdi:television-ambient-light" -async def async_setup_platform( - hass: HomeAssistantType, - config: ConfigType, - async_add_entities: Callable, - discovery_info: Optional[DiscoveryInfoType] = None, -) -> None: - """Set up Hyperion platform..""" - - # This is the entrypoint for the old YAML-style Hyperion integration. The goal here - # is to auto-convert the YAML configuration into a config entry, with no human - # interaction, preserving the entity_id. This should be possible, as the YAML - # configuration did not support any of the things that should otherwise require - # human interaction in the config flow (e.g. it did not support auth). - - host = config[CONF_HOST] - port = config[CONF_PORT] - instance = 0 # YAML only supports a single instance. - - # First, connect to the server and get the server id (which will be unique_id on a config_entry - # if there is one). - async with create_hyperion_client(host, port) as hyperion_client: - if not hyperion_client: - raise PlatformNotReady - hyperion_id = await hyperion_client.async_sysinfo_id() - if not hyperion_id: - raise PlatformNotReady - - future_unique_id = get_hyperion_unique_id( - hyperion_id, instance, TYPE_HYPERION_LIGHT - ) - - # Possibility 1: Already converted. - # There is already a config entry with the unique id reporting by the - # server. Nothing to do here. - for entry in hass.config_entries.async_entries(domain=DOMAIN): - if entry.unique_id == hyperion_id: - return - - # Possibility 2: Upgraded to the new Hyperion component pre-config-flow. - # No config entry for this unique_id, but have an entity_registry entry - # with an old-style unique_id: - # :- (instance will always be 0, as YAML - # configuration does not support multiple - # instances) - # The unique_id needs to be updated, then the config_flow should do the rest. - registry = await async_get_registry(hass) - for entity_id, entity in registry.entities.items(): - if entity.config_entry_id is not None or entity.platform != DOMAIN: - continue - result = re.search(rf"([^:]+):(\d+)-{instance}", entity.unique_id) - if result and result.group(1) == host and int(result.group(2)) == port: - registry.async_update_entity(entity_id, new_unique_id=future_unique_id) - break - else: - # Possibility 3: This is the first upgrade to the new Hyperion component. - # No config entry and no entity_registry entry, in which case the CONF_NAME - # variable will be used as the preferred name. Rather than pollute the config - # entry with a "suggested name" type variable, instead create an entry in the - # registry that will subsequently be used when the entity is created with this - # unique_id. - - # This also covers the case that should not occur in the wild (no config entry, - # but new style unique_id). - registry.async_get_or_create( - domain=LIGHT_DOMAIN, - platform=DOMAIN, - unique_id=future_unique_id, - suggested_object_id=config[CONF_NAME], - ) - - async def migrate_yaml_to_config_entry_and_options( - host: str, port: int, priority: int - ) -> None: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_HOST: host, - CONF_PORT: port, - }, - ) - if ( - result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY - or result.get("result") is None - ): - _LOGGER.warning( - "Could not automatically migrate Hyperion YAML to a config entry." - ) - return - config_entry = result.get("result") - options = {**config_entry.options, CONF_PRIORITY: config[CONF_PRIORITY]} - hass.config_entries.async_update_entry(config_entry, options=options) - - _LOGGER.info( - "Successfully migrated Hyperion YAML configuration to a config entry." - ) - - # Kick off a config flow to create the config entry. - hass.async_create_task( - migrate_yaml_to_config_entry_and_options(host, port, config[CONF_PRIORITY]) - ) - - async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable ) -> bool: diff --git a/tests/components/hyperion/__init__.py b/tests/components/hyperion/__init__.py index e427cf46a83..f3b2ad383bd 100644 --- a/tests/components/hyperion/__init__.py +++ b/tests/components/hyperion/__init__.py @@ -9,7 +9,6 @@ from unittest.mock import AsyncMock, Mock, patch from hyperion import const from homeassistant.components.hyperion.const import CONF_PRIORITY, DOMAIN -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.helpers.typing import HomeAssistantType @@ -24,8 +23,6 @@ TEST_ID = "default" TEST_SYSINFO_ID = "f9aab089-f85a-55cf-b7c1-222a72faebe9" TEST_SYSINFO_VERSION = "2.0.0-alpha.8" TEST_PRIORITY = 180 -TEST_YAML_NAME = f"{TEST_HOST}_{TEST_PORT}_{TEST_INSTANCE}" -TEST_YAML_ENTITY_ID = f"{LIGHT_DOMAIN}.{TEST_YAML_NAME}" TEST_ENTITY_ID_1 = "light.test_instance_1" TEST_ENTITY_ID_2 = "light.test_instance_2" TEST_ENTITY_ID_3 = "light.test_instance_3" diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py index 776d5b3b25b..ef7046660d6 100644 --- a/tests/components/hyperion/test_config_flow.py +++ b/tests/components/hyperion/test_config_flow.py @@ -14,12 +14,7 @@ from homeassistant.components.hyperion.const import ( DOMAIN, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.config_entries import ( - SOURCE_IMPORT, - SOURCE_REAUTH, - SOURCE_SSDP, - SOURCE_USER, -) +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, @@ -606,56 +601,6 @@ async def test_ssdp_abort_duplicates(hass: HomeAssistantType) -> None: assert result_2["reason"] == "already_in_progress" -async def test_import_success(hass: HomeAssistantType) -> None: - """Check an import flow from the old-style YAML.""" - - client = create_mock_client() - with patch( - "homeassistant.components.hyperion.client.HyperionClient", return_value=client - ): - result = await _init_flow( - hass, - source=SOURCE_IMPORT, - data={ - CONF_HOST: TEST_HOST, - CONF_PORT: TEST_PORT, - }, - ) - await hass.async_block_till_done() - - # No human interaction should be required. - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["handler"] == DOMAIN - assert result["title"] == TEST_TITLE - assert result["data"] == { - CONF_HOST: TEST_HOST, - CONF_PORT: TEST_PORT, - } - - -async def test_import_cannot_connect(hass: HomeAssistantType) -> None: - """Check an import flow that cannot connect.""" - - client = create_mock_client() - client.async_client_connect = AsyncMock(return_value=False) - - with patch( - "homeassistant.components.hyperion.client.HyperionClient", return_value=client - ): - result = await _init_flow( - hass, - source=SOURCE_IMPORT, - data={ - CONF_HOST: TEST_HOST, - CONF_PORT: TEST_PORT, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "cannot_connect" - - async def test_options(hass: HomeAssistantType) -> None: """Check an options flow.""" diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index 7559af4d3c7..c3226cdd389 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -1,21 +1,12 @@ """Tests for the Hyperion integration.""" import logging -from types import MappingProxyType from typing import Optional from unittest.mock import AsyncMock, Mock, call, patch from hyperion import const -from homeassistant import setup -from homeassistant.components.hyperion import ( - get_hyperion_unique_id, - light as hyperion_light, -) -from homeassistant.components.hyperion.const import ( - DEFAULT_ORIGIN, - DOMAIN, - TYPE_HYPERION_LIGHT, -) +from homeassistant.components.hyperion import light as hyperion_light +from homeassistant.components.hyperion.const import DEFAULT_ORIGIN, DOMAIN from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_EFFECT, @@ -43,7 +34,6 @@ import homeassistant.util.color as color_util from . import ( TEST_AUTH_NOT_REQUIRED_RESP, TEST_AUTH_REQUIRED_RESP, - TEST_CONFIG_ENTRY_OPTIONS, TEST_ENTITY_ID_1, TEST_ENTITY_ID_2, TEST_ENTITY_ID_3, @@ -56,8 +46,6 @@ from . import ( TEST_PRIORITY, TEST_PRIORITY_LIGHT_ENTITY_ID_1, TEST_SYSINFO_ID, - TEST_YAML_ENTITY_ID, - TEST_YAML_NAME, add_test_config_entry, call_registered_callback, create_mock_client, @@ -69,28 +57,6 @@ _LOGGER = logging.getLogger(__name__) COLOR_BLACK = color_util.COLORS["black"] -async def _setup_entity_yaml(hass: HomeAssistantType, client: AsyncMock = None) -> None: - """Add a test Hyperion entity to hass.""" - client = client or create_mock_client() - with patch( - "homeassistant.components.hyperion.client.HyperionClient", return_value=client - ): - assert await setup.async_setup_component( - hass, - LIGHT_DOMAIN, - { - LIGHT_DOMAIN: { - "platform": "hyperion", - "name": TEST_YAML_NAME, - "host": TEST_HOST, - "port": TEST_PORT, - "priority": TEST_PRIORITY, - } - }, - ) - await hass.async_block_till_done() - - def _get_config_entry_from_unique_id( hass: HomeAssistantType, unique_id: str ) -> Optional[ConfigEntry]: @@ -100,127 +66,6 @@ def _get_config_entry_from_unique_id( return None -async def test_setup_yaml_already_converted(hass: HomeAssistantType) -> None: - """Test an already converted YAML style config.""" - # This tests "Possibility 1" from async_setup_platform() - - # Add a pre-existing config entry. - add_test_config_entry(hass) - client = create_mock_client() - await _setup_entity_yaml(hass, client=client) - assert client.async_client_disconnect.called - - # Setup should be skipped for the YAML config as there is a pre-existing config - # entry. - assert hass.states.get(TEST_YAML_ENTITY_ID) is None - - -async def test_setup_yaml_old_style_unique_id(hass: HomeAssistantType) -> None: - """Test an already converted YAML style config.""" - # This tests "Possibility 2" from async_setup_platform() - old_unique_id = f"{TEST_HOST}:{TEST_PORT}-0" - - # Add a pre-existing registry entry. - registry = await async_get_registry(hass) - registry.async_get_or_create( - domain=LIGHT_DOMAIN, - platform=DOMAIN, - unique_id=old_unique_id, - suggested_object_id=TEST_YAML_NAME, - ) - - client = create_mock_client() - await _setup_entity_yaml(hass, client=client) - assert client.async_client_disconnect.called - - # The entity should have been created with the same entity_id. - assert hass.states.get(TEST_YAML_ENTITY_ID) is not None - - # The unique_id should have been updated in the registry (rather than the one - # specified above). - assert registry.async_get(TEST_YAML_ENTITY_ID).unique_id == get_hyperion_unique_id( - TEST_SYSINFO_ID, 0, TYPE_HYPERION_LIGHT - ) - assert registry.async_get_entity_id(LIGHT_DOMAIN, DOMAIN, old_unique_id) is None - - # There should be a config entry with the correct server unique_id. - entry = _get_config_entry_from_unique_id(hass, TEST_SYSINFO_ID) - assert entry - assert entry.options == MappingProxyType(TEST_CONFIG_ENTRY_OPTIONS) - - -async def test_setup_yaml_new_style_unique_id_wo_config( - hass: HomeAssistantType, -) -> None: - """Test an a new unique_id without a config entry.""" - # Note: This casde should not happen in the wild, as no released version of Home - # Assistant should this combination, but verify correct behavior for defense in - # depth. - - new_unique_id = get_hyperion_unique_id(TEST_SYSINFO_ID, 0, TYPE_HYPERION_LIGHT) - entity_id_to_preserve = "light.magic_entity" - - # Add a pre-existing registry entry. - registry = await async_get_registry(hass) - registry.async_get_or_create( - domain=LIGHT_DOMAIN, - platform=DOMAIN, - unique_id=new_unique_id, - suggested_object_id=entity_id_to_preserve.split(".")[1], - ) - - client = create_mock_client() - await _setup_entity_yaml(hass, client=client) - assert client.async_client_disconnect.called - - # The entity should have been created with the same entity_id. - assert hass.states.get(entity_id_to_preserve) is not None - - # The unique_id should have been updated in the registry (rather than the one - # specified above). - assert registry.async_get(entity_id_to_preserve).unique_id == new_unique_id - - # There should be a config entry with the correct server unique_id. - entry = _get_config_entry_from_unique_id(hass, TEST_SYSINFO_ID) - assert entry - assert entry.options == MappingProxyType(TEST_CONFIG_ENTRY_OPTIONS) - - -async def test_setup_yaml_no_registry_entity(hass: HomeAssistantType) -> None: - """Test an already converted YAML style config.""" - # This tests "Possibility 3" from async_setup_platform() - - registry = await async_get_registry(hass) - - # Add a pre-existing config entry. - client = create_mock_client() - await _setup_entity_yaml(hass, client=client) - assert client.async_client_disconnect.called - - # The entity should have been created with the same entity_id. - assert hass.states.get(TEST_YAML_ENTITY_ID) is not None - - # The unique_id should have been updated in the registry (rather than the one - # specified above). - assert registry.async_get(TEST_YAML_ENTITY_ID).unique_id == get_hyperion_unique_id( - TEST_SYSINFO_ID, 0, TYPE_HYPERION_LIGHT - ) - - # There should be a config entry with the correct server unique_id. - entry = _get_config_entry_from_unique_id(hass, TEST_SYSINFO_ID) - assert entry - assert entry.options == MappingProxyType(TEST_CONFIG_ENTRY_OPTIONS) - - -async def test_setup_yaml_not_ready(hass: HomeAssistantType) -> None: - """Test the component not being ready.""" - client = create_mock_client() - client.async_client_connect = AsyncMock(return_value=False) - await _setup_entity_yaml(hass, client=client) - assert client.async_client_disconnect.called - assert hass.states.get(TEST_YAML_ENTITY_ID) is None - - async def test_setup_config_entry(hass: HomeAssistantType) -> None: """Test setting up the component via config entries.""" await setup_test_config_entry(hass, hyperion_client=create_mock_client()) From aacf6bd100d357852cafef444111ff4204aed64a Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Fri, 29 Jan 2021 03:07:18 -0500 Subject: [PATCH 039/796] Fix formatting IntEnum as hex in 3.8.x (#45686) --- homeassistant/components/insteon/insteon_entity.py | 2 +- homeassistant/components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index 3bef7dd0247..e2b9dd39f34 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -84,7 +84,7 @@ class InsteonEntity(Entity): return { "identifiers": {(DOMAIN, str(self._insteon_device.address))}, "name": f"{self._insteon_device.description} {self._insteon_device.address}", - "model": f"{self._insteon_device.model} (0x{self._insteon_device.cat:02x}, 0x{self._insteon_device.subcat:02x})", + "model": f"{self._insteon_device.model} ({self._insteon_device.cat!r}, 0x{self._insteon_device.subcat:02x})", "sw_version": f"{self._insteon_device.firmware:02x} Engine Version: {self._insteon_device.engine_version}", "manufacturer": "Smart Home", "via_device": (DOMAIN, str(devices.modem.address)), diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index d20f56054b3..57c750c4429 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -2,7 +2,7 @@ "domain": "insteon", "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", - "requirements": ["pyinsteon==1.0.8"], + "requirements": ["pyinsteon==1.0.9"], "codeowners": ["@teharris1"], "config_flow": true } \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 25edf3e7bfb..ac7ac5be451 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1446,7 +1446,7 @@ pyhomeworks==0.0.6 pyicloud==0.9.7 # homeassistant.components.insteon -pyinsteon==1.0.8 +pyinsteon==1.0.9 # homeassistant.components.intesishome pyintesishome==1.7.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e0d99bc8574..80341520116 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -745,7 +745,7 @@ pyhomematic==0.1.71 pyicloud==0.9.7 # homeassistant.components.insteon -pyinsteon==1.0.8 +pyinsteon==1.0.9 # homeassistant.components.ipma pyipma==2.0.5 From c6105900f6e65af7d86c4cdc78adab8fdbe2fc2c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Jan 2021 02:11:24 -0600 Subject: [PATCH 040/796] Update httpcore to prevent unhandled exception on dropped connection (#45667) Co-authored-by: Paulus Schoutsen --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 30927b9cd32..630eb789d00 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -40,6 +40,10 @@ 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 httpcore to fix exception when connection dropped +# https://github.com/encode/httpcore/issues/239 +httpcore>=0.12.3 + # 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 130fd2cc245..dc1ef9a471b 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -68,6 +68,10 @@ 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 httpcore to fix exception when connection dropped +# https://github.com/encode/httpcore/issues/239 +httpcore>=0.12.3 + # Constrain httplib2 to protect against CVE-2020-11078 httplib2>=0.18.0 From fb884e3afda42e39065a052dc0356767af3c1d2f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Jan 2021 09:19:32 +0100 Subject: [PATCH 041/796] Update bootstrap script (#45692) --- script/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/bootstrap b/script/bootstrap index f58268ff1a8..32e9d11bc4d 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -16,4 +16,4 @@ fi echo "Installing development dependencies..." python3 -m pip install wheel --constraint homeassistant/package_constraints.txt -python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) $(grep stdlib-list requirements_test.txt) $(grep tqdm requirements_test.txt) $(grep pipdeptree requirements_test.txt) --constraint homeassistant/package_constraints.txt +python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) $(grep stdlib-list requirements_test.txt) $(grep tqdm requirements_test.txt) $(grep pipdeptree requirements_test.txt) $(grep awesomeversion requirements_test.txt) --constraint homeassistant/package_constraints.txt From f080af698d87767072de0c478c5e19e2b2496ce1 Mon Sep 17 00:00:00 2001 From: aizerin Date: Fri, 29 Jan 2021 09:36:52 +0100 Subject: [PATCH 042/796] Use pure rgb and allow to set only brightness for fibaro (#45673) Co-authored-by: Paulus Schoutsen --- homeassistant/components/fibaro/light.py | 26 +++++++++++------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py index 6dc69df1343..aed1da543ee 100644 --- a/homeassistant/components/fibaro/light.py +++ b/homeassistant/components/fibaro/light.py @@ -118,13 +118,11 @@ class FibaroLight(FibaroDevice, LightEntity): # We set it to the target brightness and turn it on self._brightness = scaleto100(target_brightness) - if self._supported_flags & SUPPORT_COLOR: - if ( - self._reset_color - and kwargs.get(ATTR_WHITE_VALUE) is None - and kwargs.get(ATTR_HS_COLOR) is None - and kwargs.get(ATTR_BRIGHTNESS) is None - ): + if self._supported_flags & SUPPORT_COLOR and ( + kwargs.get(ATTR_WHITE_VALUE) is not None + or kwargs.get(ATTR_HS_COLOR) is not None + ): + if self._reset_color: self._color = (100, 0) # Update based on parameters @@ -132,14 +130,14 @@ class FibaroLight(FibaroDevice, LightEntity): self._color = kwargs.get(ATTR_HS_COLOR, self._color) rgb = color_util.color_hs_to_RGB(*self._color) self.call_set_color( - round(rgb[0] * self._brightness / 100.0), - round(rgb[1] * self._brightness / 100.0), - round(rgb[2] * self._brightness / 100.0), - round(self._white * self._brightness / 100.0), + round(rgb[0]), + round(rgb[1]), + round(rgb[2]), + round(self._white), ) if self.state == "off": - self.set_level(int(self._brightness)) + self.set_level(min(int(self._brightness), 99)) return if self._reset_color: @@ -147,7 +145,7 @@ class FibaroLight(FibaroDevice, LightEntity): self.call_set_color(bri255, bri255, bri255, bri255) if self._supported_flags & SUPPORT_BRIGHTNESS: - self.set_level(int(self._brightness)) + self.set_level(min(int(self._brightness), 99)) return # The simplest case is left for last. No dimming, just switch on @@ -203,4 +201,4 @@ class FibaroLight(FibaroDevice, LightEntity): if rgbw_list[0] or rgbw_list[1] or rgbw_list[2]: self._color = color_util.color_RGB_to_hs(*rgbw_list[:3]) if (self._supported_flags & SUPPORT_WHITE_VALUE) and self.brightness != 0: - self._white = min(255, max(0, rgbw_list[3] * 100.0 / self._brightness)) + self._white = min(255, max(0, rgbw_list[3])) From 25c5c6aec9b1df65d515fa906e6f2a41b573cea3 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Fri, 29 Jan 2021 10:23:34 +0100 Subject: [PATCH 043/796] Refactoring upnp component (#43646) --- homeassistant/components/upnp/__init__.py | 55 +++---- homeassistant/components/upnp/config_flow.py | 120 +++++++++------- homeassistant/components/upnp/const.py | 9 +- homeassistant/components/upnp/device.py | 53 ++++--- homeassistant/components/upnp/sensor.py | 8 +- tests/components/upnp/mock_device.py | 14 -- tests/components/upnp/test_config_flow.py | 142 ++++++++++--------- tests/components/upnp/test_init.py | 90 +++++++++++- 8 files changed, 284 insertions(+), 207 deletions(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index c9f96a0e9d7..7b46037d99d 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -1,7 +1,6 @@ """Open ports in your router for Home Assistant and provide statistics.""" import asyncio from ipaddress import ip_address -from operator import itemgetter import voluptuous as vol @@ -19,7 +18,6 @@ from .const import ( DISCOVERY_LOCATION, DISCOVERY_ST, DISCOVERY_UDN, - DISCOVERY_USN, DOMAIN, DOMAIN_CONFIG, DOMAIN_COORDINATORS, @@ -38,46 +36,27 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_discover_and_construct( - hass: HomeAssistantType, udn: str = None, st: str = None -) -> Device: +async def async_construct_device(hass: HomeAssistantType, udn: str, st: str) -> 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: - _LOGGER.info("No UPnP/IGD devices discovered") + discoveries = [ + discovery + for discovery in await Device.async_discover(hass) + if discovery[DISCOVERY_UDN] == udn and discovery[DISCOVERY_ST] == st + ] + if not discoveries: + _LOGGER.info("Device not discovered") return None - if udn: - # 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 filtered if di[DISCOVERY_ST] == st] - if not filtered: - _LOGGER.warning( - 'Wanted UPnP/IGD device with UDN/ST "%s"/"%s" not found, aborting', - udn, - st, - ) - return None + # Some additional clues for remote debugging. + if len(discoveries) > 1: + _LOGGER.info("Multiple devices discovered: %s", discoveries) - # Ensure we're always taking the latest, if we filtered only on UDN. - filtered = sorted(filtered, key=itemgetter(DISCOVERY_ST), reverse=True) - discovery_info = filtered[0] - else: - # Get the first/any. - discovery_info = discovery_infos[0] - if len(discovery_infos) > 1: - device_name = discovery_info.get( - DISCOVERY_USN, discovery_info.get(DISCOVERY_LOCATION, "") - ) - _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] + discovery = discoveries[0] + _LOGGER.debug("Constructing from discovery: %s", discovery) + location = discovery[DISCOVERY_LOCATION] return await Device.async_create_device(hass, location) @@ -110,10 +89,10 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) _LOGGER.debug("Setting up config entry: %s", config_entry.unique_id) # Discover and construct. - udn = config_entry.data.get(CONFIG_ENTRY_UDN) - st = config_entry.data.get(CONFIG_ENTRY_ST) # pylint: disable=invalid-name + udn = config_entry.data[CONFIG_ENTRY_UDN] + st = config_entry.data[CONFIG_ENTRY_ST] # pylint: disable=invalid-name try: - device = await async_discover_and_construct(hass, udn, st) + device = await async_construct_device(hass, udn, st) except asyncio.TimeoutError as err: raise ConfigEntryNotReady from err diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 7b20c7709a0..41c56dddb29 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -1,6 +1,6 @@ """Config flow for UPNP.""" from datetime import timedelta -from typing import Mapping, Optional +from typing import Any, Mapping, Optional import voluptuous as vol @@ -9,7 +9,7 @@ from homeassistant.components import ssdp from homeassistant.const import CONF_SCAN_INTERVAL from homeassistant.core import callback -from .const import ( # pylint: disable=unused-import +from .const import ( CONFIG_ENTRY_SCAN_INTERVAL, CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, @@ -18,6 +18,7 @@ from .const import ( # pylint: disable=unused-import DISCOVERY_NAME, DISCOVERY_ST, DISCOVERY_UDN, + DISCOVERY_UNIQUE_ID, DISCOVERY_USN, DOMAIN, DOMAIN_COORDINATORS, @@ -26,6 +27,16 @@ from .const import ( # pylint: disable=unused-import from .device import Device +def discovery_info_to_discovery(discovery_info: Mapping) -> Mapping: + """Convert a SSDP-discovery to 'our' discovery.""" + return { + DISCOVERY_UDN: discovery_info[ssdp.ATTR_UPNP_UDN], + DISCOVERY_ST: discovery_info[ssdp.ATTR_SSDP_ST], + DISCOVERY_LOCATION: discovery_info[ssdp.ATTR_SSDP_LOCATION], + DISCOVERY_USN: discovery_info[ssdp.ATTR_SSDP_USN], + } + + class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a UPnP/IGD config flow.""" @@ -37,43 +48,46 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # - user(None): scan --> user({...}) --> create_entry() # - import(None) --> create_entry() - def __init__(self): + def __init__(self) -> None: """Initialize the UPnP/IGD config flow.""" self._discoveries: Mapping = None - async def async_step_user(self, user_input: Optional[Mapping] = None): + async def async_step_user( + self, user_input: Optional[Mapping] = None + ) -> Mapping[str, Any]: """Handle a flow start.""" _LOGGER.debug("async_step_user: user_input: %s", user_input) - # This uses DISCOVERY_USN as the identifier for the device. if user_input is not None: # Ensure wanted device was discovered. matching_discoveries = [ discovery for discovery in self._discoveries - if discovery[DISCOVERY_USN] == user_input["usn"] + if discovery[DISCOVERY_UNIQUE_ID] == user_input["unique_id"] ] if not matching_discoveries: return self.async_abort(reason="no_devices_found") discovery = matching_discoveries[0] await self.async_set_unique_id( - discovery[DISCOVERY_USN], raise_on_progress=False + discovery[DISCOVERY_UNIQUE_ID], raise_on_progress=False ) return await self._async_create_entry_from_discovery(discovery) # Discover devices. - discoveries = await Device.async_discover(self.hass) + discoveries = [ + await Device.async_supplement_discovery(self.hass, discovery) + for discovery in await Device.async_discover(self.hass) + ] - # Store discoveries which have not been configured, add name for each discovery. - current_usns = {entry.unique_id for entry in self._async_current_entries()} + # Store discoveries which have not been configured. + current_unique_ids = { + entry.unique_id for entry in self._async_current_entries() + } self._discoveries = [ - { - **discovery, - DISCOVERY_NAME: await self._async_get_name_for_discovery(discovery), - } + discovery for discovery in discoveries - if discovery[DISCOVERY_USN] not in current_usns + if discovery[DISCOVERY_UNIQUE_ID] not in current_unique_ids ] # Ensure anything to add. @@ -82,9 +96,9 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema = vol.Schema( { - vol.Required("usn"): vol.In( + vol.Required("unique_id"): vol.In( { - discovery[DISCOVERY_USN]: discovery[DISCOVERY_NAME] + discovery[DISCOVERY_UNIQUE_ID]: discovery[DISCOVERY_NAME] for discovery in self._discoveries } ), @@ -95,7 +109,9 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema=data_schema, ) - async def async_step_import(self, import_info: Optional[Mapping]): + async def async_step_import( + self, import_info: Optional[Mapping] + ) -> Mapping[str, Any]: """Import a new UPnP/IGD device as a config entry. This flow is triggered by `async_setup`. If no device has been @@ -119,18 +135,24 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="no_devices_found") # Ensure complete discovery. - discovery_info = self._discoveries[0] - if DISCOVERY_USN not in discovery_info: + discovery = self._discoveries[0] + if ( + DISCOVERY_UDN not in discovery + or DISCOVERY_ST not in discovery + or DISCOVERY_LOCATION not in discovery + or DISCOVERY_USN not in discovery + ): _LOGGER.debug("Incomplete discovery, ignoring") return self.async_abort(reason="incomplete_discovery") # Ensure not already configuring/configured. - usn = discovery_info[DISCOVERY_USN] - await self.async_set_unique_id(usn) + discovery = await Device.async_supplement_discovery(self.hass, discovery) + unique_id = discovery[DISCOVERY_UNIQUE_ID] + await self.async_set_unique_id(unique_id) - return await self._async_create_entry_from_discovery(discovery_info) + return await self._async_create_entry_from_discovery(discovery) - async def async_step_ssdp(self, discovery_info: Mapping): + async def async_step_ssdp(self, discovery_info: Mapping) -> Mapping[str, Any]: """Handle a discovered UPnP/IGD device. This flow is triggered by the SSDP component. It will check if the @@ -142,36 +164,35 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if ( ssdp.ATTR_UPNP_UDN not in discovery_info or ssdp.ATTR_SSDP_ST not in discovery_info + or ssdp.ATTR_SSDP_LOCATION not in discovery_info + or ssdp.ATTR_SSDP_USN not in discovery_info ): _LOGGER.debug("Incomplete discovery, ignoring") return self.async_abort(reason="incomplete_discovery") + # Convert to something we understand/speak. + discovery = discovery_info_to_discovery(discovery_info) + # Ensure not already configuring/configured. - udn = discovery_info[ssdp.ATTR_UPNP_UDN] - st = discovery_info[ssdp.ATTR_SSDP_ST] # pylint: disable=invalid-name - usn = f"{udn}::{st}" - await self.async_set_unique_id(usn) + discovery = await Device.async_supplement_discovery(self.hass, discovery) + unique_id = discovery[DISCOVERY_UNIQUE_ID] + await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() # Store discovery. - _LOGGER.debug("New discovery, continuing") - name = discovery_info.get("friendlyName", "") - discovery = { - DISCOVERY_UDN: udn, - DISCOVERY_ST: st, - DISCOVERY_NAME: name, - } self._discoveries = [discovery] # Ensure user recognizable. # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { - "name": name, + "name": discovery[DISCOVERY_NAME], } return await self.async_step_ssdp_confirm() - async def async_step_ssdp_confirm(self, user_input: Optional[Mapping] = None): + async def async_step_ssdp_confirm( + self, user_input: Optional[Mapping] = None + ) -> Mapping[str, Any]: """Confirm integration via SSDP.""" _LOGGER.debug("async_step_ssdp_confirm: user_input: %s", user_input) if user_input is None: @@ -182,24 +203,21 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: """Define the config flow to handle options.""" return UpnpOptionsFlowHandler(config_entry) async def _async_create_entry_from_discovery( self, discovery: Mapping, - ): + ) -> Mapping[str, Any]: """Create an entry from discovery.""" _LOGGER.debug( "_async_create_entry_from_discovery: discovery: %s", discovery, ) - # Get name from device, if not found already. - if DISCOVERY_NAME not in discovery and DISCOVERY_LOCATION in discovery: - discovery[DISCOVERY_NAME] = await self._async_get_name_for_discovery( - discovery - ) title = discovery.get(DISCOVERY_NAME, "") data = { @@ -208,26 +226,18 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } return self.async_create_entry(title=title, data=data) - async def _async_get_name_for_discovery(self, discovery: Mapping): - """Get the name of the device from a discovery.""" - _LOGGER.debug("_async_get_name_for_discovery: discovery: %s", discovery) - device = await Device.async_create_device( - self.hass, discovery[DISCOVERY_LOCATION] - ) - return device.name - class UpnpOptionsFlowHandler(config_entries.OptionsFlow): """Handle a UPnP options flow.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init(self, user_input: Mapping = None) -> None: """Manage the options.""" if user_input is not None: - udn = self.config_entry.data.get(CONFIG_ENTRY_UDN) + udn = self.config_entry.data[CONFIG_ENTRY_UDN] coordinator = self.hass.data[DOMAIN][DOMAIN_COORDINATORS][udn] update_interval_sec = user_input.get( CONFIG_ENTRY_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py index 8256fdd9fc9..4ccf6d3d7ea 100644 --- a/homeassistant/components/upnp/const.py +++ b/homeassistant/components/upnp/const.py @@ -8,10 +8,10 @@ LOGGER = logging.getLogger(__package__) CONF_LOCAL_IP = "local_ip" DOMAIN = "upnp" +DOMAIN_CONFIG = "config" DOMAIN_COORDINATORS = "coordinators" DOMAIN_DEVICES = "devices" DOMAIN_LOCAL_IP = "local_ip" -DOMAIN_CONFIG = "config" BYTES_RECEIVED = "bytes_received" BYTES_SENT = "bytes_sent" PACKETS_RECEIVED = "packets_received" @@ -21,12 +21,13 @@ DATA_PACKETS = "packets" DATA_RATE_PACKETS_PER_SECOND = f"{DATA_PACKETS}/{TIME_SECONDS}" KIBIBYTE = 1024 UPDATE_INTERVAL = timedelta(seconds=30) -DISCOVERY_NAME = "name" DISCOVERY_LOCATION = "location" +DISCOVERY_NAME = "name" DISCOVERY_ST = "st" DISCOVERY_UDN = "udn" +DISCOVERY_UNIQUE_ID = "unique_id" DISCOVERY_USN = "usn" -CONFIG_ENTRY_UDN = "udn" -CONFIG_ENTRY_ST = "st" CONFIG_ENTRY_SCAN_INTERVAL = "scan_interval" +CONFIG_ENTRY_ST = "st" +CONFIG_ENTRY_UDN = "udn" DEFAULT_SCAN_INTERVAL = timedelta(seconds=30).seconds diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 6bc497170ca..39fd09089b4 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -1,4 +1,6 @@ """Home Assistant representation of an UPnP/IGD.""" +from __future__ import annotations + import asyncio from ipaddress import IPv4Address from typing import List, Mapping @@ -16,8 +18,10 @@ from .const import ( BYTES_SENT, CONF_LOCAL_IP, DISCOVERY_LOCATION, + DISCOVERY_NAME, DISCOVERY_ST, DISCOVERY_UDN, + DISCOVERY_UNIQUE_ID, DISCOVERY_USN, DOMAIN, DOMAIN_CONFIG, @@ -29,12 +33,11 @@ from .const import ( class Device: - """Home Assistant representation of an UPnP/IGD.""" + """Home Assistant representation of a UPnP/IGD device.""" def __init__(self, igd_device): """Initialize UPnP/IGD device.""" self._igd_device: IgdDevice = igd_device - self._mapped_ports = [] @classmethod async def async_discover(cls, hass: HomeAssistantType) -> List[Mapping]: @@ -46,24 +49,35 @@ class Device: if local_ip: local_ip = IPv4Address(local_ip) - discovery_infos = await IgdDevice.async_search(source_ip=local_ip, timeout=10) + discoveries = await IgdDevice.async_search(source_ip=local_ip, timeout=10) - # add extra info and store devices - devices = [] - for discovery_info in discovery_infos: - discovery_info[DISCOVERY_UDN] = discovery_info["_udn"] - discovery_info[DISCOVERY_ST] = discovery_info["st"] - discovery_info[DISCOVERY_LOCATION] = discovery_info["location"] - usn = f"{discovery_info[DISCOVERY_UDN]}::{discovery_info[DISCOVERY_ST]}" - discovery_info[DISCOVERY_USN] = usn - _LOGGER.debug("Discovered device: %s", discovery_info) + # Supplement/standardize discovery. + for discovery in discoveries: + discovery[DISCOVERY_UDN] = discovery["_udn"] + discovery[DISCOVERY_ST] = discovery["st"] + discovery[DISCOVERY_LOCATION] = discovery["location"] + discovery[DISCOVERY_USN] = discovery["usn"] + _LOGGER.debug("Discovered device: %s", discovery) - devices.append(discovery_info) - - return devices + return discoveries @classmethod - async def async_create_device(cls, hass: HomeAssistantType, ssdp_location: str): + async def async_supplement_discovery( + cls, hass: HomeAssistantType, discovery: Mapping + ) -> Mapping: + """Get additional data from device and supplement discovery.""" + device = await Device.async_create_device(hass, discovery[DISCOVERY_LOCATION]) + discovery[DISCOVERY_NAME] = device.name + + # Set unique_id. + discovery[DISCOVERY_UNIQUE_ID] = discovery[DISCOVERY_USN] + + return discovery + + @classmethod + async def async_create_device( + cls, hass: HomeAssistantType, ssdp_location: str + ) -> Device: """Create UPnP/IGD device.""" # build async_upnp_client requester session = async_get_clientsession(hass) @@ -102,10 +116,15 @@ class Device: """Get the device type.""" return self._igd_device.device_type + @property + def usn(self) -> str: + """Get the USN.""" + return f"{self.udn}::{self.device_type}" + @property def unique_id(self) -> str: """Get the unique id.""" - return f"{self.udn}::{self.device_type}" + return self.usn def __str__(self) -> str: """Get string representation.""" diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index a9906e535b9..59205f49667 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -83,13 +83,7 @@ async def async_setup_entry( hass, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the UPnP/IGD sensors.""" - data = config_entry.data - if CONFIG_ENTRY_UDN in data: - udn = data[CONFIG_ENTRY_UDN] - else: - # any device will do - udn = list(hass.data[DOMAIN][DOMAIN_DEVICES])[0] - + udn = config_entry.data[CONFIG_ENTRY_UDN] device: Device = hass.data[DOMAIN][DOMAIN_DEVICES][udn] update_interval_sec = config_entry.options.get( diff --git a/tests/components/upnp/mock_device.py b/tests/components/upnp/mock_device.py index 17d9b5659c5..a70b3fa0237 100644 --- a/tests/components/upnp/mock_device.py +++ b/tests/components/upnp/mock_device.py @@ -21,8 +21,6 @@ class MockDevice(Device): igd_device = object() super().__init__(igd_device) self._udn = udn - self.added_port_mappings = [] - self.removed_port_mappings = [] @classmethod async def async_create_device(cls, hass, ssdp_location): @@ -54,18 +52,6 @@ class MockDevice(Device): """Get the device type.""" return "urn:schemas-upnp-org:device:InternetGatewayDevice:1" - async def _async_add_port_mapping( - self, external_port: int, local_ip: str, internal_port: int - ) -> None: - """Add a port mapping.""" - entry = [external_port, local_ip, internal_port] - self.added_port_mappings.append(entry) - - async def _async_delete_port_mapping(self, external_port: int) -> None: - """Remove a port mapping.""" - entry = external_port - self.removed_port_mappings.append(entry) - async def async_get_traffic_data(self) -> Mapping[str, any]: """Get traffic data.""" return { diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index be7794ce8e9..f702d770ee6 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -11,10 +11,13 @@ from homeassistant.components.upnp.const import ( CONFIG_ENTRY_UDN, DEFAULT_SCAN_INTERVAL, DISCOVERY_LOCATION, + DISCOVERY_NAME, DISCOVERY_ST, DISCOVERY_UDN, + DISCOVERY_UNIQUE_ID, DISCOVERY_USN, DOMAIN, + DOMAIN_COORDINATORS, ) from homeassistant.components.upnp.device import Device from homeassistant.helpers.typing import HomeAssistantType @@ -28,25 +31,34 @@ from tests.common import MockConfigEntry async def test_flow_ssdp_discovery(hass: HomeAssistantType): """Test config flow: discovered + configured through ssdp.""" udn = "uuid:device_1" + location = "dummy" mock_device = MockDevice(udn) - discovery_infos = [ + discoveries = [ { + DISCOVERY_LOCATION: location, + DISCOVERY_NAME: mock_device.name, DISCOVERY_ST: mock_device.device_type, DISCOVERY_UDN: mock_device.udn, - DISCOVERY_LOCATION: "dummy", + DISCOVERY_UNIQUE_ID: mock_device.unique_id, + DISCOVERY_USN: mock_device.usn, } ] with patch.object( Device, "async_create_device", AsyncMock(return_value=mock_device) - ), patch.object(Device, "async_discover", AsyncMock(return_value=discovery_infos)): + ), patch.object( + Device, "async_discover", AsyncMock(return_value=discoveries) + ), patch.object( + Device, "async_supplement_discovery", AsyncMock(return_value=discoveries[0]) + ): # Discovered via step ssdp. result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data={ + ssdp.ATTR_SSDP_LOCATION: location, ssdp.ATTR_SSDP_ST: mock_device.device_type, + ssdp.ATTR_SSDP_USN: mock_device.usn, ssdp.ATTR_UPNP_UDN: mock_device.udn, - "friendlyName": mock_device.name, }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -69,47 +81,46 @@ async def test_flow_ssdp_discovery(hass: HomeAssistantType): async def test_flow_ssdp_discovery_incomplete(hass: HomeAssistantType): """Test config flow: incomplete discovery through ssdp.""" udn = "uuid:device_1" + location = "dummy" mock_device = MockDevice(udn) - discovery_infos = [ - { - DISCOVERY_ST: mock_device.device_type, - DISCOVERY_UDN: mock_device.udn, - DISCOVERY_LOCATION: "dummy", - } - ] - with patch.object( - Device, "async_create_device", AsyncMock(return_value=mock_device) - ), patch.object(Device, "async_discover", AsyncMock(return_value=discovery_infos)): - # Discovered via step ssdp. - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data={ - ssdp.ATTR_SSDP_ST: mock_device.device_type, - # ssdp.ATTR_UPNP_UDN: mock_device.udn, # Not provided. - "friendlyName": mock_device.name, - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "incomplete_discovery" + + # Discovered via step ssdp. + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + ssdp.ATTR_SSDP_ST: mock_device.device_type, + # ssdp.ATTR_UPNP_UDN: mock_device.udn, # Not provided. + ssdp.ATTR_SSDP_LOCATION: location, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "incomplete_discovery" async def test_flow_user(hass: HomeAssistantType): """Test config flow: discovered + configured through user.""" udn = "uuid:device_1" + location = "dummy" mock_device = MockDevice(udn) - discovery_infos = [ + discoveries = [ { - DISCOVERY_USN: mock_device.unique_id, + DISCOVERY_LOCATION: location, + DISCOVERY_NAME: mock_device.name, DISCOVERY_ST: mock_device.device_type, DISCOVERY_UDN: mock_device.udn, - DISCOVERY_LOCATION: "dummy", + DISCOVERY_UNIQUE_ID: mock_device.unique_id, + DISCOVERY_USN: mock_device.usn, } ] with patch.object( Device, "async_create_device", AsyncMock(return_value=mock_device) - ), patch.object(Device, "async_discover", AsyncMock(return_value=discovery_infos)): + ), patch.object( + Device, "async_discover", AsyncMock(return_value=discoveries) + ), patch.object( + Device, "async_supplement_discovery", AsyncMock(return_value=discoveries[0]) + ): # Discovered via step user. result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -120,7 +131,7 @@ async def test_flow_user(hass: HomeAssistantType): # Confirmed via step user. result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={"usn": mock_device.unique_id}, + user_input={"unique_id": mock_device.unique_id}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -135,18 +146,25 @@ async def test_flow_import(hass: HomeAssistantType): """Test config flow: discovered + configured through configuration.yaml.""" udn = "uuid:device_1" mock_device = MockDevice(udn) - discovery_infos = [ + location = "dummy" + discoveries = [ { - DISCOVERY_USN: mock_device.unique_id, + DISCOVERY_LOCATION: location, + DISCOVERY_NAME: mock_device.name, DISCOVERY_ST: mock_device.device_type, DISCOVERY_UDN: mock_device.udn, - DISCOVERY_LOCATION: "dummy", + DISCOVERY_UNIQUE_ID: mock_device.unique_id, + DISCOVERY_USN: mock_device.usn, } ] with patch.object( Device, "async_create_device", AsyncMock(return_value=mock_device) - ), patch.object(Device, "async_discover", AsyncMock(return_value=discovery_infos)): + ), patch.object( + Device, "async_discover", AsyncMock(return_value=discoveries) + ), patch.object( + Device, "async_supplement_discovery", AsyncMock(return_value=discoveries[0]) + ): # Discovered via step import. result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT} @@ -160,18 +178,10 @@ async def test_flow_import(hass: HomeAssistantType): } -async def test_flow_import_duplicate(hass: HomeAssistantType): +async def test_flow_import_already_configured(hass: HomeAssistantType): """Test config flow: discovered, but already configured.""" udn = "uuid:device_1" mock_device = MockDevice(udn) - discovery_infos = [ - { - DISCOVERY_USN: mock_device.unique_id, - DISCOVERY_ST: mock_device.device_type, - DISCOVERY_UDN: mock_device.udn, - DISCOVERY_LOCATION: "dummy", - } - ] # Existing entry. config_entry = MockConfigEntry( @@ -184,33 +194,32 @@ async def test_flow_import_duplicate(hass: HomeAssistantType): ) config_entry.add_to_hass(hass) - with patch.object( - Device, "async_create_device", AsyncMock(return_value=mock_device) - ), patch.object(Device, "async_discover", AsyncMock(return_value=discovery_infos)): - # Discovered via step import. - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT} - ) + # Discovered via step import. + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT} + ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" async def test_flow_import_incomplete(hass: HomeAssistantType): """Test config flow: incomplete discovery, configured through configuration.yaml.""" udn = "uuid:device_1" mock_device = MockDevice(udn) - discovery_infos = [ + location = "dummy" + discoveries = [ { - DISCOVERY_ST: mock_device.device_type, + DISCOVERY_LOCATION: location, + DISCOVERY_NAME: mock_device.name, + # DISCOVERY_ST: mock_device.device_type, DISCOVERY_UDN: mock_device.udn, - DISCOVERY_LOCATION: "dummy", + DISCOVERY_UNIQUE_ID: mock_device.unique_id, + DISCOVERY_USN: mock_device.usn, } ] - with patch.object( - Device, "async_create_device", AsyncMock(return_value=mock_device) - ), patch.object(Device, "async_discover", AsyncMock(return_value=discovery_infos)): + with patch.object(Device, "async_discover", AsyncMock(return_value=discoveries)): # Discovered via step import. result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT} @@ -224,12 +233,16 @@ async def test_options_flow(hass: HomeAssistantType): """Test options flow.""" # Set up config entry. udn = "uuid:device_1" + location = "http://192.168.1.1/desc.xml" mock_device = MockDevice(udn) - discovery_infos = [ + discoveries = [ { - DISCOVERY_UDN: mock_device.udn, + DISCOVERY_LOCATION: location, + DISCOVERY_NAME: mock_device.name, DISCOVERY_ST: mock_device.device_type, - DISCOVERY_LOCATION: "http://192.168.1.1/desc.xml", + DISCOVERY_UDN: mock_device.udn, + DISCOVERY_UNIQUE_ID: mock_device.unique_id, + DISCOVERY_USN: mock_device.usn, } ] config_entry = MockConfigEntry( @@ -245,16 +258,15 @@ async def test_options_flow(hass: HomeAssistantType): config = { # no upnp, ensures no import-flow is started. } - async_discover = AsyncMock(return_value=discovery_infos) with patch.object( Device, "async_create_device", AsyncMock(return_value=mock_device) - ), patch.object(Device, "async_discover", async_discover): + ), patch.object(Device, "async_discover", AsyncMock(return_value=discoveries)): # Initialisation of component. await async_setup_component(hass, "upnp", config) await hass.async_block_till_done() # DataUpdateCoordinator gets a default of 30 seconds for updates. - coordinator = hass.data[DOMAIN]["coordinators"][mock_device.udn] + coordinator = hass.data[DOMAIN][DOMAIN_COORDINATORS][mock_device.udn] assert coordinator.update_interval == timedelta(seconds=DEFAULT_SCAN_INTERVAL) # Options flow with no input results in form. diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index 4373e175bc9..3f7c64ab3ad 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -4,9 +4,16 @@ from unittest.mock import AsyncMock, patch from homeassistant.components import upnp from homeassistant.components.upnp.const import ( + CONFIG_ENTRY_ST, + CONFIG_ENTRY_UDN, DISCOVERY_LOCATION, + DISCOVERY_NAME, DISCOVERY_ST, DISCOVERY_UDN, + DISCOVERY_UNIQUE_ID, + DISCOVERY_USN, + DOMAIN, + DOMAIN_DEVICES, ) from homeassistant.components.upnp.device import Device from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -20,35 +27,104 @@ from tests.common import MockConfigEntry async def test_async_setup_entry_default(hass): """Test async_setup_entry.""" udn = "uuid:device_1" + location = "http://192.168.1.1/desc.xml" mock_device = MockDevice(udn) - discovery_infos = [ + discoveries = [ { - DISCOVERY_UDN: mock_device.udn, + DISCOVERY_LOCATION: location, + DISCOVERY_NAME: mock_device.name, DISCOVERY_ST: mock_device.device_type, - DISCOVERY_LOCATION: "http://192.168.1.1/desc.xml", + DISCOVERY_UDN: mock_device.udn, + DISCOVERY_UNIQUE_ID: mock_device.unique_id, + DISCOVERY_USN: mock_device.usn, } ] entry = MockConfigEntry( - domain=upnp.DOMAIN, data={"udn": mock_device.udn, "st": mock_device.device_type} + domain=upnp.DOMAIN, + data={ + CONFIG_ENTRY_UDN: mock_device.udn, + CONFIG_ENTRY_ST: mock_device.device_type, + }, ) config = { # no upnp } - async_discover = AsyncMock(return_value=[]) + async_discover = AsyncMock() with patch.object( Device, "async_create_device", AsyncMock(return_value=mock_device) ), patch.object(Device, "async_discover", async_discover): # initialisation of component, no device discovered + async_discover.return_value = [] await async_setup_component(hass, "upnp", config) await hass.async_block_till_done() # loading of config_entry, device discovered - async_discover.return_value = discovery_infos + async_discover.return_value = discoveries assert await upnp.async_setup_entry(hass, entry) is True # ensure device is stored/used - assert hass.data[upnp.DOMAIN]["devices"][udn] == mock_device + assert hass.data[DOMAIN][DOMAIN_DEVICES][udn] == mock_device + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + +async def test_sync_setup_entry_multiple_discoveries(hass): + """Test async_setup_entry.""" + udn_0 = "uuid:device_1" + location_0 = "http://192.168.1.1/desc.xml" + mock_device_0 = MockDevice(udn_0) + udn_1 = "uuid:device_2" + location_1 = "http://192.168.1.2/desc.xml" + mock_device_1 = MockDevice(udn_1) + discoveries = [ + { + DISCOVERY_LOCATION: location_0, + DISCOVERY_NAME: mock_device_0.name, + DISCOVERY_ST: mock_device_0.device_type, + DISCOVERY_UDN: mock_device_0.udn, + DISCOVERY_UNIQUE_ID: mock_device_0.unique_id, + DISCOVERY_USN: mock_device_0.usn, + }, + { + DISCOVERY_LOCATION: location_1, + DISCOVERY_NAME: mock_device_1.name, + DISCOVERY_ST: mock_device_1.device_type, + DISCOVERY_UDN: mock_device_1.udn, + DISCOVERY_UNIQUE_ID: mock_device_1.unique_id, + DISCOVERY_USN: mock_device_1.usn, + }, + ] + entry = MockConfigEntry( + domain=upnp.DOMAIN, + data={ + CONFIG_ENTRY_UDN: mock_device_1.udn, + CONFIG_ENTRY_ST: mock_device_1.device_type, + }, + ) + + config = { + # no upnp + } + async_create_device = AsyncMock(return_value=mock_device_1) + async_discover = AsyncMock() + with patch.object(Device, "async_create_device", async_create_device), patch.object( + Device, "async_discover", async_discover + ): + # initialisation of component, no device discovered + async_discover.return_value = [] + await async_setup_component(hass, "upnp", config) + await hass.async_block_till_done() + + # loading of config_entry, device discovered + async_discover.return_value = discoveries + assert await upnp.async_setup_entry(hass, entry) is True + + # ensure device is stored/used + async_create_device.assert_called_with(hass, discoveries[1][DISCOVERY_LOCATION]) + assert udn_0 not in hass.data[DOMAIN][DOMAIN_DEVICES] + assert hass.data[DOMAIN][DOMAIN_DEVICES][udn_1] == mock_device_1 hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() From b74cbb2a596095d122469ea4fce0ab2b2f987797 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Jan 2021 10:33:44 +0100 Subject: [PATCH 044/796] Bump crpytography to 3.3.1 (#45691) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 630eb789d00..49c1b2df2ff 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,7 +8,7 @@ attrs==19.3.0 bcrypt==3.1.7 certifi>=2020.12.5 ciso8601==2.1.3 -cryptography==3.2 +cryptography==3.3.1 defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 diff --git a/requirements.txt b/requirements.txt index c973f4e4030..ef7d73e1cb5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ ciso8601==2.1.3 httpx==0.16.1 jinja2>=2.11.2 PyJWT==1.7.1 -cryptography==3.2 +cryptography==3.3.1 pip>=8.0.3,<20.3 python-slugify==4.0.1 pytz>=2020.5 diff --git a/setup.py b/setup.py index 7f77e3795b4..5998a40a24e 100755 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ REQUIRES = [ "jinja2>=2.11.2", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. - "cryptography==3.2", + "cryptography==3.3.1", "pip>=8.0.3,<20.3", "python-slugify==4.0.1", "pytz>=2020.5", From ba55f1ff4b00d24d402001b6503535f09d97cc18 Mon Sep 17 00:00:00 2001 From: Pascal Reeb Date: Fri, 29 Jan 2021 11:05:13 +0100 Subject: [PATCH 045/796] Add config flow for nuki (#45664) * implemented config_flow for nuki component * warn -> warning * exception handling & config_flow tests * gen_requirements_all * Update config_flow.py Co-authored-by: Pascal Vizeli --- .coveragerc | 2 + CODEOWNERS | 2 +- homeassistant/components/nuki/__init__.py | 62 +++++++++++- homeassistant/components/nuki/config_flow.py | 97 +++++++++++++++++++ homeassistant/components/nuki/const.py | 6 ++ homeassistant/components/nuki/lock.py | 14 ++- homeassistant/components/nuki/manifest.json | 13 +-- homeassistant/components/nuki/strings.json | 18 ++++ .../components/nuki/translations/en.json | 18 ++++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/nuki/__init__.py | 1 + tests/components/nuki/test_config_flow.py | 93 ++++++++++++++++++ 13 files changed, 319 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/nuki/config_flow.py create mode 100644 homeassistant/components/nuki/const.py create mode 100644 homeassistant/components/nuki/strings.json create mode 100644 homeassistant/components/nuki/translations/en.json create mode 100644 tests/components/nuki/__init__.py create mode 100644 tests/components/nuki/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 30ea684740d..c692dfbba5e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -621,6 +621,8 @@ omit = homeassistant/components/notify_events/notify.py homeassistant/components/nsw_fuel_station/sensor.py homeassistant/components/nuimo_controller/* + homeassistant/components/nuki/__init__.py + homeassistant/components/nuki/const.py homeassistant/components/nuki/lock.py homeassistant/components/nut/sensor.py homeassistant/components/nx584/alarm_control_panel.py diff --git a/CODEOWNERS b/CODEOWNERS index b8175614fb5..73d42f4efcf 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -310,7 +310,7 @@ homeassistant/components/notion/* @bachya homeassistant/components/nsw_fuel_station/* @nickw444 homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte homeassistant/components/nuheat/* @bdraco -homeassistant/components/nuki/* @pschmitt @pvizeli +homeassistant/components/nuki/* @pschmitt @pvizeli @pree homeassistant/components/numato/* @clssn homeassistant/components/number/* @home-assistant/core @Shulyaka homeassistant/components/nut/* @bdraco diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index c8b19082585..627cf20b16b 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -1,3 +1,63 @@ """The nuki component.""" -DOMAIN = "nuki" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN +import homeassistant.helpers.config_validation as cv + +from .const import DEFAULT_PORT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +NUKI_PLATFORMS = ["lock"] +UPDATE_INTERVAL = timedelta(seconds=30) + +NUKI_SCHEMA = vol.Schema( + vol.All( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Required(CONF_TOKEN): cv.string, + }, + ) +) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema(NUKI_SCHEMA)}, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the Nuki component.""" + hass.data.setdefault(DOMAIN, {}) + _LOGGER.debug("Config: %s", config) + + for platform in NUKI_PLATFORMS: + confs = config.get(platform) + if confs is None: + continue + + for conf in confs: + _LOGGER.debug("Conf: %s", conf) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + ) + + return True + + +async def async_setup_entry(hass, entry): + """Set up the Nuki entry.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, LOCK_DOMAIN) + ) + + return True diff --git a/homeassistant/components/nuki/config_flow.py b/homeassistant/components/nuki/config_flow.py new file mode 100644 index 00000000000..9af74cb4423 --- /dev/null +++ b/homeassistant/components/nuki/config_flow.py @@ -0,0 +1,97 @@ +"""Config flow to configure the Nuki integration.""" +import logging + +from pynuki import NukiBridge +from pynuki.bridge import InvalidCredentialsException +from requests.exceptions import RequestException +import voluptuous as vol + +from homeassistant import config_entries, exceptions +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN + +from .const import ( # pylint: disable=unused-import + DEFAULT_PORT, + DEFAULT_TIMEOUT, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +USER_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): vol.Coerce(int), + vol.Required(CONF_TOKEN): str, + } +) + + +async def validate_input(hass, data): + """Validate the user input allows us to connect. + + Data has the keys from USER_SCHEMA with values provided by the user. + """ + + try: + bridge = await hass.async_add_executor_job( + NukiBridge, + data[CONF_HOST], + data[CONF_TOKEN], + data[CONF_PORT], + True, + DEFAULT_TIMEOUT, + ) + + info = bridge.info() + except InvalidCredentialsException as err: + raise InvalidAuth from err + except RequestException as err: + raise CannotConnect from err + + return info + + +class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Nuki config flow.""" + + async def async_step_import(self, user_input=None): + """Handle a flow initiated by import.""" + return await self.async_step_validate(user_input) + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + return await self.async_step_validate(user_input) + + async def async_step_validate(self, user_input): + """Handle init step of a flow.""" + + errors = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if "base" not in errors: + await self.async_set_unique_id(info["ids"]["hardwareId"]) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=info["ids"]["hardwareId"], data=user_input + ) + + return self.async_show_form( + step_id="user", data_schema=USER_SCHEMA, errors=errors + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/nuki/const.py b/homeassistant/components/nuki/const.py new file mode 100644 index 00000000000..07ef49ebd88 --- /dev/null +++ b/homeassistant/components/nuki/const.py @@ -0,0 +1,6 @@ +"""Constants for Nuki.""" +DOMAIN = "nuki" + +# Defaults +DEFAULT_PORT = 8080 +DEFAULT_TIMEOUT = 20 diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index d0b55514a63..fe024405908 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -11,10 +11,9 @@ from homeassistant.components.lock import PLATFORM_SCHEMA, SUPPORT_OPEN, LockEnt from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN from homeassistant.helpers import config_validation as cv, entity_platform -_LOGGER = logging.getLogger(__name__) +from .const import DEFAULT_PORT, DEFAULT_TIMEOUT -DEFAULT_PORT = 8080 -DEFAULT_TIMEOUT = 20 +_LOGGER = logging.getLogger(__name__) ATTR_BATTERY_CRITICAL = "battery_critical" ATTR_NUKI_ID = "nuki_id" @@ -38,6 +37,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Nuki lock platform.""" + _LOGGER.warning( + "Loading Nuki by lock platform configuration is deprecated and will be removed in the future" + ) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Nuki lock platform.""" + config = config_entry.data + _LOGGER.debug("Config: %s", config) def get_entities(): bridge = NukiBridge( diff --git a/homeassistant/components/nuki/manifest.json b/homeassistant/components/nuki/manifest.json index 09cf112d41c..9385821845a 100644 --- a/homeassistant/components/nuki/manifest.json +++ b/homeassistant/components/nuki/manifest.json @@ -1,7 +1,8 @@ { - "domain": "nuki", - "name": "Nuki", - "documentation": "https://www.home-assistant.io/integrations/nuki", - "requirements": ["pynuki==1.3.8"], - "codeowners": ["@pschmitt", "@pvizeli"] -} + "domain": "nuki", + "name": "Nuki", + "documentation": "https://www.home-assistant.io/integrations/nuki", + "requirements": ["pynuki==1.3.8"], + "codeowners": ["@pschmitt", "@pvizeli", "@pree"], + "config_flow": true +} \ No newline at end of file diff --git a/homeassistant/components/nuki/strings.json b/homeassistant/components/nuki/strings.json new file mode 100644 index 00000000000..9e1e4f5e5ab --- /dev/null +++ b/homeassistant/components/nuki/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "token": "[%key:common::config_flow::data::access_token%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/en.json b/homeassistant/components/nuki/translations/en.json new file mode 100644 index 00000000000..70ae9c6a1fe --- /dev/null +++ b/homeassistant/components/nuki/translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Could not login with provided token", + "unknown": "Unknown error" + }, + "step": { + "user": { + "data": { + "token": "Access Token", + "host": "Host", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 77a1dc91dd7..4c12ff30e49 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -141,6 +141,7 @@ FLOWS = [ "nightscout", "notion", "nuheat", + "nuki", "nut", "nws", "nzbget", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 80341520116..d40bb96da45 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -804,6 +804,9 @@ pymonoprice==0.3 # homeassistant.components.myq pymyq==2.0.14 +# homeassistant.components.nuki +pynuki==1.3.8 + # homeassistant.components.nut pynut2==2.1.2 diff --git a/tests/components/nuki/__init__.py b/tests/components/nuki/__init__.py new file mode 100644 index 00000000000..a774935b9db --- /dev/null +++ b/tests/components/nuki/__init__.py @@ -0,0 +1 @@ +"""The tests for nuki integration.""" diff --git a/tests/components/nuki/test_config_flow.py b/tests/components/nuki/test_config_flow.py new file mode 100644 index 00000000000..45168e42c9d --- /dev/null +++ b/tests/components/nuki/test_config_flow.py @@ -0,0 +1,93 @@ +"""Test the nuki config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries, setup +from homeassistant.components.nuki.config_flow import CannotConnect, InvalidAuth +from homeassistant.components.nuki.const import DOMAIN + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + mock_info = {"ids": {"hardwareId": "0001"}} + + with patch( + "homeassistant.components.nuki.config_flow.NukiBridge.info", + return_value=mock_info, + ), patch( + "homeassistant.components.nuki.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.nuki.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "port": 8080, + "token": "test-token", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "0001" + assert result2["data"] == { + "host": "1.1.1.1", + "port": 8080, + "token": "test-token", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.nuki.config_flow.NukiBridge.info", + side_effect=InvalidAuth, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "port": 8080, + "token": "test-token", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.nuki.config_flow.NukiBridge.info", + side_effect=CannotConnect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "port": 8080, + "token": "test-token", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} From 71c169c84f83c8dd23262a9fe783ee6ecc992dae Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Fri, 29 Jan 2021 12:27:57 +0100 Subject: [PATCH 046/796] Address late review comments for upnp (#45696) --- tests/components/upnp/test_init.py | 35 ++++++++++++------------------ 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index 3f7c64ab3ad..26b2f970fed 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -2,7 +2,6 @@ from unittest.mock import AsyncMock, patch -from homeassistant.components import upnp from homeassistant.components.upnp.const import ( CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, @@ -13,10 +12,9 @@ from homeassistant.components.upnp.const import ( DISCOVERY_UNIQUE_ID, DISCOVERY_USN, DOMAIN, - DOMAIN_DEVICES, ) from homeassistant.components.upnp.device import Device -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component from .mock_device import MockDevice @@ -24,7 +22,7 @@ from .mock_device import MockDevice from tests.common import MockConfigEntry -async def test_async_setup_entry_default(hass): +async def test_async_setup_entry_default(hass: HomeAssistantType): """Test async_setup_entry.""" udn = "uuid:device_1" location = "http://192.168.1.1/desc.xml" @@ -40,7 +38,7 @@ async def test_async_setup_entry_default(hass): } ] entry = MockConfigEntry( - domain=upnp.DOMAIN, + domain=DOMAIN, data={ CONFIG_ENTRY_UDN: mock_device.udn, CONFIG_ENTRY_ST: mock_device.device_type, @@ -50,10 +48,11 @@ async def test_async_setup_entry_default(hass): config = { # no upnp } + async_create_device = AsyncMock(return_value=mock_device) async_discover = AsyncMock() - with patch.object( - Device, "async_create_device", AsyncMock(return_value=mock_device) - ), patch.object(Device, "async_discover", async_discover): + with patch.object(Device, "async_create_device", async_create_device), patch.object( + Device, "async_discover", async_discover + ): # initialisation of component, no device discovered async_discover.return_value = [] await async_setup_component(hass, "upnp", config) @@ -61,16 +60,14 @@ async def test_async_setup_entry_default(hass): # loading of config_entry, device discovered async_discover.return_value = discoveries - assert await upnp.async_setup_entry(hass, entry) is True + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) is True # ensure device is stored/used - assert hass.data[DOMAIN][DOMAIN_DEVICES][udn] == mock_device - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() + async_create_device.assert_called_with(hass, discoveries[0][DISCOVERY_LOCATION]) -async def test_sync_setup_entry_multiple_discoveries(hass): +async def test_sync_setup_entry_multiple_discoveries(hass: HomeAssistantType): """Test async_setup_entry.""" udn_0 = "uuid:device_1" location_0 = "http://192.168.1.1/desc.xml" @@ -97,7 +94,7 @@ async def test_sync_setup_entry_multiple_discoveries(hass): }, ] entry = MockConfigEntry( - domain=upnp.DOMAIN, + domain=DOMAIN, data={ CONFIG_ENTRY_UDN: mock_device_1.udn, CONFIG_ENTRY_ST: mock_device_1.device_type, @@ -119,12 +116,8 @@ async def test_sync_setup_entry_multiple_discoveries(hass): # loading of config_entry, device discovered async_discover.return_value = discoveries - assert await upnp.async_setup_entry(hass, entry) is True + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) is True # ensure device is stored/used async_create_device.assert_called_with(hass, discoveries[1][DISCOVERY_LOCATION]) - assert udn_0 not in hass.data[DOMAIN][DOMAIN_DEVICES] - assert hass.data[DOMAIN][DOMAIN_DEVICES][udn_1] == mock_device_1 - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() From da713e206df394ef958bbf236f50b0c0bd1829c2 Mon Sep 17 00:00:00 2001 From: GeoffAtHome Date: Fri, 29 Jan 2021 11:44:56 +0000 Subject: [PATCH 047/796] Add override duration for genius hub switches (#45558) --- .../components/geniushub/services.yaml | 12 +++++++ homeassistant/components/geniushub/switch.py | 31 +++++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/geniushub/services.yaml b/homeassistant/components/geniushub/services.yaml index 50cd8d7d01e..fa46c1d4c09 100644 --- a/homeassistant/components/geniushub/services.yaml +++ b/homeassistant/components/geniushub/services.yaml @@ -26,3 +26,15 @@ set_zone_override: description: >- The duration of the override. Optional, default 1 hour, maximum 24 hours. example: '{"minutes": 135}' + +set_switch_override: + description: >- + Override switch for a given duration. + fields: + entity_id: + description: The zone's entity_id. + example: switch.study + duration: + description: >- + The duration of the override. Optional, default 1 hour, maximum 24 hours. + example: '{"minutes": 135}' diff --git a/homeassistant/components/geniushub/switch.py b/homeassistant/components/geniushub/switch.py index e73468321bd..cb45911d250 100644 --- a/homeassistant/components/geniushub/switch.py +++ b/homeassistant/components/geniushub/switch.py @@ -1,13 +1,29 @@ """Support for Genius Hub switch/outlet devices.""" +from datetime import timedelta + +import voluptuous as vol + from homeassistant.components.switch import DEVICE_CLASS_OUTLET, SwitchEntity +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import DOMAIN, GeniusZone - -ATTR_DURATION = "duration" +from . import ATTR_DURATION, DOMAIN, GeniusZone GH_ON_OFF_ZONE = "on / off" +SVC_SET_SWITCH_OVERRIDE = "set_switch_override" + +SET_SWITCH_OVERRIDE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Optional(ATTR_DURATION): vol.All( + cv.time_period, + vol.Range(min=timedelta(minutes=5), max=timedelta(days=1)), + ), + } +) + async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None @@ -26,6 +42,15 @@ async def async_setup_platform( ] ) + # Register custom services + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SVC_SET_SWITCH_OVERRIDE, + SET_SWITCH_OVERRIDE_SCHEMA, + "async_turn_on", + ) + class GeniusSwitch(GeniusZone, SwitchEntity): """Representation of a Genius Hub switch.""" From b8ff1129209236ee29fc1b7c781b4073f24bdd85 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 29 Jan 2021 12:53:35 +0100 Subject: [PATCH 048/796] Update docker base image 2021.01.1 (#45697) --- build.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.json b/build.json index a7ce097ae84..1cf4217146d 100644 --- a/build.json +++ b/build.json @@ -1,11 +1,11 @@ { "image": "homeassistant/{arch}-homeassistant", "build_from": { - "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" + "aarch64": "homeassistant/aarch64-homeassistant-base:2021.01.1", + "armhf": "homeassistant/armhf-homeassistant-base:2021.01.1", + "armv7": "homeassistant/armv7-homeassistant-base:2021.01.1", + "amd64": "homeassistant/amd64-homeassistant-base:2021.01.1", + "i386": "homeassistant/i386-homeassistant-base:2021.01.1" }, "labels": { "io.hass.type": "core" From 3f67f9e09ce47062637f210f926b6be1e75810f4 Mon Sep 17 00:00:00 2001 From: bsmappee <58250533+bsmappee@users.noreply.github.com> Date: Fri, 29 Jan 2021 15:01:55 +0100 Subject: [PATCH 049/796] Bump pysmappee to 0.2.16 (#45699) --- homeassistant/components/smappee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smappee/manifest.json b/homeassistant/components/smappee/manifest.json index ba1005b87d4..ddbff4e7738 100644 --- a/homeassistant/components/smappee/manifest.json +++ b/homeassistant/components/smappee/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/smappee", "dependencies": ["http"], "requirements": [ - "pysmappee==0.2.13" + "pysmappee==0.2.16" ], "codeowners": [ "@bsmappee" diff --git a/requirements_all.txt b/requirements_all.txt index ac7ac5be451..cdb1290fbf9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1687,7 +1687,7 @@ pyskyqhub==0.1.3 pysma==0.3.5 # homeassistant.components.smappee -pysmappee==0.2.13 +pysmappee==0.2.16 # homeassistant.components.smartthings pysmartapp==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d40bb96da45..bdf88a4a4e0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -872,7 +872,7 @@ pysignalclirestapi==0.3.4 pysma==0.3.5 # homeassistant.components.smappee -pysmappee==0.2.13 +pysmappee==0.2.16 # homeassistant.components.smartthings pysmartapp==0.3.3 From c1586f97db125a7421f6513e127d28a8d85b00ec Mon Sep 17 00:00:00 2001 From: Pieter Mulder Date: Fri, 29 Jan 2021 15:25:01 +0100 Subject: [PATCH 050/796] Only show matching caldav events in calendar (#45701) --- homeassistant/components/caldav/calendar.py | 2 ++ tests/components/caldav/test_calendar.py | 29 +++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index 7abb0ad8444..66b3c974306 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -173,6 +173,8 @@ class WebDavCalendarData: event_list = [] for event in vevent_list: vevent = event.instance.vevent + if not self.is_matching(vevent, self.search): + continue uid = None if hasattr(vevent, "uid"): uid = vevent.uid.value diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index 3e380b44de4..d8c6a44a3ea 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -774,3 +774,32 @@ async def test_event_rrule_hourly_ended(mock_now, hass, calendar): state = hass.states.get("calendar.private") assert state.name == calendar.name assert state.state == STATE_OFF + + +async def test_get_events(hass, calendar): + """Test that all events are returned on API.""" + assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) + await hass.async_block_till_done() + entity = hass.data["calendar"].get_entity("calendar.private") + events = await entity.async_get_events( + hass, datetime.date(2015, 11, 27), datetime.date(2015, 11, 28) + ) + assert len(events) == 14 + + +async def test_get_events_custom_calendars(hass, calendar): + """Test that only searched events are returned on API.""" + config = dict(CALDAV_CONFIG) + config["custom_calendars"] = [ + {"name": "Private", "calendar": "Private", "search": "This is a normal event"} + ] + + assert await async_setup_component(hass, "calendar", {"calendar": config}) + await hass.async_block_till_done() + + entity = hass.data["calendar"].get_entity("calendar.private_private") + events = await entity.async_get_events( + hass, datetime.date(2015, 11, 27), datetime.date(2015, 11, 28) + ) + assert len(events) == 1 + assert events[0]["summary"] == "This is a normal event" From bc3610c8e1828c528ebd050630dee36093341903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleksander=20=C5=BBarczy=C5=84ski?= Date: Fri, 29 Jan 2021 16:03:00 +0100 Subject: [PATCH 051/796] Add patch method to rest switch component (#45663) --- homeassistant/components/rest/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index b6bd759d0bf..ea480d549f3 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -40,7 +40,7 @@ DEFAULT_NAME = "REST Switch" DEFAULT_TIMEOUT = 10 DEFAULT_VERIFY_SSL = True -SUPPORT_REST_METHODS = ["post", "put"] +SUPPORT_REST_METHODS = ["post", "put", "patch"] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { From 5f9a1d105c32fd13d360e53de8e418b13c0c8f56 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Jan 2021 09:57:13 -0600 Subject: [PATCH 052/796] Improve HomeKit Accessory Mode UX (#45402) --- homeassistant/components/homekit/__init__.py | 12 +- .../components/homekit/config_flow.py | 236 +++++++++++------- homeassistant/components/homekit/const.py | 1 + homeassistant/components/homekit/strings.json | 26 +- .../components/homekit/translations/en.json | 90 ++++--- tests/components/homekit/test_config_flow.py | 216 ++++++++++++++-- 6 files changed, 421 insertions(+), 160 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 53fbd7cf8f1..5cbc9bb6f18 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -575,13 +575,15 @@ class HomeKit: bridged_states = [] for state in self.hass.states.async_all(): - if not self._filter(state.entity_id): + entity_id = state.entity_id + + if not self._filter(entity_id): continue - ent_reg_ent = ent_reg.async_get(state.entity_id) + ent_reg_ent = ent_reg.async_get(entity_id) if ent_reg_ent: await self._async_set_device_info_attributes( - ent_reg_ent, dev_reg, state.entity_id + ent_reg_ent, dev_reg, entity_id ) self._async_configure_linked_sensors(ent_reg_ent, device_lookup, state) @@ -612,13 +614,15 @@ class HomeKit: connection = (device_registry.CONNECTION_NETWORK_MAC, formatted_mac) identifier = (DOMAIN, self._entry_id, BRIDGE_SERIAL_NUMBER) self._async_purge_old_bridges(dev_reg, identifier, connection) + is_accessory_mode = self._homekit_mode == HOMEKIT_MODE_ACCESSORY + hk_mode_name = "Accessory" if is_accessory_mode else "Bridge" dev_reg.async_get_or_create( config_entry_id=self._entry_id, identifiers={identifier}, connections={connection}, manufacturer=MANUFACTURER, name=self._name, - model="Home Assistant HomeKit Bridge", + model=f"Home Assistant HomeKit {hk_mode_name}", ) @callback diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 8d763581615..d8708168e12 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -1,4 +1,5 @@ """Config flow for HomeKit integration.""" +import logging import random import string @@ -6,7 +7,14 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_NAME, CONF_PORT +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + CONF_DOMAINS, + CONF_ENTITIES, + CONF_ENTITY_ID, + CONF_NAME, + CONF_PORT, +) from homeassistant.core import callback, split_entity_id import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import ( @@ -27,6 +35,7 @@ from .const import ( DEFAULT_HOMEKIT_MODE, HOMEKIT_MODE_ACCESSORY, HOMEKIT_MODES, + SHORT_ACCESSORY_NAME, SHORT_BRIDGE_NAME, VIDEO_CODEC_COPY, ) @@ -80,6 +89,19 @@ DEFAULT_DOMAINS = [ "water_heater", ] +DOMAINS_PREFER_ACCESSORY_MODE = ["camera", "media_player"] + +CAMERA_ENTITY_PREFIX = "camera." + +_EMPTY_ENTITY_FILTER = { + CONF_INCLUDE_DOMAINS: [], + CONF_EXCLUDE_DOMAINS: [], + CONF_INCLUDE_ENTITIES: [], + CONF_EXCLUDE_ENTITIES: [], +} + +_LOGGER = logging.getLogger(__name__) + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for HomeKit.""" @@ -89,51 +111,90 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize config flow.""" - self.homekit_data = {} + self.hk_data = {} self.entry_title = None + async def async_step_accessory_mode(self, user_input=None): + """Choose specific entity in accessory mode.""" + if user_input is not None: + entity_id = user_input[CONF_ENTITY_ID] + entity_filter = _EMPTY_ENTITY_FILTER.copy() + entity_filter[CONF_INCLUDE_ENTITIES] = [entity_id] + self.hk_data[CONF_FILTER] = entity_filter + if entity_id.startswith(CAMERA_ENTITY_PREFIX): + self.hk_data[CONF_ENTITY_CONFIG] = { + entity_id: {CONF_VIDEO_CODEC: VIDEO_CODEC_COPY} + } + return await self.async_step_pairing() + + all_supported_entities = _async_get_matching_entities( + self.hass, domains=DOMAINS_PREFER_ACCESSORY_MODE + ) + return self.async_show_form( + step_id="accessory_mode", + data_schema=vol.Schema( + {vol.Required(CONF_ENTITY_ID): vol.In(all_supported_entities)} + ), + ) + + async def async_step_bridge_mode(self, user_input=None): + """Choose specific domains in bridge mode.""" + if user_input is not None: + entity_filter = _EMPTY_ENTITY_FILTER.copy() + entity_filter[CONF_INCLUDE_DOMAINS] = user_input[CONF_INCLUDE_DOMAINS] + self.hk_data[CONF_FILTER] = entity_filter + return await self.async_step_pairing() + + default_domains = [] if self._async_current_names() else DEFAULT_DOMAINS + return self.async_show_form( + step_id="bridge_mode", + data_schema=vol.Schema( + { + vol.Required( + CONF_INCLUDE_DOMAINS, default=default_domains + ): cv.multi_select(SUPPORTED_DOMAINS), + } + ), + ) + async def async_step_pairing(self, user_input=None): """Pairing instructions.""" if user_input is not None: - return self.async_create_entry( - title=self.entry_title, data=self.homekit_data - ) + return self.async_create_entry(title=self.entry_title, data=self.hk_data) + + self.hk_data[CONF_PORT] = await self._async_available_port() + self.hk_data[CONF_NAME] = self._async_available_name( + self.hk_data[CONF_HOMEKIT_MODE] + ) + self.entry_title = f"{self.hk_data[CONF_NAME]}:{self.hk_data[CONF_PORT]}" return self.async_show_form( step_id="pairing", - description_placeholders={CONF_NAME: self.homekit_data[CONF_NAME]}, + description_placeholders={CONF_NAME: self.hk_data[CONF_NAME]}, ) async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} + if user_input is not None: - port = await self._async_available_port() - name = self._async_available_name() - title = f"{name}:{port}" - self.homekit_data = user_input.copy() - self.homekit_data[CONF_NAME] = name - self.homekit_data[CONF_PORT] = port - self.homekit_data[CONF_FILTER] = { - CONF_INCLUDE_DOMAINS: user_input[CONF_INCLUDE_DOMAINS], - CONF_INCLUDE_ENTITIES: [], - CONF_EXCLUDE_DOMAINS: [], - CONF_EXCLUDE_ENTITIES: [], + self.hk_data = { + CONF_HOMEKIT_MODE: user_input[CONF_HOMEKIT_MODE], } - del self.homekit_data[CONF_INCLUDE_DOMAINS] - self.entry_title = title - return await self.async_step_pairing() - - default_domains = [] if self._async_current_names() else DEFAULT_DOMAINS - setup_schema = vol.Schema( - { - vol.Required( - CONF_INCLUDE_DOMAINS, default=default_domains - ): cv.multi_select(SUPPORTED_DOMAINS), - } - ) + if user_input[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_ACCESSORY: + return await self.async_step_accessory_mode() + return await self.async_step_bridge_mode() + homekit_mode = self.hk_data.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE) return self.async_show_form( - step_id="user", data_schema=setup_schema, errors=errors + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOMEKIT_MODE, default=homekit_mode): vol.In( + HOMEKIT_MODES + ) + } + ), + errors=errors, ) async def async_step_import(self, user_input=None): @@ -153,28 +214,28 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @callback def _async_current_names(self): """Return a set of bridge names.""" - current_entries = self._async_current_entries() - return { entry.data[CONF_NAME] - for entry in current_entries + for entry in self._async_current_entries() if CONF_NAME in entry.data } @callback - def _async_available_name(self): + def _async_available_name(self, homekit_mode): """Return an available for the bridge.""" + base_name = SHORT_BRIDGE_NAME + if homekit_mode == HOMEKIT_MODE_ACCESSORY: + base_name = SHORT_ACCESSORY_NAME + # We always pick a RANDOM name to avoid Zeroconf # name collisions. If the name has been seen before # pairing will probably fail. acceptable_chars = string.ascii_uppercase + string.digits - trailer = "".join(random.choices(acceptable_chars, k=4)) - all_names = self._async_current_names() - suggested_name = f"{SHORT_BRIDGE_NAME} {trailer}" - while suggested_name in all_names: + suggested_name = None + while not suggested_name or suggested_name in self._async_current_names(): trailer = "".join(random.choices(acceptable_chars, k=4)) - suggested_name = f"{SHORT_BRIDGE_NAME} {trailer}" + suggested_name = f"{base_name} {trailer}" return suggested_name @@ -196,12 +257,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): - """Handle a option flow for tado.""" + """Handle a option flow for homekit.""" def __init__(self, config_entry: config_entries.ConfigEntry): """Initialize options flow.""" self.config_entry = config_entry - self.homekit_options = {} + self.hk_options = {} self.included_cameras = set() async def async_step_yaml(self, user_input=None): @@ -217,17 +278,17 @@ class OptionsFlowHandler(config_entries.OptionsFlow): """Choose advanced options.""" if not self.show_advanced_options or user_input is not None: if user_input: - self.homekit_options.update(user_input) + self.hk_options.update(user_input) - self.homekit_options[CONF_AUTO_START] = self.homekit_options.get( + self.hk_options[CONF_AUTO_START] = self.hk_options.get( CONF_AUTO_START, DEFAULT_AUTO_START ) for key in (CONF_DOMAINS, CONF_ENTITIES): - if key in self.homekit_options: - del self.homekit_options[key] + if key in self.hk_options: + del self.hk_options[key] - return self.async_create_entry(title="", data=self.homekit_options) + return self.async_create_entry(title="", data=self.hk_options) return self.async_show_form( step_id="advanced", @@ -235,7 +296,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): { vol.Optional( CONF_AUTO_START, - default=self.homekit_options.get( + default=self.hk_options.get( CONF_AUTO_START, DEFAULT_AUTO_START ), ): bool @@ -246,7 +307,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_cameras(self, user_input=None): """Choose camera config.""" if user_input is not None: - entity_config = self.homekit_options[CONF_ENTITY_CONFIG] + entity_config = self.hk_options[CONF_ENTITY_CONFIG] for entity_id in self.included_cameras: if entity_id in user_input[CONF_CAMERA_COPY]: entity_config.setdefault(entity_id, {})[ @@ -260,7 +321,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return await self.async_step_advanced() cameras_with_copy = [] - entity_config = self.homekit_options.setdefault(CONF_ENTITY_CONFIG, {}) + entity_config = self.hk_options.setdefault(CONF_ENTITY_CONFIG, {}) for entity in self.included_cameras: hk_entity_config = entity_config.get(entity, {}) if hk_entity_config.get(CONF_VIDEO_CODEC) == VIDEO_CODEC_COPY: @@ -279,19 +340,14 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_include_exclude(self, user_input=None): """Choose entities to include or exclude from the domain.""" if user_input is not None: - entity_filter = { - CONF_INCLUDE_DOMAINS: [], - CONF_EXCLUDE_DOMAINS: [], - CONF_INCLUDE_ENTITIES: [], - CONF_EXCLUDE_ENTITIES: [], - } + entity_filter = _EMPTY_ENTITY_FILTER.copy() if isinstance(user_input[CONF_ENTITIES], list): entities = user_input[CONF_ENTITIES] else: entities = [user_input[CONF_ENTITIES]] if ( - self.homekit_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_ACCESSORY + self.hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_ACCESSORY or user_input[CONF_INCLUDE_EXCLUDE_MODE] == MODE_INCLUDE ): entity_filter[CONF_INCLUDE_ENTITIES] = entities @@ -300,7 +356,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): domains_with_entities_selected = _domains_set_from_entities(entities) entity_filter[CONF_INCLUDE_DOMAINS] = [ domain - for domain in self.homekit_options[CONF_DOMAINS] + for domain in self.hk_options[CONF_DOMAINS] if domain not in domains_with_entities_selected ] @@ -308,34 +364,33 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if entity_id not in entities: self.included_cameras.remove(entity_id) else: - entity_filter[CONF_INCLUDE_DOMAINS] = self.homekit_options[CONF_DOMAINS] + entity_filter[CONF_INCLUDE_DOMAINS] = self.hk_options[CONF_DOMAINS] entity_filter[CONF_EXCLUDE_ENTITIES] = entities for entity_id in entities: if entity_id in self.included_cameras: self.included_cameras.remove(entity_id) - self.homekit_options[CONF_FILTER] = entity_filter + self.hk_options[CONF_FILTER] = entity_filter if self.included_cameras: return await self.async_step_cameras() return await self.async_step_advanced() - entity_filter = self.homekit_options.get(CONF_FILTER, {}) - all_supported_entities = await self.hass.async_add_executor_job( - _get_entities_matching_domains, + entity_filter = self.hk_options.get(CONF_FILTER, {}) + all_supported_entities = _async_get_matching_entities( self.hass, - self.homekit_options[CONF_DOMAINS], + domains=self.hk_options[CONF_DOMAINS], ) self.included_cameras = { entity_id for entity_id in all_supported_entities - if entity_id.startswith("camera.") + if entity_id.startswith(CAMERA_ENTITY_PREFIX) } data_schema = {} entities = entity_filter.get(CONF_INCLUDE_ENTITIES, []) - if self.homekit_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_ACCESSORY: + if self.hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_ACCESSORY: entity_schema = vol.In else: if entities: @@ -362,42 +417,43 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return await self.async_step_yaml(user_input) if user_input is not None: - self.homekit_options.update(user_input) + self.hk_options.update(user_input) return await self.async_step_include_exclude() - self.homekit_options = dict(self.config_entry.options) - entity_filter = self.homekit_options.get(CONF_FILTER, {}) + hk_options = dict(self.config_entry.options) + entity_filter = hk_options.get(CONF_FILTER, {}) - homekit_mode = self.homekit_options.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE) + homekit_mode = hk_options.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE) domains = entity_filter.get(CONF_INCLUDE_DOMAINS, []) include_entities = entity_filter.get(CONF_INCLUDE_ENTITIES) if include_entities: domains.extend(_domains_set_from_entities(include_entities)) - data_schema = vol.Schema( - { - vol.Optional(CONF_HOMEKIT_MODE, default=homekit_mode): vol.In( - HOMEKIT_MODES - ), - vol.Optional( - CONF_DOMAINS, - default=domains, - ): cv.multi_select(SUPPORTED_DOMAINS), - } + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required(CONF_HOMEKIT_MODE, default=homekit_mode): vol.In( + HOMEKIT_MODES + ), + vol.Required( + CONF_DOMAINS, + default=domains, + ): cv.multi_select(SUPPORTED_DOMAINS), + } + ), ) - return self.async_show_form(step_id="init", data_schema=data_schema) -def _get_entities_matching_domains(hass, domains): - """List entities in the given domains.""" - included_domains = set(domains) - entity_ids = [ - state.entity_id - for state in hass.states.all() - if (split_entity_id(state.entity_id))[0] in included_domains - ] - entity_ids.sort() - return entity_ids +def _async_get_matching_entities(hass, domains=None): + """Fetch all entities or entities in the given domains.""" + return { + state.entity_id: f"{state.entity_id} ({state.attributes.get(ATTR_FRIENDLY_NAME, state.entity_id)})" + for state in sorted( + hass.states.async_all(domains and set(domains)), + key=lambda item: item.entity_id, + ) + } def _domains_set_from_entities(entity_ids): diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 77c5dbff0f9..fac4168a79b 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -104,6 +104,7 @@ SERVICE_HOMEKIT_RESET_ACCESSORY = "reset_accessory" BRIDGE_MODEL = "Bridge" BRIDGE_NAME = "Home Assistant Bridge" SHORT_BRIDGE_NAME = "HASS Bridge" +SHORT_ACCESSORY_NAME = "HASS Accessory" BRIDGE_SERIAL_NUMBER = "homekit.bridge" MANUFACTURER = "Home Assistant" diff --git a/homeassistant/components/homekit/strings.json b/homeassistant/components/homekit/strings.json index 5ba578f38c3..ed825ada23c 100644 --- a/homeassistant/components/homekit/strings.json +++ b/homeassistant/components/homekit/strings.json @@ -8,18 +8,18 @@ "init": { "data": { "mode": "[%key:common::config_flow::data::mode%]", - "include_domains": "[%key:component::homekit::config::step::user::data::include_domains%]" + "include_domains": "[%key:component::homekit::config::step::bridge_mode::data::include_domains%]" }, - "description": "HomeKit can be configured expose a bridge or a single accessory. In accessory mode, only a single entity can be used. Accessory mode is required for media players with the TV device class to function properly. Entities in the \u201cDomains to include\u201d will be exposed to HomeKit. You will be able to select which entities to include or exclude from this list on the next screen.", - "title": "Select domains to expose." + "description": "HomeKit can be configured expose a bridge or a single accessory. In accessory mode, only a single entity can be used. Accessory mode is required for media players with the TV device class to function properly. Entities in the \u201cDomains to include\u201d will be included to HomeKit. You will be able to select which entities to include or exclude from this list on the next screen.", + "title": "Select domains to be included." }, "include_exclude": { "data": { "mode": "[%key:common::config_flow::data::mode%]", "entities": "Entities" }, - "description": "Choose the entities to be exposed. In accessory mode, only a single entity is exposed. In bridge include mode, all entities in the domain will be exposed unless specific entities are selected. In bridge exclude mode, all entities in the domain will be exposed except for the excluded entities. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.", - "title": "Select entities to be exposed" + "description": "Choose the entities to be included. In accessory mode, only a single entity is included. In bridge include mode, all entities in the domain will be included unless specific entities are selected. In bridge exclude mode, all entities in the domain will be included except for the excluded entities. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.", + "title": "Select entities to be included" }, "cameras": { "data": { @@ -41,11 +41,25 @@ "step": { "user": { "data": { - "include_domains": "Domains to include" + "mode": "[%key:common::config_flow::data::mode%]" }, "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 best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.", "title": "Activate HomeKit" }, + "accessory_mode": { + "data": { + "entity_id": "Entity" + }, + "description": "Choose the entity to be included. In accessory mode, only a single entity is included.", + "title": "Select entity to be included" + }, + "bridge_mode": { + "data": { + "include_domains": "Domains to include" + }, + "description": "Choose the domains to be included. All supported entities in the domain will be included.", + "title": "Select domains to be included" + }, "pairing": { "title": "Pair HomeKit", "description": "As soon as the {name} is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d." diff --git a/homeassistant/components/homekit/translations/en.json b/homeassistant/components/homekit/translations/en.json index 5ba578f38c3..cc6c8f8dc31 100644 --- a/homeassistant/components/homekit/translations/en.json +++ b/homeassistant/components/homekit/translations/en.json @@ -1,25 +1,44 @@ { + "config": { + "abort": { + "port_name_in_use": "An accessory or bridge with the same name or port is already configured." + }, + "step": { + "accessory_mode": { + "data": { + "entity_id": "Entity" + }, + "description": "Choose the entity to be included. In accessory mode, only a single entity is included.", + "title": "Select entity to be included" + }, + "bridge_mode": { + "data": { + "include_domains": "Domains to include" + }, + "description": "Choose the domains to be included. All supported entities in the domain will be included.", + "title": "Select domains to be included" + }, + "pairing": { + "description": "As soon as the {name} is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d.", + "title": "Pair HomeKit" + }, + "user": { + "data": { + "mode": "Mode" + }, + "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 best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each TV, media player and camera.", + "title": "Activate HomeKit" + } + } + }, "options": { "step": { - "yaml": { - "title": "Adjust HomeKit Options", - "description": "This entry is controlled via YAML" - }, - "init": { + "advanced": { "data": { - "mode": "[%key:common::config_flow::data::mode%]", - "include_domains": "[%key:component::homekit::config::step::user::data::include_domains%]" + "auto_start": "Autostart (disable if you are calling the homekit.start service manually)" }, - "description": "HomeKit can be configured expose a bridge or a single accessory. In accessory mode, only a single entity can be used. Accessory mode is required for media players with the TV device class to function properly. Entities in the \u201cDomains to include\u201d will be exposed to HomeKit. You will be able to select which entities to include or exclude from this list on the next screen.", - "title": "Select domains to expose." - }, - "include_exclude": { - "data": { - "mode": "[%key:common::config_flow::data::mode%]", - "entities": "Entities" - }, - "description": "Choose the entities to be exposed. In accessory mode, only a single entity is exposed. In bridge include mode, all entities in the domain will be exposed unless specific entities are selected. In bridge exclude mode, all entities in the domain will be exposed except for the excluded entities. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.", - "title": "Select entities to be exposed" + "description": "These settings only need to be adjusted if HomeKit is not functional.", + "title": "Advanced Configuration" }, "cameras": { "data": { @@ -28,31 +47,26 @@ "description": "Check all cameras that support native H.264 streams. If the camera does not output a H.264 stream, the system will transcode the video to H.264 for HomeKit. Transcoding requires a performant CPU and is unlikely to work on single board computers.", "title": "Select camera video codec." }, - "advanced": { + "include_exclude": { "data": { - "auto_start": "Autostart (disable if you are calling the homekit.start service manually)" + "entities": "Entities", + "mode": "Mode" }, - "description": "These settings only need to be adjusted if HomeKit is not functional.", - "title": "Advanced Configuration" - } - } - }, - "config": { - "step": { - "user": { - "data": { - "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 best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.", - "title": "Activate HomeKit" + "description": "Choose the entities to be included. In accessory mode, only a single entity is included. In bridge include mode, all entities in the domain will be included unless specific entities are selected. In bridge exclude mode, all entities in the domain will be included except for the excluded entities. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.", + "title": "Select entities to be included" }, - "pairing": { - "title": "Pair HomeKit", - "description": "As soon as the {name} is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d." + "init": { + "data": { + "include_domains": "Domains to include", + "mode": "Mode" + }, + "description": "HomeKit can be configured expose a bridge or a single accessory. In accessory mode, only a single entity can be used. Accessory mode is required for media players with the TV device class to function properly. Entities in the \u201cDomains to include\u201d will be included to HomeKit. You will be able to select which entities to include or exclude from this list on the next screen.", + "title": "Select domains to be included." + }, + "yaml": { + "description": "This entry is controlled via YAML", + "title": "Adjust HomeKit Options" } - }, - "abort": { - "port_name_in_use": "An accessory or bridge with the same name or port is already configured." } } } diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index 4438404af2e..1dd628af18d 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -32,8 +32,8 @@ def _mock_config_entry_with_options_populated(): ) -async def test_user_form(hass): - """Test we can setup a new instance.""" +async def test_setup_in_bridge_mode(hass): + """Test we can setup a new instance in bridge mode.""" await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -41,17 +41,23 @@ async def test_user_form(hass): assert result["type"] == "form" assert result["errors"] == {} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"mode": "bridge"}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "bridge_mode" + with patch( "homeassistant.components.homekit.config_flow.find_next_available_port", return_value=12345, ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], {"include_domains": ["light"]}, ) - - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["step_id"] == "pairing" + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["step_id"] == "pairing" with patch( "homeassistant.components.homekit.async_setup", return_value=True @@ -59,22 +65,23 @@ async def test_user_form(hass): "homeassistant.components.homekit.async_setup_entry", return_value=True, ) as mock_setup_entry: - result3 = await hass.config_entries.flow.async_configure( - result["flow_id"], + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], {}, ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result3["title"][:11] == "HASS Bridge" - bridge_name = (result3["title"].split(":"))[0] - assert result3["data"] == { + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["title"][:11] == "HASS Bridge" + bridge_name = (result4["title"].split(":"))[0] + assert result4["data"] == { "filter": { "exclude_domains": [], "exclude_entities": [], "include_domains": ["light"], "include_entities": [], }, + "mode": "bridge", "name": bridge_name, "port": 12345, } @@ -82,6 +89,66 @@ async def test_user_form(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_setup_in_accessory_mode(hass): + """Test we can setup a new instance in accessory.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + hass.states.async_set("camera.mine", "off") + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"mode": "accessory"}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "accessory_mode" + + with patch( + "homeassistant.components.homekit.config_flow.find_next_available_port", + return_value=12345, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"entity_id": "camera.mine"}, + ) + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["step_id"] == "pairing" + + with patch( + "homeassistant.components.homekit.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.homekit.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["title"][:14] == "HASS Accessory" + bridge_name = (result4["title"].split(":"))[0] + assert result4["data"] == { + "filter": { + "exclude_domains": [], + "exclude_entities": [], + "include_domains": [], + "include_entities": ["camera.mine"], + }, + "mode": "accessory", + "name": bridge_name, + "entity_config": {"camera.mine": {"video_codec": "copy"}}, + "port": 12345, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_import(hass): """Test we can import instance.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -343,10 +410,11 @@ async def test_options_flow_exclude_mode_with_cameras(hass): result3 = await hass.config_entries.options.async_configure( result2["flow_id"], - user_input={"camera_copy": []}, + user_input={"camera_copy": ["camera.native_h264"]}, ) assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { "auto_start": True, "mode": "bridge", @@ -356,7 +424,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass): "include_domains": ["fan", "vacuum", "climate", "camera"], "include_entities": [], }, - "entity_config": {"camera.native_h264": {}}, + "entity_config": {"camera.native_h264": {"video_codec": "copy"}}, } @@ -458,7 +526,7 @@ async def test_options_flow_include_mode_with_cameras(hass): "include_domains": ["fan", "vacuum", "climate", "camera"], "include_entities": [], }, - "entity_config": {"camera.native_h264": {}}, + "entity_config": {}, } @@ -519,19 +587,19 @@ async def test_options_flow_include_mode_basic_accessory(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" - result = await hass.config_entries.options.async_configure( + result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"domains": ["media_player"], "mode": "accessory"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "include_exclude" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "include_exclude" - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], + result3 = await hass.config_entries.options.async_configure( + result2["flow_id"], user_input={"entities": "media_player.tv"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { "auto_start": True, "mode": "accessory", @@ -542,3 +610,107 @@ async def test_options_flow_include_mode_basic_accessory(hass): "include_entities": ["media_player.tv"], }, } + + +async def test_converting_bridge_to_accessory_mode(hass): + """Test we can convert a bridge to accessory mode.""" + 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"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"mode": "bridge"}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "bridge_mode" + + with patch( + "homeassistant.components.homekit.config_flow.find_next_available_port", + return_value=12345, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"include_domains": ["light"]}, + ) + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["step_id"] == "pairing" + + with patch( + "homeassistant.components.homekit.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.homekit.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["title"][:11] == "HASS Bridge" + bridge_name = (result4["title"].split(":"))[0] + assert result4["data"] == { + "filter": { + "exclude_domains": [], + "exclude_entities": [], + "include_domains": ["light"], + "include_entities": [], + }, + "mode": "bridge", + "name": bridge_name, + "port": 12345, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = result4["result"] + + hass.states.async_set("camera.tv", "off") + hass.states.async_set("camera.sonos", "off") + + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": False} + ) + + 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={"domains": ["camera"], "mode": "accessory"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "include_exclude" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"entities": "camera.tv"}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "cameras" + + result3 = await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={"camera_copy": ["camera.tv"]}, + ) + + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + "auto_start": True, + "entity_config": {"camera.tv": {"video_codec": "copy"}}, + "mode": "accessory", + "filter": { + "exclude_domains": [], + "exclude_entities": [], + "include_domains": [], + "include_entities": ["camera.tv"], + }, + } From b2789621bd8437bb818ee9bf6f6347f309cd4fb0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Jan 2021 17:47:54 +0100 Subject: [PATCH 053/796] Updates to dev container (#45706) --- .devcontainer/devcontainer.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e01a97425e1..26e4b2e78ad 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,7 +8,6 @@ "extensions": [ "ms-python.vscode-pylance", "visualstudioexptteam.vscodeintellicode", - "ms-azure-devops.azure-pipelines", "redhat.vscode-yaml", "esbenp.prettier-vscode" ], @@ -19,12 +18,11 @@ "python.linting.enabled": true, "python.formatting.provider": "black", "python.testing.pytestArgs": ["--no-cov"], - "python.testing.pytestEnabled": true, "editor.formatOnPaste": false, "editor.formatOnSave": true, "editor.formatOnType": true, "files.trimTrailingWhitespace": true, - "terminal.integrated.shell.linux": "/bin/bash", + "terminal.integrated.shell.linux": "/usr/bin/zsh", "yaml.customTags": [ "!input scalar", "!secret scalar", From bcc9add0b4244b19e1e645f1d7efb57ba030680f Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 29 Jan 2021 17:57:39 +0100 Subject: [PATCH 054/796] Fix mqtt check in ozw (#45709) --- homeassistant/components/ozw/__init__.py | 11 +++++++++-- homeassistant/components/ozw/config_flow.py | 6 +++++- tests/components/ozw/common.py | 3 ++- tests/components/ozw/conftest.py | 12 +++++++++++- tests/components/ozw/test_config_flow.py | 6 ++---- tests/components/ozw/test_init.py | 20 ++++++++++++++++++++ 6 files changed, 49 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index 0636671188d..a75c05416dc 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -21,7 +21,7 @@ from openzwavemqtt.util.mqtt_client import MQTTClient from homeassistant.components import mqtt from homeassistant.components.hassio.handler import HassioAPIError -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ENTRY_STATE_LOADED, ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -95,12 +95,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): manager_options["send_message"] = mqtt_client.send_message else: - if "mqtt" not in hass.config.components: + mqtt_entries = hass.config_entries.async_entries("mqtt") + if not mqtt_entries or mqtt_entries[0].state != ENTRY_STATE_LOADED: _LOGGER.error("MQTT integration is not set up") return False + mqtt_entry = mqtt_entries[0] # MQTT integration only has one entry. + @callback def send_message(topic, payload): + if mqtt_entry.state != ENTRY_STATE_LOADED: + _LOGGER.error("MQTT integration is not set up") + return + mqtt.async_publish(hass, topic, json.dumps(payload)) manager_options["send_message"] = send_message diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py index 14d875e0a70..00917c0609c 100644 --- a/homeassistant/components/ozw/config_flow.py +++ b/homeassistant/components/ozw/config_flow.py @@ -97,7 +97,11 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): This is the entry point for the logic that is needed when this integration will depend on the MQTT integration. """ - if "mqtt" not in self.hass.config.components: + mqtt_entries = self.hass.config_entries.async_entries("mqtt") + if ( + not mqtt_entries + or mqtt_entries[0].state != config_entries.ENTRY_STATE_LOADED + ): return self.async_abort(reason="mqtt_required") return self._async_create_entry_from_vars() diff --git a/tests/components/ozw/common.py b/tests/components/ozw/common.py index 6b44d364413..1467d619afe 100644 --- a/tests/components/ozw/common.py +++ b/tests/components/ozw/common.py @@ -10,7 +10,8 @@ from tests.common import MockConfigEntry async def setup_ozw(hass, entry=None, fixture=None): """Set up OZW and load a dump.""" - hass.config.components.add("mqtt") + mqtt_entry = MockConfigEntry(domain="mqtt", state=config_entries.ENTRY_STATE_LOADED) + mqtt_entry.add_to_hass(hass) if entry is None: entry = MockConfigEntry( diff --git a/tests/components/ozw/conftest.py b/tests/components/ozw/conftest.py index 00f8d8e52d2..a59388f118f 100644 --- a/tests/components/ozw/conftest.py +++ b/tests/components/ozw/conftest.py @@ -4,9 +4,11 @@ from unittest.mock import patch import pytest +from homeassistant.config_entries import ENTRY_STATE_LOADED + from .common import MQTTMessage -from tests.common import load_fixture +from tests.common import MockConfigEntry, load_fixture from tests.components.light.conftest import mock_light_profiles # noqa @@ -268,3 +270,11 @@ def mock_get_addon_discovery_info(): "homeassistant.components.hassio.async_get_addon_discovery_info" ) as get_addon_discovery_info: yield get_addon_discovery_info + + +@pytest.fixture(name="mqtt") +async def mock_mqtt_fixture(hass): + """Mock the MQTT integration.""" + mqtt_entry = MockConfigEntry(domain="mqtt", state=ENTRY_STATE_LOADED) + mqtt_entry.add_to_hass(hass) + return mqtt_entry diff --git a/tests/components/ozw/test_config_flow.py b/tests/components/ozw/test_config_flow.py index d1ac413270d..0a746398cf9 100644 --- a/tests/components/ozw/test_config_flow.py +++ b/tests/components/ozw/test_config_flow.py @@ -79,9 +79,8 @@ def mock_start_addon(): yield start_addon -async def test_user_not_supervisor_create_entry(hass): +async def test_user_not_supervisor_create_entry(hass, mqtt): """Test the user step creates an entry not on Supervisor.""" - hass.config.components.add("mqtt") await setup.async_setup_component(hass, "persistent_notification", {}) with patch( @@ -128,9 +127,8 @@ async def test_one_instance_allowed(hass): assert result["reason"] == "single_instance_allowed" -async def test_not_addon(hass, supervisor): +async def test_not_addon(hass, supervisor, mqtt): """Test opting out of add-on on Supervisor.""" - hass.config.components.add("mqtt") await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( diff --git a/tests/components/ozw/test_init.py b/tests/components/ozw/test_init.py index ac7ad59f3cb..c76bfd4a3a0 100644 --- a/tests/components/ozw/test_init.py +++ b/tests/components/ozw/test_init.py @@ -37,6 +37,26 @@ async def test_setup_entry_without_mqtt(hass): assert not await hass.config_entries.async_setup(entry.entry_id) +async def test_publish_without_mqtt(hass, caplog): + """Test publish without mqtt integration setup.""" + with patch("homeassistant.components.ozw.OZWOptions") as ozw_options: + await setup_ozw(hass) + + send_message = ozw_options.call_args[1]["send_message"] + + mqtt_entries = hass.config_entries.async_entries("mqtt") + mqtt_entry = mqtt_entries[0] + await hass.config_entries.async_remove(mqtt_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.config_entries.async_entries("mqtt") + + # Sending a message should not error with the MQTT integration not set up. + send_message("test_topic", "test_payload") + + assert "MQTT integration is not set up" in caplog.text + + async def test_unload_entry(hass, generic_data, switch_msg, caplog): """Test unload the config entry.""" entry = MockConfigEntry( From af68d5fb412d753d87dbee1b641953d5ef30d851 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 29 Jan 2021 17:58:25 +0100 Subject: [PATCH 055/796] Use a fully mocked credential (#45707) --- tests/conftest.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 27559e9659d..f6ef33e8f25 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -204,9 +204,13 @@ def mock_device_tracker_conf(): @pytest.fixture async def hass_admin_credential(hass, local_auth): """Provide credentials for admin user.""" - await hass.async_add_executor_job(local_auth.data.add_auth, "admin", "admin-pass") - - return await local_auth.async_get_or_create_credentials({"username": "admin"}) + return Credentials( + id="mock-credential-id", + auth_provider_type="homeassistant", + auth_provider_id=None, + data={"username": "admin"}, + is_new=False, + ) @pytest.fixture From 97fd05eb9f71ad16707dd5358501b69e4d296aa0 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 29 Jan 2021 18:14:39 +0100 Subject: [PATCH 056/796] Allow new UniFi flows to update existing entries if host and site match (#45668) --- homeassistant/components/unifi/config_flow.py | 58 +++++++++-------- homeassistant/components/unifi/strings.json | 1 + tests/components/unifi/test_config_flow.py | 63 ++++++++++++++++--- 3 files changed, 90 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 85203204b2f..f5e947c5e6f 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -38,9 +38,9 @@ from .const import ( LOGGER, ) from .controller import get_controller -from .errors import AlreadyConfigured, AuthenticationRequired, CannotConnect +from .errors import AuthenticationRequired, CannotConnect -DEFAULT_PORT = 8443 +DEFAULT_PORT = 443 DEFAULT_SITE_ID = "default" DEFAULT_VERIFY_SSL = False @@ -76,7 +76,7 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): """Initialize the UniFi flow.""" self.config = {} self.sites = None - self.reauth_config_entry = {} + self.reauth_config_entry = None self.reauth_config = {} self.reauth_schema = {} @@ -146,32 +146,40 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): errors = {} if user_input is not None: - try: - self.config[CONF_SITE_ID] = user_input[CONF_SITE_ID] - data = {CONF_CONTROLLER: self.config} - if self.reauth_config_entry: - self.hass.config_entries.async_update_entry( - self.reauth_config_entry, data=data - ) - await self.hass.config_entries.async_reload( - self.reauth_config_entry.entry_id - ) - return self.async_abort(reason="reauth_successful") + self.config[CONF_SITE_ID] = user_input[CONF_SITE_ID] + data = {CONF_CONTROLLER: self.config} - for entry in self._async_current_entries(): - controller = entry.data[CONF_CONTROLLER] - if ( - controller[CONF_HOST] == self.config[CONF_HOST] - and controller[CONF_SITE_ID] == self.config[CONF_SITE_ID] - ): - raise AlreadyConfigured + if self.reauth_config_entry: + self.hass.config_entries.async_update_entry( + self.reauth_config_entry, data=data + ) + await self.hass.config_entries.async_reload( + self.reauth_config_entry.entry_id + ) + return self.async_abort(reason="reauth_successful") - site_nice_name = self.sites[self.config[CONF_SITE_ID]] - return self.async_create_entry(title=site_nice_name, data=data) + for config_entry in self._async_current_entries(): + controller_data = config_entry.data[CONF_CONTROLLER] + if ( + controller_data[CONF_HOST] != self.config[CONF_HOST] + or controller_data[CONF_SITE_ID] != self.config[CONF_SITE_ID] + ): + continue - except AlreadyConfigured: - return self.async_abort(reason="already_configured") + controller = self.hass.data.get(UNIFI_DOMAIN, {}).get( + config_entry.entry_id + ) + + if controller and controller.available: + return self.async_abort(reason="already_configured") + + self.hass.config_entries.async_update_entry(config_entry, data=data) + await self.hass.config_entries.async_reload(config_entry.entry_id) + return self.async_abort(reason="configuration_updated") + + site_nice_name = self.sites[self.config[CONF_SITE_ID]] + return self.async_create_entry(title=site_nice_name, data=data) if len(self.sites) == 1: return await self.async_step_site({CONF_SITE_ID: next(iter(self.sites))}) diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index 15cc2fb45e7..be0bda37971 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -21,6 +21,7 @@ }, "abort": { "already_configured": "Controller site is already configured", + "configuration_updated": "Configuration updated.", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 6eb049b573a..54c5fe291ea 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -97,7 +97,7 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery): CONF_HOST: "unifi", CONF_USERNAME: "", CONF_PASSWORD: "", - CONF_PORT: 8443, + CONF_PORT: 443, CONF_VERIFY_SSL: False, } @@ -189,12 +189,9 @@ async def test_flow_works_multiple_sites(hass, aioclient_mock): assert result["data_schema"]({"site": "site2"}) -async def test_flow_fails_site_already_configured(hass, aioclient_mock): - """Test config flow.""" - entry = MockConfigEntry( - domain=UNIFI_DOMAIN, data={"controller": {"host": "1.2.3.4", "site": "site_id"}} - ) - entry.add_to_hass(hass) +async def test_flow_raise_already_configured(hass, aioclient_mock): + """Test config flow aborts since a connected config entry already exists.""" + await setup_unifi_integration(hass) result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": "user"} @@ -235,6 +232,58 @@ async def test_flow_fails_site_already_configured(hass, aioclient_mock): assert result["reason"] == "already_configured" +async def test_flow_aborts_configuration_updated(hass, aioclient_mock): + """Test config flow aborts since a connected config entry already exists.""" + entry = MockConfigEntry( + domain=UNIFI_DOMAIN, data={"controller": {"host": "1.2.3.4", "site": "office"}} + ) + entry.add_to_hass(hass) + + entry = MockConfigEntry( + domain=UNIFI_DOMAIN, data={"controller": {"host": "1.2.3.4", "site": "site_id"}} + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + UNIFI_DOMAIN, context={"source": "user"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + aioclient_mock.get("https://1.2.3.4:1234", status=302) + + aioclient_mock.post( + "https://1.2.3.4:1234/api/login", + json={"data": "login successful", "meta": {"rc": "ok"}}, + headers={"content-type": CONTENT_TYPE_JSON}, + ) + + aioclient_mock.get( + "https://1.2.3.4:1234/api/self/sites", + json={ + "data": [{"desc": "Site name", "name": "site_id", "role": "admin"}], + "meta": {"rc": "ok"}, + }, + headers={"content-type": CONTENT_TYPE_JSON}, + ) + + with patch("homeassistant.components.unifi.async_setup_entry"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 1234, + CONF_VERIFY_SSL: True, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "configuration_updated" + + async def test_flow_fails_user_credentials_faulty(hass, aioclient_mock): """Test config flow.""" result = await hass.config_entries.flow.async_init( From fbffea6b6131898b36711dc0f29660255fcdf7f6 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Fri, 29 Jan 2021 10:22:44 -0800 Subject: [PATCH 057/796] Add unit of measurement and icon for sleep score (#45705) --- homeassistant/components/withings/common.py | 4 ++-- homeassistant/components/withings/const.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 52e22f9501a..c08ddddf4a5 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -402,8 +402,8 @@ WITHINGS_ATTRIBUTES = [ Measurement.SLEEP_SCORE, GetSleepSummaryField.SLEEP_SCORE, "Sleep score", - "", - None, + const.SCORE_POINTS, + "mdi:medal", SENSOR_DOMAIN, False, UpdateType.POLL, diff --git a/homeassistant/components/withings/const.py b/homeassistant/components/withings/const.py index c6cad929f81..d88f4e38c6a 100644 --- a/homeassistant/components/withings/const.py +++ b/homeassistant/components/withings/const.py @@ -55,6 +55,7 @@ class Measurement(Enum): WEIGHT_KG = "weight_kg" +SCORE_POINTS = "points" UOM_BEATS_PER_MINUTE = "bpm" UOM_BREATHS_PER_MINUTE = f"br/{const.TIME_MINUTES}" UOM_FREQUENCY = "times" From d4f186078c7f19866a28ba132b89cf4e3fb84d08 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 29 Jan 2021 19:57:14 +0100 Subject: [PATCH 058/796] During tests we can run with lowest rounds (#45710) --- tests/conftest.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index f6ef33e8f25..6e3edbd73e8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -98,6 +98,21 @@ def verify_cleanup(): assert not threads +@pytest.fixture(autouse=True) +def bcrypt_cost(): + """Run with reduced rounds during tests, to speed up uses.""" + import bcrypt + + gensalt_orig = bcrypt.gensalt + + def gensalt_mock(rounds=12, prefix=b"2b"): + return gensalt_orig(4, prefix) + + bcrypt.gensalt = gensalt_mock + yield + bcrypt.gensalt = gensalt_orig + + @pytest.fixture def hass_storage(): """Fixture to mock storage.""" From 0fe3d6ea8183a2513e0255b4ef88e4d0a167df5d Mon Sep 17 00:00:00 2001 From: cristian-vescan Date: Fri, 29 Jan 2021 21:02:04 +0200 Subject: [PATCH 059/796] Added Romanian voice to Google Cloud TTS (#45704) See https://cloud.google.com/text-to-speech/docs/voices --- homeassistant/components/google_cloud/tts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index 6ffa3a9acd1..b0ae28bf5b1 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -55,6 +55,7 @@ SUPPORTED_LANGUAGES = [ "pl-PL", "pt-BR", "pt-PT", + "ro-RO", "ru-RU", "sk-SK", "sv-SE", From 48e899ca3ad947f17ec83eda68ea964a2a43a826 Mon Sep 17 00:00:00 2001 From: Paul Daumlechner Date: Fri, 29 Jan 2021 20:07:58 +0100 Subject: [PATCH 060/796] Add reboot_gateway service to Velux (#43198) --- homeassistant/components/velux/__init__.py | 7 +++++++ homeassistant/components/velux/services.yaml | 4 ++++ 2 files changed, 11 insertions(+) create mode 100644 homeassistant/components/velux/services.yaml diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py index bac65c969cf..90ed0a91b14 100644 --- a/homeassistant/components/velux/__init__.py +++ b/homeassistant/components/velux/__init__.py @@ -58,11 +58,18 @@ class VeluxModule: _LOGGER.debug("Velux interface terminated") await self.pyvlx.disconnect() + async def async_reboot_gateway(service_call): + await self.pyvlx.reboot_gateway() + self._hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) host = self._domain_config.get(CONF_HOST) password = self._domain_config.get(CONF_PASSWORD) self.pyvlx = PyVLX(host=host, password=password) + self._hass.services.async_register( + DOMAIN, "reboot_gateway", async_reboot_gateway + ) + async def async_start(self): """Start velux component.""" _LOGGER.debug("Velux interface started") diff --git a/homeassistant/components/velux/services.yaml b/homeassistant/components/velux/services.yaml new file mode 100644 index 00000000000..2460db0bbb0 --- /dev/null +++ b/homeassistant/components/velux/services.yaml @@ -0,0 +1,4 @@ +# Velux Integration services + +reboot_gateway: + description: Reboots the KLF200 Gateway. From df00f32dfcf87bd7e1eec5efcf322b130e2af9ff Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 29 Jan 2021 20:12:03 +0100 Subject: [PATCH 061/796] Updated frontend to 20210127.5 (#45714) --- 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 6f05ab04e6f..eb455a5a6c1 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==20210127.3"], + "requirements": ["home-assistant-frontend==20210127.5"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 49c1b2df2ff..0845cebc663 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.41.0 -home-assistant-frontend==20210127.3 +home-assistant-frontend==20210127.5 httpx==0.16.1 jinja2>=2.11.2 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index cdb1290fbf9..2db6ddc37e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.10.4 # homeassistant.components.frontend -home-assistant-frontend==20210127.3 +home-assistant-frontend==20210127.5 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bdf88a4a4e0..345edcfbb35 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -402,7 +402,7 @@ hole==0.5.1 holidays==0.10.4 # homeassistant.components.frontend -home-assistant-frontend==20210127.3 +home-assistant-frontend==20210127.5 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From ace5b58337a9938816f2ff1e4f005b4dc22ed180 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 29 Jan 2021 20:58:57 +0100 Subject: [PATCH 062/796] Fix ozw init tests (#45718) --- tests/components/ozw/test_init.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/components/ozw/test_init.py b/tests/components/ozw/test_init.py index c76bfd4a3a0..2e57c4c01f3 100644 --- a/tests/components/ozw/test_init.py +++ b/tests/components/ozw/test_init.py @@ -53,6 +53,7 @@ async def test_publish_without_mqtt(hass, caplog): # Sending a message should not error with the MQTT integration not set up. send_message("test_topic", "test_payload") + await hass.async_block_till_done() assert "MQTT integration is not set up" in caplog.text @@ -127,8 +128,8 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog): await hass.config_entries.async_remove(entry.entry_id) - stop_addon.call_count == 1 - uninstall_addon.call_count == 1 + assert stop_addon.call_count == 1 + assert uninstall_addon.call_count == 1 assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 stop_addon.reset_mock() @@ -141,8 +142,8 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog): await hass.config_entries.async_remove(entry.entry_id) - stop_addon.call_count == 1 - uninstall_addon.call_count == 0 + assert stop_addon.call_count == 1 + assert uninstall_addon.call_count == 0 assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert "Failed to stop the OpenZWave add-on" in caplog.text @@ -157,8 +158,8 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog): await hass.config_entries.async_remove(entry.entry_id) - stop_addon.call_count == 1 - uninstall_addon.call_count == 1 + assert stop_addon.call_count == 1 + assert uninstall_addon.call_count == 1 assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert "Failed to uninstall the OpenZWave add-on" in caplog.text From 84f506efb719b16d5bf441a44b2e85d863117069 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Fri, 29 Jan 2021 21:11:12 +0100 Subject: [PATCH 063/796] Set default position value for cover action (#45670) Co-authored-by: Bram Kragten Co-authored-by: Franck Nijhof --- homeassistant/components/cover/device_action.py | 4 +++- homeassistant/helpers/config_validation.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cover/device_action.py b/homeassistant/components/cover/device_action.py index 29dd97909e3..490ce162d9a 100644 --- a/homeassistant/components/cover/device_action.py +++ b/homeassistant/components/cover/device_action.py @@ -49,7 +49,9 @@ POSITION_ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): vol.In(POSITION_ACTION_TYPES), vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), - vol.Required("position"): vol.All(vol.Coerce(int), vol.Range(min=0, max=100)), + vol.Optional("position", default=0): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), } ) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index acf6139708a..d47ba30c114 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -266,7 +266,7 @@ def entity_id(value: Any) -> str: if valid_entity_id(str_value): return str_value - raise vol.Invalid(f"Entity ID {value} is an invalid entity id") + raise vol.Invalid(f"Entity ID {value} is an invalid entity ID") def entity_ids(value: Union[str, List]) -> List[str]: From 14c205384171dee59c1a908f8449f9864778b2dc Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 29 Jan 2021 13:30:21 -0700 Subject: [PATCH 064/796] Bump simplisafe-python to 9.6.4 (#45716) * Bump simplisafe-python to 9.6.4 * Fix imports --- homeassistant/components/simplisafe/__init__.py | 8 ++++---- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 89f5c40b1ff..495ba29fefb 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -10,10 +10,10 @@ from simplipy.websocket import ( EVENT_CONNECTION_LOST, EVENT_CONNECTION_RESTORED, EVENT_DOORBELL_DETECTED, - EVENT_ENTRY_DETECTED, + EVENT_ENTRY_DELAY, EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED, - EVENT_MOTION_DETECTED, + EVENT_SECRET_ALERT_TRIGGERED, ) import voluptuous as vol @@ -82,8 +82,8 @@ WEBSOCKET_EVENTS_REQUIRING_SERIAL = [EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED] WEBSOCKET_EVENTS_TO_TRIGGER_HASS_EVENT = [ EVENT_CAMERA_MOTION_DETECTED, EVENT_DOORBELL_DETECTED, - EVENT_ENTRY_DETECTED, - EVENT_MOTION_DETECTED, + EVENT_ENTRY_DELAY, + EVENT_SECRET_ALERT_TRIGGERED, ] ATTR_CATEGORY = "category" diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index a502a7908f0..b18bafb0bbf 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.2"], + "requirements": ["simplisafe-python==9.6.4"], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2db6ddc37e6..f1755b3ed1b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2028,7 +2028,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.6.2 +simplisafe-python==9.6.4 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 345edcfbb35..90505f1297c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1017,7 +1017,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.6.2 +simplisafe-python==9.6.4 # homeassistant.components.slack slackclient==2.5.0 From adf8873e5695907fd16673870ec2c063169c1404 Mon Sep 17 00:00:00 2001 From: Patrik <21142447+ggravlingen@users.noreply.github.com> Date: Fri, 29 Jan 2021 23:00:27 +0100 Subject: [PATCH 065/796] Remove ggravlingen from codeowners (#45723) --- CODEOWNERS | 1 - homeassistant/components/tradfri/manifest.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 73d42f4efcf..e3cf58f9056 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -477,7 +477,6 @@ homeassistant/components/toon/* @frenck homeassistant/components/totalconnect/* @austinmroczek homeassistant/components/tplink/* @rytilahti @thegardenmonkey homeassistant/components/traccar/* @ludeeus -homeassistant/components/tradfri/* @ggravlingen homeassistant/components/trafikverket_train/* @endor-force homeassistant/components/trafikverket_weatherstation/* @endor-force homeassistant/components/transmission/* @engrbm87 @JPHutchins diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index 5c6bf76a169..99b9dff6d22 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -7,5 +7,5 @@ "homekit": { "models": ["TRADFRI"] }, - "codeowners": ["@ggravlingen"] + "codeowners": [] } From f07ffee535980254d39734763158cb6257ca64a0 Mon Sep 17 00:00:00 2001 From: Pascal Reeb Date: Fri, 29 Jan 2021 23:01:25 +0100 Subject: [PATCH 066/796] Advanced testing for Nuki config flow (#45721) --- homeassistant/components/nuki/__init__.py | 10 -- homeassistant/components/nuki/lock.py | 1 - tests/components/nuki/test_config_flow.py | 107 ++++++++++++++++++++-- 3 files changed, 99 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 627cf20b16b..4af3e0d8ed4 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -1,7 +1,6 @@ """The nuki component.""" from datetime import timedelta -import logging import voluptuous as vol @@ -12,8 +11,6 @@ import homeassistant.helpers.config_validation as cv from .const import DEFAULT_PORT, DOMAIN -_LOGGER = logging.getLogger(__name__) - NUKI_PLATFORMS = ["lock"] UPDATE_INTERVAL = timedelta(seconds=30) @@ -27,16 +24,10 @@ NUKI_SCHEMA = vol.Schema( ) ) -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Schema(NUKI_SCHEMA)}, - extra=vol.ALLOW_EXTRA, -) - async def async_setup(hass, config): """Set up the Nuki component.""" hass.data.setdefault(DOMAIN, {}) - _LOGGER.debug("Config: %s", config) for platform in NUKI_PLATFORMS: confs = config.get(platform) @@ -44,7 +35,6 @@ async def async_setup(hass, config): continue for conf in confs: - _LOGGER.debug("Conf: %s", conf) hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=conf diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index fe024405908..818784a2b2e 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -45,7 +45,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Nuki lock platform.""" config = config_entry.data - _LOGGER.debug("Config: %s", config) def get_entities(): bridge = NukiBridge( diff --git a/tests/components/nuki/test_config_flow.py b/tests/components/nuki/test_config_flow.py index 45168e42c9d..bcdedad371a 100644 --- a/tests/components/nuki/test_config_flow.py +++ b/tests/components/nuki/test_config_flow.py @@ -1,10 +1,14 @@ """Test the nuki config flow.""" from unittest.mock import patch -from homeassistant import config_entries, setup -from homeassistant.components.nuki.config_flow import CannotConnect, InvalidAuth +from pynuki.bridge import InvalidCredentialsException +from requests.exceptions import RequestException + +from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.nuki.const import DOMAIN +from tests.common import MockConfigEntry + async def test_form(hass): """Test we get the form.""" @@ -12,7 +16,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} mock_info = {"ids": {"hardwareId": "0001"}} @@ -36,7 +40,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["title"] == "0001" assert result2["data"] == { "host": "1.1.1.1", @@ -47,6 +51,39 @@ async def test_form(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_import(hass): + """Test that the import works.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mock_info = {"ids": {"hardwareId": "0001"}} + + with patch( + "homeassistant.components.nuki.config_flow.NukiBridge.info", + return_value=mock_info, + ), patch( + "homeassistant.components.nuki.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.nuki.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={"host": "1.1.1.1", "port": 8080, "token": "test-token"}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "0001" + assert result["data"] == { + "host": "1.1.1.1", + "port": 8080, + "token": "test-token", + } + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_form_invalid_auth(hass): """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( @@ -55,7 +92,7 @@ async def test_form_invalid_auth(hass): with patch( "homeassistant.components.nuki.config_flow.NukiBridge.info", - side_effect=InvalidAuth, + side_effect=InvalidCredentialsException, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -66,7 +103,7 @@ async def test_form_invalid_auth(hass): }, ) - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -78,7 +115,7 @@ async def test_form_cannot_connect(hass): with patch( "homeassistant.components.nuki.config_flow.NukiBridge.info", - side_effect=CannotConnect, + side_effect=RequestException, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -89,5 +126,59 @@ async def test_form_cannot_connect(hass): }, ) - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_unknown_exception(hass): + """Test we handle unknown exceptions.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.nuki.config_flow.NukiBridge.info", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "port": 8080, + "token": "test-token", + }, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_already_configured(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain="nuki", + unique_id="0001", + data={"host": "1.1.1.1", "port": 8080, "token": "test-token"}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.nuki.config_flow.NukiBridge.info", + return_value={"ids": {"hardwareId": "0001"}}, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "port": 8080, + "token": "test-token", + }, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" From 41e2e5043b8c5ede1531fd24d8f8d836ba3a6c6c Mon Sep 17 00:00:00 2001 From: chpego <38792705+chpego@users.noreply.github.com> Date: Fri, 29 Jan 2021 23:14:17 +0100 Subject: [PATCH 067/796] Upgrade youtube_dl to version 2021.01.24.1 (#45724) * Upgrade youtube_dl to version 2021.01.24.1 * Update requirements_all.txt --- 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 5a09171df80..c6ee6ccb8a4 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==2021.01.16"], + "requirements": ["youtube_dl==2021.01.24.1"], "dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal" diff --git a/requirements_all.txt b/requirements_all.txt index f1755b3ed1b..af3f8235a9b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2339,7 +2339,7 @@ yeelight==0.5.4 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2021.01.16 +youtube_dl==2021.01.24.1 # homeassistant.components.onvif zeep[async]==4.0.0 From 87d40ff81581005d1356ef8ead5c59c398bba378 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 30 Jan 2021 00:05:06 +0100 Subject: [PATCH 068/796] Do not cache frontend files during dev (#45698) --- homeassistant/components/frontend/__init__.py | 8 +- tests/components/frontend/test_init.py | 221 +++++++++--------- 2 files changed, 109 insertions(+), 120 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 080d786d4e4..cdf25d22fe8 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -262,10 +262,10 @@ async def async_setup(hass, config): for path, should_cache in ( ("service_worker.js", False), ("robots.txt", False), - ("onboarding.html", True), - ("static", True), - ("frontend_latest", True), - ("frontend_es5", True), + ("onboarding.html", not is_dev), + ("static", not is_dev), + ("frontend_latest", not is_dev), + ("frontend_es5", not is_dev), ): hass.http.register_static_path(f"/{path}", str(root_path / path), should_cache) diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 5ae8d707cb1..0e8e31bb20d 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -33,44 +33,67 @@ CONFIG_THEMES = { @pytest.fixture -def mock_http_client(hass, aiohttp_client): - """Start the Home Assistant HTTP component.""" - hass.loop.run_until_complete(async_setup_component(hass, "frontend", {})) - return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) +async def ignore_frontend_deps(hass): + """Frontend dependencies.""" + frontend = await async_get_integration(hass, "frontend") + for dep in frontend.dependencies: + if dep not in ("http", "websocket_api"): + hass.config.components.add(dep) @pytest.fixture -def mock_http_client_with_themes(hass, aiohttp_client): - """Start the Home Assistant HTTP component.""" - hass.loop.run_until_complete( - async_setup_component( - hass, - "frontend", - {DOMAIN: {CONF_THEMES: {"happy": {"primary-color": "red"}}}}, - ) +async def frontend(hass, ignore_frontend_deps): + """Frontend setup with themes.""" + assert await async_setup_component( + hass, + "frontend", + {}, ) - return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture -def mock_http_client_with_urls(hass, aiohttp_client): - """Start the Home Assistant HTTP component.""" - hass.loop.run_until_complete( - async_setup_component( - hass, - "frontend", - { - DOMAIN: { - CONF_JS_VERSION: "auto", - CONF_EXTRA_HTML_URL: ["https://domain.com/my_extra_url.html"], - CONF_EXTRA_HTML_URL_ES5: [ - "https://domain.com/my_extra_url_es5.html" - ], - } - }, - ) +async def frontend_themes(hass): + """Frontend setup with themes.""" + assert await async_setup_component( + hass, + "frontend", + CONFIG_THEMES, ) - return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) + + +@pytest.fixture +async def mock_http_client(hass, aiohttp_client, frontend): + """Start the Home Assistant HTTP component.""" + return await aiohttp_client(hass.http.app) + + +@pytest.fixture +async def themes_ws_client(hass, hass_ws_client, frontend_themes): + """Start the Home Assistant HTTP component.""" + return await hass_ws_client(hass) + + +@pytest.fixture +async def ws_client(hass, hass_ws_client, frontend): + """Start the Home Assistant HTTP component.""" + return await hass_ws_client(hass) + + +@pytest.fixture +async def mock_http_client_with_urls(hass, aiohttp_client, ignore_frontend_deps): + """Start the Home Assistant HTTP component.""" + assert await async_setup_component( + hass, + "frontend", + { + DOMAIN: { + CONF_JS_VERSION: "auto", + CONF_EXTRA_HTML_URL: ["https://domain.com/my_extra_url.html"], + CONF_EXTRA_HTML_URL_ES5: ["https://domain.com/my_extra_url_es5.html"], + } + }, + ) + return await aiohttp_client(hass.http.app) @pytest.fixture @@ -118,13 +141,10 @@ async def test_we_cannot_POST_to_root(mock_http_client): assert resp.status == 405 -async def test_themes_api(hass, hass_ws_client): +async def test_themes_api(hass, themes_ws_client): """Test that /api/themes returns correct data.""" - assert await async_setup_component(hass, "frontend", CONFIG_THEMES) - client = await hass_ws_client(hass) - - await client.send_json({"id": 5, "type": "frontend/get_themes"}) - msg = await client.receive_json() + await themes_ws_client.send_json({"id": 5, "type": "frontend/get_themes"}) + msg = await themes_ws_client.receive_json() assert msg["result"]["default_theme"] == "default" assert msg["result"]["default_dark_theme"] is None @@ -135,8 +155,8 @@ async def test_themes_api(hass, hass_ws_client): # safe mode hass.config.safe_mode = True - await client.send_json({"id": 6, "type": "frontend/get_themes"}) - msg = await client.receive_json() + await themes_ws_client.send_json({"id": 6, "type": "frontend/get_themes"}) + msg = await themes_ws_client.receive_json() assert msg["result"]["default_theme"] == "safe_mode" assert msg["result"]["themes"] == { @@ -144,9 +164,8 @@ async def test_themes_api(hass, hass_ws_client): } -async def test_themes_persist(hass, hass_ws_client, hass_storage): +async def test_themes_persist(hass, hass_storage, hass_ws_client, ignore_frontend_deps): """Test that theme settings are restores after restart.""" - hass_storage[THEMES_STORAGE_KEY] = { "key": THEMES_STORAGE_KEY, "version": 1, @@ -157,26 +176,18 @@ async def test_themes_persist(hass, hass_ws_client, hass_storage): } assert await async_setup_component(hass, "frontend", CONFIG_THEMES) - client = await hass_ws_client(hass) + themes_ws_client = await hass_ws_client(hass) - await client.send_json({"id": 5, "type": "frontend/get_themes"}) - msg = await client.receive_json() + await themes_ws_client.send_json({"id": 5, "type": "frontend/get_themes"}) + msg = await themes_ws_client.receive_json() assert msg["result"]["default_theme"] == "happy" assert msg["result"]["default_dark_theme"] == "dark" -async def test_themes_save_storage(hass, hass_storage): +async def test_themes_save_storage(hass, hass_storage, frontend_themes): """Test that theme settings are restores after restart.""" - hass_storage[THEMES_STORAGE_KEY] = { - "key": THEMES_STORAGE_KEY, - "version": 1, - "data": {}, - } - - assert await async_setup_component(hass, "frontend", CONFIG_THEMES) - await hass.services.async_call( DOMAIN, "set_theme", {"name": "happy"}, blocking=True ) @@ -196,17 +207,14 @@ async def test_themes_save_storage(hass, hass_storage): } -async def test_themes_set_theme(hass, hass_ws_client): +async def test_themes_set_theme(hass, themes_ws_client): """Test frontend.set_theme service.""" - assert await async_setup_component(hass, "frontend", CONFIG_THEMES) - client = await hass_ws_client(hass) - await hass.services.async_call( DOMAIN, "set_theme", {"name": "happy"}, blocking=True ) - await client.send_json({"id": 5, "type": "frontend/get_themes"}) - msg = await client.receive_json() + await themes_ws_client.send_json({"id": 5, "type": "frontend/get_themes"}) + msg = await themes_ws_client.receive_json() assert msg["result"]["default_theme"] == "happy" @@ -214,8 +222,8 @@ async def test_themes_set_theme(hass, hass_ws_client): DOMAIN, "set_theme", {"name": "default"}, blocking=True ) - await client.send_json({"id": 6, "type": "frontend/get_themes"}) - msg = await client.receive_json() + await themes_ws_client.send_json({"id": 6, "type": "frontend/get_themes"}) + msg = await themes_ws_client.receive_json() assert msg["result"]["default_theme"] == "default" @@ -225,39 +233,35 @@ async def test_themes_set_theme(hass, hass_ws_client): await hass.services.async_call(DOMAIN, "set_theme", {"name": "none"}, blocking=True) - await client.send_json({"id": 7, "type": "frontend/get_themes"}) - msg = await client.receive_json() + await themes_ws_client.send_json({"id": 7, "type": "frontend/get_themes"}) + msg = await themes_ws_client.receive_json() assert msg["result"]["default_theme"] == "default" -async def test_themes_set_theme_wrong_name(hass, hass_ws_client): +async def test_themes_set_theme_wrong_name(hass, themes_ws_client): """Test frontend.set_theme service called with wrong name.""" - assert await async_setup_component(hass, "frontend", CONFIG_THEMES) - client = await hass_ws_client(hass) await hass.services.async_call( DOMAIN, "set_theme", {"name": "wrong"}, blocking=True ) - await client.send_json({"id": 5, "type": "frontend/get_themes"}) + await themes_ws_client.send_json({"id": 5, "type": "frontend/get_themes"}) - msg = await client.receive_json() + msg = await themes_ws_client.receive_json() assert msg["result"]["default_theme"] == "default" -async def test_themes_set_dark_theme(hass, hass_ws_client): +async def test_themes_set_dark_theme(hass, themes_ws_client): """Test frontend.set_theme service called with dark mode.""" - assert await async_setup_component(hass, "frontend", CONFIG_THEMES) - client = await hass_ws_client(hass) await hass.services.async_call( DOMAIN, "set_theme", {"name": "dark", "mode": "dark"}, blocking=True ) - await client.send_json({"id": 5, "type": "frontend/get_themes"}) - msg = await client.receive_json() + await themes_ws_client.send_json({"id": 5, "type": "frontend/get_themes"}) + msg = await themes_ws_client.receive_json() assert msg["result"]["default_dark_theme"] == "dark" @@ -265,8 +269,8 @@ async def test_themes_set_dark_theme(hass, hass_ws_client): DOMAIN, "set_theme", {"name": "default", "mode": "dark"}, blocking=True ) - await client.send_json({"id": 6, "type": "frontend/get_themes"}) - msg = await client.receive_json() + await themes_ws_client.send_json({"id": 6, "type": "frontend/get_themes"}) + msg = await themes_ws_client.receive_json() assert msg["result"]["default_dark_theme"] == "default" @@ -274,32 +278,27 @@ async def test_themes_set_dark_theme(hass, hass_ws_client): DOMAIN, "set_theme", {"name": "none", "mode": "dark"}, blocking=True ) - await client.send_json({"id": 7, "type": "frontend/get_themes"}) - msg = await client.receive_json() + await themes_ws_client.send_json({"id": 7, "type": "frontend/get_themes"}) + msg = await themes_ws_client.receive_json() assert msg["result"]["default_dark_theme"] is None -async def test_themes_set_dark_theme_wrong_name(hass, hass_ws_client): +async def test_themes_set_dark_theme_wrong_name(hass, frontend, themes_ws_client): """Test frontend.set_theme service called with mode dark and wrong name.""" - assert await async_setup_component(hass, "frontend", CONFIG_THEMES) - client = await hass_ws_client(hass) - await hass.services.async_call( DOMAIN, "set_theme", {"name": "wrong", "mode": "dark"}, blocking=True ) - await client.send_json({"id": 5, "type": "frontend/get_themes"}) + await themes_ws_client.send_json({"id": 5, "type": "frontend/get_themes"}) - msg = await client.receive_json() + msg = await themes_ws_client.receive_json() assert msg["result"]["default_dark_theme"] is None -async def test_themes_reload_themes(hass, hass_ws_client): +async def test_themes_reload_themes(hass, frontend, themes_ws_client): """Test frontend.reload_themes service.""" - assert await async_setup_component(hass, "frontend", CONFIG_THEMES) - client = await hass_ws_client(hass) with patch( "homeassistant.components.frontend.async_hass_config_yaml", @@ -310,22 +309,19 @@ async def test_themes_reload_themes(hass, hass_ws_client): ) await hass.services.async_call(DOMAIN, "reload_themes", blocking=True) - await client.send_json({"id": 5, "type": "frontend/get_themes"}) + await themes_ws_client.send_json({"id": 5, "type": "frontend/get_themes"}) - msg = await client.receive_json() + msg = await themes_ws_client.receive_json() assert msg["result"]["themes"] == {"sad": {"primary-color": "blue"}} assert msg["result"]["default_theme"] == "default" -async def test_missing_themes(hass, hass_ws_client): +async def test_missing_themes(hass, ws_client): """Test that themes API works when themes are not defined.""" - await async_setup_component(hass, "frontend", {}) + await ws_client.send_json({"id": 5, "type": "frontend/get_themes"}) - client = await hass_ws_client(hass) - await client.send_json({"id": 5, "type": "frontend/get_themes"}) - - msg = await client.receive_json() + msg = await ws_client.receive_json() assert msg["id"] == 5 assert msg["type"] == TYPE_RESULT @@ -372,10 +368,10 @@ async def test_get_panels(hass, hass_ws_client, mock_http_client): assert len(events) == 2 -async def test_get_panels_non_admin(hass, hass_ws_client, hass_admin_user): +async def test_get_panels_non_admin(hass, ws_client, hass_admin_user): """Test get_panels command.""" hass_admin_user.groups = [] - await async_setup_component(hass, "frontend", {}) + hass.components.frontend.async_register_built_in_panel( "map", "Map", "mdi:tooltip-account", require_admin=True ) @@ -383,10 +379,9 @@ async def test_get_panels_non_admin(hass, hass_ws_client, hass_admin_user): "history", "History", "mdi:history" ) - client = await hass_ws_client(hass) - await client.send_json({"id": 5, "type": "get_panels"}) + await ws_client.send_json({"id": 5, "type": "get_panels"}) - msg = await client.receive_json() + msg = await ws_client.receive_json() assert msg["id"] == 5 assert msg["type"] == TYPE_RESULT @@ -395,18 +390,15 @@ async def test_get_panels_non_admin(hass, hass_ws_client, hass_admin_user): assert "map" not in msg["result"] -async def test_get_translations(hass, hass_ws_client): +async def test_get_translations(hass, ws_client): """Test get_translations command.""" - await async_setup_component(hass, "frontend", {}) - client = await hass_ws_client(hass) - with patch( "homeassistant.components.frontend.async_get_translations", side_effect=lambda hass, lang, category, integration, config_flow: { "lang": lang }, ): - await client.send_json( + await ws_client.send_json( { "id": 5, "type": "frontend/get_translations", @@ -414,7 +406,7 @@ async def test_get_translations(hass, hass_ws_client): "category": "lang", } ) - msg = await client.receive_json() + msg = await ws_client.receive_json() assert msg["id"] == 5 assert msg["type"] == TYPE_RESULT @@ -422,16 +414,16 @@ async def test_get_translations(hass, hass_ws_client): assert msg["result"] == {"resources": {"lang": "nl"}} -async def test_auth_load(mock_http_client, mock_onboarded): +async def test_auth_load(hass): """Test auth component loaded by default.""" - resp = await mock_http_client.get("/auth/providers") - assert resp.status == 200 + frontend = await async_get_integration(hass, "frontend") + assert "auth" in frontend.dependencies -async def test_onboarding_load(mock_http_client): +async def test_onboarding_load(hass): """Test onboarding component loaded by default.""" - resp = await mock_http_client.get("/api/onboarding") - assert resp.status == 200 + frontend = await async_get_integration(hass, "frontend") + assert "onboarding" in frontend.dependencies async def test_auth_authorize(mock_http_client): @@ -457,7 +449,7 @@ async def test_auth_authorize(mock_http_client): assert "public" in resp.headers.get("cache-control") -async def test_get_version(hass, hass_ws_client): +async def test_get_version(hass, ws_client): """Test get_version command.""" frontend = await async_get_integration(hass, "frontend") cur_version = next( @@ -466,11 +458,8 @@ async def test_get_version(hass, hass_ws_client): if req.startswith("home-assistant-frontend==") ) - await async_setup_component(hass, "frontend", {}) - client = await hass_ws_client(hass) - - await client.send_json({"id": 5, "type": "frontend/get_version"}) - msg = await client.receive_json() + await ws_client.send_json({"id": 5, "type": "frontend/get_version"}) + msg = await ws_client.receive_json() assert msg["id"] == 5 assert msg["type"] == TYPE_RESULT From 8a112721fade61b2e670d4f90bfb6e239eca66f3 Mon Sep 17 00:00:00 2001 From: Ryan Fleming Date: Sat, 30 Jan 2021 02:00:27 -0500 Subject: [PATCH 069/796] Fix feedback from UVC (#45630) * Fixing feedback from UVC * Couple of fixes --- homeassistant/components/uvc/camera.py | 10 ++++++---- tests/components/uvc/test_camera.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index 4f5cfa3907e..ae10c7db48f 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -2,6 +2,7 @@ from datetime import datetime import logging import re +from typing import Optional import requests from uvcclient import camera as uvc_camera, nvr @@ -111,9 +112,9 @@ class UnifiVideoCamera(Camera): return 0 @property - def state_attributes(self): + def device_state_attributes(self): """Return the camera state attributes.""" - attr = super().state_attributes + attr = {} if self.motion_detection_enabled: attr["last_recording_start_time"] = timestamp_ms_to_date( self._caminfo["lastRecordingStartTime"] @@ -124,7 +125,7 @@ class UnifiVideoCamera(Camera): def is_recording(self): """Return true if the camera is recording.""" recording_state = "DISABLED" - if "recordingIndicator" in self._caminfo.keys(): + if "recordingIndicator" in self._caminfo: recording_state = self._caminfo["recordingIndicator"] return ( @@ -256,7 +257,8 @@ class UnifiVideoCamera(Camera): self._caminfo = self._nvr.get_camera(self._uuid) -def timestamp_ms_to_date(epoch_ms) -> datetime or None: +def timestamp_ms_to_date(epoch_ms: int) -> Optional[datetime]: """Convert millisecond timestamp to datetime.""" if epoch_ms: return datetime.fromtimestamp(epoch_ms / 1000) + return None diff --git a/tests/components/uvc/test_camera.py b/tests/components/uvc/test_camera.py index 00c827b9973..1dd44625ebe 100644 --- a/tests/components/uvc/test_camera.py +++ b/tests/components/uvc/test_camera.py @@ -257,7 +257,7 @@ class TestUVC(unittest.TestCase): assert not self.uvc.is_recording assert ( datetime(2021, 1, 8, 1, 56, 32, 367000) - == self.uvc.state_attributes["last_recording_start_time"] + == self.uvc.device_state_attributes["last_recording_start_time"] ) self.nvr.get_camera.return_value["recordingIndicator"] = "DISABLED" From 07a4422a704aafbb6d42b3155312b9dbbc3428b2 Mon Sep 17 00:00:00 2001 From: Nathan Tilley Date: Sat, 30 Jan 2021 02:05:58 -0500 Subject: [PATCH 070/796] Implement person significant change (#45713) --- .../components/person/significant_change.py | 21 +++++++++++++++++++ .../person/test_significant_change.py | 16 ++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 homeassistant/components/person/significant_change.py create mode 100644 tests/components/person/test_significant_change.py diff --git a/homeassistant/components/person/significant_change.py b/homeassistant/components/person/significant_change.py new file mode 100644 index 00000000000..d9c1ec6cc23 --- /dev/null +++ b/homeassistant/components/person/significant_change.py @@ -0,0 +1,21 @@ +"""Helper to test significant Person state changes.""" +from typing import Any, Optional + +from homeassistant.core import HomeAssistant, callback + + +@callback +def async_check_significant_change( + hass: HomeAssistant, + old_state: str, + old_attrs: dict, + new_state: str, + new_attrs: dict, + **kwargs: Any, +) -> Optional[bool]: + """Test if state significantly changed.""" + + if new_state != old_state: + return True + + return False diff --git a/tests/components/person/test_significant_change.py b/tests/components/person/test_significant_change.py new file mode 100644 index 00000000000..1b4f6940e90 --- /dev/null +++ b/tests/components/person/test_significant_change.py @@ -0,0 +1,16 @@ +"""Test the Person significant change platform.""" +from homeassistant.components.person.significant_change import ( + async_check_significant_change, +) + + +async def test_significant_change(): + """Detect Person significant changes and ensure that attribute changes do not trigger a significant change.""" + old_attrs = {"source": "device_tracker.wifi_device"} + new_attrs = {"source": "device_tracker.gps_device"} + assert not async_check_significant_change( + None, "home", old_attrs, "home", new_attrs + ) + assert async_check_significant_change( + None, "home", new_attrs, "not_home", new_attrs + ) From 85e6bc581f9475df289ed0f76ccd7c03ed749304 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 30 Jan 2021 01:03:54 -0700 Subject: [PATCH 071/796] Add significant change support to lock (#45726) --- .../components/lock/significant_change.py | 20 ++++++++++++++++ .../lock/test_significant_change.py | 23 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 homeassistant/components/lock/significant_change.py create mode 100644 tests/components/lock/test_significant_change.py diff --git a/homeassistant/components/lock/significant_change.py b/homeassistant/components/lock/significant_change.py new file mode 100644 index 00000000000..59a3b1a95c5 --- /dev/null +++ b/homeassistant/components/lock/significant_change.py @@ -0,0 +1,20 @@ +"""Helper to test significant Lock state changes.""" +from typing import Any, Optional + +from homeassistant.core import HomeAssistant, callback + + +@callback +def async_check_significant_change( + hass: HomeAssistant, + old_state: str, + old_attrs: dict, + new_state: str, + new_attrs: dict, + **kwargs: Any, +) -> Optional[bool]: + """Test if state significantly changed.""" + if old_state != new_state: + return True + + return False diff --git a/tests/components/lock/test_significant_change.py b/tests/components/lock/test_significant_change.py new file mode 100644 index 00000000000..a9ffbc0d1c4 --- /dev/null +++ b/tests/components/lock/test_significant_change.py @@ -0,0 +1,23 @@ +"""Test the Lock significant change platform.""" +from homeassistant.components.lock.significant_change import ( + async_check_significant_change, +) + + +async def test_significant_change(): + """Detect Lock significant changes.""" + old_attrs = {"attr_1": "a"} + new_attrs = {"attr_1": "b"} + + assert ( + async_check_significant_change(None, "locked", old_attrs, "locked", old_attrs) + is False + ) + assert ( + async_check_significant_change(None, "locked", old_attrs, "locked", new_attrs) + is False + ) + assert ( + async_check_significant_change(None, "locked", old_attrs, "unlocked", old_attrs) + is True + ) From 6bf59dbeabb0a8d211b56e79f58e9d943b74bc0e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 30 Jan 2021 01:04:35 -0700 Subject: [PATCH 072/796] Add significant change support to binary_sensor (#45677) * Add significant change support to binary_sensor --- .../binary_sensor/significant_change.py | 20 +++++++++++++++++++ .../binary_sensor/translations/en.json | 4 ++-- .../binary_sensor/test_significant_change.py | 20 +++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/binary_sensor/significant_change.py create mode 100644 tests/components/binary_sensor/test_significant_change.py diff --git a/homeassistant/components/binary_sensor/significant_change.py b/homeassistant/components/binary_sensor/significant_change.py new file mode 100644 index 00000000000..bc2dba04f09 --- /dev/null +++ b/homeassistant/components/binary_sensor/significant_change.py @@ -0,0 +1,20 @@ +"""Helper to test significant Binary Sensor state changes.""" +from typing import Any, Optional + +from homeassistant.core import HomeAssistant, callback + + +@callback +def async_check_significant_change( + hass: HomeAssistant, + old_state: str, + old_attrs: dict, + new_state: str, + new_attrs: dict, + **kwargs: Any, +) -> Optional[bool]: + """Test if state significantly changed.""" + if old_state != new_state: + return True + + return False diff --git a/homeassistant/components/binary_sensor/translations/en.json b/homeassistant/components/binary_sensor/translations/en.json index 98c8a3a220a..a9a20e3fa50 100644 --- a/homeassistant/components/binary_sensor/translations/en.json +++ b/homeassistant/components/binary_sensor/translations/en.json @@ -159,8 +159,8 @@ "on": "Plugged in" }, "presence": { - "off": "Away", - "on": "Home" + "off": "[%key:common::state::not_home%]", + "on": "[%key:common::state::home%]" }, "problem": { "off": "OK", diff --git a/tests/components/binary_sensor/test_significant_change.py b/tests/components/binary_sensor/test_significant_change.py new file mode 100644 index 00000000000..673374a15e4 --- /dev/null +++ b/tests/components/binary_sensor/test_significant_change.py @@ -0,0 +1,20 @@ +"""Test the Binary Sensor significant change platform.""" +from homeassistant.components.binary_sensor.significant_change import ( + async_check_significant_change, +) + + +async def test_significant_change(): + """Detect Binary Sensor significant changes.""" + old_attrs = {"attr_1": "value_1"} + new_attrs = {"attr_1": "value_2"} + + assert ( + async_check_significant_change(None, "on", old_attrs, "on", old_attrs) is False + ) + assert ( + async_check_significant_change(None, "on", old_attrs, "on", new_attrs) is False + ) + assert ( + async_check_significant_change(None, "on", old_attrs, "off", old_attrs) is True + ) From d81017f62ef39fd9d87a08e18fde2490e3d78ead Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 30 Jan 2021 02:56:51 -0600 Subject: [PATCH 073/796] Use DataUpdateCoordinator for solaredge (#45734) Co-authored-by: Martin Hjelmare --- homeassistant/components/solaredge/sensor.py | 266 ++++++++++--------- 1 file changed, 146 insertions(+), 120 deletions(-) diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index e3e59676bf5..8609e578e5e 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -1,4 +1,5 @@ """Support for SolarEdge Monitoring API.""" +from abc import abstractmethod from datetime import date, datetime import logging @@ -7,8 +8,14 @@ import solaredge from stringcase import snakecase from homeassistant.const import CONF_API_KEY, DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER +from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from .const import ( CONF_SITE_ID, @@ -37,14 +44,20 @@ async def async_setup_entry(hass, entry, async_add_entities): _LOGGER.error("SolarEdge site is not active") return _LOGGER.debug("Credentials correct and site is active") - except KeyError: + except KeyError as ex: _LOGGER.error("Missing details data in SolarEdge response") - return - except (ConnectTimeout, HTTPError): + raise ConfigEntryNotReady from ex + except (ConnectTimeout, HTTPError) as ex: _LOGGER.error("Could not retrieve details from SolarEdge API") - return + raise ConfigEntryNotReady from ex + + sensor_factory = SolarEdgeSensorFactory( + hass, entry.title, entry.data[CONF_SITE_ID], api + ) + for service in sensor_factory.all_services: + service.async_setup() + await service.coordinator.async_refresh() - sensor_factory = SolarEdgeSensorFactory(entry.title, entry.data[CONF_SITE_ID], api) entities = [] for sensor_key in SENSOR_TYPES: sensor = sensor_factory.create_sensor(sensor_key) @@ -56,15 +69,17 @@ async def async_setup_entry(hass, entry, async_add_entities): class SolarEdgeSensorFactory: """Factory which creates sensors based on the sensor_key.""" - def __init__(self, platform_name, site_id, api): + def __init__(self, hass, platform_name, site_id, api): """Initialize the factory.""" self.platform_name = platform_name - details = SolarEdgeDetailsDataService(api, site_id) - overview = SolarEdgeOverviewDataService(api, site_id) - inventory = SolarEdgeInventoryDataService(api, site_id) - flow = SolarEdgePowerFlowDataService(api, site_id) - energy = SolarEdgeEnergyDetailsService(api, site_id) + details = SolarEdgeDetailsDataService(hass, api, site_id) + overview = SolarEdgeOverviewDataService(hass, api, site_id) + inventory = SolarEdgeInventoryDataService(hass, api, site_id) + flow = SolarEdgePowerFlowDataService(hass, api, site_id) + energy = SolarEdgeEnergyDetailsService(hass, api, site_id) + + self.all_services = (details, overview, inventory, flow, energy) self.services = {"site_details": (SolarEdgeDetailsSensor, details)} @@ -102,39 +117,30 @@ class SolarEdgeSensorFactory: return sensor_class(self.platform_name, sensor_key, service) -class SolarEdgeSensor(Entity): +class SolarEdgeSensor(CoordinatorEntity, Entity): """Abstract class for a solaredge sensor.""" def __init__(self, platform_name, sensor_key, data_service): """Initialize the sensor.""" + super().__init__(data_service.coordinator) self.platform_name = platform_name self.sensor_key = sensor_key self.data_service = data_service - self._state = None - - self._unit_of_measurement = SENSOR_TYPES[self.sensor_key][2] - self._icon = SENSOR_TYPES[self.sensor_key][3] + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return SENSOR_TYPES[self.sensor_key][2] @property def name(self): """Return the name.""" return "{} ({})".format(self.platform_name, SENSOR_TYPES[self.sensor_key][1]) - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit_of_measurement - @property def icon(self): """Return the sensor icon.""" - return self._icon - - @property - def state(self): - """Return the state of the sensor.""" - return self._state + return SENSOR_TYPES[self.sensor_key][3] class SolarEdgeOverviewSensor(SolarEdgeSensor): @@ -146,31 +152,24 @@ class SolarEdgeOverviewSensor(SolarEdgeSensor): self._json_key = SENSOR_TYPES[self.sensor_key][0] - def update(self): - """Get the latest data from the sensor and update the state.""" - self.data_service.update() - self._state = self.data_service.data.get(self._json_key) + @property + def state(self): + """Return the state of the sensor.""" + return self.data_service.data.get(self._json_key) class SolarEdgeDetailsSensor(SolarEdgeSensor): """Representation of an SolarEdge Monitoring API details sensor.""" - def __init__(self, platform_name, sensor_key, data_service): - """Initialize the details sensor.""" - super().__init__(platform_name, sensor_key, data_service) - - self._attributes = {} - @property def device_state_attributes(self): """Return the state attributes.""" - return self._attributes + return self.data_service.attributes - def update(self): - """Get the latest details and update state and attributes.""" - self.data_service.update() - self._state = self.data_service.data - self._attributes = self.data_service.attributes + @property + def state(self): + """Return the state of the sensor.""" + return self.data_service.data class SolarEdgeInventorySensor(SolarEdgeSensor): @@ -182,18 +181,15 @@ class SolarEdgeInventorySensor(SolarEdgeSensor): self._json_key = SENSOR_TYPES[self.sensor_key][0] - self._attributes = {} - @property def device_state_attributes(self): """Return the state attributes.""" - return self._attributes + return self.data_service.attributes.get(self._json_key) - def update(self): - """Get the latest inventory data and update state and attributes.""" - self.data_service.update() - self._state = self.data_service.data.get(self._json_key) - self._attributes = self.data_service.attributes.get(self._json_key) + @property + def state(self): + """Return the state of the sensor.""" + return self.data_service.data.get(self._json_key) class SolarEdgeEnergyDetailsSensor(SolarEdgeSensor): @@ -205,19 +201,20 @@ class SolarEdgeEnergyDetailsSensor(SolarEdgeSensor): self._json_key = SENSOR_TYPES[self.sensor_key][0] - self._attributes = {} - @property def device_state_attributes(self): """Return the state attributes.""" - return self._attributes + return self.data_service.attributes.get(self._json_key) - def update(self): - """Get the latest inventory data and update state and attributes.""" - self.data_service.update() - self._state = self.data_service.data.get(self._json_key) - self._attributes = self.data_service.attributes.get(self._json_key) - self._unit_of_measurement = self.data_service.unit + @property + def state(self): + """Return the state of the sensor.""" + return self.data_service.data.get(self._json_key) + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self.data_service.unit class SolarEdgePowerFlowSensor(SolarEdgeSensor): @@ -229,24 +226,25 @@ class SolarEdgePowerFlowSensor(SolarEdgeSensor): self._json_key = SENSOR_TYPES[self.sensor_key][0] - self._attributes = {} - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._attributes - @property def device_class(self): """Device Class.""" return DEVICE_CLASS_POWER - def update(self): - """Get the latest inventory data and update state and attributes.""" - self.data_service.update() - self._state = self.data_service.data.get(self._json_key) - self._attributes = self.data_service.attributes.get(self._json_key) - self._unit_of_measurement = self.data_service.unit + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self.data_service.attributes.get(self._json_key) + + @property + def state(self): + """Return the state of the sensor.""" + return self.data_service.data.get(self._json_key) + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self.data_service.unit class SolarEdgeStorageLevelSensor(SolarEdgeSensor): @@ -263,18 +261,19 @@ class SolarEdgeStorageLevelSensor(SolarEdgeSensor): """Return the device_class of the device.""" return DEVICE_CLASS_BATTERY - def update(self): - """Get the latest inventory data and update state and attributes.""" - self.data_service.update() + @property + def state(self): + """Return the state of the sensor.""" attr = self.data_service.attributes.get(self._json_key) if attr and "soc" in attr: - self._state = attr["soc"] + return attr["soc"] + return None class SolarEdgeDataService: """Get and update the latest data.""" - def __init__(self, api, site_id): + def __init__(self, hass, api, site_id): """Initialize the data object.""" self.api = api self.site_id = site_id @@ -282,22 +281,49 @@ class SolarEdgeDataService: self.data = {} self.attributes = {} + self.hass = hass + self.coordinator = None + + @callback + def async_setup(self): + """Coordinator creation.""" + self.coordinator = DataUpdateCoordinator( + self.hass, + _LOGGER, + name=str(self), + update_method=self.async_update_data, + update_interval=self.update_interval, + ) + + @property + @abstractmethod + def update_interval(self): + """Update interval.""" + + @abstractmethod + def update(self): + """Update data in executor.""" + + async def async_update_data(self): + """Update data.""" + await self.hass.async_add_executor_job(self.update) + class SolarEdgeOverviewDataService(SolarEdgeDataService): """Get and update the latest overview data.""" - @Throttle(OVERVIEW_UPDATE_DELAY) + @property + def update_interval(self): + """Update interval.""" + return OVERVIEW_UPDATE_DELAY + def update(self): """Update the data from the SolarEdge Monitoring API.""" try: data = self.api.get_overview(self.site_id) overview = data["overview"] - except KeyError: - _LOGGER.error("Missing overview data, skipping update") - return - except (ConnectTimeout, HTTPError): - _LOGGER.error("Could not retrieve data, skipping update") - return + except KeyError as ex: + raise UpdateFailed("Missing overview data, skipping update") from ex self.data = {} @@ -316,25 +342,25 @@ class SolarEdgeOverviewDataService(SolarEdgeDataService): class SolarEdgeDetailsDataService(SolarEdgeDataService): """Get and update the latest details data.""" - def __init__(self, api, site_id): + def __init__(self, hass, api, site_id): """Initialize the details data service.""" - super().__init__(api, site_id) + super().__init__(hass, api, site_id) self.data = None - @Throttle(DETAILS_UPDATE_DELAY) + @property + def update_interval(self): + """Update interval.""" + return DETAILS_UPDATE_DELAY + def update(self): """Update the data from the SolarEdge Monitoring API.""" try: data = self.api.get_details(self.site_id) details = data["details"] - except KeyError: - _LOGGER.error("Missing details data, skipping update") - return - except (ConnectTimeout, HTTPError): - _LOGGER.error("Could not retrieve data, skipping update") - return + except KeyError as ex: + raise UpdateFailed("Missing details data, skipping update") from ex self.data = None self.attributes = {} @@ -362,18 +388,18 @@ class SolarEdgeDetailsDataService(SolarEdgeDataService): class SolarEdgeInventoryDataService(SolarEdgeDataService): """Get and update the latest inventory data.""" - @Throttle(INVENTORY_UPDATE_DELAY) + @property + def update_interval(self): + """Update interval.""" + return INVENTORY_UPDATE_DELAY + def update(self): """Update the data from the SolarEdge Monitoring API.""" try: data = self.api.get_inventory(self.site_id) inventory = data["Inventory"] - except KeyError: - _LOGGER.error("Missing inventory data, skipping update") - return - except (ConnectTimeout, HTTPError): - _LOGGER.error("Could not retrieve data, skipping update") - return + except KeyError as ex: + raise UpdateFailed("Missing inventory data, skipping update") from ex self.data = {} self.attributes = {} @@ -388,13 +414,17 @@ class SolarEdgeInventoryDataService(SolarEdgeDataService): class SolarEdgeEnergyDetailsService(SolarEdgeDataService): """Get and update the latest power flow data.""" - def __init__(self, api, site_id): + def __init__(self, hass, api, site_id): """Initialize the power flow data service.""" - super().__init__(api, site_id) + super().__init__(hass, api, site_id) self.unit = None - @Throttle(ENERGY_DETAILS_DELAY) + @property + def update_interval(self): + """Update interval.""" + return ENERGY_DETAILS_DELAY + def update(self): """Update the data from the SolarEdge Monitoring API.""" try: @@ -409,12 +439,8 @@ class SolarEdgeEnergyDetailsService(SolarEdgeDataService): time_unit="DAY", ) energy_details = data["energyDetails"] - except KeyError: - _LOGGER.error("Missing power flow data, skipping update") - return - except (ConnectTimeout, HTTPError): - _LOGGER.error("Could not retrieve data, skipping update") - return + except KeyError as ex: + raise UpdateFailed("Missing power flow data, skipping update") from ex if "meters" not in energy_details: _LOGGER.debug( @@ -449,24 +475,24 @@ class SolarEdgeEnergyDetailsService(SolarEdgeDataService): class SolarEdgePowerFlowDataService(SolarEdgeDataService): """Get and update the latest power flow data.""" - def __init__(self, api, site_id): + def __init__(self, hass, api, site_id): """Initialize the power flow data service.""" - super().__init__(api, site_id) + super().__init__(hass, api, site_id) self.unit = None - @Throttle(POWER_FLOW_UPDATE_DELAY) + @property + def update_interval(self): + """Update interval.""" + return POWER_FLOW_UPDATE_DELAY + def update(self): """Update the data from the SolarEdge Monitoring API.""" try: data = self.api.get_current_power_flow(self.site_id) power_flow = data["siteCurrentPowerFlow"] - except KeyError: - _LOGGER.error("Missing power flow data, skipping update") - return - except (ConnectTimeout, HTTPError): - _LOGGER.error("Could not retrieve data, skipping update") - return + except KeyError as ex: + raise UpdateFailed("Missing power flow data, skipping update") from ex power_from = [] power_to = [] From b80571519b39427f23a399ed7a1e6d45ba309902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20=C3=98stergaard=20Nielsen?= Date: Sat, 30 Jan 2021 12:46:50 +0100 Subject: [PATCH 074/796] IHC service functions support for multiple IHC controllers (#44626) Co-authored-by: Martin Hjelmare --- homeassistant/components/ihc/__init__.py | 31 ++++++++++++++++--- homeassistant/components/ihc/const.py | 1 + .../components/ihc/ihc_auto_setup.yaml | 24 ++++++++++++++ homeassistant/components/ihc/services.yaml | 27 ++++++++++++++++ 4 files changed, 79 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py index f200c9651f0..8769f73e365 100644 --- a/homeassistant/components/ihc/__init__.py +++ b/homeassistant/components/ihc/__init__.py @@ -23,6 +23,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType from .const import ( + ATTR_CONTROLLER_ID, ATTR_IHC_ID, ATTR_VALUE, CONF_AUTOSETUP, @@ -186,13 +187,18 @@ AUTO_SETUP_SCHEMA = vol.Schema( ) SET_RUNTIME_VALUE_BOOL_SCHEMA = vol.Schema( - {vol.Required(ATTR_IHC_ID): cv.positive_int, vol.Required(ATTR_VALUE): cv.boolean} + { + vol.Required(ATTR_IHC_ID): cv.positive_int, + vol.Required(ATTR_VALUE): cv.boolean, + vol.Optional(ATTR_CONTROLLER_ID, default=0): cv.positive_int, + } ) SET_RUNTIME_VALUE_INT_SCHEMA = vol.Schema( { vol.Required(ATTR_IHC_ID): cv.positive_int, vol.Required(ATTR_VALUE): vol.Coerce(int), + vol.Optional(ATTR_CONTROLLER_ID, default=0): cv.positive_int, } ) @@ -200,10 +206,16 @@ SET_RUNTIME_VALUE_FLOAT_SCHEMA = vol.Schema( { vol.Required(ATTR_IHC_ID): cv.positive_int, vol.Required(ATTR_VALUE): vol.Coerce(float), + vol.Optional(ATTR_CONTROLLER_ID, default=0): cv.positive_int, } ) -PULSE_SCHEMA = vol.Schema({vol.Required(ATTR_IHC_ID): cv.positive_int}) +PULSE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_IHC_ID): cv.positive_int, + vol.Optional(ATTR_CONTROLLER_ID, default=0): cv.positive_int, + } +) def setup(hass, config): @@ -237,7 +249,9 @@ def ihc_setup(hass, config, conf, controller_id): # Store controller configuration ihc_key = f"ihc{controller_id}" hass.data[ihc_key] = {IHC_CONTROLLER: ihc_controller, IHC_INFO: conf[CONF_INFO]} - setup_service_functions(hass, ihc_controller) + # We only want to register the service functions once for the first controller + if controller_id == 0: + setup_service_functions(hass) return True @@ -329,30 +343,39 @@ def get_discovery_info(component_setup, groups, controller_id): return discovery_data -def setup_service_functions(hass: HomeAssistantType, ihc_controller): +def setup_service_functions(hass: HomeAssistantType): """Set up the IHC service functions.""" + def _get_controller(call): + controller_id = call.data[ATTR_CONTROLLER_ID] + ihc_key = f"ihc{controller_id}" + return hass.data[ihc_key][IHC_CONTROLLER] + def set_runtime_value_bool(call): """Set a IHC runtime bool value service function.""" ihc_id = call.data[ATTR_IHC_ID] value = call.data[ATTR_VALUE] + ihc_controller = _get_controller(call) ihc_controller.set_runtime_value_bool(ihc_id, value) def set_runtime_value_int(call): """Set a IHC runtime integer value service function.""" ihc_id = call.data[ATTR_IHC_ID] value = call.data[ATTR_VALUE] + ihc_controller = _get_controller(call) ihc_controller.set_runtime_value_int(ihc_id, value) def set_runtime_value_float(call): """Set a IHC runtime float value service function.""" ihc_id = call.data[ATTR_IHC_ID] value = call.data[ATTR_VALUE] + ihc_controller = _get_controller(call) ihc_controller.set_runtime_value_float(ihc_id, value) async def async_pulse_runtime_input(call): """Pulse a IHC controller input function.""" ihc_id = call.data[ATTR_IHC_ID] + ihc_controller = _get_controller(call) await async_pulse(hass, ihc_controller, ihc_id) hass.services.register( diff --git a/homeassistant/components/ihc/const.py b/homeassistant/components/ihc/const.py index 15db19ba58b..30103e2bdba 100644 --- a/homeassistant/components/ihc/const.py +++ b/homeassistant/components/ihc/const.py @@ -18,6 +18,7 @@ CONF_XPATH = "xpath" ATTR_IHC_ID = "ihc_id" ATTR_VALUE = "value" +ATTR_CONTROLLER_ID = "controller_id" SERVICE_SET_RUNTIME_VALUE_BOOL = "set_runtime_value_bool" SERVICE_SET_RUNTIME_VALUE_FLOAT = "set_runtime_value_float" diff --git a/homeassistant/components/ihc/ihc_auto_setup.yaml b/homeassistant/components/ihc/ihc_auto_setup.yaml index d5f8d26e2b7..7a94afdae44 100644 --- a/homeassistant/components/ihc/ihc_auto_setup.yaml +++ b/homeassistant/components/ihc/ihc_auto_setup.yaml @@ -34,6 +34,30 @@ binary_sensor: type: "light" light: + # Swedish Wireless dimmer (Mobil VU/Dimmer 1-knapp/touch) + - xpath: './/product_airlink[@product_identifier="_0x4301"]' + node: "airlink_dimming" + dimmable: true + # Swedish Wireless dimmer (Lamputtag/Dimmer 1-knapp/touch) + - xpath: './/product_airlink[@product_identifier="_0x4302"]' + node: "airlink_dimming" + dimmable: true + # Swedish Wireless dimmer (Blind/Dimmer 1-knapp/touch) + - xpath: './/product_airlink[@product_identifier="_0x4305"]' + node: "airlink_dimming" + dimmable: true + # Swedish Wireless dimmer (3-tråds Puck/Dimmer 1-knapp/touch) + - xpath: './/product_airlink[@product_identifier="_0x4307"]' + node: "airlink_dimming" + dimmable: true + # Swedish Wireless dimmer (3-tråds Puck/Dimmer 2-knapp) + - xpath: './/product_airlink[@product_identifier="_0x4308"]' + node: "airlink_dimming" + dimmable: true + # 2 channel RS485 dimmer + - xpath: './/rs485_led_dimmer_channel[@product_identifier="_0x4410"]' + node: "airlink_dimming" + dimmable: true # Wireless Combi dimmer 4 buttons - xpath: './/product_airlink[@product_identifier="_0x4406"]' node: "airlink_dimming" diff --git a/homeassistant/components/ihc/services.yaml b/homeassistant/components/ihc/services.yaml index ad41539162c..a65d5f5b78c 100644 --- a/homeassistant/components/ihc/services.yaml +++ b/homeassistant/components/ihc/services.yaml @@ -3,29 +3,56 @@ set_runtime_value_bool: description: Set a boolean runtime value on the IHC controller. fields: + controller_id: + description: | + If you have multiple controller, this is the index of you controller + starting with 0 (0 is default) + example: 0 ihc_id: description: The integer IHC resource ID. + example: 123456 value: description: The boolean value to set. + example: true set_runtime_value_int: description: Set an integer runtime value on the IHC controller. fields: + controller_id: + description: | + If you have multiple controller, this is the index of you controller + starting with 0 (0 is default) + example: 0 ihc_id: description: The integer IHC resource ID. + example: 123456 value: description: The integer value to set. + example: 50 set_runtime_value_float: description: Set a float runtime value on the IHC controller. fields: + controller_id: + description: | + If you have multiple controller, this is the index of you controller + starting with 0 (0 is default) + example: 0 ihc_id: description: The integer IHC resource ID. + example: 123456 value: description: The float value to set. + example: 1.47 pulse: description: Pulses an input on the IHC controller. fields: + controller_id: + description: | + If you have multiple controller, this is the index of you controller + starting with 0 (0 is default) + example: 0 ihc_id: description: The integer IHC resource ID. + example: 123456 From 0964393002732dead6cc85a14aa56ecd18fd78ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 30 Jan 2021 14:09:16 +0100 Subject: [PATCH 075/796] Bump awesomeversion from 21.1.3 to 21.1.6 (#45738) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 5815ddd8ed0..8bd10f3ed61 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,7 +13,7 @@ pre-commit==2.9.3 pylint==2.6.0 astroid==2.4.2 pipdeptree==1.0.0 -awesomeversion==21.1.3 +awesomeversion==21.1.6 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 pytest-cov==2.10.1 From 1fd3a86239d33952cc083fd91f27ccc9a801c7c0 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 30 Jan 2021 15:38:43 +0100 Subject: [PATCH 076/796] Upgrade pysonos to 0.0.40 (#45743) --- homeassistant/components/sonos/manifest.json | 2 +- .../components/sonos/media_player.py | 27 ++++++++++--------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 1852f9c3849..e208a0e7a32 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.37"], + "requirements": ["pysonos==0.0.40"], "after_dependencies": ["plex"], "ssdp": [ { diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 9d89bdf68f8..2c69730211b 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -10,11 +10,11 @@ import async_timeout import pysonos from pysonos import alarms from pysonos.core import ( + MUSIC_SRC_LINE_IN, + MUSIC_SRC_RADIO, + MUSIC_SRC_TV, PLAY_MODE_BY_MEANING, PLAY_MODES, - PLAYING_LINE_IN, - PLAYING_RADIO, - PLAYING_TV, ) from pysonos.exceptions import SoCoException, SoCoUPnPException import pysonos.music_library @@ -759,11 +759,12 @@ class SonosEntity(MediaPlayerEntity): self._status = new_status track_uri = variables["current_track_uri"] if variables else None - whats_playing = self.soco.whats_playing(track_uri) - if whats_playing == PLAYING_TV: + music_source = self.soco.music_source_from_uri(track_uri) + + if music_source == MUSIC_SRC_TV: self.update_media_linein(SOURCE_TV) - elif whats_playing == PLAYING_LINE_IN: + elif music_source == MUSIC_SRC_LINE_IN: self.update_media_linein(SOURCE_LINEIN) else: track_info = self.soco.get_current_track_info() @@ -775,7 +776,7 @@ class SonosEntity(MediaPlayerEntity): self._media_album_name = track_info.get("album") self._media_title = track_info.get("title") - if whats_playing == PLAYING_RADIO: + if music_source == MUSIC_SRC_RADIO: self.update_media_radio(variables, track_info) else: self.update_media_music(update_position, track_info) @@ -816,7 +817,7 @@ class SonosEntity(MediaPlayerEntity): uri_meta_data, pysonos.data_structures.DidlAudioBroadcast ) and ( self.state != STATE_PLAYING - or self.soco.is_radio_uri(self._media_title) + or self.soco.music_source_from_uri(self._media_title) == MUSIC_SRC_RADIO or self._media_title in self._uri ): self._media_title = uri_meta_data.title @@ -1117,7 +1118,7 @@ class SonosEntity(MediaPlayerEntity): if len(fav) == 1: src = fav.pop() uri = src.reference.get_uri() - if self.soco.is_radio_uri(uri): + if self.soco.music_source_from_uri(uri) == MUSIC_SRC_RADIO: self.soco.play_uri(uri, title=source) else: self.soco.clear_queue() @@ -1201,8 +1202,8 @@ class SonosEntity(MediaPlayerEntity): elif media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_TRACK): if kwargs.get(ATTR_MEDIA_ENQUEUE): try: - if self.soco.is_spotify_uri(media_id): - self.soco.add_spotify_uri_to_queue(media_id) + if self.soco.is_service_uri(media_id): + self.soco.add_service_uri_to_queue(media_id) else: self.soco.add_uri_to_queue(media_id) except SoCoUPnPException: @@ -1213,9 +1214,9 @@ class SonosEntity(MediaPlayerEntity): media_id, ) else: - if self.soco.is_spotify_uri(media_id): + if self.soco.is_service_uri(media_id): self.soco.clear_queue() - self.soco.add_spotify_uri_to_queue(media_id) + self.soco.add_service_uri_to_queue(media_id) self.soco.play_from_queue(0) else: self.soco.play_uri(media_id) diff --git a/requirements_all.txt b/requirements_all.txt index af3f8235a9b..5e0792afcec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1708,7 +1708,7 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.37 +pysonos==0.0.40 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 90505f1297c..1aa5494a7ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -884,7 +884,7 @@ pysmartthings==0.7.6 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.37 +pysonos==0.0.40 # homeassistant.components.spc pyspcwebgw==0.4.0 From 88c4031e5779cb7e936008712959351a45131b7a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 30 Jan 2021 09:45:46 -0600 Subject: [PATCH 077/796] Fix exception when a unifi config entry is ignored (#45735) * Fix exception when a unifi config entry is ignored * Fix existing test --- homeassistant/components/unifi/config_flow.py | 2 +- tests/components/unifi/test_config_flow.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index f5e947c5e6f..85fe55a4076 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -244,7 +244,7 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): def _host_already_configured(self, host): """See if we already have a unifi entry matching the host.""" for entry in self._async_current_entries(): - if not entry.data: + if not entry.data or CONF_CONTROLLER not in entry.data: continue if entry.data[CONF_CONTROLLER][CONF_HOST] == host: return True diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 54c5fe291ea..15220e68914 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -595,7 +595,7 @@ async def test_form_ssdp_gets_form_with_ignored_entry(hass): await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=UNIFI_DOMAIN, - data={}, + data={"not_controller_key": None}, source=config_entries.SOURCE_IGNORE, ) entry.add_to_hass(hass) From b00086ca1f2d80add5f1cae3c08dfbe58fb964f0 Mon Sep 17 00:00:00 2001 From: Guliver Date: Sat, 30 Jan 2021 17:21:04 +0100 Subject: [PATCH 078/796] Use fixed due date only for comparison in todoist (#43300) --- homeassistant/components/todoist/calendar.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 978e58c2500..1188831c26d 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -246,7 +246,7 @@ class TodoistProjectDevice(CalendarEventDevice): data, labels, token, - latest_task_due_date=None, + due_date_days=None, whitelisted_labels=None, whitelisted_projects=None, ): @@ -255,7 +255,7 @@ class TodoistProjectDevice(CalendarEventDevice): data, labels, token, - latest_task_due_date, + due_date_days, whitelisted_labels, whitelisted_projects, ) @@ -338,7 +338,7 @@ class TodoistProjectData: project_data, labels, api, - latest_task_due_date=None, + due_date_days=None, whitelisted_labels=None, whitelisted_projects=None, ): @@ -356,12 +356,12 @@ class TodoistProjectData: self.all_project_tasks = [] - # The latest date a task can be due (for making lists of everything + # The days a task can be due (for making lists of everything # due today, or everything due in the next week, for example). - if latest_task_due_date is not None: - self._latest_due_date = dt.utcnow() + timedelta(days=latest_task_due_date) + if due_date_days is not None: + self._due_date_days = timedelta(days=due_date_days) else: - self._latest_due_date = None + self._due_date_days = None # Only tasks with one of these labels will be included. if whitelisted_labels is not None: @@ -409,8 +409,8 @@ class TodoistProjectData: if data[DUE] is not None: task[END] = _parse_due_date(data[DUE]) - if self._latest_due_date is not None and ( - task[END] > self._latest_due_date + if self._due_date_days is not None and ( + task[END] > dt.utcnow() + self._due_date_days ): # This task is out of range of our due date; # it shouldn't be counted. @@ -430,7 +430,7 @@ class TodoistProjectData: else: # If we ask for everything due before a certain date, don't count # things which have no due dates. - if self._latest_due_date is not None: + if self._due_date_days is not None: return None # Define values for tasks without due dates From 63fb8307fb1189670fad98d0068550e8cc3b4428 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 30 Jan 2021 21:10:42 +0100 Subject: [PATCH 079/796] Add initial GitHub Issue Form (#45752) --- .github/ISSUE_TEMPLATE/z_bug_report.yml | 102 ++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/z_bug_report.yml diff --git a/.github/ISSUE_TEMPLATE/z_bug_report.yml b/.github/ISSUE_TEMPLATE/z_bug_report.yml new file mode 100644 index 00000000000..85a85b1793a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/z_bug_report.yml @@ -0,0 +1,102 @@ +name: Report an issue with Home Assistant Core (Test) +about: Report an issue with Home Assistant Core. +title: "" +issue_body: true +inputs: + - type: description + attributes: + value: | + This issue form is for reporting bugs only! + + If you have a feature or enhancement request, please use the [feature request][fr] section of our [Community Forum][fr]. + [fr]: https://community.home-assistant.io/c/feature-requests + - type: textarea + attributes: + label: The problem + required: true + description: >- + Describe the issue you are experiencing here to communicate to the + maintainers. Tell us what you were trying to do and what happened. + + Provide a clear and concise description of what the problem is. + - type: description + attributes: + value: | + ## Environment + - type: input + attributes: + label: What is version of Home Assistant Core has the issue? + required: true + placeholder: core- + description: > + Can be found in the Configuration panel -> Info. + - type: input + attributes: + label: What was the last working version of Home Assistant Core? + required: false + placeholder: core- + description: > + If known, otherwise leave blank. + - type: dropdown + attributes: + label: What type of installation are you running? + required: true + description: > + If you don't know, you can find it in: Configuration panel -> Info. + choices: + - Home Assistant OS + - Home Assistant Container + - Home Assistant Supervised + - Home Assistant Core + - type: input + attributes: + label: Integration causing the issue + required: false + description: > + The name of the integration, for example, Automation or Philips Hue. + - type: input + attributes: + label: Link to integration documentation on our website + required: false + placeholder: "https://www.home-assistant.io/integrations/..." + description: > + Providing a link [to the documentation][docs] help us categorizing the + issue, while providing a useful reference at the same time. + + [docs]: https://www.home-assistant.io/integrations + + - type: description + attributes: + value: | + # Details + - type: textarea + attributes: + label: Example YAML snippet + required: false + description: | + It this issue has an example piece of YAML that can help reproducing + this problem, please provide. + + This can be an piece of YAML from, e.g., an automation, script, scene + or configuration. + value: | + ```yaml + # Put your YAML below this line + + ``` + - type: textarea + attributes: + label: Anything in the logs that might be useful for us? + description: For example, error message, or stack traces. + required: false + value: | + ```txt + # Put your logs below this line + + ``` + - type: description + attributes: + value: | + If you have any additional information for us, use the field below. + Please note, you can attach screenshots or screen recordings here, + by dragging and dropping files in the field below. From 8a6469cfce598e2d4894a0a91ac54f70692c3a29 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 30 Jan 2021 21:17:36 +0100 Subject: [PATCH 080/796] newline --- .github/ISSUE_TEMPLATE/z_bug_report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/z_bug_report.yml b/.github/ISSUE_TEMPLATE/z_bug_report.yml index 85a85b1793a..99d70288ff2 100644 --- a/.github/ISSUE_TEMPLATE/z_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/z_bug_report.yml @@ -9,6 +9,7 @@ inputs: This issue form is for reporting bugs only! If you have a feature or enhancement request, please use the [feature request][fr] section of our [Community Forum][fr]. + [fr]: https://community.home-assistant.io/c/feature-requests - type: textarea attributes: From 726bc6210bf9bfb3c546917ef498313e896f3c80 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 30 Jan 2021 21:21:57 +0100 Subject: [PATCH 081/796] Tiny tweaks to issue form --- .github/ISSUE_TEMPLATE/z_bug_report.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/z_bug_report.yml b/.github/ISSUE_TEMPLATE/z_bug_report.yml index 99d70288ff2..80827cf35e6 100644 --- a/.github/ISSUE_TEMPLATE/z_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/z_bug_report.yml @@ -60,7 +60,7 @@ inputs: label: Link to integration documentation on our website required: false placeholder: "https://www.home-assistant.io/integrations/..." - description: > + description: | Providing a link [to the documentation][docs] help us categorizing the issue, while providing a useful reference at the same time. @@ -95,6 +95,11 @@ inputs: # Put your logs below this line ``` + + - type: description + attributes: + value: | + ## Additional information - type: description attributes: value: | From 27407c116077d30aa62168262a17bd771419b248 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 30 Jan 2021 21:24:56 +0100 Subject: [PATCH 082/796] Tiny tweaks to issue form --- .github/ISSUE_TEMPLATE/z_bug_report.yml | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/z_bug_report.yml b/.github/ISSUE_TEMPLATE/z_bug_report.yml index 80827cf35e6..c672da11819 100644 --- a/.github/ISSUE_TEMPLATE/z_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/z_bug_report.yml @@ -75,11 +75,8 @@ inputs: label: Example YAML snippet required: false description: | - It this issue has an example piece of YAML that can help reproducing - this problem, please provide. - - This can be an piece of YAML from, e.g., an automation, script, scene - or configuration. + It this issue has an example piece of YAML that can help reproducing this problem, please provide. + This can be an piece of YAML from, e.g., an automation, script, scene or configuration. value: | ```yaml # Put your YAML below this line @@ -95,14 +92,13 @@ inputs: # Put your logs below this line ``` - - type: description attributes: value: | ## Additional information - type: description attributes: - value: | + value: > If you have any additional information for us, use the field below. - Please note, you can attach screenshots or screen recordings here, - by dragging and dropping files in the field below. + Please note, you can attach screenshots or screen recordings here, by + dragging and dropping files in the field below. From da29855967ac8120f7722359e60102d19db69ed6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 30 Jan 2021 21:32:46 +0100 Subject: [PATCH 083/796] Enable issue form as default --- .github/ISSUE_TEMPLATE/BUG_REPORT.md | 53 ------------------- .../{z_bug_report.yml => bug_report.yml} | 2 +- 2 files changed, 1 insertion(+), 54 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/BUG_REPORT.md rename .github/ISSUE_TEMPLATE/{z_bug_report.yml => bug_report.yml} (98%) diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md deleted file mode 100644 index bdadc5678ff..00000000000 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -name: Report a bug with Home Assistant Core -about: Report an issue with Home Assistant Core ---- - -## The problem - - - -## Environment - - -- Home Assistant Core release with the issue: -- Last working Home Assistant Core release (if known): -- Operating environment (OS/Container/Supervised/Core): -- Integration causing this issue: -- Link to integration documentation on our website: - -## Problem-relevant `configuration.yaml` - - -```yaml - -``` - -## Traceback/Error logs - - -```txt - -``` - -## Additional information - diff --git a/.github/ISSUE_TEMPLATE/z_bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml similarity index 98% rename from .github/ISSUE_TEMPLATE/z_bug_report.yml rename to .github/ISSUE_TEMPLATE/bug_report.yml index c672da11819..377e1452373 100644 --- a/.github/ISSUE_TEMPLATE/z_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,4 +1,4 @@ -name: Report an issue with Home Assistant Core (Test) +name: Report an issue with Home Assistant Core about: Report an issue with Home Assistant Core. title: "" issue_body: true From e43cee163f821b4880658b4eb6cbcafd5066ef9b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 30 Jan 2021 21:36:13 +0100 Subject: [PATCH 084/796] Fix typo in issue form --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 377e1452373..88e8afed67d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -75,7 +75,7 @@ inputs: label: Example YAML snippet required: false description: | - It this issue has an example piece of YAML that can help reproducing this problem, please provide. + If this issue has an example piece of YAML that can help reproducing this problem, please provide. This can be an piece of YAML from, e.g., an automation, script, scene or configuration. value: | ```yaml From d13b58a4e60cfd59a29047dfd6284f0a223680f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 30 Jan 2021 23:33:53 +0200 Subject: [PATCH 085/796] Upgrade mypy to 0.800 (#45485) * Upgrade mypy to 0.800 https://mypy-lang.blogspot.com/2021/01/mypy-0800-released.html * Fix issues flagged by mypy 0.800 * Add overloads + small changes * Apply grammar Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Co-authored-by: Martin Hjelmare --- homeassistant/core.py | 4 ++-- homeassistant/helpers/event.py | 4 +++- homeassistant/helpers/location.py | 3 +-- homeassistant/util/logging.py | 25 ++++++++++++++++++++----- requirements_test.txt | 2 +- 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 58d9d1e6754..6d187225685 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -205,7 +205,7 @@ class CoreState(enum.Enum): def __str__(self) -> str: # pylint: disable=invalid-str-returned """Return the event.""" - return self.value # type: ignore + return self.value class HomeAssistant: @@ -584,7 +584,7 @@ class EventOrigin(enum.Enum): def __str__(self) -> str: # pylint: disable=invalid-str-returned """Return the event.""" - return self.value # type: ignore + return self.value class Event: diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index f06ac8aca3f..da7f6cd52e8 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -116,7 +116,9 @@ class TrackTemplateResult: result: Any -def threaded_listener_factory(async_factory: Callable[..., Any]) -> CALLBACK_TYPE: +def threaded_listener_factory( + async_factory: Callable[..., Any] +) -> Callable[..., CALLBACK_TYPE]: """Convert an async event helper to a threaded one.""" @ft.wraps(async_factory) diff --git a/homeassistant/helpers/location.py b/homeassistant/helpers/location.py index bca2996dfa2..19058bc3e7f 100644 --- a/homeassistant/helpers/location.py +++ b/homeassistant/helpers/location.py @@ -18,9 +18,8 @@ def has_location(state: State) -> bool: Async friendly. """ - # type ignore: https://github.com/python/mypy/issues/7207 return ( - isinstance(state, State) # type: ignore + isinstance(state, State) and isinstance(state.attributes.get(ATTR_LATITUDE), float) and isinstance(state.attributes.get(ATTR_LONGITUDE), float) ) diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index feef339a200..9b04c2ab007 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -6,7 +6,7 @@ import logging import logging.handlers import queue import traceback -from typing import Any, Callable, Coroutine +from typing import Any, Awaitable, Callable, Coroutine, Union, cast, overload from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE from homeassistant.core import HomeAssistant, callback @@ -106,9 +106,23 @@ def log_exception(format_err: Callable[..., Any], *args: Any) -> None: logging.getLogger(module_name).error("%s\n%s", friendly_msg, exc_msg) +@overload +def catch_log_exception( # type: ignore + func: Callable[..., Awaitable[Any]], format_err: Callable[..., Any], *args: Any +) -> Callable[..., Awaitable[None]]: + """Overload for Callables that return an Awaitable.""" + + +@overload def catch_log_exception( func: Callable[..., Any], format_err: Callable[..., Any], *args: Any -) -> Callable[[], None]: +) -> Callable[..., None]: + """Overload for Callables that return Any.""" + + +def catch_log_exception( + func: Callable[..., Any], format_err: Callable[..., Any], *args: Any +) -> Union[Callable[..., None], Callable[..., Awaitable[None]]]: """Decorate a callback to catch and log exceptions.""" # Check for partials to properly determine if coroutine function @@ -116,14 +130,15 @@ def catch_log_exception( while isinstance(check_func, partial): check_func = check_func.func - wrapper_func = None + wrapper_func: Union[Callable[..., None], Callable[..., Awaitable[None]]] if asyncio.iscoroutinefunction(check_func): + async_func = cast(Callable[..., Awaitable[None]], func) - @wraps(func) + @wraps(async_func) async def async_wrapper(*args: Any) -> None: """Catch and log exception.""" try: - await func(*args) + await async_func(*args) except Exception: # pylint: disable=broad-except log_exception(format_err, *args) diff --git a/requirements_test.txt b/requirements_test.txt index 8bd10f3ed61..69e66239f83 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ codecov==2.1.10 coverage==5.4 jsonpickle==1.4.1 mock-open==1.4.0 -mypy==0.790 +mypy==0.800 pre-commit==2.9.3 pylint==2.6.0 astroid==2.4.2 From 6b446363445d928bb14a403204df15177154b4ac Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sun, 31 Jan 2021 00:51:33 +0100 Subject: [PATCH 086/796] Update frontend to 20210127.6 (#45760) --- 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 eb455a5a6c1..9d21be79912 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==20210127.5"], + "requirements": ["home-assistant-frontend==20210127.6"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0845cebc663..139f577ff63 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.41.0 -home-assistant-frontend==20210127.5 +home-assistant-frontend==20210127.6 httpx==0.16.1 jinja2>=2.11.2 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 5e0792afcec..e8bd1fb8040 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.10.4 # homeassistant.components.frontend -home-assistant-frontend==20210127.5 +home-assistant-frontend==20210127.6 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1aa5494a7ca..dbb840b1a10 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -402,7 +402,7 @@ hole==0.5.1 holidays==0.10.4 # homeassistant.components.frontend -home-assistant-frontend==20210127.5 +home-assistant-frontend==20210127.6 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From ea7aa6af590317f9d352e0f25f51c167cdcd4d04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 30 Jan 2021 23:26:02 -1000 Subject: [PATCH 087/796] Update dyson for the new fan entity model (#45762) * Update dyson for the new fan entity model * Fix test * tweak * fix * adj * Update homeassistant/components/dyson/fan.py Co-authored-by: Martin Hjelmare * move percentage is None block * move percentage is None block * no need to list comp Co-authored-by: Martin Hjelmare --- homeassistant/components/dyson/fan.py | 164 +++++++++++++------------- tests/components/dyson/test_fan.py | 51 +++++++- 2 files changed, 128 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/dyson/fan.py b/homeassistant/components/dyson/fan.py index 7a57a75523e..7a403902ee8 100644 --- a/homeassistant/components/dyson/fan.py +++ b/homeassistant/components/dyson/fan.py @@ -1,5 +1,6 @@ """Support for Dyson Pure Cool link fan.""" import logging +import math from typing import Optional from libpurecool.const import FanMode, FanSpeed, NightMode, Oscillation @@ -9,15 +10,12 @@ from libpurecool.dyson_pure_state import DysonPureCoolState from libpurecool.dyson_pure_state_v2 import DysonPureCoolV2State import voluptuous as vol -from homeassistant.components.fan import ( - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SUPPORT_OSCILLATE, - SUPPORT_SET_SPEED, - FanEntity, -) +from homeassistant.components.fan import SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) from . import DYSON_DEVICES, DysonEntity @@ -70,40 +68,30 @@ SET_DYSON_SPEED_SCHEMA = { } -SPEED_LIST_HA = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] +PRESET_MODE_AUTO = "auto" +PRESET_MODES = [PRESET_MODE_AUTO] -SPEED_LIST_DYSON = [ - int(FanSpeed.FAN_SPEED_1.value), - int(FanSpeed.FAN_SPEED_2.value), - int(FanSpeed.FAN_SPEED_3.value), - int(FanSpeed.FAN_SPEED_4.value), - int(FanSpeed.FAN_SPEED_5.value), - int(FanSpeed.FAN_SPEED_6.value), - int(FanSpeed.FAN_SPEED_7.value), - int(FanSpeed.FAN_SPEED_8.value), - int(FanSpeed.FAN_SPEED_9.value), - int(FanSpeed.FAN_SPEED_10.value), +ORDERED_DYSON_SPEEDS = [ + FanSpeed.FAN_SPEED_1, + FanSpeed.FAN_SPEED_2, + FanSpeed.FAN_SPEED_3, + FanSpeed.FAN_SPEED_4, + FanSpeed.FAN_SPEED_5, + FanSpeed.FAN_SPEED_6, + FanSpeed.FAN_SPEED_7, + FanSpeed.FAN_SPEED_8, + FanSpeed.FAN_SPEED_9, + FanSpeed.FAN_SPEED_10, ] +DYSON_SPEED_TO_INT_VALUE = {k: int(k.value) for k in ORDERED_DYSON_SPEEDS} +INT_VALUE_TO_DYSON_SPEED = {v: k for k, v in DYSON_SPEED_TO_INT_VALUE.items()} -SPEED_DYSON_TO_HA = { - FanSpeed.FAN_SPEED_1.value: SPEED_LOW, - FanSpeed.FAN_SPEED_2.value: SPEED_LOW, - FanSpeed.FAN_SPEED_3.value: SPEED_LOW, - FanSpeed.FAN_SPEED_4.value: SPEED_LOW, - FanSpeed.FAN_SPEED_AUTO.value: SPEED_MEDIUM, - FanSpeed.FAN_SPEED_5.value: SPEED_MEDIUM, - FanSpeed.FAN_SPEED_6.value: SPEED_MEDIUM, - FanSpeed.FAN_SPEED_7.value: SPEED_MEDIUM, - FanSpeed.FAN_SPEED_8.value: SPEED_HIGH, - FanSpeed.FAN_SPEED_9.value: SPEED_HIGH, - FanSpeed.FAN_SPEED_10.value: SPEED_HIGH, -} +SPEED_LIST_DYSON = list(DYSON_SPEED_TO_INT_VALUE.values()) -SPEED_HA_TO_DYSON = { - SPEED_LOW: FanSpeed.FAN_SPEED_4, - SPEED_MEDIUM: FanSpeed.FAN_SPEED_7, - SPEED_HIGH: FanSpeed.FAN_SPEED_10, -} +SPEED_RANGE = ( + SPEED_LIST_DYSON[0], + SPEED_LIST_DYSON[-1], +) # off is not included async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -160,14 +148,23 @@ class DysonFanEntity(DysonEntity, FanEntity): """Representation of a Dyson fan.""" @property - def speed(self): - """Return the current speed.""" - return SPEED_DYSON_TO_HA[self._device.state.speed] + def percentage(self): + """Return the current speed percentage.""" + if self.auto_mode: + return None + return ranged_value_to_percentage(SPEED_RANGE, int(self._device.state.speed)) @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return SPEED_LIST_HA + def preset_modes(self): + """Return the available preset modes.""" + return PRESET_MODES + + @property + def preset_mode(self): + """Return the current preset mode.""" + if self.auto_mode: + return PRESET_MODE_AUTO + return None @property def dyson_speed(self): @@ -206,12 +203,25 @@ class DysonFanEntity(DysonEntity, FanEntity): ATTR_DYSON_SPEED_LIST: self.dyson_speed_list, } - def set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - if speed not in SPEED_LIST_HA: - raise ValueError(f'"{speed}" is not a valid speed') - _LOGGER.debug("Set fan speed to: %s", speed) - self.set_dyson_speed(SPEED_HA_TO_DYSON[speed]) + def set_auto_mode(self, auto_mode: bool) -> None: + """Set auto mode.""" + raise NotImplementedError + + def set_percentage(self, percentage: int) -> None: + """Set the speed percentage of the fan.""" + if percentage == 0: + self.turn_off() + return + dyson_speed = INT_VALUE_TO_DYSON_SPEED[ + math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) + ] + self.set_dyson_speed(dyson_speed) + + def set_preset_mode(self, preset_mode: str) -> None: + """Set a preset mode on the fan.""" + self._valid_preset_mode_or_raise(preset_mode) + # There currently is only one + self.set_auto_mode(True) def set_dyson_speed(self, speed: FanSpeed) -> None: """Set the exact speed of the fan.""" @@ -225,21 +235,6 @@ class DysonFanEntity(DysonEntity, FanEntity): speed = FanSpeed(f"{int(dyson_speed):04d}") self.set_dyson_speed(speed) - -class DysonPureCoolLinkEntity(DysonFanEntity): - """Representation of a Dyson fan.""" - - def __init__(self, device): - """Initialize the fan.""" - super().__init__(device, DysonPureCoolState) - - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # def turn_on( self, speed: Optional[str] = None, @@ -248,12 +243,22 @@ class DysonPureCoolLinkEntity(DysonFanEntity): **kwargs, ) -> None: """Turn on the fan.""" - _LOGGER.debug("Turn on fan %s with speed %s", self.name, speed) - if speed is not None: - self.set_speed(speed) - else: - # Speed not set, just turn on + _LOGGER.debug("Turn on fan %s with percentage %s", self.name, percentage) + if preset_mode: + self.set_preset_mode(preset_mode) + elif percentage is None: + # percentage not set, just turn on self._device.set_configuration(fan_mode=FanMode.FAN) + else: + self.set_percentage(percentage) + + +class DysonPureCoolLinkEntity(DysonFanEntity): + """Representation of a Dyson fan.""" + + def __init__(self, device): + """Initialize the fan.""" + super().__init__(device, DysonPureCoolState) def turn_off(self, **kwargs) -> None: """Turn off the fan.""" @@ -312,13 +317,6 @@ class DysonPureCoolEntity(DysonFanEntity): """Initialize the fan.""" super().__init__(device, DysonPureCoolV2State) - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # def turn_on( self, speed: Optional[str] = None, @@ -327,12 +325,14 @@ class DysonPureCoolEntity(DysonFanEntity): **kwargs, ) -> None: """Turn on the fan.""" - _LOGGER.debug("Turn on fan %s", self.name) - - if speed is not None: - self.set_speed(speed) - else: + _LOGGER.debug("Turn on fan %s with percentage %s", self.name, percentage) + if preset_mode: + self.set_preset_mode(preset_mode) + elif percentage is None: + # percentage not set, just turn on self._device.turn_on() + else: + self.set_percentage(percentage) def turn_off(self, **kwargs): """Turn off the fan.""" diff --git a/tests/components/dyson/test_fan.py b/tests/components/dyson/test_fan.py index 310d9197133..dacde12c569 100644 --- a/tests/components/dyson/test_fan.py +++ b/tests/components/dyson/test_fan.py @@ -19,16 +19,18 @@ from homeassistant.components.dyson.fan import ( ATTR_HEPA_FILTER, ATTR_NIGHT_MODE, ATTR_TIMER, + PRESET_MODE_AUTO, SERVICE_SET_ANGLE, SERVICE_SET_AUTO_MODE, SERVICE_SET_DYSON_SPEED, SERVICE_SET_FLOW_DIRECTION_FRONT, SERVICE_SET_NIGHT_MODE, SERVICE_SET_TIMER, - SPEED_LOW, ) from homeassistant.components.fan import ( ATTR_OSCILLATING, + ATTR_PERCENTAGE, + ATTR_PRESET_MODE, ATTR_SPEED, ATTR_SPEED_LIST, DOMAIN as PLATFORM_DOMAIN, @@ -37,7 +39,9 @@ from homeassistant.components.fan import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, SPEED_HIGH, + SPEED_LOW, SPEED_MEDIUM, + SPEED_OFF, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, ) @@ -84,8 +88,16 @@ async def test_state_purecoollink( attributes = state.attributes assert attributes[ATTR_NIGHT_MODE] is True assert attributes[ATTR_OSCILLATING] is True + assert attributes[ATTR_PERCENTAGE] == 10 + assert attributes[ATTR_PRESET_MODE] is None assert attributes[ATTR_SPEED] == SPEED_LOW - assert attributes[ATTR_SPEED_LIST] == [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + assert attributes[ATTR_SPEED_LIST] == [ + SPEED_OFF, + SPEED_LOW, + SPEED_MEDIUM, + SPEED_HIGH, + PRESET_MODE_AUTO, + ] assert attributes[ATTR_DYSON_SPEED] == 1 assert attributes[ATTR_DYSON_SPEED_LIST] == list(range(1, 11)) assert attributes[ATTR_AUTO_MODE] is False @@ -106,7 +118,9 @@ async def test_state_purecoollink( attributes = state.attributes assert attributes[ATTR_NIGHT_MODE] is False assert attributes[ATTR_OSCILLATING] is False - assert attributes[ATTR_SPEED] == SPEED_MEDIUM + assert attributes[ATTR_PERCENTAGE] is None + assert attributes[ATTR_PRESET_MODE] == "auto" + assert attributes[ATTR_SPEED] == PRESET_MODE_AUTO assert attributes[ATTR_DYSON_SPEED] == "AUTO" assert attributes[ATTR_AUTO_MODE] is True @@ -125,8 +139,16 @@ async def test_state_purecool(hass: HomeAssistant, device: DysonPureCool) -> Non assert attributes[ATTR_OSCILLATING] is True assert attributes[ATTR_ANGLE_LOW] == 24 assert attributes[ATTR_ANGLE_HIGH] == 254 + assert attributes[ATTR_PERCENTAGE] == 10 + assert attributes[ATTR_PRESET_MODE] is None assert attributes[ATTR_SPEED] == SPEED_LOW - assert attributes[ATTR_SPEED_LIST] == [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + assert attributes[ATTR_SPEED_LIST] == [ + SPEED_OFF, + SPEED_LOW, + SPEED_MEDIUM, + SPEED_HIGH, + PRESET_MODE_AUTO, + ] assert attributes[ATTR_DYSON_SPEED] == 1 assert attributes[ATTR_DYSON_SPEED_LIST] == list(range(1, 11)) assert attributes[ATTR_AUTO_MODE] is False @@ -148,7 +170,9 @@ async def test_state_purecool(hass: HomeAssistant, device: DysonPureCool) -> Non attributes = state.attributes assert attributes[ATTR_NIGHT_MODE] is False assert attributes[ATTR_OSCILLATING] is False - assert attributes[ATTR_SPEED] == SPEED_MEDIUM + assert attributes[ATTR_PERCENTAGE] is None + assert attributes[ATTR_PRESET_MODE] == "auto" + assert attributes[ATTR_SPEED] == PRESET_MODE_AUTO assert attributes[ATTR_DYSON_SPEED] == "AUTO" assert attributes[ATTR_AUTO_MODE] is True assert attributes[ATTR_FLOW_DIRECTION_FRONT] is False @@ -170,6 +194,11 @@ async def test_state_purecool(hass: HomeAssistant, device: DysonPureCool) -> Non {ATTR_SPEED: SPEED_LOW}, {"fan_mode": FanMode.FAN, "fan_speed": FanSpeed.FAN_SPEED_4}, ), + ( + SERVICE_TURN_ON, + {ATTR_PERCENTAGE: 40}, + {"fan_mode": FanMode.FAN, "fan_speed": FanSpeed.FAN_SPEED_4}, + ), (SERVICE_TURN_OFF, {}, {"fan_mode": FanMode.OFF}), ( SERVICE_OSCILLATE, @@ -229,6 +258,18 @@ async def test_commands_purecoollink( "set_fan_speed", [FanSpeed.FAN_SPEED_4], ), + ( + SERVICE_TURN_ON, + {ATTR_PERCENTAGE: 40}, + "set_fan_speed", + [FanSpeed.FAN_SPEED_4], + ), + ( + SERVICE_TURN_ON, + {ATTR_PRESET_MODE: "auto"}, + "enable_auto_mode", + [], + ), (SERVICE_TURN_OFF, {}, "turn_off", []), (SERVICE_OSCILLATE, {ATTR_OSCILLATING: True}, "enable_oscillation", []), (SERVICE_OSCILLATE, {ATTR_OSCILLATING: False}, "disable_oscillation", []), From 275946b96da2ce8c7a2d501b14195d72ea025818 Mon Sep 17 00:00:00 2001 From: MtK Date: Sun, 31 Jan 2021 11:30:26 +0100 Subject: [PATCH 088/796] Bump ROVA package requirement (#45755) --- homeassistant/components/rova/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rova/manifest.json b/homeassistant/components/rova/manifest.json index a4ba931da43..b3635b39f38 100644 --- a/homeassistant/components/rova/manifest.json +++ b/homeassistant/components/rova/manifest.json @@ -2,6 +2,6 @@ "domain": "rova", "name": "ROVA", "documentation": "https://www.home-assistant.io/integrations/rova", - "requirements": ["rova==0.1.0"], + "requirements": ["rova==0.2.1"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index e8bd1fb8040..dd6671f4412 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1964,7 +1964,7 @@ roombapy==1.6.2 roonapi==0.0.31 # homeassistant.components.rova -rova==0.1.0 +rova==0.2.1 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 From f372bcf306bd4d757a1c069fe18137f862b060b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Jan 2021 01:13:55 -1000 Subject: [PATCH 089/796] Update insteon to use new fan entity model (#45767) --- homeassistant/components/insteon/fan.py | 60 +++++++++---------------- 1 file changed, 20 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/insteon/fan.py b/homeassistant/components/insteon/fan.py index f9d1c381f49..3327f9df5eb 100644 --- a/homeassistant/components/insteon/fan.py +++ b/homeassistant/components/insteon/fan.py @@ -1,28 +1,24 @@ """Support for INSTEON fans via PowerLinc Modem.""" +import math + from pyinsteon.constants import FanSpeed from homeassistant.components.fan import ( DOMAIN as FAN_DOMAIN, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, SUPPORT_SET_SPEED, FanEntity, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) from .const import SIGNAL_ADD_ENTITIES from .insteon_entity import InsteonEntity from .utils import async_add_insteon_entities -FAN_SPEEDS = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] -SPEED_TO_VALUE = { - SPEED_OFF: FanSpeed.OFF, - SPEED_LOW: FanSpeed.LOW, - SPEED_MEDIUM: FanSpeed.MEDIUM, - SPEED_HIGH: FanSpeed.HIGH, -} +SPEED_RANGE = (1, FanSpeed.HIGH) # off is not included async def async_setup_entry(hass, config_entry, async_add_entities): @@ -43,33 +39,17 @@ class InsteonFanEntity(InsteonEntity, FanEntity): """An INSTEON fan entity.""" @property - def speed(self) -> str: - """Return the current speed.""" - if self._insteon_device_group.value == FanSpeed.HIGH: - return SPEED_HIGH - if self._insteon_device_group.value == FanSpeed.MEDIUM: - return SPEED_MEDIUM - if self._insteon_device_group.value == FanSpeed.LOW: - return SPEED_LOW - return SPEED_OFF - - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return FAN_SPEEDS + def percentage(self) -> str: + """Return the current speed percentage.""" + if self._insteon_device_group.value is None: + return None + return ranged_value_to_percentage(SPEED_RANGE, self._insteon_device_group.value) @property def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_SET_SPEED - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # async def async_turn_on( self, speed: str = None, @@ -78,18 +58,18 @@ class InsteonFanEntity(InsteonEntity, FanEntity): **kwargs, ) -> None: """Turn on the fan.""" - if speed is None: - speed = SPEED_MEDIUM - await self.async_set_speed(speed) + if percentage is None: + percentage = 50 + await self.async_set_percentage(percentage) async def async_turn_off(self, **kwargs) -> None: """Turn off the fan.""" await self._insteon_device.async_fan_off() - async def async_set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - fan_speed = SPEED_TO_VALUE[speed] - if fan_speed == FanSpeed.OFF: + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed percentage of the fan.""" + if percentage == 0: await self._insteon_device.async_fan_off() else: - await self._insteon_device.async_fan_on(on_level=fan_speed) + on_level = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) + await self._insteon_device.async_fan_on(on_level=on_level) From 78934af6e6cbf68a7cb812fdc4fc82c1044915b1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 31 Jan 2021 13:50:48 +0100 Subject: [PATCH 090/796] Disable Osramlightify, upstream package is missing (#45775) --- homeassistant/components/osramlightify/manifest.json | 1 + requirements_all.txt | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/osramlightify/manifest.json b/homeassistant/components/osramlightify/manifest.json index dfe71d4b9e1..7e4b810b223 100644 --- a/homeassistant/components/osramlightify/manifest.json +++ b/homeassistant/components/osramlightify/manifest.json @@ -1,4 +1,5 @@ { + "disabled": "Upstream package has been removed from PyPi", "domain": "osramlightify", "name": "Osramlightify", "documentation": "https://www.home-assistant.io/integrations/osramlightify", diff --git a/requirements_all.txt b/requirements_all.txt index dd6671f4412..a8aab590df5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -879,9 +879,6 @@ life360==4.1.1 # homeassistant.components.lifx_legacy liffylights==0.9.4 -# homeassistant.components.osramlightify -lightify==1.0.7.2 - # homeassistant.components.lightwave lightwave==0.19 From ca43b3a8bb687c3110f05a3a07b957f90dd899c1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 31 Jan 2021 16:35:29 +0100 Subject: [PATCH 091/796] Bump pychromecast to 8.0.0 (#45776) --- 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 5f3deb36552..88dabc8d04d 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.7.2"], + "requirements": ["pychromecast==8.0.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 a8aab590df5..d41b4c8fe0a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1307,7 +1307,7 @@ pycfdns==1.2.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==7.7.2 +pychromecast==8.0.0 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dbb840b1a10..f2847be80a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -678,7 +678,7 @@ pybotvac==0.0.20 pycfdns==1.2.1 # homeassistant.components.cast -pychromecast==7.7.2 +pychromecast==8.0.0 # homeassistant.components.comfoconnect pycomfoconnect==0.4 From ee55223065f3082ea1fdcae5e878dc4c55e2f961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Z=C3=A1hradn=C3=ADk?= Date: Sun, 31 Jan 2021 17:59:14 +0100 Subject: [PATCH 092/796] SSDP response decode: replace invalid utf-8 characters (#42681) * SSDP response decode: replace invalid utf-8 characters * Add test to validate replaced data Co-authored-by: Joakim Plate --- homeassistant/components/ssdp/__init__.py | 4 +-- tests/components/ssdp/test_init.py | 43 +++++++++++++++++++++++ tests/test_util/aiohttp.py | 4 +-- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index e962c141bef..f07e88d811a 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -171,13 +171,13 @@ class Scanner: session = self.hass.helpers.aiohttp_client.async_get_clientsession() try: resp = await session.get(xml_location, timeout=5) - xml = await resp.text() + xml = await resp.text(errors="replace") # Samsung Smart TV sometimes returns an empty document the # first time. Retry once. if not xml: resp = await session.get(xml_location, timeout=5) - xml = await resp.text() + xml = await resp.text(errors="replace") except (aiohttp.ClientError, asyncio.TimeoutError) as err: _LOGGER.debug("Error fetching %s: %s", xml_location, err) return {} diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 008995cd78d..bba809aedbb 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -170,3 +170,46 @@ async def test_scan_description_parse_fail(hass, aioclient_mock): return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})], ): await scanner.async_scan(None) + + +async def test_invalid_characters(hass, aioclient_mock): + """Test that we replace bad characters with placeholders.""" + aioclient_mock.get( + "http://1.1.1.1", + text=""" + + + ABC + \xff\xff\xff\xff + + + """, + ) + scanner = ssdp.Scanner( + hass, + { + "mock-domain": [ + { + ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC", + } + ] + }, + ) + + with patch( + "netdisco.ssdp.scan", + return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})], + ), patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + await scanner.async_scan(None) + + assert len(mock_init.mock_calls) == 1 + assert mock_init.mock_calls[0][1][0] == "mock-domain" + assert mock_init.mock_calls[0][2]["context"] == {"source": "ssdp"} + assert mock_init.mock_calls[0][2]["data"] == { + "ssdp_location": "http://1.1.1.1", + "ssdp_st": "mock-st", + "deviceType": "ABC", + "serialNumber": "ÿÿÿÿ", + } diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 53949c20b06..5219212f1cf 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -245,9 +245,9 @@ class AiohttpClientMockResponse: """Return mock response.""" return self.response - async def text(self, encoding="utf-8"): + async def text(self, encoding="utf-8", errors="strict"): """Return mock response as a string.""" - return self.response.decode(encoding) + return self.response.decode(encoding, errors=errors) async def json(self, encoding="utf-8", content_type=None): """Return mock response as a json.""" From 2d10c83150e6cdb8cefb7b10fc688f83f2271283 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 31 Jan 2021 17:51:31 +0000 Subject: [PATCH 093/796] Honeywell Lyric Integration (#39695) Co-authored-by: J. Nick Koston --- .coveragerc | 3 + CODEOWNERS | 1 + homeassistant/components/lyric/__init__.py | 200 +++++++++++++ homeassistant/components/lyric/api.py | 55 ++++ homeassistant/components/lyric/climate.py | 280 ++++++++++++++++++ homeassistant/components/lyric/config_flow.py | 23 ++ homeassistant/components/lyric/const.py | 20 ++ homeassistant/components/lyric/manifest.json | 24 ++ homeassistant/components/lyric/services.yaml | 9 + homeassistant/components/lyric/strings.json | 16 + .../components/lyric/translations/en.json | 17 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/dhcp.py | 15 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/lyric/__init__.py | 1 + tests/components/lyric/test_config_flow.py | 128 ++++++++ 17 files changed, 799 insertions(+) create mode 100644 homeassistant/components/lyric/__init__.py create mode 100644 homeassistant/components/lyric/api.py create mode 100644 homeassistant/components/lyric/climate.py create mode 100644 homeassistant/components/lyric/config_flow.py create mode 100644 homeassistant/components/lyric/const.py create mode 100644 homeassistant/components/lyric/manifest.json create mode 100644 homeassistant/components/lyric/services.yaml create mode 100644 homeassistant/components/lyric/strings.json create mode 100644 homeassistant/components/lyric/translations/en.json create mode 100644 tests/components/lyric/__init__.py create mode 100644 tests/components/lyric/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index c692dfbba5e..65b499c372f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -518,6 +518,9 @@ omit = homeassistant/components/lutron_caseta/switch.py homeassistant/components/lw12wifi/light.py homeassistant/components/lyft/sensor.py + homeassistant/components/lyric/__init__.py + homeassistant/components/lyric/api.py + homeassistant/components/lyric/climate.py homeassistant/components/magicseaweed/sensor.py homeassistant/components/mailgun/notify.py homeassistant/components/map/* diff --git a/CODEOWNERS b/CODEOWNERS index e3cf58f9056..dc0129c8b8e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -260,6 +260,7 @@ homeassistant/components/luftdaten/* @fabaff homeassistant/components/lupusec/* @majuss homeassistant/components/lutron/* @JonGilmore homeassistant/components/lutron_caseta/* @swails @bdraco +homeassistant/components/lyric/* @timmo001 homeassistant/components/mastodon/* @fabaff homeassistant/components/matrix/* @tinloaf homeassistant/components/mcp23017/* @jardiamj diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py new file mode 100644 index 00000000000..d29fca09166 --- /dev/null +++ b/homeassistant/components/lyric/__init__.py @@ -0,0 +1,200 @@ +"""The Honeywell Lyric integration.""" +import asyncio +from datetime import timedelta +import logging +from typing import Any, Dict, Optional + +from aiolyric import Lyric +from aiolyric.objects.device import LyricDevice +from aiolyric.objects.location import LyricLocation +import async_timeout +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import ( + aiohttp_client, + config_entry_oauth2_flow, + config_validation as cv, + device_registry as dr, +) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .api import ConfigEntryLyricClient, LyricLocalOAuth2Implementation +from .config_flow import OAuth2FlowHandler +from .const import DOMAIN, LYRIC_EXCEPTIONS, OAUTH2_AUTHORIZE, OAUTH2_TOKEN + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["climate"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Honeywell Lyric component.""" + hass.data[DOMAIN] = {} + + if DOMAIN not in config: + return True + + hass.data[DOMAIN][CONF_CLIENT_ID] = config[DOMAIN][CONF_CLIENT_ID] + + OAuth2FlowHandler.async_register_implementation( + hass, + LyricLocalOAuth2Implementation( + hass, + DOMAIN, + config[DOMAIN][CONF_CLIENT_ID], + config[DOMAIN][CONF_CLIENT_SECRET], + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, + ), + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Honeywell Lyric from a config entry.""" + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + ) + + session = aiohttp_client.async_get_clientsession(hass) + oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + + client = ConfigEntryLyricClient(session, oauth_session) + + client_id = hass.data[DOMAIN][CONF_CLIENT_ID] + lyric: Lyric = Lyric(client, client_id) + + async def async_update_data() -> Lyric: + """Fetch data from Lyric.""" + try: + async with async_timeout.timeout(60): + await lyric.get_locations() + return lyric + except (*LYRIC_EXCEPTIONS, TimeoutError) as exception: + raise UpdateFailed(exception) from exception + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="lyric_coordinator", + update_method=async_update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=timedelta(seconds=120), + ) + + hass.data[DOMAIN][entry.entry_id] = coordinator + + # Fetch initial data so we have data when entities subscribe + await coordinator.async_refresh() + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class LyricEntity(CoordinatorEntity): + """Defines a base Honeywell Lyric entity.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + location: LyricLocation, + device: LyricDevice, + key: str, + name: str, + icon: Optional[str], + ) -> None: + """Initialize the Honeywell Lyric entity.""" + super().__init__(coordinator) + self._key = key + self._name = name + self._icon = icon + self._location = location + self._mac_id = device.macID + self._device_name = device.name + self._device_model = device.deviceModel + self._update_thermostat = coordinator.data.update_thermostat + + @property + def unique_id(self) -> str: + """Return the unique ID for this entity.""" + return self._key + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def icon(self) -> str: + """Return the mdi icon of the entity.""" + return self._icon + + @property + def location(self) -> LyricLocation: + """Get the Lyric Location.""" + return self.coordinator.data.locations_dict[self._location.locationID] + + @property + def device(self) -> LyricDevice: + """Get the Lyric Device.""" + return self.location.devices_dict[self._mac_id] + + +class LyricDeviceEntity(LyricEntity): + """Defines a Honeywell Lyric device entity.""" + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this Honeywell Lyric instance.""" + return { + "connections": {(dr.CONNECTION_NETWORK_MAC, self._mac_id)}, + "manufacturer": "Honeywell", + "model": self._device_model, + "name": self._device_name, + } diff --git a/homeassistant/components/lyric/api.py b/homeassistant/components/lyric/api.py new file mode 100644 index 00000000000..a77c6365baf --- /dev/null +++ b/homeassistant/components/lyric/api.py @@ -0,0 +1,55 @@ +"""API for Honeywell Lyric bound to Home Assistant OAuth.""" +import logging +from typing import cast + +from aiohttp import BasicAuth, ClientSession +from aiolyric.client import LyricClient + +from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +_LOGGER = logging.getLogger(__name__) + + +class ConfigEntryLyricClient(LyricClient): + """Provide Honeywell Lyric authentication tied to an OAuth2 based config entry.""" + + def __init__( + self, + websession: ClientSession, + oauth_session: config_entry_oauth2_flow.OAuth2Session, + ): + """Initialize Honeywell Lyric auth.""" + super().__init__(websession) + self._oauth_session = oauth_session + + async def async_get_access_token(self): + """Return a valid access token.""" + if not self._oauth_session.valid_token: + await self._oauth_session.async_ensure_token_valid() + + return self._oauth_session.token["access_token"] + + +class LyricLocalOAuth2Implementation( + config_entry_oauth2_flow.LocalOAuth2Implementation +): + """Lyric Local OAuth2 implementation.""" + + async def _token_request(self, data: dict) -> dict: + """Make a token request.""" + session = async_get_clientsession(self.hass) + + data["client_id"] = self.client_id + + if self.client_secret is not None: + data["client_secret"] = self.client_secret + + headers = { + "Authorization": BasicAuth(self.client_id, self.client_secret).encode(), + "Content-Type": "application/x-www-form-urlencoded", + } + + resp = await session.post(self.token_url, headers=headers, data=data) + resp.raise_for_status() + return cast(dict, await resp.json()) diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py new file mode 100644 index 00000000000..bcfefef9c93 --- /dev/null +++ b/homeassistant/components/lyric/climate.py @@ -0,0 +1,280 @@ +"""Support for Honeywell Lyric climate platform.""" +import logging +from time import gmtime, strftime, time +from typing import List, Optional + +from aiolyric.objects.device import LyricDevice +from aiolyric.objects.location import LyricLocation +import voluptuous as vol + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_platform +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from . import LyricDeviceEntity +from .const import ( + DOMAIN, + LYRIC_EXCEPTIONS, + PRESET_HOLD_UNTIL, + PRESET_NO_HOLD, + PRESET_PERMANENT_HOLD, + PRESET_TEMPORARY_HOLD, + PRESET_VACATION_HOLD, +) + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE + +LYRIC_HVAC_MODE_OFF = "Off" +LYRIC_HVAC_MODE_HEAT = "Heat" +LYRIC_HVAC_MODE_COOL = "Cool" +LYRIC_HVAC_MODE_HEAT_COOL = "Auto" + +LYRIC_HVAC_MODES = { + HVAC_MODE_OFF: LYRIC_HVAC_MODE_OFF, + HVAC_MODE_HEAT: LYRIC_HVAC_MODE_HEAT, + HVAC_MODE_COOL: LYRIC_HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL: LYRIC_HVAC_MODE_HEAT_COOL, +} + +HVAC_MODES = { + LYRIC_HVAC_MODE_OFF: HVAC_MODE_OFF, + LYRIC_HVAC_MODE_HEAT: HVAC_MODE_HEAT, + LYRIC_HVAC_MODE_COOL: HVAC_MODE_COOL, + LYRIC_HVAC_MODE_HEAT_COOL: HVAC_MODE_HEAT_COOL, +} + +SERVICE_HOLD_TIME = "set_hold_time" +ATTR_TIME_PERIOD = "time_period" + +SCHEMA_HOLD_TIME = { + vol.Required(ATTR_TIME_PERIOD, default="01:00:00"): vol.All( + cv.time_period, + cv.positive_timedelta, + lambda td: strftime("%H:%M:%S", gmtime(time() + td.total_seconds())), + ) +} + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Set up the Honeywell Lyric climate platform based on a config entry.""" + coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + entities = [] + + for location in coordinator.data.locations: + for device in location.devices: + entities.append(LyricClimate(hass, coordinator, location, device)) + + async_add_entities(entities, True) + + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_HOLD_TIME, + SCHEMA_HOLD_TIME, + "async_set_hold_time", + ) + + +class LyricClimate(LyricDeviceEntity, ClimateEntity): + """Defines a Honeywell Lyric climate entity.""" + + def __init__( + self, + hass: HomeAssistantType, + coordinator: DataUpdateCoordinator, + location: LyricLocation, + device: LyricDevice, + ) -> None: + """Initialize Honeywell Lyric climate entity.""" + self._temperature_unit = hass.config.units.temperature_unit + + # Setup supported hvac modes + self._hvac_modes = [HVAC_MODE_OFF] + + # Add supported lyric thermostat features + if LYRIC_HVAC_MODE_HEAT in device.allowedModes: + self._hvac_modes.append(HVAC_MODE_HEAT) + + if LYRIC_HVAC_MODE_COOL in device.allowedModes: + self._hvac_modes.append(HVAC_MODE_COOL) + + if ( + LYRIC_HVAC_MODE_HEAT in device.allowedModes + and LYRIC_HVAC_MODE_COOL in device.allowedModes + ): + self._hvac_modes.append(HVAC_MODE_HEAT_COOL) + + super().__init__( + coordinator, + location, + device, + f"{device.macID}_thermostat", + device.name, + None, + ) + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_FLAGS + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement.""" + return self._temperature_unit + + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature.""" + return self.device.indoorTemperature + + @property + def hvac_mode(self) -> str: + """Return the hvac mode.""" + return HVAC_MODES[self.device.changeableValues.mode] + + @property + def hvac_modes(self) -> List[str]: + """List of available hvac modes.""" + return self._hvac_modes + + @property + def target_temperature(self) -> Optional[float]: + """Return the temperature we try to reach.""" + device: LyricDevice = self.device + if not device.hasDualSetpointStatus: + return device.changeableValues.heatSetpoint + + @property + def target_temperature_low(self) -> Optional[float]: + """Return the upper bound temperature we try to reach.""" + device: LyricDevice = self.device + if device.hasDualSetpointStatus: + return device.changeableValues.coolSetpoint + + @property + def target_temperature_high(self) -> Optional[float]: + """Return the upper bound temperature we try to reach.""" + device: LyricDevice = self.device + if device.hasDualSetpointStatus: + return device.changeableValues.heatSetpoint + + @property + def preset_mode(self) -> Optional[str]: + """Return current preset mode.""" + return self.device.changeableValues.thermostatSetpointStatus + + @property + def preset_modes(self) -> Optional[List[str]]: + """Return preset modes.""" + return [ + PRESET_NO_HOLD, + PRESET_HOLD_UNTIL, + PRESET_PERMANENT_HOLD, + PRESET_TEMPORARY_HOLD, + PRESET_VACATION_HOLD, + ] + + @property + def min_temp(self) -> float: + """Identify min_temp in Lyric API or defaults if not available.""" + device: LyricDevice = self.device + if LYRIC_HVAC_MODE_COOL in device.allowedModes: + return device.minCoolSetpoint + return device.minHeatSetpoint + + @property + def max_temp(self) -> float: + """Identify max_temp in Lyric API or defaults if not available.""" + device: LyricDevice = self.device + if LYRIC_HVAC_MODE_HEAT in device.allowedModes: + return device.maxHeatSetpoint + return device.maxCoolSetpoint + + async def async_set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) + target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + + device: LyricDevice = self.device + if device.hasDualSetpointStatus: + if target_temp_low is not None and target_temp_high is not None: + temp = (target_temp_low, target_temp_high) + else: + raise HomeAssistantError( + "Could not find target_temp_low and/or target_temp_high in arguments" + ) + else: + temp = kwargs.get(ATTR_TEMPERATURE) + _LOGGER.debug("Set temperature: %s", temp) + try: + await self._update_thermostat(self.location, device, heatSetpoint=temp) + except LYRIC_EXCEPTIONS as exception: + _LOGGER.error(exception) + await self.coordinator.async_refresh() + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set hvac mode.""" + _LOGGER.debug("Set hvac mode: %s", hvac_mode) + try: + await self._update_thermostat( + self.location, self.device, mode=LYRIC_HVAC_MODES[hvac_mode] + ) + except LYRIC_EXCEPTIONS as exception: + _LOGGER.error(exception) + await self.coordinator.async_refresh() + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set preset (PermanentHold, HoldUntil, NoHold, VacationHold) mode.""" + _LOGGER.debug("Set preset mode: %s", preset_mode) + try: + await self._update_thermostat( + self.location, self.device, thermostatSetpointStatus=preset_mode + ) + except LYRIC_EXCEPTIONS as exception: + _LOGGER.error(exception) + await self.coordinator.async_refresh() + + async def async_set_preset_period(self, period: str) -> None: + """Set preset period (time).""" + try: + await self._update_thermostat( + self.location, self.device, nextPeriodTime=period + ) + except LYRIC_EXCEPTIONS as exception: + _LOGGER.error(exception) + await self.coordinator.async_refresh() + + async def async_set_hold_time(self, time_period: str) -> None: + """Set the time to hold until.""" + _LOGGER.debug("set_hold_time: %s", time_period) + try: + await self._update_thermostat( + self.location, + self.device, + thermostatSetpointStatus=PRESET_HOLD_UNTIL, + nextPeriodTime=time_period, + ) + except LYRIC_EXCEPTIONS as exception: + _LOGGER.error(exception) + await self.coordinator.async_refresh() diff --git a/homeassistant/components/lyric/config_flow.py b/homeassistant/components/lyric/config_flow.py new file mode 100644 index 00000000000..1370d5e67ea --- /dev/null +++ b/homeassistant/components/lyric/config_flow.py @@ -0,0 +1,23 @@ +"""Config flow for Honeywell Lyric.""" +import logging + +from homeassistant import config_entries +from homeassistant.helpers import config_entry_oauth2_flow + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class OAuth2FlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN +): + """Config flow to handle Honeywell Lyric OAuth2 authentication.""" + + DOMAIN = DOMAIN + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) diff --git a/homeassistant/components/lyric/const.py b/homeassistant/components/lyric/const.py new file mode 100644 index 00000000000..4f2f72b937b --- /dev/null +++ b/homeassistant/components/lyric/const.py @@ -0,0 +1,20 @@ +"""Constants for the Honeywell Lyric integration.""" +from aiohttp.client_exceptions import ClientResponseError +from aiolyric.exceptions import LyricAuthenticationException, LyricException + +DOMAIN = "lyric" + +OAUTH2_AUTHORIZE = "https://api.honeywell.com/oauth2/authorize" +OAUTH2_TOKEN = "https://api.honeywell.com/oauth2/token" + +PRESET_NO_HOLD = "NoHold" +PRESET_TEMPORARY_HOLD = "TemporaryHold" +PRESET_HOLD_UNTIL = "HoldUntil" +PRESET_PERMANENT_HOLD = "PermanentHold" +PRESET_VACATION_HOLD = "VacationHold" + +LYRIC_EXCEPTIONS = ( + LyricAuthenticationException, + LyricException, + ClientResponseError, +) diff --git a/homeassistant/components/lyric/manifest.json b/homeassistant/components/lyric/manifest.json new file mode 100644 index 00000000000..460eb6e2a3d --- /dev/null +++ b/homeassistant/components/lyric/manifest.json @@ -0,0 +1,24 @@ +{ + "domain": "lyric", + "name": "Honeywell Lyric", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/lyric", + "dependencies": ["http"], + "requirements": ["aiolyric==1.0.5"], + "codeowners": ["@timmo001"], + "quality_scale": "silver", + "dhcp": [ + { + "hostname": "lyric-*", + "macaddress": "48A2E6" + }, + { + "hostname": "lyric-*", + "macaddress": "B82CA0" + }, + { + "hostname": "lyric-*", + "macaddress": "00D02D" + } + ] +} diff --git a/homeassistant/components/lyric/services.yaml b/homeassistant/components/lyric/services.yaml new file mode 100644 index 00000000000..b4ea74a9644 --- /dev/null +++ b/homeassistant/components/lyric/services.yaml @@ -0,0 +1,9 @@ +set_hold_time: + description: "Sets the time to hold until" + fields: + entity_id: + description: Name(s) of entities to change + example: "climate.thermostat" + time_period: + description: Time to hold until + example: "01:00:00" diff --git a/homeassistant/components/lyric/strings.json b/homeassistant/components/lyric/strings.json new file mode 100644 index 00000000000..4e5f2330840 --- /dev/null +++ b/homeassistant/components/lyric/strings.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + } + }, + "abort": { + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]" + }, + "create_entry": { + "default": "[%key:common::config_flow::create_entry::authenticated%]" + } + } +} diff --git a/homeassistant/components/lyric/translations/en.json b/homeassistant/components/lyric/translations/en.json new file mode 100644 index 00000000000..b183398663e --- /dev/null +++ b/homeassistant/components/lyric/translations/en.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Timeout generating authorize URL.", + "missing_configuration": "The component is not configured. Please follow the documentation." + }, + "create_entry": { + "default": "Successfully authenticated" + }, + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + } + } + }, + "title": "Honeywell Lyric" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 4c12ff30e49..282d039128d 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -121,6 +121,7 @@ FLOWS = [ "logi_circle", "luftdaten", "lutron_caseta", + "lyric", "mailgun", "melcloud", "met", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 0b6f5166f88..61223bf00f7 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -41,6 +41,21 @@ DHCP = [ "hostname": "flume-gw-*", "macaddress": "B4E62D*" }, + { + "domain": "lyric", + "hostname": "lyric-*", + "macaddress": "48A2E6" + }, + { + "domain": "lyric", + "hostname": "lyric-*", + "macaddress": "B82CA0" + }, + { + "domain": "lyric", + "hostname": "lyric-*", + "macaddress": "00D02D" + }, { "domain": "nest", "macaddress": "18B430*" diff --git a/requirements_all.txt b/requirements_all.txt index d41b4c8fe0a..a8127ed7097 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -199,6 +199,9 @@ aiolifx_effects==0.2.2 # homeassistant.components.lutron_caseta aiolip==1.0.1 +# homeassistant.components.lyric +aiolyric==1.0.5 + # homeassistant.components.keyboard_remote aionotify==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f2847be80a2..3e8972c9864 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -118,6 +118,9 @@ aiokafka==0.6.0 # homeassistant.components.lutron_caseta aiolip==1.0.1 +# homeassistant.components.lyric +aiolyric==1.0.5 + # homeassistant.components.notion aionotion==1.1.0 diff --git a/tests/components/lyric/__init__.py b/tests/components/lyric/__init__.py new file mode 100644 index 00000000000..794c6bf1ba0 --- /dev/null +++ b/tests/components/lyric/__init__.py @@ -0,0 +1 @@ +"""Tests for the Honeywell Lyric integration.""" diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py new file mode 100644 index 00000000000..24b6b68d731 --- /dev/null +++ b/tests/components/lyric/test_config_flow.py @@ -0,0 +1,128 @@ +"""Test the Honeywell Lyric config flow.""" +import asyncio +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.http import CONF_BASE_URL, DOMAIN as DOMAIN_HTTP +from homeassistant.components.lyric import config_flow +from homeassistant.components.lyric.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 + +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" + + +@pytest.fixture() +async def mock_impl(hass): + """Mock implementation.""" + await setup.async_setup_component(hass, "http", {}) + + impl = config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, + DOMAIN, + CLIENT_ID, + CLIENT_SECRET, + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, + ) + config_flow.OAuth2FlowHandler.async_register_implementation(hass, impl) + return impl + + +async def test_abort_if_no_configuration(hass): + """Check flow abort when no configuration.""" + flow = config_flow.OAuth2FlowHandler() + flow.hass = hass + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "missing_configuration" + + +async def test_full_flow( + hass, aiohttp_client, aioclient_mock, current_request_with_host +): + """Check full flow.""" + assert await setup.async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_CLIENT_ID: CLIENT_ID, + CONF_CLIENT_SECRET: CLIENT_SECRET, + }, + DOMAIN_HTTP: {CONF_BASE_URL: "https://example.com"}, + }, + ) + + 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", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + ) + + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch("homeassistant.components.lyric.api.ConfigEntryLyricClient"): + with patch( + "homeassistant.components.lyric.async_setup_entry", return_value=True + ) as mock_setup: + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["data"]["auth_implementation"] == DOMAIN + + result["data"]["token"].pop("expires_at") + assert result["data"]["token"] == { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + } + + assert DOMAIN in hass.config.components + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.state == config_entries.ENTRY_STATE_LOADED + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup.mock_calls) == 1 + + +async def test_abort_if_authorization_timeout(hass, mock_impl): + """Check Somfy authorization timeout.""" + flow = config_flow.OAuth2FlowHandler() + flow.hass = hass + + with patch.object( + mock_impl, "async_generate_authorize_url", side_effect=asyncio.TimeoutError + ): + result = await flow.async_step_user() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "authorize_url_timeout" From f1d3af1a13ada7a521871a437d900b4ebf556085 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 31 Jan 2021 18:59:39 +0100 Subject: [PATCH 094/796] Add WLED unload entry result correctly (#45783) --- homeassistant/components/wled/__init__.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index d8aacd59881..7cc91d32062 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -72,19 +72,22 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload WLED config entry.""" # Unload entities for this entry/device. - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_unload(entry, component) - for component in WLED_COMPONENTS + unload_ok = all( + await asyncio.gather( + *( + hass.config_entries.async_forward_entry_unload(entry, component) + for component in WLED_COMPONENTS + ) ) ) - # Cleanup - del hass.data[DOMAIN][entry.entry_id] + if unload_ok: + del hass.data[DOMAIN][entry.entry_id] + if not hass.data[DOMAIN]: del hass.data[DOMAIN] - return True + return unload_ok def wled_exception_handler(func): From c74ddf47202da6d362eccae33572ffe909224a00 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 31 Jan 2021 19:00:49 +0100 Subject: [PATCH 095/796] Upgrade pre-commit to 2.10.0 (#45777) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 69e66239f83..91380d14c5e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,7 +9,7 @@ coverage==5.4 jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.800 -pre-commit==2.9.3 +pre-commit==2.10.0 pylint==2.6.0 astroid==2.4.2 pipdeptree==1.0.0 From e506d8616f7c9e77b87c96ebacb1105d3b74ef58 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 31 Jan 2021 20:22:46 +0100 Subject: [PATCH 096/796] Fix polling and update of camera state for synology_dsm (#43683) Co-authored-by: J. Nick Koston --- .../components/synology_dsm/__init__.py | 118 ++++++++++++++++-- .../components/synology_dsm/binary_sensor.py | 6 +- .../components/synology_dsm/camera.py | 99 +++++++++++---- .../components/synology_dsm/const.py | 1 + .../components/synology_dsm/sensor.py | 6 +- .../components/synology_dsm/switch.py | 19 ++- 6 files changed, 205 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 06696865d03..b0d78ca6716 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -4,6 +4,7 @@ from datetime import timedelta import logging from typing import Dict +import async_timeout from synology_dsm import SynologyDSM from synology_dsm.api.core.security import SynoCoreSecurity from synology_dsm.api.core.system import SynoCoreSystem @@ -14,6 +15,7 @@ from synology_dsm.api.dsm.network import SynoDSMNetwork from synology_dsm.api.storage.storage import SynoStorage from synology_dsm.api.surveillance_station import SynoSurveillanceStation from synology_dsm.exceptions import ( + SynologyDSMAPIErrorException, SynologyDSMLoginFailedException, SynologyDSMRequestException, ) @@ -44,10 +46,16 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from .const import ( CONF_SERIAL, CONF_VOLUMES, + COORDINATOR_SURVEILLANCE, DEFAULT_SCAN_INTERVAL, DEFAULT_USE_SSL, DEFAULT_VERIFY_SSL, @@ -185,7 +193,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): try: await api.async_setup() except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: - _LOGGER.debug("async_setup_entry - Unable to connect to DSM: %s", err) + _LOGGER.debug("async_setup_entry() - Unable to connect to DSM: %s", err) raise ConfigEntryNotReady from err undo_listener = entry.add_update_listener(_async_update_listener) @@ -206,6 +214,35 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): entry, data={**entry.data, CONF_MAC: network.macs} ) + # setup DataUpdateCoordinator + async def async_coordinator_update_data_surveillance_station(): + """Fetch all surveillance station data from api.""" + surveillance_station = api.surveillance_station + try: + async with async_timeout.timeout(10): + await hass.async_add_executor_job(surveillance_station.update) + except SynologyDSMAPIErrorException as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + + if SynoSurveillanceStation.CAMERA_API_KEY not in api.dsm.apis: + return + + return { + "cameras": { + camera.id: camera for camera in surveillance_station.get_all_cameras() + } + } + + hass.data[DOMAIN][entry.unique_id][ + COORDINATOR_SURVEILLANCE + ] = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"{entry.unique_id}_surveillance_station", + update_method=async_coordinator_update_data_surveillance_station, + update_interval=timedelta(seconds=30), + ) + for platform in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, platform) @@ -314,6 +351,7 @@ class SynoApi: async def async_setup(self): """Start interacting with the NAS.""" + # init SynologyDSM object and login self.dsm = SynologyDSM( self._entry.data[CONF_HOST], self._entry.data[CONF_PORT], @@ -326,9 +364,14 @@ class SynoApi: ) await self._hass.async_add_executor_job(self.dsm.login) + # check if surveillance station is used self._with_surveillance_station = bool( self.dsm.apis.get(SynoSurveillanceStation.CAMERA_API_KEY) ) + _LOGGER.debug( + "SynoAPI.async_setup() - self._with_surveillance_station:%s", + self._with_surveillance_station, + ) self._async_setup_api_requests() @@ -348,6 +391,9 @@ class SynoApi: @callback def subscribe(self, api_key, unique_id): """Subscribe an entity from API fetches.""" + _LOGGER.debug( + "SynoAPI.subscribe() - api_key:%s, unique_id:%s", api_key, unique_id + ) if api_key not in self._fetching_entities: self._fetching_entities[api_key] = set() self._fetching_entities[api_key].add(unique_id) @@ -362,8 +408,16 @@ class SynoApi: @callback def _async_setup_api_requests(self): """Determine if we should fetch each API, if one entity needs it.""" + _LOGGER.debug( + "SynoAPI._async_setup_api_requests() - self._fetching_entities:%s", + self._fetching_entities, + ) + # Entities not added yet, fetch all if not self._fetching_entities: + _LOGGER.debug( + "SynoAPI._async_setup_api_requests() - Entities not added yet, fetch all" + ) return # Determine if we should fetch an API @@ -380,33 +434,39 @@ class SynoApi: self._fetching_entities.get(SynoDSMInformation.API_KEY) ) self._with_surveillance_station = bool( - self._fetching_entities.get(SynoSurveillanceStation.CAMERA_API_KEY) - ) or bool( - self._fetching_entities.get(SynoSurveillanceStation.HOME_MODE_API_KEY) + self.dsm.apis.get(SynoSurveillanceStation.CAMERA_API_KEY) ) # Reset not used API, information is not reset since it's used in device_info if not self._with_security: + _LOGGER.debug("SynoAPI._async_setup_api_requests() - disable security") self.dsm.reset(self.security) self.security = None if not self._with_storage: + _LOGGER.debug("SynoAPI._async_setup_api_requests() - disable storage") self.dsm.reset(self.storage) self.storage = None if not self._with_system: + _LOGGER.debug("SynoAPI._async_setup_api_requests() - disable system") self.dsm.reset(self.system) self.system = None if not self._with_upgrade: + _LOGGER.debug("SynoAPI._async_setup_api_requests() - disable upgrade") self.dsm.reset(self.upgrade) self.upgrade = None if not self._with_utilisation: + _LOGGER.debug("SynoAPI._async_setup_api_requests() - disable utilisation") self.dsm.reset(self.utilisation) self.utilisation = None if not self._with_surveillance_station: + _LOGGER.debug( + "SynoAPI._async_setup_api_requests() - disable surveillance_station" + ) self.dsm.reset(self.surveillance_station) self.surveillance_station = None @@ -417,34 +477,42 @@ class SynoApi: self.network.update() if self._with_security: + _LOGGER.debug("SynoAPI._fetch_device_configuration() - fetch security") self.security = self.dsm.security if self._with_storage: + _LOGGER.debug("SynoAPI._fetch_device_configuration() - fetch storage") self.storage = self.dsm.storage if self._with_upgrade: + _LOGGER.debug("SynoAPI._fetch_device_configuration() - fetch upgrade") self.upgrade = self.dsm.upgrade if self._with_system: + _LOGGER.debug("SynoAPI._fetch_device_configuration() - fetch system") self.system = self.dsm.system if self._with_utilisation: + _LOGGER.debug("SynoAPI._fetch_device_configuration() - fetch utilisation") self.utilisation = self.dsm.utilisation if self._with_surveillance_station: + _LOGGER.debug( + "SynoAPI._fetch_device_configuration() - fetch surveillance_station" + ) self.surveillance_station = self.dsm.surveillance_station async def async_reboot(self): """Reboot NAS.""" if not self.system: - _LOGGER.debug("async_reboot - System API not ready: %s", self) + _LOGGER.debug("SynoAPI.async_reboot() - System API not ready: %s", self) return await self._hass.async_add_executor_job(self.system.reboot) async def async_shutdown(self): """Shutdown NAS.""" if not self.system: - _LOGGER.debug("async_shutdown - System API not ready: %s", self) + _LOGGER.debug("SynoAPI.async_shutdown() - System API not ready: %s", self) return await self._hass.async_add_executor_job(self.system.shutdown) @@ -454,6 +522,7 @@ class SynoApi: async def async_update(self, now=None): """Update function for updating API information.""" + _LOGGER.debug("SynoAPI.async_update()") self._async_setup_api_requests() try: await self._hass.async_add_executor_job( @@ -463,13 +532,13 @@ class SynoApi: _LOGGER.warning( "async_update - connection error during update, fallback by reloading the entry" ) - _LOGGER.debug("async_update - exception: %s", err) + _LOGGER.debug("SynoAPI.async_update() - exception: %s", err) await self._hass.config_entries.async_reload(self._entry.entry_id) return async_dispatcher_send(self._hass, self.signal_sensor_update) -class SynologyDSMEntity(Entity): +class SynologyDSMBaseEntity(Entity): """Representation of a Synology NAS entry.""" def __init__( @@ -479,8 +548,6 @@ class SynologyDSMEntity(Entity): entity_info: Dict[str, str], ): """Initialize the Synology DSM entity.""" - super().__init__() - self._api = api self._api_key = entity_type.split(":")[0] self.entity_type = entity_type.split(":")[-1] @@ -539,6 +606,20 @@ class SynologyDSMEntity(Entity): """Return if the entity should be enabled when first added to the entity registry.""" return self._enable_default + +class SynologyDSMDispatcherEntity(SynologyDSMBaseEntity, Entity): + """Representation of a Synology NAS entry.""" + + def __init__( + self, + api: SynoApi, + entity_type: str, + entity_info: Dict[str, str], + ): + """Initialize the Synology DSM entity.""" + super().__init__(api, entity_type, entity_info) + Entity.__init__(self) + @property def should_poll(self) -> bool: """No polling needed.""" @@ -562,7 +643,22 @@ class SynologyDSMEntity(Entity): self.async_on_remove(self._api.subscribe(self._api_key, self.unique_id)) -class SynologyDSMDeviceEntity(SynologyDSMEntity): +class SynologyDSMCoordinatorEntity(SynologyDSMBaseEntity, CoordinatorEntity): + """Representation of a Synology NAS entry.""" + + def __init__( + self, + api: SynoApi, + entity_type: str, + entity_info: Dict[str, str], + coordinator: DataUpdateCoordinator, + ): + """Initialize the Synology DSM entity.""" + super().__init__(api, entity_type, entity_info) + CoordinatorEntity.__init__(self, coordinator) + + +class SynologyDSMDeviceEntity(SynologyDSMDispatcherEntity): """Representation of a Synology NAS disk or volume entry.""" def __init__( diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index 69f217a4b4e..2bbfb8f4641 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DISKS from homeassistant.helpers.typing import HomeAssistantType -from . import SynologyDSMDeviceEntity, SynologyDSMEntity +from . import SynologyDSMDeviceEntity, SynologyDSMDispatcherEntity from .const import ( DOMAIN, SECURITY_BINARY_SENSORS, @@ -50,7 +50,7 @@ async def async_setup_entry( async_add_entities(entities) -class SynoDSMSecurityBinarySensor(SynologyDSMEntity, BinarySensorEntity): +class SynoDSMSecurityBinarySensor(SynologyDSMDispatcherEntity, BinarySensorEntity): """Representation a Synology Security binary sensor.""" @property @@ -78,7 +78,7 @@ class SynoDSMStorageBinarySensor(SynologyDSMDeviceEntity, BinarySensorEntity): return getattr(self._api.storage, self.entity_type)(self._device_id) -class SynoDSMUpgradeBinarySensor(SynologyDSMEntity, BinarySensorEntity): +class SynoDSMUpgradeBinarySensor(SynologyDSMDispatcherEntity, BinarySensorEntity): """Representation a Synology Upgrade binary sensor.""" @property diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index 1dfd8ff945b..f24615bd28e 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -1,15 +1,18 @@ """Support for Synology DSM cameras.""" +import logging from typing import Dict from synology_dsm.api.surveillance_station import SynoSurveillanceStation -from synology_dsm.api.surveillance_station.camera import SynoCamera +from synology_dsm.exceptions import SynologyDSMAPIErrorException from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import SynoApi, SynologyDSMEntity +from . import SynoApi, SynologyDSMCoordinatorEntity from .const import ( + COORDINATOR_SURVEILLANCE, DOMAIN, ENTITY_CLASS, ENTITY_ENABLE, @@ -19,50 +22,72 @@ from .const import ( SYNO_API, ) +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities ) -> None: - """Set up the Synology NAS binary sensor.""" + """Set up the Synology NAS cameras.""" - api = hass.data[DOMAIN][entry.unique_id][SYNO_API] + data = hass.data[DOMAIN][entry.unique_id] + api = data[SYNO_API] if SynoSurveillanceStation.CAMERA_API_KEY not in api.dsm.apis: return - surveillance_station = api.surveillance_station - await hass.async_add_executor_job(surveillance_station.update) - cameras = surveillance_station.get_all_cameras() - entities = [SynoDSMCamera(api, camera) for camera in cameras] + # initial data fetch + coordinator = data[COORDINATOR_SURVEILLANCE] + await coordinator.async_refresh() - async_add_entities(entities) + async_add_entities( + SynoDSMCamera(api, coordinator, camera_id) + for camera_id in coordinator.data["cameras"] + ) -class SynoDSMCamera(SynologyDSMEntity, Camera): +class SynoDSMCamera(SynologyDSMCoordinatorEntity, Camera): """Representation a Synology camera.""" - def __init__(self, api: SynoApi, camera: SynoCamera): + def __init__( + self, api: SynoApi, coordinator: DataUpdateCoordinator, camera_id: int + ): """Initialize a Synology camera.""" super().__init__( api, - f"{SynoSurveillanceStation.CAMERA_API_KEY}:{camera.id}", + f"{SynoSurveillanceStation.CAMERA_API_KEY}:{camera_id}", { - ENTITY_NAME: camera.name, + ENTITY_NAME: coordinator.data["cameras"][camera_id].name, + ENTITY_ENABLE: coordinator.data["cameras"][camera_id].is_enabled, ENTITY_CLASS: None, ENTITY_ICON: None, - ENTITY_ENABLE: True, ENTITY_UNIT: None, }, + coordinator, ) - self._camera = camera + Camera.__init__(self) + + self._camera_id = camera_id + self._api = api + + @property + def camera_data(self): + """Camera data.""" + return self.coordinator.data["cameras"][self._camera_id] @property def device_info(self) -> Dict[str, any]: """Return the device information.""" return { - "identifiers": {(DOMAIN, self._api.information.serial, self._camera.id)}, - "name": self._camera.name, - "model": self._camera.model, + "identifiers": { + ( + DOMAIN, + self._api.information.serial, + self.camera_data.id, + ) + }, + "name": self.camera_data.name, + "model": self.camera_data.model, "via_device": ( DOMAIN, self._api.information.serial, @@ -73,7 +98,7 @@ class SynoDSMCamera(SynologyDSMEntity, Camera): @property def available(self) -> bool: """Return the availability of the camera.""" - return self._camera.is_enabled + return self.camera_data.is_enabled and self.coordinator.last_update_success @property def supported_features(self) -> int: @@ -83,29 +108,53 @@ class SynoDSMCamera(SynologyDSMEntity, Camera): @property def is_recording(self): """Return true if the device is recording.""" - return self._camera.is_recording + return self.camera_data.is_recording @property def motion_detection_enabled(self): """Return the camera motion detection status.""" - return self._camera.is_motion_detection_enabled + return self.camera_data.is_motion_detection_enabled def camera_image(self) -> bytes: """Return bytes of camera image.""" + _LOGGER.debug( + "SynoDSMCamera.camera_image(%s)", + self.camera_data.name, + ) if not self.available: return None - return self._api.surveillance_station.get_camera_image(self._camera.id) + try: + return self._api.surveillance_station.get_camera_image(self._camera_id) + except (SynologyDSMAPIErrorException) as err: + _LOGGER.debug( + "SynoDSMCamera.camera_image(%s) - Exception:%s", + self.camera_data.name, + err, + ) + return None async def stream_source(self) -> str: """Return the source of the stream.""" + _LOGGER.debug( + "SynoDSMCamera.stream_source(%s)", + self.camera_data.name, + ) if not self.available: return None - return self._camera.live_view.rtsp + return self.camera_data.live_view.rtsp def enable_motion_detection(self): """Enable motion detection in the camera.""" - self._api.surveillance_station.enable_motion_detection(self._camera.id) + _LOGGER.debug( + "SynoDSMCamera.enable_motion_detection(%s)", + self.camera_data.name, + ) + self._api.surveillance_station.enable_motion_detection(self._camera_id) def disable_motion_detection(self): """Disable motion detection in camera.""" - self._api.surveillance_station.disable_motion_detection(self._camera.id) + _LOGGER.debug( + "SynoDSMCamera.disable_motion_detection(%s)", + self.camera_data.name, + ) + self._api.surveillance_station.disable_motion_detection(self._camera_id) diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index ba1a8034223..f9bcc8b61b8 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -19,6 +19,7 @@ from homeassistant.const import ( DOMAIN = "synology_dsm" PLATFORMS = ["binary_sensor", "camera", "sensor", "switch"] +COORDINATOR_SURVEILLANCE = "coordinator_surveillance_station" # Entry keys SYNO_API = "syno_api" diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 31013451682..dd2df61165d 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -15,7 +15,7 @@ from homeassistant.helpers.temperature import display_temp from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import utcnow -from . import SynoApi, SynologyDSMDeviceEntity, SynologyDSMEntity +from . import SynoApi, SynologyDSMDeviceEntity, SynologyDSMDispatcherEntity from .const import ( CONF_VOLUMES, DOMAIN, @@ -68,7 +68,7 @@ async def async_setup_entry( async_add_entities(entities) -class SynoDSMUtilSensor(SynologyDSMEntity): +class SynoDSMUtilSensor(SynologyDSMDispatcherEntity): """Representation a Synology Utilisation sensor.""" @property @@ -117,7 +117,7 @@ class SynoDSMStorageSensor(SynologyDSMDeviceEntity): return attr -class SynoDSMInfoSensor(SynologyDSMEntity): +class SynoDSMInfoSensor(SynologyDSMDispatcherEntity): """Representation a Synology information sensor.""" def __init__(self, api: SynoApi, entity_type: str, entity_info: Dict[str, str]): diff --git a/homeassistant/components/synology_dsm/switch.py b/homeassistant/components/synology_dsm/switch.py index ee29c9f2692..21511757cf3 100644 --- a/homeassistant/components/synology_dsm/switch.py +++ b/homeassistant/components/synology_dsm/switch.py @@ -1,4 +1,5 @@ """Support for Synology DSM switch.""" +import logging from typing import Dict from synology_dsm.api.surveillance_station import SynoSurveillanceStation @@ -7,9 +8,11 @@ from homeassistant.components.switch import ToggleEntity from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType -from . import SynoApi, SynologyDSMEntity +from . import SynoApi, SynologyDSMDispatcherEntity from .const import DOMAIN, SURVEILLANCE_SWITCH, SYNO_API +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities @@ -33,7 +36,7 @@ async def async_setup_entry( async_add_entities(entities, True) -class SynoDSMSurveillanceHomeModeToggle(SynologyDSMEntity, ToggleEntity): +class SynoDSMSurveillanceHomeModeToggle(SynologyDSMDispatcherEntity, ToggleEntity): """Representation a Synology Surveillance Station Home Mode toggle.""" def __init__( @@ -62,16 +65,28 @@ class SynoDSMSurveillanceHomeModeToggle(SynologyDSMEntity, ToggleEntity): async def async_update(self): """Update the toggle state.""" + _LOGGER.debug( + "SynoDSMSurveillanceHomeModeToggle.async_update(%s)", + self._api.information.serial, + ) self._state = await self.hass.async_add_executor_job( self._api.surveillance_station.get_home_mode_status ) def turn_on(self, **kwargs) -> None: """Turn on Home mode.""" + _LOGGER.debug( + "SynoDSMSurveillanceHomeModeToggle.turn_on(%s)", + self._api.information.serial, + ) self._api.surveillance_station.set_home_mode(True) def turn_off(self, **kwargs) -> None: """Turn off Home mode.""" + _LOGGER.debug( + "SynoDSMSurveillanceHomeModeToggle.turn_off(%s)", + self._api.information.serial, + ) self._api.surveillance_station.set_home_mode(False) @property From 868e530cbb04ca500a266ea60283c58a3e287b9d Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 31 Jan 2021 20:56:42 +0100 Subject: [PATCH 097/796] Prevent AttributError for uninitilized KNX ClimateMode (#45793) --- homeassistant/components/knx/knx_entity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/knx/knx_entity.py b/homeassistant/components/knx/knx_entity.py index 296bcb2f540..f4597ad230e 100644 --- a/homeassistant/components/knx/knx_entity.py +++ b/homeassistant/components/knx/knx_entity.py @@ -40,12 +40,12 @@ class KnxEntity(Entity): """Store register state change callback.""" self._device.register_device_updated_cb(self.after_update_callback) - if isinstance(self._device, XknxClimate): + if isinstance(self._device, XknxClimate) and self._device.mode is not None: self._device.mode.register_device_updated_cb(self.after_update_callback) async def async_will_remove_from_hass(self) -> None: """Disconnect device object when removed.""" self._device.unregister_device_updated_cb(self.after_update_callback) - if isinstance(self._device, XknxClimate): + if isinstance(self._device, XknxClimate) and self._device.mode is not None: self._device.mode.unregister_device_updated_cb(self.after_update_callback) From 8be357ff4fa9c27b49b7c3b93b69fae07f77eef1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Jan 2021 10:38:08 -1000 Subject: [PATCH 098/796] Ensure lutron_caseta is only discovered once (#45792) --- homeassistant/components/lutron_caseta/config_flow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lutron_caseta/config_flow.py b/homeassistant/components/lutron_caseta/config_flow.py index bb76c4b4ff7..806096d6717 100644 --- a/homeassistant/components/lutron_caseta/config_flow.py +++ b/homeassistant/components/lutron_caseta/config_flow.py @@ -79,7 +79,9 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } return await self.async_step_link() - async_step_homekit = async_step_zeroconf + async def async_step_homekit(self, discovery_info): + """Handle a flow initialized by homekit discovery.""" + return await self.async_step_zeroconf(discovery_info) async def async_step_link(self, user_input=None): """Handle pairing with the hub.""" From dac962611274c9b55ee8ac95ba2529af83dc5487 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Jan 2021 10:39:35 -1000 Subject: [PATCH 099/796] Resolve homekit cover adjustment slowness (#45730) --- .../components/homekit/accessories.py | 42 +--------- .../components/homekit/type_covers.py | 5 +- tests/components/homekit/common.py | 10 +-- tests/components/homekit/test_accessories.py | 37 +-------- tests/components/homekit/test_homekit.py | 29 ++----- tests/components/homekit/test_type_covers.py | 77 +++++++------------ tests/components/homekit/test_type_fans.py | 41 ++++------ tests/components/homekit/test_type_lights.py | 51 +++++------- 8 files changed, 72 insertions(+), 220 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 51b6508149b..571c47a521b 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -1,7 +1,4 @@ """Extend the basic Accessory and Bridge functions.""" -from datetime import timedelta -from functools import partial, wraps -from inspect import getmodule import logging from pyhap.accessory import Accessory, Bridge @@ -37,11 +34,7 @@ from homeassistant.const import ( __version__, ) from homeassistant.core import Context, callback as ha_callback, split_entity_id -from homeassistant.helpers.event import ( - async_track_state_change_event, - track_point_in_utc_time, -) -from homeassistant.util import dt as dt_util +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.util.decorator import Registry from .const import ( @@ -60,7 +53,6 @@ from .const import ( CONF_LINKED_BATTERY_CHARGING_SENSOR, CONF_LINKED_BATTERY_SENSOR, CONF_LOW_BATTERY_THRESHOLD, - DEBOUNCE_TIMEOUT, DEFAULT_LOW_BATTERY_THRESHOLD, DEVICE_CLASS_CO, DEVICE_CLASS_CO2, @@ -98,37 +90,6 @@ SWITCH_TYPES = { TYPES = Registry() -def debounce(func): - """Decorate function to debounce callbacks from HomeKit.""" - - @ha_callback - def call_later_listener(self, *args): - """Handle call_later callback.""" - debounce_params = self.debounce.pop(func.__name__, None) - if debounce_params: - self.hass.async_add_executor_job(func, self, *debounce_params[1:]) - - @wraps(func) - def wrapper(self, *args): - """Start async timer.""" - debounce_params = self.debounce.pop(func.__name__, None) - if debounce_params: - debounce_params[0]() # remove listener - remove_listener = track_point_in_utc_time( - self.hass, - partial(call_later_listener, self), - dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT), - ) - self.debounce[func.__name__] = (remove_listener, *args) - logger.debug( - "%s: Start %s timeout", self.entity_id, func.__name__.replace("set_", "") - ) - - name = getmodule(func).__name__ - logger = logging.getLogger(name) - return wrapper - - def get_accessory(hass, driver, state, aid, config): """Take state and return an accessory object if supported.""" if not aid: @@ -278,7 +239,6 @@ class HomeAccessory(Accessory): self.category = category self.entity_id = entity_id self.hass = hass - self.debounce = {} self._subscriptions = [] self._char_battery = None self._char_charging = None diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 75cabf14483..daa782b8d67 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -33,7 +33,7 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers.event import async_track_state_change_event -from .accessories import TYPES, HomeAccessory, debounce +from .accessories import TYPES, HomeAccessory from .const import ( ATTR_OBSTRUCTION_DETECTED, CHAR_CURRENT_DOOR_STATE, @@ -233,7 +233,6 @@ class OpeningDeviceBase(HomeAccessory): return self.call_service(DOMAIN, SERVICE_STOP_COVER, {ATTR_ENTITY_ID: self.entity_id}) - @debounce def set_tilt(self, value): """Set tilt to value if call came from HomeKit.""" _LOGGER.info("%s: Set tilt to %d", self.entity_id, value) @@ -284,7 +283,6 @@ class OpeningDevice(OpeningDeviceBase, HomeAccessory): ) self.async_update_state(state) - @debounce def move_cover(self, value): """Move cover to value if call came from HomeKit.""" _LOGGER.debug("%s: Set position to %d", self.entity_id, value) @@ -360,7 +358,6 @@ class WindowCoveringBasic(OpeningDeviceBase, HomeAccessory): ) self.async_update_state(state) - @debounce def move_cover(self, value): """Move cover to value if call came from HomeKit.""" _LOGGER.debug("%s: Set position to %d", self.entity_id, value) diff --git a/tests/components/homekit/common.py b/tests/components/homekit/common.py index 20aa0e04c2b..6b1d87e3f54 100644 --- a/tests/components/homekit/common.py +++ b/tests/components/homekit/common.py @@ -1,17 +1,9 @@ """Collection of fixtures and functions for the HomeKit tests.""" -from unittest.mock import Mock, patch +from unittest.mock import Mock EMPTY_8_6_JPEG = b"empty_8_6" -def patch_debounce(): - """Return patch for debounce method.""" - return patch( - "homeassistant.components.homekit.accessories.debounce", - lambda f: lambda *args, **kwargs: f(*args, **kwargs), - ) - - def mock_turbo_jpeg( first_width=None, second_width=None, first_height=None, second_height=None ): diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 886123062c4..8ba2d9fb0b2 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -2,7 +2,6 @@ This includes tests for all mock object types. """ -from datetime import timedelta from unittest.mock import Mock, patch import pytest @@ -11,7 +10,6 @@ from homeassistant.components.homekit.accessories import ( HomeAccessory, HomeBridge, HomeDriver, - debounce, ) from homeassistant.components.homekit.const import ( ATTR_DISPLAY_NAME, @@ -45,41 +43,8 @@ from homeassistant.const import ( __version__, ) from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS -import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed, async_mock_service - - -async def test_debounce(hass): - """Test add_timeout decorator function.""" - - def demo_func(*args): - nonlocal arguments, counter - counter += 1 - arguments = args - - arguments = None - counter = 0 - mock = Mock(hass=hass, debounce={}) - - debounce_demo = debounce(demo_func) - assert debounce_demo.__name__ == "demo_func" - now = dt_util.utcnow() - - with patch("homeassistant.util.dt.utcnow", return_value=now): - await hass.async_add_executor_job(debounce_demo, mock, "value") - async_fire_time_changed(hass, now + timedelta(seconds=3)) - await hass.async_block_till_done() - assert counter == 1 - assert len(arguments) == 2 - - with patch("homeassistant.util.dt.utcnow", return_value=now): - await hass.async_add_executor_job(debounce_demo, mock, "value") - await hass.async_add_executor_job(debounce_demo, mock, "value") - - async_fire_time_changed(hass, now + timedelta(seconds=3)) - await hass.async_block_till_done() - assert counter == 2 +from tests.common import async_mock_service async def test_accessory_cancels_track_state_change_on_stop(hass, hk_driver): diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index c6f897c32a2..7b5153b825d 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -67,7 +67,6 @@ from homeassistant.util import json as json_util from .util import PATH_HOMEKIT, async_init_entry, async_init_integration from tests.common import MockConfigEntry, mock_device_registry, mock_registry -from tests.components.homekit.common import patch_debounce IP_ADDRESS = "127.0.0.1" @@ -89,14 +88,6 @@ def entity_reg_fixture(hass): return mock_registry(hass) -@pytest.fixture(name="debounce_patcher", scope="module") -def debounce_patcher_fixture(): - """Patch debounce method.""" - patcher = patch_debounce() - yield patcher.start() - patcher.stop() - - async def test_setup_min(hass, mock_zeroconf): """Test async_setup with min config options.""" entry = MockConfigEntry( @@ -485,7 +476,7 @@ async def test_homekit_entity_glob_filter(hass, mock_zeroconf): mock_get_acc.reset_mock() -async def test_homekit_start(hass, hk_driver, device_reg, debounce_patcher): +async def test_homekit_start(hass, hk_driver, device_reg): """Test HomeKit start method.""" entry = await async_init_integration(hass) @@ -573,9 +564,7 @@ async def test_homekit_start(hass, hk_driver, device_reg, debounce_patcher): assert len(device_reg.devices) == 1 -async def test_homekit_start_with_a_broken_accessory( - hass, hk_driver, debounce_patcher, mock_zeroconf -): +async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, mock_zeroconf): """Test HomeKit start method.""" pin = b"123-45-678" entry = MockConfigEntry( @@ -754,7 +743,7 @@ async def test_homekit_too_many_accessories(hass, hk_driver, caplog, mock_zeroco async def test_homekit_finds_linked_batteries( - hass, hk_driver, debounce_patcher, device_reg, entity_reg, mock_zeroconf + hass, hk_driver, device_reg, entity_reg, mock_zeroconf ): """Test HomeKit start method.""" entry = await async_init_integration(hass) @@ -840,7 +829,7 @@ async def test_homekit_finds_linked_batteries( async def test_homekit_async_get_integration_fails( - hass, hk_driver, debounce_patcher, device_reg, entity_reg, mock_zeroconf + hass, hk_driver, device_reg, entity_reg, mock_zeroconf ): """Test that we continue if async_get_integration fails.""" entry = await async_init_integration(hass) @@ -1072,7 +1061,7 @@ def _write_data(path: str, data: Dict) -> None: async def test_homekit_ignored_missing_devices( - hass, hk_driver, debounce_patcher, device_reg, entity_reg, mock_zeroconf + hass, hk_driver, device_reg, entity_reg, mock_zeroconf ): """Test HomeKit handles a device in the entity registry but missing from the device registry.""" entry = await async_init_integration(hass) @@ -1153,7 +1142,7 @@ async def test_homekit_ignored_missing_devices( async def test_homekit_finds_linked_motion_sensors( - hass, hk_driver, debounce_patcher, device_reg, entity_reg, mock_zeroconf + hass, hk_driver, device_reg, entity_reg, mock_zeroconf ): """Test HomeKit start method.""" entry = await async_init_integration(hass) @@ -1228,7 +1217,7 @@ async def test_homekit_finds_linked_motion_sensors( async def test_homekit_finds_linked_humidity_sensors( - hass, hk_driver, debounce_patcher, device_reg, entity_reg, mock_zeroconf + hass, hk_driver, device_reg, entity_reg, mock_zeroconf ): """Test HomeKit start method.""" entry = await async_init_integration(hass) @@ -1376,9 +1365,7 @@ def _get_fixtures_base_path(): return os.path.dirname(os.path.dirname(os.path.dirname(__file__))) -async def test_homekit_start_in_accessory_mode( - hass, hk_driver, device_reg, debounce_patcher -): +async def test_homekit_start_in_accessory_mode(hass, hk_driver, device_reg): """Test HomeKit start method in accessory mode.""" entry = await async_init_integration(hass) diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 48b20e8e0b8..d39e9cda7d0 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -1,7 +1,4 @@ """Test different accessory types: Covers.""" -from collections import namedtuple - -import pytest from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, @@ -22,6 +19,12 @@ from homeassistant.components.homekit.const import ( HK_DOOR_OPEN, HK_DOOR_OPENING, ) +from homeassistant.components.homekit.type_covers import ( + GarageDoorOpener, + Window, + WindowCovering, + WindowCoveringBasic, +) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -40,37 +43,15 @@ from homeassistant.core import CoreState from homeassistant.helpers import entity_registry 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_covers.""" - patcher = patch_debounce() - patcher.start() - _import = __import__( - "homeassistant.components.homekit.type_covers", - fromlist=["GarageDoorOpener", "WindowCovering", "WindowCoveringBasic"], - ) - patcher_tuple = namedtuple( - "Cls", ["window", "windowcovering", "windowcovering_basic", "garage"] - ) - yield patcher_tuple( - window=_import.Window, - windowcovering=_import.WindowCovering, - windowcovering_basic=_import.WindowCoveringBasic, - garage=_import.GarageDoorOpener, - ) - patcher.stop() - - -async def test_garage_door_open_close(hass, hk_driver, cls, events): +async def test_garage_door_open_close(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" entity_id = "cover.garage_door" hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = cls.garage(hass, hk_driver, "Garage Door", entity_id, 2, None) + acc = GarageDoorOpener(hass, hk_driver, "Garage Door", entity_id, 2, None) await acc.run_handler() await hass.async_block_till_done() @@ -148,13 +129,13 @@ async def test_garage_door_open_close(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] is None -async def test_windowcovering_set_cover_position(hass, hk_driver, cls, events): +async def test_windowcovering_set_cover_position(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" entity_id = "cover.window" hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = cls.windowcovering(hass, hk_driver, "Cover", entity_id, 2, None) + acc = WindowCovering(hass, hk_driver, "Cover", entity_id, 2, None) await acc.run_handler() await hass.async_block_till_done() @@ -218,13 +199,13 @@ async def test_windowcovering_set_cover_position(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] == 75 -async def test_window_instantiate(hass, hk_driver, cls, events): +async def test_window_instantiate(hass, hk_driver, events): """Test if Window accessory is instantiated correctly.""" entity_id = "cover.window" hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = cls.window(hass, hk_driver, "Window", entity_id, 2, None) + acc = Window(hass, hk_driver, "Window", entity_id, 2, None) await acc.run_handler() await hass.async_block_till_done() @@ -235,7 +216,7 @@ async def test_window_instantiate(hass, hk_driver, cls, events): assert acc.char_target_position.value == 0 -async def test_windowcovering_cover_set_tilt(hass, hk_driver, cls, events): +async def test_windowcovering_cover_set_tilt(hass, hk_driver, events): """Test if accessory and HA update slat tilt accordingly.""" entity_id = "cover.window" @@ -243,7 +224,7 @@ async def test_windowcovering_cover_set_tilt(hass, hk_driver, cls, events): entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_TILT_POSITION} ) await hass.async_block_till_done() - acc = cls.windowcovering(hass, hk_driver, "Cover", entity_id, 2, None) + acc = WindowCovering(hass, hk_driver, "Cover", entity_id, 2, None) await acc.run_handler() await hass.async_block_till_done() @@ -302,12 +283,12 @@ async def test_windowcovering_cover_set_tilt(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] == 75 -async def test_windowcovering_open_close(hass, hk_driver, cls, events): +async def test_windowcovering_open_close(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" entity_id = "cover.window" hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: 0}) - acc = cls.windowcovering_basic(hass, hk_driver, "Cover", entity_id, 2, None) + acc = WindowCoveringBasic(hass, hk_driver, "Cover", entity_id, 2, None) await acc.run_handler() await hass.async_block_till_done() @@ -383,14 +364,14 @@ async def test_windowcovering_open_close(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] is None -async def test_windowcovering_open_close_stop(hass, hk_driver, cls, events): +async def test_windowcovering_open_close_stop(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" entity_id = "cover.window" hass.states.async_set( entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP} ) - acc = cls.windowcovering_basic(hass, hk_driver, "Cover", entity_id, 2, None) + acc = WindowCoveringBasic(hass, hk_driver, "Cover", entity_id, 2, None) await acc.run_handler() await hass.async_block_till_done() @@ -431,7 +412,7 @@ async def test_windowcovering_open_close_stop(hass, hk_driver, cls, events): async def test_windowcovering_open_close_with_position_and_stop( - hass, hk_driver, cls, events + hass, hk_driver, events ): """Test if accessory and HA are updated accordingly.""" entity_id = "cover.stop_window" @@ -441,7 +422,7 @@ async def test_windowcovering_open_close_with_position_and_stop( STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP | SUPPORT_SET_POSITION}, ) - acc = cls.windowcovering(hass, hk_driver, "Cover", entity_id, 2, None) + acc = WindowCovering(hass, hk_driver, "Cover", entity_id, 2, None) await acc.run_handler() await hass.async_block_till_done() @@ -461,7 +442,7 @@ async def test_windowcovering_open_close_with_position_and_stop( assert events[-1].data[ATTR_VALUE] is None -async def test_windowcovering_basic_restore(hass, hk_driver, cls, events): +async def test_windowcovering_basic_restore(hass, hk_driver, events): """Test setting up an entity from state in the event registry.""" hass.state = CoreState.not_running @@ -486,22 +467,20 @@ async def test_windowcovering_basic_restore(hass, hk_driver, cls, events): hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) await hass.async_block_till_done() - acc = cls.windowcovering_basic(hass, hk_driver, "Cover", "cover.simple", 2, None) + acc = WindowCoveringBasic(hass, hk_driver, "Cover", "cover.simple", 2, None) assert acc.category == 14 assert acc.char_current_position is not None assert acc.char_target_position is not None assert acc.char_position_state is not None - acc = cls.windowcovering_basic( - hass, hk_driver, "Cover", "cover.all_info_set", 2, None - ) + acc = WindowCoveringBasic(hass, hk_driver, "Cover", "cover.all_info_set", 2, None) assert acc.category == 14 assert acc.char_current_position is not None assert acc.char_target_position is not None assert acc.char_position_state is not None -async def test_windowcovering_restore(hass, hk_driver, cls, events): +async def test_windowcovering_restore(hass, hk_driver, events): """Test setting up an entity from state in the event registry.""" hass.state = CoreState.not_running @@ -526,20 +505,20 @@ async def test_windowcovering_restore(hass, hk_driver, cls, events): hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) await hass.async_block_till_done() - acc = cls.windowcovering(hass, hk_driver, "Cover", "cover.simple", 2, None) + acc = WindowCovering(hass, hk_driver, "Cover", "cover.simple", 2, None) assert acc.category == 14 assert acc.char_current_position is not None assert acc.char_target_position is not None assert acc.char_position_state is not None - acc = cls.windowcovering(hass, hk_driver, "Cover", "cover.all_info_set", 2, None) + acc = WindowCovering(hass, hk_driver, "Cover", "cover.all_info_set", 2, None) assert acc.category == 14 assert acc.char_current_position is not None assert acc.char_target_position is not None assert acc.char_position_state is not None -async def test_garage_door_with_linked_obstruction_sensor(hass, hk_driver, cls, events): +async def test_garage_door_with_linked_obstruction_sensor(hass, hk_driver, events): """Test if accessory and HA are updated accordingly with a linked obstruction sensor.""" linked_obstruction_sensor_entity_id = "binary_sensor.obstruction" entity_id = "cover.garage_door" @@ -547,7 +526,7 @@ async def test_garage_door_with_linked_obstruction_sensor(hass, hk_driver, cls, hass.states.async_set(linked_obstruction_sensor_entity_id, STATE_OFF) hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = cls.garage( + acc = GarageDoorOpener( hass, hk_driver, "Garage Door", diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index fc5ac4344ad..8111d256594 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -1,8 +1,6 @@ """Test different accessory types: Fans.""" -from collections import namedtuple from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE -import pytest from homeassistant.components.fan import ( ATTR_DIRECTION, @@ -16,6 +14,7 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, ) from homeassistant.components.homekit.const import ATTR_VALUE +from homeassistant.components.homekit.type_fans import Fan from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -28,27 +27,15 @@ from homeassistant.core import CoreState from homeassistant.helpers import entity_registry 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_fans.""" - patcher = patch_debounce() - patcher.start() - _import = __import__("homeassistant.components.homekit.type_fans", fromlist=["Fan"]) - patcher_tuple = namedtuple("Cls", ["fan"]) - yield patcher_tuple(fan=_import.Fan) - patcher.stop() - - -async def test_fan_basic(hass, hk_driver, cls, events): +async def test_fan_basic(hass, hk_driver, events): """Test fan with char state.""" entity_id = "fan.demo" hass.states.async_set(entity_id, STATE_ON, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() - acc = cls.fan(hass, hk_driver, "Fan", entity_id, 1, None) + acc = Fan(hass, hk_driver, "Fan", entity_id, 1, None) hk_driver.add_accessory(acc) assert acc.aid == 1 @@ -120,7 +107,7 @@ async def test_fan_basic(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] is None -async def test_fan_direction(hass, hk_driver, cls, events): +async def test_fan_direction(hass, hk_driver, events): """Test fan with direction.""" entity_id = "fan.demo" @@ -130,7 +117,7 @@ async def test_fan_direction(hass, hk_driver, cls, events): {ATTR_SUPPORTED_FEATURES: SUPPORT_DIRECTION, ATTR_DIRECTION: DIRECTION_FORWARD}, ) await hass.async_block_till_done() - acc = cls.fan(hass, hk_driver, "Fan", entity_id, 1, None) + acc = Fan(hass, hk_driver, "Fan", entity_id, 1, None) hk_driver.add_accessory(acc) assert acc.char_direction.value == 0 @@ -188,7 +175,7 @@ async def test_fan_direction(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] == DIRECTION_REVERSE -async def test_fan_oscillate(hass, hk_driver, cls, events): +async def test_fan_oscillate(hass, hk_driver, events): """Test fan with oscillate.""" entity_id = "fan.demo" @@ -198,7 +185,7 @@ async def test_fan_oscillate(hass, hk_driver, cls, events): {ATTR_SUPPORTED_FEATURES: SUPPORT_OSCILLATE, ATTR_OSCILLATING: False}, ) await hass.async_block_till_done() - acc = cls.fan(hass, hk_driver, "Fan", entity_id, 1, None) + acc = Fan(hass, hk_driver, "Fan", entity_id, 1, None) hk_driver.add_accessory(acc) assert acc.char_swing.value == 0 @@ -257,7 +244,7 @@ async def test_fan_oscillate(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] is True -async def test_fan_speed(hass, hk_driver, cls, events): +async def test_fan_speed(hass, hk_driver, events): """Test fan with speed.""" entity_id = "fan.demo" @@ -270,7 +257,7 @@ async def test_fan_speed(hass, hk_driver, cls, events): }, ) await hass.async_block_till_done() - acc = cls.fan(hass, hk_driver, "Fan", entity_id, 1, None) + acc = Fan(hass, hk_driver, "Fan", entity_id, 1, None) hk_driver.add_accessory(acc) # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the @@ -336,7 +323,7 @@ async def test_fan_speed(hass, hk_driver, cls, events): assert acc.char_active.value == 1 -async def test_fan_set_all_one_shot(hass, hk_driver, cls, events): +async def test_fan_set_all_one_shot(hass, hk_driver, events): """Test fan with speed.""" entity_id = "fan.demo" @@ -353,7 +340,7 @@ async def test_fan_set_all_one_shot(hass, hk_driver, cls, events): }, ) await hass.async_block_till_done() - acc = cls.fan(hass, hk_driver, "Fan", entity_id, 1, None) + acc = Fan(hass, hk_driver, "Fan", entity_id, 1, None) hk_driver.add_accessory(acc) # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the @@ -529,7 +516,7 @@ async def test_fan_set_all_one_shot(hass, hk_driver, cls, events): assert len(call_set_direction) == 2 -async def test_fan_restore(hass, hk_driver, cls, events): +async def test_fan_restore(hass, hk_driver, events): """Test setting up an entity from state in the event registry.""" hass.state = CoreState.not_running @@ -554,14 +541,14 @@ async def test_fan_restore(hass, hk_driver, cls, events): hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) await hass.async_block_till_done() - acc = cls.fan(hass, hk_driver, "Fan", "fan.simple", 2, None) + acc = Fan(hass, hk_driver, "Fan", "fan.simple", 2, None) assert acc.category == 3 assert acc.char_active is not None assert acc.char_direction is None assert acc.char_speed is None assert acc.char_swing is None - acc = cls.fan(hass, hk_driver, "Fan", "fan.all_info_set", 2, None) + acc = Fan(hass, hk_driver, "Fan", "fan.all_info_set", 2, None) assert acc.category == 3 assert acc.char_active is not None assert acc.char_direction is not None diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index e82bc5bb15d..42ef18f3505 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -1,10 +1,9 @@ """Test different accessory types: Lights.""" -from collections import namedtuple from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE -import pytest from homeassistant.components.homekit.const import ATTR_VALUE +from homeassistant.components.homekit.type_lights import Light from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, @@ -28,29 +27,15 @@ from homeassistant.core import CoreState from homeassistant.helpers import entity_registry 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_lights.""" - patcher = patch_debounce() - patcher.start() - _import = __import__( - "homeassistant.components.homekit.type_lights", fromlist=["Light"] - ) - patcher_tuple = namedtuple("Cls", ["light"]) - yield patcher_tuple(light=_import.Light) - patcher.stop() - - -async def test_light_basic(hass, hk_driver, cls, events): +async def test_light_basic(hass, hk_driver, events): """Test light with char state.""" entity_id = "light.demo" hass.states.async_set(entity_id, STATE_ON, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() - acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None) + acc = Light(hass, hk_driver, "Light", entity_id, 1, None) hk_driver.add_accessory(acc) assert acc.aid == 1 @@ -113,7 +98,7 @@ async def test_light_basic(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] == "Set state to 0" -async def test_light_brightness(hass, hk_driver, cls, events): +async def test_light_brightness(hass, hk_driver, events): """Test light with brightness.""" entity_id = "light.demo" @@ -123,7 +108,7 @@ async def test_light_brightness(hass, hk_driver, cls, events): {ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}, ) await hass.async_block_till_done() - acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None) + acc = Light(hass, hk_driver, "Light", entity_id, 1, None) hk_driver.add_accessory(acc) # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the @@ -231,7 +216,7 @@ async def test_light_brightness(hass, hk_driver, cls, events): assert acc.char_brightness.value == 1 -async def test_light_color_temperature(hass, hk_driver, cls, events): +async def test_light_color_temperature(hass, hk_driver, events): """Test light with color temperature.""" entity_id = "light.demo" @@ -241,7 +226,7 @@ async def test_light_color_temperature(hass, hk_driver, cls, events): {ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP, ATTR_COLOR_TEMP: 190}, ) await hass.async_block_till_done() - acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None) + acc = Light(hass, hk_driver, "Light", entity_id, 1, None) hk_driver.add_accessory(acc) assert acc.char_color_temperature.value == 190 @@ -278,7 +263,7 @@ async def test_light_color_temperature(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] == "color temperature at 250" -async def test_light_color_temperature_and_rgb_color(hass, hk_driver, cls, events): +async def test_light_color_temperature_and_rgb_color(hass, hk_driver, events): """Test light with color temperature and rgb color not exposing temperature.""" entity_id = "light.demo" @@ -292,7 +277,7 @@ async def test_light_color_temperature_and_rgb_color(hass, hk_driver, cls, event }, ) await hass.async_block_till_done() - acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None) + acc = Light(hass, hk_driver, "Light", entity_id, 2, None) assert acc.char_hue.value == 260 assert acc.char_saturation.value == 90 @@ -313,7 +298,7 @@ async def test_light_color_temperature_and_rgb_color(hass, hk_driver, cls, event assert acc.char_saturation.value == 61 -async def test_light_rgb_color(hass, hk_driver, cls, events): +async def test_light_rgb_color(hass, hk_driver, events): """Test light with rgb_color.""" entity_id = "light.demo" @@ -323,7 +308,7 @@ async def test_light_rgb_color(hass, hk_driver, cls, events): {ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, ATTR_HS_COLOR: (260, 90)}, ) await hass.async_block_till_done() - acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None) + acc = Light(hass, hk_driver, "Light", entity_id, 1, None) hk_driver.add_accessory(acc) assert acc.char_hue.value == 260 @@ -365,7 +350,7 @@ async def test_light_rgb_color(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)" -async def test_light_restore(hass, hk_driver, cls, events): +async def test_light_restore(hass, hk_driver, events): """Test setting up an entity from state in the event registry.""" hass.state = CoreState.not_running @@ -385,20 +370,20 @@ async def test_light_restore(hass, hk_driver, cls, events): hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) await hass.async_block_till_done() - acc = cls.light(hass, hk_driver, "Light", "light.simple", 1, None) + acc = Light(hass, hk_driver, "Light", "light.simple", 1, None) hk_driver.add_accessory(acc) assert acc.category == 5 # Lightbulb assert acc.chars == [] assert acc.char_on.value == 0 - acc = cls.light(hass, hk_driver, "Light", "light.all_info_set", 2, None) + acc = Light(hass, hk_driver, "Light", "light.all_info_set", 2, None) assert acc.category == 5 # Lightbulb assert acc.chars == ["Brightness"] assert acc.char_on.value == 0 -async def test_light_set_brightness_and_color(hass, hk_driver, cls, events): +async def test_light_set_brightness_and_color(hass, hk_driver, events): """Test light with all chars in one go.""" entity_id = "light.demo" @@ -411,7 +396,7 @@ async def test_light_set_brightness_and_color(hass, hk_driver, cls, events): }, ) await hass.async_block_till_done() - acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None) + acc = Light(hass, hk_driver, "Light", entity_id, 1, None) hk_driver.add_accessory(acc) # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the @@ -474,7 +459,7 @@ async def test_light_set_brightness_and_color(hass, hk_driver, cls, events): ) -async def test_light_set_brightness_and_color_temp(hass, hk_driver, cls, events): +async def test_light_set_brightness_and_color_temp(hass, hk_driver, events): """Test light with all chars in one go.""" entity_id = "light.demo" @@ -487,7 +472,7 @@ async def test_light_set_brightness_and_color_temp(hass, hk_driver, cls, events) }, ) await hass.async_block_till_done() - acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None) + acc = Light(hass, hk_driver, "Light", entity_id, 1, None) hk_driver.add_accessory(acc) # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the From 852af7e3721228d0956a6818e62c044cfc319218 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Jan 2021 10:40:24 -1000 Subject: [PATCH 100/796] Update homekit for new async library changes (#45731) --- .../components/homekit/accessories.py | 8 +++---- .../components/homekit/manifest.json | 2 +- .../components/homekit/type_cameras.py | 8 ++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit/test_type_cameras.py | 23 ++++++++++--------- 6 files changed, 21 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 571c47a521b..6a33b63e89a 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -487,17 +487,17 @@ class HomeBridge(Bridge): def setup_message(self): """Prevent print of pyhap setup message to terminal.""" - def get_snapshot(self, info): + async def async_get_snapshot(self, info): """Get snapshot from accessory if supported.""" acc = self.accessories.get(info["aid"]) if acc is None: raise ValueError("Requested snapshot for missing accessory") - if not hasattr(acc, "get_snapshot"): + if not hasattr(acc, "async_get_snapshot"): raise ValueError( "Got a request for snapshot, but the Accessory " - 'does not define a "get_snapshot" method' + 'does not define a "async_get_snapshot" method' ) - return acc.get_snapshot(info) + return await acc.async_get_snapshot(info) class HomeDriver(AccessoryDriver): diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index d188dd270ab..23b43958848 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.1.0", + "HAP-python==3.2.0", "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1", diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index b61a2c57612..22bf37aa0c3 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -1,5 +1,4 @@ """Class to hold all camera accessories.""" -import asyncio from datetime import timedelta import logging @@ -444,13 +443,10 @@ class Camera(HomeAccessory, PyhapCamera): """Reconfigure the stream so that it uses the given ``stream_config``.""" return True - def get_snapshot(self, image_size): + async def async_get_snapshot(self, image_size): """Return a jpeg of a snapshot from the camera.""" return scale_jpeg_camera_image( - asyncio.run_coroutine_threadsafe( - self.hass.components.camera.async_get_image(self.entity_id), - self.hass.loop, - ).result(), + await self.hass.components.camera.async_get_image(self.entity_id), image_size["image-width"], image_size["image-height"], ) diff --git a/requirements_all.txt b/requirements_all.txt index a8127ed7097..8281744f221 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.1.0 +HAP-python==3.2.0 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3e8972c9864..d997b91f338 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.1.0 +HAP-python==3.2.0 # homeassistant.components.flick_electric PyFlick==0.0.2 diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index 804e03a4e6c..f4c7169310f 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -212,22 +212,23 @@ async def test_camera_stream_source_configured(hass, run_driver, events): ) with patch("turbojpeg.TurboJPEG", return_value=turbo_jpeg): TurboJPEGSingleton() - assert await hass.async_add_executor_job( - acc.get_snapshot, {"aid": 2, "image-width": 300, "image-height": 200} + assert await acc.async_get_snapshot( + {"aid": 2, "image-width": 300, "image-height": 200} ) - # Verify the bridge only forwards get_snapshot for + # Verify the bridge only forwards async_get_snapshot for # cameras and valid accessory ids - assert await hass.async_add_executor_job( - bridge.get_snapshot, {"aid": 2, "image-width": 300, "image-height": 200} + assert await bridge.async_get_snapshot( + {"aid": 2, "image-width": 300, "image-height": 200} ) with pytest.raises(ValueError): - assert await hass.async_add_executor_job( - bridge.get_snapshot, {"aid": 3, "image-width": 300, "image-height": 200} + assert await bridge.async_get_snapshot( + {"aid": 3, "image-width": 300, "image-height": 200} ) + with pytest.raises(ValueError): - assert await hass.async_add_executor_job( - bridge.get_snapshot, {"aid": 4, "image-width": 300, "image-height": 200} + assert await bridge.async_get_snapshot( + {"aid": 4, "image-width": 300, "image-height": 200} ) @@ -400,8 +401,8 @@ async def test_camera_with_no_stream(hass, run_driver, events): await _async_stop_all_streams(hass, acc) with pytest.raises(HomeAssistantError): - await hass.async_add_executor_job( - acc.get_snapshot, {"aid": 2, "image-width": 300, "image-height": 200} + assert await acc.async_get_snapshot( + {"aid": 2, "image-width": 300, "image-height": 200} ) From 73d7d80731a5f18915e4e871111b752d1137ff66 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Jan 2021 10:43:00 -1000 Subject: [PATCH 101/796] Add timeout to lutron_caseta to prevent it blocking startup (#45769) --- .../components/lutron_caseta/__init__.py | 26 ++++++-- .../components/lutron_caseta/config_flow.py | 47 ++++++++++----- .../components/lutron_caseta/const.py | 2 + .../lutron_caseta/test_config_flow.py | 60 +++++++++++++------ 4 files changed, 97 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 7526d4874f6..f349dde8921 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -1,10 +1,12 @@ """Component for interacting with a Lutron Caseta system.""" import asyncio import logging +import ssl from aiolip import LIP from aiolip.data import LIPMode from aiolip.protocol import LIP_BUTTON_PRESS +import async_timeout from pylutron_caseta.smartbridge import Smartbridge import voluptuous as vol @@ -29,6 +31,7 @@ from .const import ( BRIDGE_DEVICE_ID, BRIDGE_LEAP, BRIDGE_LIP, + BRIDGE_TIMEOUT, BUTTON_DEVICES, CONF_CA_CERTS, CONF_CERTFILE, @@ -94,15 +97,26 @@ async def async_setup_entry(hass, config_entry): keyfile = hass.config.path(config_entry.data[CONF_KEYFILE]) certfile = hass.config.path(config_entry.data[CONF_CERTFILE]) ca_certs = hass.config.path(config_entry.data[CONF_CA_CERTS]) + bridge = None - bridge = Smartbridge.create_tls( - hostname=host, keyfile=keyfile, certfile=certfile, ca_certs=ca_certs - ) + try: + bridge = Smartbridge.create_tls( + hostname=host, keyfile=keyfile, certfile=certfile, ca_certs=ca_certs + ) + except ssl.SSLError: + _LOGGER.error("Invalid certificate used to connect to bridge at %s.", host) + return False - await bridge.connect() - if not bridge.is_connected(): + timed_out = True + try: + with async_timeout.timeout(BRIDGE_TIMEOUT): + await bridge.connect() + timed_out = False + except asyncio.TimeoutError: + _LOGGER.error("Timeout while trying to connect to bridge at %s.", host) + + if timed_out or not bridge.is_connected(): await bridge.close() - _LOGGER.error("Unable to connect to Lutron Caseta bridge at %s", host) raise ConfigEntryNotReady _LOGGER.debug("Connected to Lutron Caseta bridge via LEAP at %s", host) diff --git a/homeassistant/components/lutron_caseta/config_flow.py b/homeassistant/components/lutron_caseta/config_flow.py index 806096d6717..30cbc03ac47 100644 --- a/homeassistant/components/lutron_caseta/config_flow.py +++ b/homeassistant/components/lutron_caseta/config_flow.py @@ -2,7 +2,9 @@ import asyncio import logging import os +import ssl +import async_timeout from pylutron_caseta.pairing import PAIR_CA, PAIR_CERT, PAIR_KEY, async_pair from pylutron_caseta.smartbridge import Smartbridge import voluptuous as vol @@ -15,6 +17,7 @@ from homeassistant.core import callback from .const import ( ABORT_REASON_ALREADY_CONFIGURED, ABORT_REASON_CANNOT_CONNECT, + BRIDGE_TIMEOUT, CONF_CA_CERTS, CONF_CERTFILE, CONF_KEYFILE, @@ -50,6 +53,8 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Initialize a Lutron Caseta flow.""" self.data = {} self.lutron_id = None + self.tls_assets_validated = False + self.attempted_tls_validation = False async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" @@ -92,11 +97,16 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._configure_tls_assets() + if ( + not self.attempted_tls_validation + and await self.hass.async_add_executor_job(self._tls_assets_exist) + and await self.async_validate_connectable_bridge_config() + ): + self.tls_assets_validated = True + self.attempted_tls_validation = True + if user_input is not None: - if ( - await self.hass.async_add_executor_job(self._tls_assets_exist) - and await self.async_validate_connectable_bridge_config() - ): + if self.tls_assets_validated: # If we previous paired and the tls assets already exist, # we do not need to go though pairing again. return self.async_create_entry(title=self.bridge_id, data=self.data) @@ -207,6 +217,8 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_validate_connectable_bridge_config(self): """Check if we can connect to the bridge with the current config.""" + bridge = None + try: bridge = Smartbridge.create_tls( hostname=self.data[CONF_HOST], @@ -214,16 +226,23 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): certfile=self.hass.config.path(self.data[CONF_CERTFILE]), ca_certs=self.hass.config.path(self.data[CONF_CA_CERTS]), ) - - await bridge.connect() - if not bridge.is_connected(): - return False - - await bridge.close() - return True - except Exception: # pylint: disable=broad-except - _LOGGER.exception( - "Unknown exception while checking connectivity to bridge %s", + except ssl.SSLError: + _LOGGER.error( + "Invalid certificate used to connect to bridge at %s.", self.data[CONF_HOST], ) return False + + connected_ok = False + try: + with async_timeout.timeout(BRIDGE_TIMEOUT): + await bridge.connect() + connected_ok = bridge.is_connected() + except asyncio.TimeoutError: + _LOGGER.error( + "Timeout while trying to connect to bridge at %s.", + self.data[CONF_HOST], + ) + + await bridge.close() + return connected_ok diff --git a/homeassistant/components/lutron_caseta/const.py b/homeassistant/components/lutron_caseta/const.py index 4226be36f05..fcc647f00ba 100644 --- a/homeassistant/components/lutron_caseta/const.py +++ b/homeassistant/components/lutron_caseta/const.py @@ -33,3 +33,5 @@ ACTION_RELEASE = "release" CONF_TYPE = "type" CONF_SUBTYPE = "subtype" + +BRIDGE_TIMEOUT = 35 diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index 8a33fab670b..58377c8e085 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Lutron Caseta config flow.""" import asyncio +import ssl from unittest.mock import AsyncMock, patch from pylutron_caseta.pairing import PAIR_CA, PAIR_CERT, PAIR_KEY @@ -21,6 +22,14 @@ from homeassistant.const import CONF_HOST from tests.common import MockConfigEntry +EMPTY_MOCK_CONFIG_ENTRY = { + CONF_HOST: "", + CONF_KEYFILE: "", + CONF_CERTFILE: "", + CONF_CA_CERTS: "", +} + + MOCK_ASYNC_PAIR_SUCCESS = { PAIR_KEY: "mock_key", PAIR_CERT: "mock_cert", @@ -115,21 +124,34 @@ async def test_bridge_cannot_connect(hass): async def test_bridge_cannot_connect_unknown_error(hass): """Test checking for connection and encountering an unknown error.""" - entry_mock_data = { - CONF_HOST: "", - CONF_KEYFILE: "", - CONF_CERTFILE: "", - CONF_CA_CERTS: "", - } - with patch.object(Smartbridge, "create_tls") as create_tls: mock_bridge = MockBridge() - mock_bridge.connect = AsyncMock(side_effect=Exception()) + mock_bridge.connect = AsyncMock(side_effect=asyncio.TimeoutError) create_tls.return_value = mock_bridge result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data=entry_mock_data, + data=EMPTY_MOCK_CONFIG_ENTRY, + ) + + assert result["type"] == "form" + assert result["step_id"] == STEP_IMPORT_FAILED + assert result["errors"] == {"base": ERROR_CANNOT_CONNECT} + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == CasetaConfigFlow.ABORT_REASON_CANNOT_CONNECT + + +async def test_bridge_invalid_ssl_error(hass): + """Test checking for connection and encountering invalid ssl certs.""" + + with patch.object(Smartbridge, "create_tls", side_effect=ssl.SSLError): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=EMPTY_MOCK_CONFIG_ENTRY, ) assert result["type"] == "form" @@ -351,23 +373,25 @@ async def test_form_user_reuses_existing_assets_when_pairing_again(hass, tmpdir) assert result["errors"] is None assert result["step_id"] == "user" - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_HOST: "1.1.1.1", - }, - ) - await hass.async_block_till_done() + with patch.object(Smartbridge, "create_tls") as create_tls: + create_tls.return_value = MockBridge(can_connect=True) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + await hass.async_block_till_done() + assert result2["type"] == "form" assert result2["step_id"] == "link" - with patch.object(Smartbridge, "create_tls") as create_tls, patch( + with patch( "homeassistant.components.lutron_caseta.async_setup", return_value=True ), patch( "homeassistant.components.lutron_caseta.async_setup_entry", return_value=True, ): - create_tls.return_value = MockBridge(can_connect=True) result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {}, From 385b7e17efa098439014026656acca291df20a09 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Jan 2021 11:36:19 -1000 Subject: [PATCH 102/796] Move homekit accessory creation to async (#45788) --- homeassistant/components/homekit/__init__.py | 49 +++++++++----------- tests/components/homekit/test_homekit.py | 12 ++--- 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 5cbc9bb6f18..e92c35ffac8 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -42,7 +42,21 @@ from homeassistant.helpers.reload import async_integration_yaml_config from homeassistant.loader import IntegrationNotFound, async_get_integration from homeassistant.util import get_local_ip -from .accessories import get_accessory +# pylint: disable=unused-import +from . import ( # noqa: F401 + type_cameras, + type_covers, + type_fans, + type_humidifiers, + type_lights, + type_locks, + type_media_players, + type_security_systems, + type_sensors, + type_switches, + type_thermostats, +) +from .accessories import HomeBridge, HomeDriver, get_accessory from .aidmanager import AccessoryAidStorage from .const import ( AID_STORAGE, @@ -441,9 +455,6 @@ class HomeKit: def setup(self, zeroconf_instance): """Set up bridge and accessory driver.""" - # pylint: disable=import-outside-toplevel - from .accessories import HomeDriver - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) ip_addr = self._ip_address or get_local_ip() persist_file = get_persist_fullpath_for_entry_id(self.hass, self._entry_id) @@ -590,7 +601,7 @@ class HomeKit: bridged_states.append(state) self._async_register_bridge(dev_reg) - await self.hass.async_add_executor_job(self._start, bridged_states) + await self._async_start(bridged_states) _LOGGER.debug("Driver start for %s", self._name) self.hass.add_job(self.driver.start_service) self.status = STATUS_RUNNING @@ -639,34 +650,20 @@ class HomeKit: for device_id in devices_to_purge: dev_reg.async_remove_device(device_id) - def _start(self, bridged_states): - # pylint: disable=unused-import, import-outside-toplevel - from . import ( # noqa: F401 - type_cameras, - type_covers, - type_fans, - type_humidifiers, - type_lights, - type_locks, - type_media_players, - type_security_systems, - type_sensors, - type_switches, - type_thermostats, - ) - + async def _async_start(self, entity_states): + """Start the accessory.""" if self._homekit_mode == HOMEKIT_MODE_ACCESSORY: - state = bridged_states[0] + state = entity_states[0] conf = self._config.pop(state.entity_id, {}) acc = get_accessory(self.hass, self.driver, state, STANDALONE_AID, conf) self.driver.add_accessory(acc) else: - from .accessories import HomeBridge - self.bridge = HomeBridge(self.hass, self.driver, self._name) - for state in bridged_states: + for state in entity_states: self.add_bridge_accessory(state) - self.driver.add_accessory(self.bridge) + acc = self.bridge + + await self.hass.async_add_executor_job(self.driver.add_accessory, acc) if not self.driver.state.paired: show_setup_message( diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 7b5153b825d..a8c3c81595e 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -201,7 +201,7 @@ async def test_homekit_setup(hass, hk_driver, mock_zeroconf): hass.states.async_set("light.demo2", "on") zeroconf_mock = MagicMock() with patch( - f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver + f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver ) as mock_driver, patch("homeassistant.util.get_local_ip") as mock_ip: mock_ip.return_value = IP_ADDRESS await hass.async_add_executor_job(homekit.setup, zeroconf_mock) @@ -245,9 +245,7 @@ async def test_homekit_setup_ip_address(hass, hk_driver, mock_zeroconf): mock_zeroconf = MagicMock() path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) - with patch( - f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver - ) as mock_driver: + with patch(f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver) as mock_driver: await hass.async_add_executor_job(homekit.setup, mock_zeroconf) mock_driver.assert_called_with( hass, @@ -283,9 +281,7 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_zeroconf): zeroconf_instance = MagicMock() path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) - with patch( - f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver - ) as mock_driver: + with patch(f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver) as mock_driver: await hass.async_add_executor_job(homekit.setup, zeroconf_instance) mock_driver.assert_called_with( hass, @@ -735,7 +731,7 @@ async def test_homekit_too_many_accessories(hass, hk_driver, caplog, mock_zeroco with patch("pyhap.accessory_driver.AccessoryDriver.start_service"), patch( "pyhap.accessory_driver.AccessoryDriver.add_accessory" ), patch(f"{PATH_HOMEKIT}.show_setup_message"), patch( - f"{PATH_HOMEKIT}.accessories.HomeBridge", _mock_bridge + f"{PATH_HOMEKIT}.HomeBridge", _mock_bridge ): await homekit.async_start() await hass.async_block_till_done() From 3e080f88c650ffb5f4a95f5c35d7654efce27904 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 31 Jan 2021 23:14:20 +0100 Subject: [PATCH 103/796] Bump zwave-js-server-python to 0.14.2 (#45800) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 08ca9668a99..5c3b82837ca 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.14.1"], + "requirements": ["zwave-js-server-python==0.14.2"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8281744f221..65936f88e3d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2381,4 +2381,4 @@ zigpy==0.32.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.14.1 +zwave-js-server-python==0.14.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d997b91f338..89489222bd2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1200,4 +1200,4 @@ zigpy-znp==0.3.0 zigpy==0.32.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.14.1 +zwave-js-server-python==0.14.2 From 1d94c10bb556d0f08fc46ee3f091bbd858b139d9 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Mon, 1 Feb 2021 07:55:18 +0100 Subject: [PATCH 104/796] Change via_hub to via_device (#45804) --- homeassistant/components/roon/media_player.py | 2 +- homeassistant/components/somfy/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py index 8abcba189da..b2ae62ec250 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -175,7 +175,7 @@ class RoonDevice(MediaPlayerEntity): "name": self.name, "manufacturer": "RoonLabs", "model": dev_model, - "via_hub": (DOMAIN, self._server.roon_id), + "via_device": (DOMAIN, self._server.roon_id), } def update_data(self, player_data=None): diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index 2fc83ea71de..75475e52f06 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -188,7 +188,7 @@ class SomfyEntity(CoordinatorEntity, Entity): "identifiers": {(DOMAIN, self.unique_id)}, "name": self.name, "model": self.device.type, - "via_hub": (DOMAIN, self.device.parent_id), + "via_device": (DOMAIN, self.device.parent_id), # For the moment, Somfy only returns their own device. "manufacturer": "Somfy", } From 03928dbe554516ccf563b632ec79cb57f9bfacde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Mon, 1 Feb 2021 08:34:55 +0100 Subject: [PATCH 105/796] Bump pyatv to 0.7.6 (#45799) --- 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 21b2df308d3..66ae2864dc4 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.5" + "pyatv==0.7.6" ], "zeroconf": [ "_mediaremotetv._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 65936f88e3d..95758342769 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1286,7 +1286,7 @@ pyatmo==4.2.2 pyatome==0.1.1 # homeassistant.components.apple_tv -pyatv==0.7.5 +pyatv==0.7.6 # homeassistant.components.bbox pybbox==0.0.5-alpha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 89489222bd2..1e0e4097f02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -669,7 +669,7 @@ pyatag==0.3.4.4 pyatmo==4.2.2 # homeassistant.components.apple_tv -pyatv==0.7.5 +pyatv==0.7.6 # homeassistant.components.blackbird pyblackbird==0.5 From 2ffdc4694aef1482e9bbe814b0af567d5af3af8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 1 Feb 2021 09:36:06 +0200 Subject: [PATCH 106/796] Remove misleading "for" from custom integration warning message (#45811) --- homeassistant/loader.py | 2 +- tests/test_loader.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 215f552a908..bedc04928af 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -49,7 +49,7 @@ DATA_CUSTOM_COMPONENTS = "custom_components" PACKAGE_CUSTOM_COMPONENTS = "custom_components" PACKAGE_BUILTIN = "homeassistant.components" CUSTOM_WARNING = ( - "You are using a custom integration for %s which has not " + "You are using a custom integration %s which has not " "been tested by Home Assistant. This component might " "cause stability problems, be sure to disable it if you " "experience issues with Home Assistant." diff --git a/tests/test_loader.py b/tests/test_loader.py index c1c27f56cb7..22f61c0a397 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -131,10 +131,10 @@ async def test_custom_component_name(hass): async def test_log_warning_custom_component(hass, caplog): """Test that we log a warning when loading a custom component.""" hass.components.test_standalone - assert "You are using a custom integration for test_standalone" in caplog.text + assert "You are using a custom integration test_standalone" in caplog.text await loader.async_get_integration(hass, "test") - assert "You are using a custom integration for test " in caplog.text + assert "You are using a custom integration test " in caplog.text async def test_get_integration(hass): From 0b63510cab085b9b7e57d27e2b2ccae4c891ce44 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 1 Feb 2021 02:45:24 -0600 Subject: [PATCH 107/796] Add zwave_js binary sensors property name for Notification CC (#45810) --- homeassistant/components/zwave_js/binary_sensor.py | 8 ++++++++ tests/components/zwave_js/common.py | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index 6dc5cc58df5..42394fe127c 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -349,6 +349,14 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity): return self.info.primary_value.value in self._mapping_info["states"] return bool(self.info.primary_value.value != 0) + @property + def name(self) -> str: + """Return default name from device name and value name combination.""" + node_name = self.info.node.name or self.info.node.device_config.description + property_name = self.info.primary_value.property_name + property_key_name = self.info.primary_value.property_key_name + return f"{node_name}: {property_name}: {property_key_name}" + @property def device_class(self) -> Optional[str]: """Return device class.""" diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index a7be137657c..399b009f4c2 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -6,7 +6,9 @@ SWITCH_ENTITY = "switch.smart_plug_with_two_usb_ports_current_value" LOW_BATTERY_BINARY_SENSOR = "binary_sensor.multisensor_6_low_battery_level" ENABLED_LEGACY_BINARY_SENSOR = "binary_sensor.z_wave_door_window_sensor_any" DISABLED_LEGACY_BINARY_SENSOR = "binary_sensor.multisensor_6_any" -NOTIFICATION_MOTION_BINARY_SENSOR = "binary_sensor.multisensor_6_motion_sensor_status" +NOTIFICATION_MOTION_BINARY_SENSOR = ( + "binary_sensor.multisensor_6_home_security_motion_sensor_status" +) PROPERTY_DOOR_STATUS_BINARY_SENSOR = ( "binary_sensor.august_smart_lock_pro_3rd_gen_the_current_status_of_the_door" ) From 91a54eecb3e0408751aae2323774356afceb1bbb Mon Sep 17 00:00:00 2001 From: Sly Gryphon Date: Mon, 1 Feb 2021 00:48:50 -0800 Subject: [PATCH 108/796] Add izone control zone (#43984) --- homeassistant/components/izone/climate.py | 54 +++++++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index 443a80298f1..776d3f120c9 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -110,6 +110,8 @@ class ControllerDevice(ClimateEntity): self._supported_features = SUPPORT_FAN_MODE + # If mode RAS, or mode master with CtrlZone 13 then can set master temperature, + # otherwise the unit determines which zone to use as target. See interface manual p. 8 if ( controller.ras_mode == "master" and controller.zone_ctrl == 13 ) or controller.ras_mode == "RAS": @@ -269,6 +271,16 @@ class ControllerDevice(ClimateEntity): self.temperature_unit, PRECISION_HALVES, ), + "control_zone": self._controller.zone_ctrl, + "control_zone_name": self.control_zone_name, + # Feature SUPPORT_TARGET_TEMPERATURE controls both displaying target temp & setting it + # As the feature is turned off for zone control, report target temp as extra state attribute + "control_zone_setpoint": show_temp( + self.hass, + self.control_zone_setpoint, + self.temperature_unit, + PRECISION_HALVES, + ), } @property @@ -314,13 +326,35 @@ class ControllerDevice(ClimateEntity): return self._controller.temp_supply return self._controller.temp_return + @property + def control_zone_name(self): + """Return the zone that currently controls the AC unit (if target temp not set by controller).""" + if self._supported_features & SUPPORT_TARGET_TEMPERATURE: + return None + zone_ctrl = self._controller.zone_ctrl + zone = next((z for z in self.zones.values() if z.zone_index == zone_ctrl), None) + if zone is None: + return None + return zone.name + + @property + def control_zone_setpoint(self) -> Optional[float]: + """Return the temperature setpoint of the zone that currently controls the AC unit (if target temp not set by controller).""" + if self._supported_features & SUPPORT_TARGET_TEMPERATURE: + return None + zone_ctrl = self._controller.zone_ctrl + zone = next((z for z in self.zones.values() if z.zone_index == zone_ctrl), None) + if zone is None: + return None + return zone.target_temperature + @property @_return_on_connection_error() def target_temperature(self) -> Optional[float]: - """Return the temperature we try to reach.""" - if not self._supported_features & SUPPORT_TARGET_TEMPERATURE: - return None - return self._controller.temp_setpoint + """Return the temperature we try to reach (either from control zone or master unit).""" + if self._supported_features & SUPPORT_TARGET_TEMPERATURE: + return self._controller.temp_setpoint + return self.control_zone_setpoint @property def supply_temperature(self) -> float: @@ -569,3 +603,15 @@ class ZoneDevice(ClimateEntity): """Turn device off (close zone).""" await self._controller.wrap_and_catch(self._zone.set_mode(Zone.Mode.CLOSE)) self.async_write_ha_state() + + @property + def zone_index(self): + """Return the zone index for matching to CtrlZone.""" + return self._zone.index + + @property + def device_state_attributes(self): + """Return the optional state attributes.""" + return { + "zone_index": self.zone_index, + } From e0bf18986bab2c92957980ba3db6ecdaf6ba5239 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Jan 2021 23:15:20 -1000 Subject: [PATCH 109/796] Fix missing async for lutron_caseta timeout (#45812) --- homeassistant/components/lutron_caseta/__init__.py | 6 +++--- homeassistant/components/lutron_caseta/config_flow.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index f349dde8921..73eb0b83fa6 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -104,16 +104,16 @@ async def async_setup_entry(hass, config_entry): hostname=host, keyfile=keyfile, certfile=certfile, ca_certs=ca_certs ) except ssl.SSLError: - _LOGGER.error("Invalid certificate used to connect to bridge at %s.", host) + _LOGGER.error("Invalid certificate used to connect to bridge at %s", host) return False timed_out = True try: - with async_timeout.timeout(BRIDGE_TIMEOUT): + async with async_timeout.timeout(BRIDGE_TIMEOUT): await bridge.connect() timed_out = False except asyncio.TimeoutError: - _LOGGER.error("Timeout while trying to connect to bridge at %s.", host) + _LOGGER.error("Timeout while trying to connect to bridge at %s", host) if timed_out or not bridge.is_connected(): await bridge.close() diff --git a/homeassistant/components/lutron_caseta/config_flow.py b/homeassistant/components/lutron_caseta/config_flow.py index 30cbc03ac47..ab9865f999a 100644 --- a/homeassistant/components/lutron_caseta/config_flow.py +++ b/homeassistant/components/lutron_caseta/config_flow.py @@ -228,19 +228,19 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) except ssl.SSLError: _LOGGER.error( - "Invalid certificate used to connect to bridge at %s.", + "Invalid certificate used to connect to bridge at %s", self.data[CONF_HOST], ) return False connected_ok = False try: - with async_timeout.timeout(BRIDGE_TIMEOUT): + async with async_timeout.timeout(BRIDGE_TIMEOUT): await bridge.connect() connected_ok = bridge.is_connected() except asyncio.TimeoutError: _LOGGER.error( - "Timeout while trying to connect to bridge at %s.", + "Timeout while trying to connect to bridge at %s", self.data[CONF_HOST], ) From 31a84555b97789c99c94f61075172e2f61350c5e Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 1 Feb 2021 10:54:07 +0100 Subject: [PATCH 110/796] Bump zwave-js-server-python to 0.15.0 (#45813) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 5c3b82837ca..586a6492a1a 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.14.2"], + "requirements": ["zwave-js-server-python==0.15.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"] } diff --git a/requirements_all.txt b/requirements_all.txt index 95758342769..60df4ea85f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2381,4 +2381,4 @@ zigpy==0.32.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.14.2 +zwave-js-server-python==0.15.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e0e4097f02..f855ab4bacc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1200,4 +1200,4 @@ zigpy-znp==0.3.0 zigpy==0.32.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.14.2 +zwave-js-server-python==0.15.0 From 9f59515bb8f300d6cb28830393efb7aa5fd37c8b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Jan 2021 23:54:39 -1000 Subject: [PATCH 111/796] Fix shutdown deadlock with run_callback_threadsafe (#45807) --- homeassistant/core.py | 14 ++++++++++- homeassistant/util/async_.py | 41 ++++++++++++++++++++++++++++++++ tests/test_core.py | 24 +++++++++++++++++++ tests/util/test_async.py | 45 +++++++++++++++++++++++++++++++++++- 4 files changed, 122 insertions(+), 2 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 6d187225685..4294eb530a7 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -71,7 +71,11 @@ from homeassistant.exceptions import ( Unauthorized, ) from homeassistant.util import location, network -from homeassistant.util.async_ import fire_coroutine_threadsafe, run_callback_threadsafe +from homeassistant.util.async_ import ( + fire_coroutine_threadsafe, + run_callback_threadsafe, + shutdown_run_callback_threadsafe, +) import homeassistant.util.dt as dt_util from homeassistant.util.timeout import TimeoutManager from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem @@ -548,6 +552,14 @@ class HomeAssistant: # stage 3 self.state = CoreState.not_running self.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE) + + # Prevent run_callback_threadsafe from scheduling any additional + # callbacks in the event loop as callbacks created on the futures + # it returns will never run after the final `self.async_block_till_done` + # which will cause the futures to block forever when waiting for + # the `result()` which will cause a deadlock when shutting down the executor. + shutdown_run_callback_threadsafe(self.loop) + try: async with self.timeout.async_timeout(30): await self.async_block_till_done() diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index ded44473038..f61225502ee 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -10,6 +10,8 @@ from typing import Any, Awaitable, Callable, Coroutine, TypeVar _LOGGER = logging.getLogger(__name__) +_SHUTDOWN_RUN_CALLBACK_THREADSAFE = "_shutdown_run_callback_threadsafe" + T = TypeVar("T") @@ -58,6 +60,28 @@ def run_callback_threadsafe( _LOGGER.warning("Exception on lost future: ", exc_info=True) loop.call_soon_threadsafe(run_callback) + + if hasattr(loop, _SHUTDOWN_RUN_CALLBACK_THREADSAFE): + # + # If the final `HomeAssistant.async_block_till_done` in + # `HomeAssistant.async_stop` has already been called, the callback + # will never run and, `future.result()` will block forever which + # will prevent the thread running this code from shutting down which + # will result in a deadlock when the main thread attempts to shutdown + # the executor and `.join()` the thread running this code. + # + # To prevent this deadlock we do the following on shutdown: + # + # 1. Set the _SHUTDOWN_RUN_CALLBACK_THREADSAFE attr on this function + # by calling `shutdown_run_callback_threadsafe` + # 2. Call `hass.async_block_till_done` at least once after shutdown + # to ensure all callbacks have run + # 3. Raise an exception here to ensure `future.result()` can never be + # called and hit the deadlock since once `shutdown_run_callback_threadsafe` + # we cannot promise the callback will be executed. + # + raise RuntimeError("The event loop is in the process of shutting down.") + return future @@ -139,3 +163,20 @@ async def gather_with_concurrency( return await gather( *(sem_task(task) for task in tasks), return_exceptions=return_exceptions ) + + +def shutdown_run_callback_threadsafe(loop: AbstractEventLoop) -> None: + """Call when run_callback_threadsafe should prevent creating new futures. + + We must finish all callbacks before the executor is shutdown + or we can end up in a deadlock state where: + + `executor.result()` is waiting for its `._condition` + and the executor shutdown is trying to `.join()` the + executor thread. + + This function is considered irreversible and should only ever + be called when Home Assistant is going to shutdown and + python is going to exit. + """ + setattr(loop, _SHUTDOWN_RUN_CALLBACK_THREADSAFE, True) diff --git a/tests/test_core.py b/tests/test_core.py index dbed2b8c0bf..0bf00d92c45 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -169,6 +169,30 @@ async def test_stage_shutdown(hass): assert len(test_all) == 2 +async def test_shutdown_calls_block_till_done_after_shutdown_run_callback_threadsafe( + hass, +): + """Ensure shutdown_run_callback_threadsafe is called before the final async_block_till_done.""" + stop_calls = [] + + async def _record_block_till_done(): + nonlocal stop_calls + stop_calls.append("async_block_till_done") + + def _record_shutdown_run_callback_threadsafe(loop): + nonlocal stop_calls + stop_calls.append(("shutdown_run_callback_threadsafe", loop)) + + with patch.object(hass, "async_block_till_done", _record_block_till_done), patch( + "homeassistant.core.shutdown_run_callback_threadsafe", + _record_shutdown_run_callback_threadsafe, + ): + await hass.async_stop() + + assert stop_calls[-2] == ("shutdown_run_callback_threadsafe", hass.loop) + assert stop_calls[-1] == "async_block_till_done" + + async def test_pending_sheduler(hass): """Add a coro to pending tasks.""" call_count = [] diff --git a/tests/util/test_async.py b/tests/util/test_async.py index db088ada93e..d4fdce1e912 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -50,7 +50,8 @@ def test_fire_coroutine_threadsafe_from_inside_event_loop( def test_run_callback_threadsafe_from_inside_event_loop(mock_ident, _): """Testing calling run_callback_threadsafe from inside an event loop.""" callback = MagicMock() - loop = MagicMock() + + loop = Mock(spec=["call_soon_threadsafe"]) loop._thread_ident = None mock_ident.return_value = 5 @@ -168,3 +169,45 @@ async def test_gather_with_concurrency(): ) assert results == [2, 2, -1, -1] + + +async def test_shutdown_run_callback_threadsafe(hass): + """Test we can shutdown run_callback_threadsafe.""" + hasync.shutdown_run_callback_threadsafe(hass.loop) + callback = MagicMock() + + with pytest.raises(RuntimeError): + hasync.run_callback_threadsafe(hass.loop, callback) + + +async def test_run_callback_threadsafe(hass): + """Test run_callback_threadsafe runs code in the event loop.""" + it_ran = False + + def callback(): + nonlocal it_ran + it_ran = True + + assert hasync.run_callback_threadsafe(hass.loop, callback) + assert it_ran is False + + # Verify that async_block_till_done will flush + # out the callback + await hass.async_block_till_done() + assert it_ran is True + + +async def test_callback_is_always_scheduled(hass): + """Test run_callback_threadsafe always calls call_soon_threadsafe before checking for shutdown.""" + # We have to check the shutdown state AFTER the callback is scheduled otherwise + # the function could continue on and the caller call `future.result()` after + # the point in the main thread where callbacks are no longer run. + + callback = MagicMock() + hasync.shutdown_run_callback_threadsafe(hass.loop) + + with patch.object(hass.loop, "call_soon_threadsafe") as mock_call_soon_threadsafe: + with pytest.raises(RuntimeError): + hasync.run_callback_threadsafe(hass.loop, callback) + + mock_call_soon_threadsafe.assert_called_once() From a8cf377ed71ac1a723bfb93066929ebc7bf69f4f Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 1 Feb 2021 08:46:36 -0700 Subject: [PATCH 112/796] Add stop_cover service for zwave_js (#45805) --- homeassistant/components/zwave_js/cover.py | 17 ++- tests/components/zwave_js/test_cover.py | 125 ++++++++++++++++++--- 2 files changed, 121 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index b7834e8e59c..5f473f80957 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -21,6 +21,8 @@ from .entity import ZWaveBaseEntity LOGGER = logging.getLogger(__name__) SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE +PRESS_BUTTON = True +RELEASE_BUTTON = False async def async_setup_entry( @@ -77,10 +79,17 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" - target_value = self.get_zwave_value("targetValue") - await self.info.node.async_set_value(target_value, 99) + target_value = self.get_zwave_value("Open") + await self.info.node.async_set_value(target_value, PRESS_BUTTON) async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" - target_value = self.get_zwave_value("targetValue") - await self.info.node.async_set_value(target_value, 0) + target_value = self.get_zwave_value("Close") + await self.info.node.async_set_value(target_value, PRESS_BUTTON) + + async def async_stop_cover(self, **kwargs: Any) -> None: + """Stop cover.""" + target_value = self.get_zwave_value("Open") + await self.info.node.async_set_value(target_value, RELEASE_BUTTON) + target_value = self.get_zwave_value("Close") + await self.info.node.async_set_value(target_value, RELEASE_BUTTON) diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index c327034b61c..f014245a5f8 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -95,21 +95,65 @@ async def test_cover(hass, client, chain_actuator_zws12, integration): "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, - "property": "targetValue", - "propertyName": "targetValue", + "property": "Open", + "propertyName": "Open", "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", + "type": "boolean", "readable": True, "writeable": True, - "label": "Target value", + "label": "Perform a level change (Open)", + "ccSpecific": {"switchType": 3}, }, } - assert args["value"] == 99 + assert args["value"] client.async_send_command.reset_mock() + # Test stop after opening + await hass.services.async_call( + "cover", + "stop_cover", + {"entity_id": WINDOW_COVER_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 2 + open_args = client.async_send_command.call_args_list[0][0][0] + assert open_args["command"] == "node.set_value" + assert open_args["nodeId"] == 6 + assert open_args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "Open", + "propertyName": "Open", + "metadata": { + "type": "boolean", + "readable": True, + "writeable": True, + "label": "Perform a level change (Open)", + "ccSpecific": {"switchType": 3}, + }, + } + assert not open_args["value"] + + close_args = client.async_send_command.call_args_list[1][0][0] + assert close_args["command"] == "node.set_value" + assert close_args["nodeId"] == 6 + assert close_args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "Close", + "propertyName": "Close", + "metadata": { + "type": "boolean", + "readable": True, + "writeable": True, + "label": "Perform a level change (Close)", + "ccSpecific": {"switchType": 3}, + }, + } + assert not close_args["value"] # Test position update from value updated event event = Event( @@ -130,6 +174,7 @@ async def test_cover(hass, client, chain_actuator_zws12, integration): }, ) node.receive_event(event) + client.async_send_command.reset_mock() state = hass.states.get(WINDOW_COVER_ENTITY) assert state.state == "open" @@ -141,7 +186,6 @@ async def test_cover(hass, client, chain_actuator_zws12, integration): {"entity_id": WINDOW_COVER_ENTITY}, blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" @@ -150,19 +194,66 @@ async def test_cover(hass, client, chain_actuator_zws12, integration): "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, - "property": "targetValue", - "propertyName": "targetValue", + "property": "Close", + "propertyName": "Close", "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", + "type": "boolean", "readable": True, "writeable": True, - "label": "Target value", + "label": "Perform a level change (Close)", + "ccSpecific": {"switchType": 3}, }, } - assert args["value"] == 0 + assert args["value"] + + client.async_send_command.reset_mock() + + # Test stop after closing + await hass.services.async_call( + "cover", + "stop_cover", + {"entity_id": WINDOW_COVER_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 2 + open_args = client.async_send_command.call_args_list[0][0][0] + assert open_args["command"] == "node.set_value" + assert open_args["nodeId"] == 6 + assert open_args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "Open", + "propertyName": "Open", + "metadata": { + "type": "boolean", + "readable": True, + "writeable": True, + "label": "Perform a level change (Open)", + "ccSpecific": {"switchType": 3}, + }, + } + assert not open_args["value"] + + close_args = client.async_send_command.call_args_list[1][0][0] + assert close_args["command"] == "node.set_value" + assert close_args["nodeId"] == 6 + assert close_args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "Close", + "propertyName": "Close", + "metadata": { + "type": "boolean", + "readable": True, + "writeable": True, + "label": "Perform a level change (Close)", + "ccSpecific": {"switchType": 3}, + }, + } + assert not close_args["value"] client.async_send_command.reset_mock() From 374817fbaaff46d232c42343907e25e763e2f8f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 1 Feb 2021 16:54:25 +0100 Subject: [PATCH 113/796] Bump awesomeversion from 21.1.6 to 21.2.0 (#45821) --- requirements_test.txt | 2 +- script/hassfest/manifest.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 91380d14c5e..2c10083ecfd 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,7 +13,7 @@ pre-commit==2.10.0 pylint==2.6.0 astroid==2.4.2 pipdeptree==1.0.0 -awesomeversion==21.1.6 +awesomeversion==21.2.0 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 pytest-cov==2.10.1 diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index e02df86f4e1..3beb6aadfc5 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -59,6 +59,7 @@ def verify_version(value: str): AwesomeVersionStrategy.SEMVER, AwesomeVersionStrategy.SIMPLEVER, AwesomeVersionStrategy.BUILDVER, + AwesomeVersionStrategy.PEP440, ]: raise vol.Invalid( f"'{version}' is not a valid version. This will cause a future version of Home Assistant to block this integration.", From 6e67b943da09a13409e1a61ec34ed7144e1093c3 Mon Sep 17 00:00:00 2001 From: Tobias Bielohlawek Date: Mon, 1 Feb 2021 16:58:00 +0100 Subject: [PATCH 114/796] Remove Nuimo integration (#45600) --- .coveragerc | 1 - .../components/nuimo_controller/__init__.py | 190 ------------------ .../components/nuimo_controller/manifest.json | 7 - .../components/nuimo_controller/services.yaml | 17 -- requirements_all.txt | 3 - script/gen_requirements_all.py | 1 - 6 files changed, 219 deletions(-) delete mode 100644 homeassistant/components/nuimo_controller/__init__.py delete mode 100644 homeassistant/components/nuimo_controller/manifest.json delete mode 100644 homeassistant/components/nuimo_controller/services.yaml diff --git a/.coveragerc b/.coveragerc index 65b499c372f..0609051f196 100644 --- a/.coveragerc +++ b/.coveragerc @@ -623,7 +623,6 @@ omit = homeassistant/components/norway_air/air_quality.py homeassistant/components/notify_events/notify.py homeassistant/components/nsw_fuel_station/sensor.py - homeassistant/components/nuimo_controller/* homeassistant/components/nuki/__init__.py homeassistant/components/nuki/const.py homeassistant/components/nuki/lock.py diff --git a/homeassistant/components/nuimo_controller/__init__.py b/homeassistant/components/nuimo_controller/__init__.py deleted file mode 100644 index 013c2caf23d..00000000000 --- a/homeassistant/components/nuimo_controller/__init__.py +++ /dev/null @@ -1,190 +0,0 @@ -"""Support for Nuimo device over Bluetooth LE.""" -import logging -import threading -import time - -# pylint: disable=import-error -from nuimo import NuimoController, NuimoDiscoveryManager -import voluptuous as vol - -from homeassistant.const import CONF_MAC, CONF_NAME, EVENT_HOMEASSISTANT_STOP -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "nuimo_controller" -EVENT_NUIMO = "nuimo_input" - -DEFAULT_NAME = "None" - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_MAC): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - -SERVICE_NUIMO = "led_matrix" -DEFAULT_INTERVAL = 2.0 - -SERVICE_NUIMO_SCHEMA = vol.Schema( - { - vol.Required("matrix"): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional("interval", default=DEFAULT_INTERVAL): float, - } -) - -DEFAULT_ADAPTER = "hci0" - - -def setup(hass, config): - """Set up the Nuimo component.""" - conf = config[DOMAIN] - mac = conf.get(CONF_MAC) - name = conf.get(CONF_NAME) - NuimoThread(hass, mac, name).start() - return True - - -class NuimoLogger: - """Handle Nuimo Controller event callbacks.""" - - def __init__(self, hass, name): - """Initialize Logger object.""" - self._hass = hass - self._name = name - - def received_gesture_event(self, event): - """Input Event received.""" - _LOGGER.debug( - "Received event: name=%s, gesture_id=%s,value=%s", - event.name, - event.gesture, - event.value, - ) - self._hass.bus.fire( - EVENT_NUIMO, {"type": event.name, "value": event.value, "name": self._name} - ) - - -class NuimoThread(threading.Thread): - """Manage one Nuimo controller.""" - - def __init__(self, hass, mac, name): - """Initialize thread object.""" - super().__init__() - self._hass = hass - self._mac = mac - self._name = name - self._hass_is_running = True - self._nuimo = None - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.stop) - - def run(self): - """Set up the connection or be idle.""" - while self._hass_is_running: - if not self._nuimo or not self._nuimo.is_connected(): - self._attach() - self._connect() - else: - time.sleep(1) - - if self._nuimo: - self._nuimo.disconnect() - self._nuimo = None - - def stop(self, event): - """Terminate Thread by unsetting flag.""" - _LOGGER.debug("Stopping thread for Nuimo %s", self._mac) - self._hass_is_running = False - - def _attach(self): - """Create a Nuimo object from MAC address or discovery.""" - - if self._nuimo: - self._nuimo.disconnect() - self._nuimo = None - - if self._mac: - self._nuimo = NuimoController(self._mac) - else: - nuimo_manager = NuimoDiscoveryManager( - bluetooth_adapter=DEFAULT_ADAPTER, delegate=DiscoveryLogger() - ) - nuimo_manager.start_discovery() - # Were any Nuimos found? - if not nuimo_manager.nuimos: - _LOGGER.debug("No Nuimo devices detected") - return - # Take the first Nuimo found. - self._nuimo = nuimo_manager.nuimos[0] - self._mac = self._nuimo.addr - - def _connect(self): - """Build up connection and set event delegator and service.""" - if not self._nuimo: - return - - try: - self._nuimo.connect() - _LOGGER.debug("Connected to %s", self._mac) - except RuntimeError as error: - _LOGGER.error("Could not connect to %s: %s", self._mac, error) - time.sleep(1) - return - - nuimo_event_delegate = NuimoLogger(self._hass, self._name) - self._nuimo.set_delegate(nuimo_event_delegate) - - def handle_write_matrix(call): - """Handle led matrix service.""" - matrix = call.data.get("matrix", None) - name = call.data.get(CONF_NAME, DEFAULT_NAME) - interval = call.data.get("interval", DEFAULT_INTERVAL) - if self._name == name and matrix: - self._nuimo.write_matrix(matrix, interval) - - self._hass.services.register( - DOMAIN, SERVICE_NUIMO, handle_write_matrix, schema=SERVICE_NUIMO_SCHEMA - ) - - self._nuimo.write_matrix(HOMEASSIST_LOGO, 2.0) - - -# must be 9x9 matrix -HOMEASSIST_LOGO = ( - " . " - + " ... " - + " ..... " - + " ....... " - + "..... ..." - + " ....... " - + " .. .... " - + " .. .... " - + "........." -) - - -class DiscoveryLogger: - """Handle Nuimo Discovery callbacks.""" - - # pylint: disable=no-self-use - def discovery_started(self): - """Discovery started.""" - _LOGGER.info("Started discovery") - - # pylint: disable=no-self-use - def discovery_finished(self): - """Discovery finished.""" - _LOGGER.info("Finished discovery") - - # pylint: disable=no-self-use - def controller_added(self, nuimo): - """Return that a controller was found.""" - _LOGGER.info("Added Nuimo: %s", nuimo) diff --git a/homeassistant/components/nuimo_controller/manifest.json b/homeassistant/components/nuimo_controller/manifest.json deleted file mode 100644 index dddd4a97523..00000000000 --- a/homeassistant/components/nuimo_controller/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "nuimo_controller", - "name": "Nuimo controller", - "documentation": "https://www.home-assistant.io/integrations/nuimo_controller", - "requirements": ["--only-binary=all nuimo==0.1.0"], - "codeowners": [] -} diff --git a/homeassistant/components/nuimo_controller/services.yaml b/homeassistant/components/nuimo_controller/services.yaml deleted file mode 100644 index d98659caa8b..00000000000 --- a/homeassistant/components/nuimo_controller/services.yaml +++ /dev/null @@ -1,17 +0,0 @@ -led_matrix: - description: Sends an LED Matrix to your display - fields: - matrix: - description: "A string representation of the matrix to be displayed. See the SDK documentation for more info: https://github.com/getSenic/nuimo-linux-python#write-to-nuimos-led-matrix" - example: "........ - 0000000. - .000000. - ..00000. - .0.0000. - .00.000. - .000000. - .000000. - ........" - interval: - description: Display interval in seconds - example: 0.5 diff --git a/requirements_all.txt b/requirements_all.txt index 60df4ea85f0..804b0495297 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,9 +1,6 @@ # Home Assistant Core, full dependency set -r requirements.txt -# homeassistant.components.nuimo_controller -# --only-binary=all nuimo==0.1.0 - # homeassistant.components.dht # Adafruit-DHT==1.4.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index dc1ef9a471b..52820bfa572 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -28,7 +28,6 @@ COMMENT_REQUIREMENTS = ( "evdev", "face_recognition", "i2csense", - "nuimo", "opencv-python-headless", "py_noaa", "pybluez", From 2136b3013f42af385e88ad2fa991d58262600812 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 1 Feb 2021 08:48:49 -0800 Subject: [PATCH 115/796] Increase test coverage for stream worker (#44161) Co-authored-by: Justin Wong <46082645+uvjustin@users.noreply.github.com> --- tests/components/stream/test_worker.py | 489 +++++++++++++++++++++++++ 1 file changed, 489 insertions(+) create mode 100644 tests/components/stream/test_worker.py diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py new file mode 100644 index 00000000000..8196899dcf9 --- /dev/null +++ b/tests/components/stream/test_worker.py @@ -0,0 +1,489 @@ +"""Test the stream worker corner cases. + +Exercise the stream worker functionality by mocking av.open calls to return a +fake media container as well a fake decoded stream in the form of a series of +packets. This is needed as some of these cases can't be encoded using pyav. It +is preferred to use test_hls.py for example, when possible. + +The worker opens the stream source (typically a URL) and gets back a +container that has audio/video streams. The worker iterates over the sequence +of packets and sends them to the appropriate output buffers. Each test +creates a packet sequence, with a mocked output buffer to capture the segments +pushed to the output streams. The packet sequence can be used to exercise +failure modes or corner cases like how out of order packets are handled. +""" + +import fractions +import math +import threading +from unittest.mock import patch + +import av + +from homeassistant.components.stream import Stream +from homeassistant.components.stream.const import ( + MAX_MISSING_DTS, + MIN_SEGMENT_DURATION, + PACKETS_TO_WAIT_FOR_AUDIO, +) +from homeassistant.components.stream.worker import stream_worker + +STREAM_SOURCE = "some-stream-source" +# Formats here are arbitrary, not exercised by tests +STREAM_OUTPUT_FORMAT = "hls" +AUDIO_STREAM_FORMAT = "mp3" +VIDEO_STREAM_FORMAT = "h264" +VIDEO_FRAME_RATE = 12 +AUDIO_SAMPLE_RATE = 11025 +PACKET_DURATION = fractions.Fraction(1, VIDEO_FRAME_RATE) # in seconds +SEGMENT_DURATION = ( + math.ceil(MIN_SEGMENT_DURATION / PACKET_DURATION) * PACKET_DURATION +) # in seconds +TEST_SEQUENCE_LENGTH = 5 * VIDEO_FRAME_RATE +LONGER_TEST_SEQUENCE_LENGTH = 20 * VIDEO_FRAME_RATE +OUT_OF_ORDER_PACKET_INDEX = 3 * VIDEO_FRAME_RATE +PACKETS_PER_SEGMENT = SEGMENT_DURATION / PACKET_DURATION +SEGMENTS_PER_PACKET = PACKET_DURATION / SEGMENT_DURATION + + +class FakePyAvStream: + """A fake pyav Stream.""" + + def __init__(self, name, rate): + """Initialize the stream.""" + self.name = name + self.time_base = fractions.Fraction(1, rate) + self.profile = "ignored-profile" + + +VIDEO_STREAM = FakePyAvStream(VIDEO_STREAM_FORMAT, VIDEO_FRAME_RATE) +AUDIO_STREAM = FakePyAvStream(AUDIO_STREAM_FORMAT, AUDIO_SAMPLE_RATE) + + +class PacketSequence: + """Creates packets in a sequence for exercising stream worker behavior. + + A test can create a PacketSequence(N) that will raise a StopIteration after + N packets. Each packet has an arbitrary monotomically increasing dts/pts value + that is parseable by the worker, but a test can manipulate the values to + exercise corner cases. + """ + + def __init__(self, num_packets): + """Initialize the sequence with the number of packets it provides.""" + self.packet = 0 + self.num_packets = num_packets + + def __iter__(self): + """Reset the sequence.""" + self.packet = 0 + return self + + def __next__(self): + """Return the next packet.""" + if self.packet >= self.num_packets: + raise StopIteration + self.packet += 1 + + class FakePacket: + time_base = fractions.Fraction(1, VIDEO_FRAME_RATE) + dts = self.packet * PACKET_DURATION / time_base + pts = self.packet * PACKET_DURATION / time_base + duration = PACKET_DURATION / time_base + stream = VIDEO_STREAM + is_keyframe = True + + return FakePacket() + + +class FakePyAvContainer: + """A fake container returned by mock av.open for a stream.""" + + def __init__(self, video_stream, audio_stream): + """Initialize the fake container.""" + # Tests can override this to trigger different worker behavior + self.packets = PacketSequence(0) + + class FakePyAvStreams: + video = video_stream + audio = audio_stream + + self.streams = FakePyAvStreams() + + class FakePyAvFormat: + name = "ignored-format" + + self.format = FakePyAvFormat() + + def demux(self, streams): + """Decode the streams from container, and return a packet sequence.""" + return self.packets + + def close(self): + """Close the container.""" + return + + +class FakePyAvBuffer: + """Holds outputs of the decoded stream for tests to assert on results.""" + + def __init__(self): + """Initialize the FakePyAvBuffer.""" + self.segments = [] + self.audio_packets = [] + self.video_packets = [] + self.finished = False + + def add_stream(self, template=None): + """Create an output buffer that captures packets for test to examine.""" + + class FakeStream: + def __init__(self, capture_packets): + self.capture_packets = capture_packets + + def close(self): + return + + def mux(self, packet): + self.capture_packets.append(packet) + + if template.name == AUDIO_STREAM_FORMAT: + return FakeStream(self.audio_packets) + return FakeStream(self.video_packets) + + def mux(self, packet): + """Capture a packet for tests to examine.""" + # Forward to appropriate FakeStream + packet.stream.mux(packet) + + def close(self): + """Close the buffer.""" + return + + def capture_output_segment(self, segment): + """Capture the output segment for tests to inspect.""" + assert not self.finished + if segment is None: + self.finished = True + else: + self.segments.append(segment) + + +class MockPyAv: + """Mocks out av.open.""" + + def __init__(self, video=True, audio=False): + """Initialize the MockPyAv.""" + video_stream = [VIDEO_STREAM] if video else [] + audio_stream = [AUDIO_STREAM] if audio else [] + self.container = FakePyAvContainer( + video_stream=video_stream, audio_stream=audio_stream + ) + self.capture_buffer = FakePyAvBuffer() + + def open(self, stream_source, *args, **kwargs): + """Return a stream or buffer depending on args.""" + if stream_source == STREAM_SOURCE: + return self.container + return self.capture_buffer + + +async def async_decode_stream(hass, packets, py_av=None): + """Start a stream worker that decodes incoming stream packets into output segments.""" + stream = Stream(hass, STREAM_SOURCE) + stream.add_provider(STREAM_OUTPUT_FORMAT) + + if not py_av: + py_av = MockPyAv() + py_av.container.packets = packets + + with patch("av.open", new=py_av.open), patch( + "homeassistant.components.stream.core.StreamOutput.put", + side_effect=py_av.capture_buffer.capture_output_segment, + ): + stream_worker(hass, stream, threading.Event()) + await hass.async_block_till_done() + + return py_av.capture_buffer + + +async def test_stream_open_fails(hass): + """Test failure on stream open.""" + stream = Stream(hass, STREAM_SOURCE) + stream.add_provider(STREAM_OUTPUT_FORMAT) + with patch("av.open") as av_open: + av_open.side_effect = av.error.InvalidDataError(-2, "error") + stream_worker(hass, stream, threading.Event()) + await hass.async_block_till_done() + av_open.assert_called_once() + + +async def test_stream_worker_success(hass): + """Test a short stream that ends and outputs everything correctly.""" + decoded_stream = await async_decode_stream( + hass, PacketSequence(TEST_SEQUENCE_LENGTH) + ) + assert decoded_stream.finished + segments = decoded_stream.segments + # Check number of segments. A segment is only formed when a packet from the next + # segment arrives, hence the subtraction of one from the sequence length. + assert len(segments) == int((TEST_SEQUENCE_LENGTH - 1) * SEGMENTS_PER_PACKET) + # Check sequence numbers + assert all([segments[i].sequence == i + 1 for i in range(len(segments))]) + # Check segment durations + assert all([s.duration == SEGMENT_DURATION for s in segments]) + assert len(decoded_stream.video_packets) == TEST_SEQUENCE_LENGTH + assert len(decoded_stream.audio_packets) == 0 + + +async def test_skip_out_of_order_packet(hass): + """Skip a single out of order packet.""" + packets = list(PacketSequence(TEST_SEQUENCE_LENGTH)) + # This packet is out of order + packets[OUT_OF_ORDER_PACKET_INDEX].dts = -9090 + + decoded_stream = await async_decode_stream(hass, iter(packets)) + assert decoded_stream.finished + segments = decoded_stream.segments + # Check sequence numbers + assert all([segments[i].sequence == i + 1 for i in range(len(segments))]) + # If skipped packet would have been the first packet of a segment, the previous + # segment will be longer by a packet duration + # We also may possibly lose a segment due to the shifting pts boundary + if OUT_OF_ORDER_PACKET_INDEX % PACKETS_PER_SEGMENT == 0: + # Check duration of affected segment and remove it + longer_segment_index = int( + (OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET + ) + assert ( + segments[longer_segment_index].duration + == SEGMENT_DURATION + PACKET_DURATION + ) + del segments[longer_segment_index] + # Check number of segments + assert len(segments) == int((len(packets) - 1 - 1) * SEGMENTS_PER_PACKET - 1) + else: # Otherwise segment durations and number of segments are unaffected + # Check number of segments + assert len(segments) == int((len(packets) - 1) * SEGMENTS_PER_PACKET) + # Check remaining segment durations + assert all([s.duration == SEGMENT_DURATION for s in segments]) + assert len(decoded_stream.video_packets) == len(packets) - 1 + assert len(decoded_stream.audio_packets) == 0 + + +async def test_discard_old_packets(hass): + """Skip a series of out of order packets.""" + + packets = list(PacketSequence(TEST_SEQUENCE_LENGTH)) + # Packets after this one are considered out of order + packets[OUT_OF_ORDER_PACKET_INDEX - 1].dts = 9090 + + decoded_stream = await async_decode_stream(hass, iter(packets)) + assert decoded_stream.finished + segments = decoded_stream.segments + # Check number of segments + assert len(segments) == int((OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET) + # Check sequence numbers + assert all([segments[i].sequence == i + 1 for i in range(len(segments))]) + # Check segment durations + assert all([s.duration == SEGMENT_DURATION for s in segments]) + assert len(decoded_stream.video_packets) == OUT_OF_ORDER_PACKET_INDEX + assert len(decoded_stream.audio_packets) == 0 + + +async def test_packet_overflow(hass): + """Packet is too far out of order, and looks like overflow, ending stream early.""" + + packets = list(PacketSequence(TEST_SEQUENCE_LENGTH)) + # Packet is so far out of order, exceeds max gap and looks like overflow + packets[OUT_OF_ORDER_PACKET_INDEX].dts = -9000000 + + decoded_stream = await async_decode_stream(hass, iter(packets)) + assert decoded_stream.finished + segments = decoded_stream.segments + # Check number of segments + assert len(segments) == int((OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET) + # Check sequence numbers + assert all([segments[i].sequence == i + 1 for i in range(len(segments))]) + # Check segment durations + assert all([s.duration == SEGMENT_DURATION for s in segments]) + assert len(decoded_stream.video_packets) == OUT_OF_ORDER_PACKET_INDEX + assert len(decoded_stream.audio_packets) == 0 + + +async def test_skip_initial_bad_packets(hass): + """Tests a small number of initial "bad" packets with missing dts.""" + + num_packets = LONGER_TEST_SEQUENCE_LENGTH + packets = list(PacketSequence(num_packets)) + num_bad_packets = MAX_MISSING_DTS - 1 + for i in range(0, num_bad_packets): + packets[i].dts = None + + decoded_stream = await async_decode_stream(hass, iter(packets)) + assert decoded_stream.finished + segments = decoded_stream.segments + # Check number of segments + assert len(segments) == int( + (num_packets - num_bad_packets - 1) * SEGMENTS_PER_PACKET + ) + # Check sequence numbers + assert all([segments[i].sequence == i + 1 for i in range(len(segments))]) + # Check segment durations + assert all([s.duration == SEGMENT_DURATION for s in segments]) + assert len(decoded_stream.video_packets) == num_packets - num_bad_packets + assert len(decoded_stream.audio_packets) == 0 + + +async def test_too_many_initial_bad_packets_fails(hass): + """Test initial bad packets are too high, causing it to never start.""" + + num_packets = LONGER_TEST_SEQUENCE_LENGTH + packets = list(PacketSequence(num_packets)) + num_bad_packets = MAX_MISSING_DTS + 1 + for i in range(0, num_bad_packets): + packets[i].dts = None + + decoded_stream = await async_decode_stream(hass, iter(packets)) + assert decoded_stream.finished + segments = decoded_stream.segments + assert len(segments) == 0 + assert len(decoded_stream.video_packets) == 0 + assert len(decoded_stream.audio_packets) == 0 + + +async def test_skip_missing_dts(hass): + """Test packets in the middle of the stream missing DTS are skipped.""" + + num_packets = LONGER_TEST_SEQUENCE_LENGTH + packets = list(PacketSequence(num_packets)) + bad_packet_start = int(LONGER_TEST_SEQUENCE_LENGTH / 2) + num_bad_packets = MAX_MISSING_DTS - 1 + for i in range(bad_packet_start, bad_packet_start + num_bad_packets): + packets[i].dts = None + + decoded_stream = await async_decode_stream(hass, iter(packets)) + assert decoded_stream.finished + segments = decoded_stream.segments + # Check sequence numbers + assert all([segments[i].sequence == i + 1 for i in range(len(segments))]) + # Check segment durations (not counting the elongated segment) + assert ( + sum([segments[i].duration == SEGMENT_DURATION for i in range(len(segments))]) + >= len(segments) - 1 + ) + assert len(decoded_stream.video_packets) == num_packets - num_bad_packets + assert len(decoded_stream.audio_packets) == 0 + + +async def test_too_many_bad_packets(hass): + """Test bad packets are too many, causing it to end.""" + + num_packets = LONGER_TEST_SEQUENCE_LENGTH + packets = list(PacketSequence(num_packets)) + bad_packet_start = int(LONGER_TEST_SEQUENCE_LENGTH / 2) + num_bad_packets = MAX_MISSING_DTS + 1 + for i in range(bad_packet_start, bad_packet_start + num_bad_packets): + packets[i].dts = None + + decoded_stream = await async_decode_stream(hass, iter(packets)) + assert decoded_stream.finished + segments = decoded_stream.segments + assert len(segments) == int((bad_packet_start - 1) * SEGMENTS_PER_PACKET) + assert len(decoded_stream.video_packets) == bad_packet_start + assert len(decoded_stream.audio_packets) == 0 + + +async def test_no_video_stream(hass): + """Test no video stream in the container means no resulting output.""" + py_av = MockPyAv(video=False) + + decoded_stream = await async_decode_stream( + hass, PacketSequence(TEST_SEQUENCE_LENGTH), py_av=py_av + ) + # Note: This failure scenario does not output an end of stream + assert not decoded_stream.finished + segments = decoded_stream.segments + assert len(segments) == 0 + assert len(decoded_stream.video_packets) == 0 + assert len(decoded_stream.audio_packets) == 0 + + +async def test_audio_packets_not_found(hass): + """Set up an audio stream, but no audio packets are found.""" + py_av = MockPyAv(audio=True) + + num_packets = PACKETS_TO_WAIT_FOR_AUDIO + 1 + packets = PacketSequence(num_packets) # Contains only video packets + + decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av) + assert decoded_stream.finished + segments = decoded_stream.segments + assert len(segments) == int((num_packets - 1) * SEGMENTS_PER_PACKET) + assert len(decoded_stream.video_packets) == num_packets + assert len(decoded_stream.audio_packets) == 0 + + +async def test_audio_is_first_packet(hass): + """Set up an audio stream and audio packet is the first packet in the stream.""" + py_av = MockPyAv(audio=True) + + num_packets = PACKETS_TO_WAIT_FOR_AUDIO + 1 + packets = list(PacketSequence(num_packets)) + # Pair up an audio packet for each video packet + packets[0].stream = AUDIO_STREAM + packets[0].dts = packets[1].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE + packets[0].pts = packets[1].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE + packets[2].stream = AUDIO_STREAM + packets[2].dts = packets[3].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE + packets[2].pts = packets[3].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE + + decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av) + assert decoded_stream.finished + segments = decoded_stream.segments + # The audio packets are segmented with the video packets + assert len(segments) == int((num_packets - 2 - 1) * SEGMENTS_PER_PACKET) + assert len(decoded_stream.video_packets) == num_packets - 2 + assert len(decoded_stream.audio_packets) == 1 + + +async def test_audio_packets_found(hass): + """Set up an audio stream and audio packets are found at the start of the stream.""" + py_av = MockPyAv(audio=True) + + num_packets = PACKETS_TO_WAIT_FOR_AUDIO + 1 + packets = list(PacketSequence(num_packets)) + packets[1].stream = AUDIO_STREAM + packets[1].dts = packets[0].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE + packets[1].pts = packets[0].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE + + decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av) + assert decoded_stream.finished + segments = decoded_stream.segments + # The audio packet above is buffered with the video packet + assert len(segments) == int((num_packets - 1 - 1) * SEGMENTS_PER_PACKET) + assert len(decoded_stream.video_packets) == num_packets - 1 + assert len(decoded_stream.audio_packets) == 1 + + +async def test_pts_out_of_order(hass): + """Test pts can be out of order and still be valid.""" + + # Create a sequence of packets with some out of order pts + packets = list(PacketSequence(TEST_SEQUENCE_LENGTH)) + for i, _ in enumerate(packets): + if i % PACKETS_PER_SEGMENT == 1: + packets[i].pts = packets[i - 1].pts - 1 + packets[i].is_keyframe = False + + decoded_stream = await async_decode_stream(hass, iter(packets)) + assert decoded_stream.finished + segments = decoded_stream.segments + # Check number of segments + assert len(segments) == int((TEST_SEQUENCE_LENGTH - 1) * SEGMENTS_PER_PACKET) + # Check sequence numbers + assert all([segments[i].sequence == i + 1 for i in range(len(segments))]) + # Check segment durations + assert all([s.duration == SEGMENT_DURATION for s in segments]) + assert len(decoded_stream.video_packets) == len(packets) + assert len(decoded_stream.audio_packets) == 0 From 83a75b02ea84fe34726c5215044ac9a185c36487 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 1 Feb 2021 17:55:16 +0100 Subject: [PATCH 116/796] Code quality improvements to UniFi integration (#45794) * Do less inside try statements * Replace controller id with config entry id since it doesn't serve a purpose anymore * Improve how controller connection state is communicated in the client and device tracker Remove the need to disable arguments-differ lint * Remove broad exception handling from config flow I'm not sure there ever was a reason for this more than to catch all exceptions * Replace site string with constant for SSDP title_placeholders * Unload platforms in the defacto way * Noone reads the method descriptions * Improve file descriptions --- homeassistant/components/unifi/__init__.py | 14 +--- homeassistant/components/unifi/config_flow.py | 67 ++++++++----------- homeassistant/components/unifi/const.py | 2 - homeassistant/components/unifi/controller.py | 53 ++++++++------- .../components/unifi/device_tracker.py | 24 ++++--- homeassistant/components/unifi/sensor.py | 6 +- homeassistant/components/unifi/switch.py | 7 +- tests/components/unifi/test_config_flow.py | 26 ------- tests/components/unifi/test_controller.py | 8 ++- 9 files changed, 89 insertions(+), 118 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 439073497a2..a9d39251838 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -1,9 +1,8 @@ -"""Support for devices connected to UniFi POE.""" +"""Integration to UniFi controllers and its various features.""" from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from .config_flow import get_controller_id_from_config_entry from .const import ( ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN, @@ -82,21 +81,12 @@ class UnifiWirelessClients: @callback def get_data(self, config_entry): """Get data related to a specific controller.""" - controller_id = get_controller_id_from_config_entry(config_entry) - key = config_entry.entry_id - if controller_id in self.data: - key = controller_id - - data = self.data.get(key, {"wireless_devices": []}) + data = self.data.get(config_entry.entry_id, {"wireless_devices": []}) return set(data["wireless_devices"]) @callback def update_data(self, data, config_entry): """Update data and schedule to save to file.""" - controller_id = get_controller_id_from_config_entry(config_entry) - if controller_id in self.data: - self.data.pop(controller_id) - self.data[config_entry.entry_id] = {"wireless_devices": list(data)} self._store.async_delay_save(self._data_to_save, SAVE_DELAY) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 85fe55a4076..07b81621750 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -1,4 +1,10 @@ -"""Config flow for UniFi.""" +"""Config flow for UniFi. + +Provides user initiated configuration flow. +Discovery of controllers hosted on UDM and UDM Pro devices through SSDP. +Reauthentication when issue with credentials are reported. +Configuration of options through options flow. +""" import socket from urllib.parse import urlparse @@ -31,11 +37,9 @@ from .const import ( CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, - CONTROLLER_ID, DEFAULT_DPI_RESTRICTIONS, DEFAULT_POE_CLIENTS, DOMAIN as UNIFI_DOMAIN, - LOGGER, ) from .controller import get_controller from .errors import AuthenticationRequired, CannotConnect @@ -51,15 +55,6 @@ MODEL_PORTS = { } -@callback -def get_controller_id_from_config_entry(config_entry): - """Return controller with a matching bridge id.""" - return CONTROLLER_ID.format( - host=config_entry.data[CONF_CONTROLLER][CONF_HOST], - site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID], - ) - - class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): """Handle a UniFi config flow.""" @@ -86,19 +81,26 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): if user_input is not None: + self.config = { + CONF_HOST: user_input[CONF_HOST], + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_PORT: user_input.get(CONF_PORT), + CONF_VERIFY_SSL: user_input.get(CONF_VERIFY_SSL), + CONF_SITE_ID: DEFAULT_SITE_ID, + } + try: - self.config = { - CONF_HOST: user_input[CONF_HOST], - CONF_USERNAME: user_input[CONF_USERNAME], - CONF_PASSWORD: user_input[CONF_PASSWORD], - CONF_PORT: user_input.get(CONF_PORT), - CONF_VERIFY_SSL: user_input.get(CONF_VERIFY_SSL), - CONF_SITE_ID: DEFAULT_SITE_ID, - } - controller = await get_controller(self.hass, **self.config) - sites = await controller.sites() + + except AuthenticationRequired: + errors["base"] = "faulty_credentials" + + except CannotConnect: + errors["base"] = "service_unavailable" + + else: self.sites = {site["name"]: site["desc"] for site in sites.values()} if self.reauth_config.get(CONF_SITE_ID) in self.sites: @@ -108,19 +110,6 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): return await self.async_step_site() - except AuthenticationRequired: - errors["base"] = "faulty_credentials" - - except CannotConnect: - errors["base"] = "service_unavailable" - - except Exception: # pylint: disable=broad-except - LOGGER.error( - "Unknown error connecting with UniFi Controller at %s", - user_input[CONF_HOST], - ) - return self.async_abort(reason="unknown") - host = self.config.get(CONF_HOST) if not host and await async_discover_unifi(self.hass): host = "unifi" @@ -214,7 +203,7 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): return await self.async_step_user() async def async_step_ssdp(self, discovery_info): - """Handle a discovered unifi device.""" + """Handle a discovered UniFi device.""" parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) model_description = discovery_info[ssdp.ATTR_UPNP_MODEL_DESCRIPTION] mac_address = format_mac(discovery_info[ssdp.ATTR_UPNP_SERIAL]) @@ -232,7 +221,7 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): # pylint: disable=no-member self.context["title_placeholders"] = { CONF_HOST: self.config[CONF_HOST], - CONF_SITE_ID: "default", + CONF_SITE_ID: DEFAULT_SITE_ID, } port = MODEL_PORTS.get(model_description) @@ -242,7 +231,7 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): return await self.async_step_user() def _host_already_configured(self, host): - """See if we already have a unifi entry matching the host.""" + """See if we already have a UniFi entry matching the host.""" for entry in self._async_current_entries(): if not entry.data or CONF_CONTROLLER not in entry.data: continue @@ -271,7 +260,7 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): return await self.async_step_simple_options() async def async_step_simple_options(self, user_input=None): - """For simple Jack.""" + """For users without advanced settings enabled.""" if user_input is not None: self.options.update(user_input) return await self._update_options() diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index ba16612a903..94e2fad35ed 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -4,8 +4,6 @@ import logging LOGGER = logging.getLogger(__package__) DOMAIN = "unifi" -CONTROLLER_ID = "{host}-{site}" - CONF_CONTROLLER = "controller" CONF_SITE_ID = "site" diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 11e02d60a3f..bd55f4479fa 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -51,7 +51,6 @@ from .const import ( CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, - CONTROLLER_ID, DEFAULT_ALLOW_BANDWIDTH_SENSORS, DEFAULT_ALLOW_UPTIME_SENSORS, DEFAULT_DETECTION_TIME, @@ -109,9 +108,10 @@ class UniFiController: def load_config_entry_options(self): """Store attributes to avoid property call overhead since they are called frequently.""" - # Device tracker options options = self.config_entry.options + # Device tracker options + # Config entry option to not track clients. self.option_track_clients = options.get( CONF_TRACK_CLIENTS, DEFAULT_TRACK_CLIENTS @@ -157,11 +157,6 @@ class UniFiController: CONF_ALLOW_UPTIME_SENSORS, DEFAULT_ALLOW_UPTIME_SENSORS ) - @property - def controller_id(self): - """Return the controller ID.""" - return CONTROLLER_ID.format(host=self.host, site=self.site) - @property def host(self): """Return the host of this controller.""" @@ -260,25 +255,25 @@ class UniFiController: @property def signal_reachable(self) -> str: """Integration specific event to signal a change in connection status.""" - return f"unifi-reachable-{self.controller_id}" + return f"unifi-reachable-{self.config_entry.entry_id}" @property - def signal_update(self): + def signal_update(self) -> str: """Event specific per UniFi entry to signal new data.""" - return f"unifi-update-{self.controller_id}" + return f"unifi-update-{self.config_entry.entry_id}" @property - def signal_remove(self): + def signal_remove(self) -> str: """Event specific per UniFi entry to signal removal of entities.""" - return f"unifi-remove-{self.controller_id}" + return f"unifi-remove-{self.config_entry.entry_id}" @property - def signal_options_update(self): + def signal_options_update(self) -> str: """Event specific per UniFi entry to signal new options.""" - return f"unifi-options-{self.controller_id}" + return f"unifi-options-{self.config_entry.entry_id}" @property - def signal_heartbeat_missed(self): + def signal_heartbeat_missed(self) -> str: """Event specific per UniFi device tracker to signal new heartbeat missed.""" return "unifi-heartbeat-missed" @@ -309,14 +304,7 @@ class UniFiController: await self.api.initialize() sites = await self.api.sites() - - for site in sites.values(): - if self.site == site["name"]: - self._site_name = site["desc"] - break - description = await self.api.site_description() - self._site_role = description[0]["site_role"] except CannotConnect as err: raise ConfigEntryNotReady from err @@ -331,6 +319,13 @@ class UniFiController: ) return False + for site in sites.values(): + if self.site == site["name"]: + self._site_name = site["desc"] + break + + self._site_role = description[0]["site_role"] + # Restore clients that is not a part of active clients list. entity_registry = await self.hass.helpers.entity_registry.async_get_registry() for entity in entity_registry.entities.values(): @@ -452,10 +447,18 @@ class UniFiController: """ self.api.stop_websocket() - for platform in SUPPORTED_PLATFORMS: - await self.hass.config_entries.async_forward_entry_unload( - self.config_entry, platform + unload_ok = all( + await asyncio.gather( + *[ + self.hass.config_entries.async_forward_entry_unload( + self.config_entry, platform + ) + for platform in SUPPORTED_PLATFORMS + ] ) + ) + if not unload_ok: + return False for unsub_dispatcher in self.listeners: unsub_dispatcher() diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 6a4d986d5b2..ac28f7475f6 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -1,4 +1,4 @@ -"""Track devices using UniFi controllers.""" +"""Track both clients and devices using UniFi controllers.""" from datetime import timedelta from aiounifi.api import SOURCE_DATA, SOURCE_EVENT @@ -145,6 +145,7 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): self.heartbeat_check = False self._is_connected = False + self._controller_connection_state_changed = False if client.last_seen: self._is_connected = ( @@ -175,14 +176,16 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): @callback def async_signal_reachable_callback(self) -> None: """Call when controller connection state change.""" - self.async_update_callback(controller_state_change=True) + self._controller_connection_state_changed = True + super().async_signal_reachable_callback() - # pylint: disable=arguments-differ @callback - def async_update_callback(self, controller_state_change: bool = False) -> None: + def async_update_callback(self) -> None: """Update the clients state.""" - if controller_state_change: + if self._controller_connection_state_changed: + self._controller_connection_state_changed = False + if self.controller.available: self.schedule_update = True @@ -304,6 +307,7 @@ class UniFiDeviceTracker(UniFiBase, ScannerEntity): self.device = self._item self._is_connected = device.state == 1 + self._controller_connection_state_changed = False self.schedule_update = False async def async_added_to_hass(self) -> None: @@ -325,14 +329,16 @@ class UniFiDeviceTracker(UniFiBase, ScannerEntity): @callback def async_signal_reachable_callback(self) -> None: """Call when controller connection state change.""" - self.async_update_callback(controller_state_change=True) + self._controller_connection_state_changed = True + super().async_signal_reachable_callback() - # pylint: disable=arguments-differ @callback - def async_update_callback(self, controller_state_change: bool = False) -> None: + def async_update_callback(self) -> None: """Update the devices' state.""" - if controller_state_change: + if self._controller_connection_state_changed: + self._controller_connection_state_changed = False + if self.controller.available: if self._is_connected: self.schedule_update = True diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index c0b8cea09c2..f78ec614da1 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -1,4 +1,8 @@ -"""Support for bandwidth sensors with UniFi clients.""" +"""Sensor platform for UniFi integration. + +Support for bandwidth sensors of network clients. +Support for uptime sensors of network clients. +""" from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP, DOMAIN from homeassistant.const import DATA_MEGABYTES from homeassistant.core import callback diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 6aa42b0d291..e596e0b1e2a 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -1,4 +1,9 @@ -"""Support for devices connected to UniFi POE.""" +"""Switch platform for UniFi integration. + +Support for controlling power supply of clients which are powered over Ethernet (POE). +Support for controlling network access of clients selected in option flow. +Support for controlling deep packet inspection (DPI) restriction groups. +""" import logging from typing import Any diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 15220e68914..790e204c1dd 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -338,32 +338,6 @@ async def test_flow_fails_controller_unavailable(hass, aioclient_mock): assert result["errors"] == {"base": "service_unavailable"} -async def test_flow_fails_unknown_problem(hass, aioclient_mock): - """Test config flow.""" - result = await hass.config_entries.flow.async_init( - UNIFI_DOMAIN, context={"source": "user"} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - aioclient_mock.get("https://1.2.3.4:1234", status=302) - - with patch("aiounifi.Controller.login", side_effect=Exception): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_PORT: 1234, - CONF_VERIFY_SSL: True, - }, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - - async def test_reauth_flow_update_configuration(hass, aioclient_mock): """Verify reauth flow can update controller configuration.""" controller = await setup_unifi_integration(hass) diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 6acd507eaad..e484e041a88 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -201,9 +201,11 @@ async def test_controller_setup(hass): assert controller.mac is None - assert controller.signal_update == "unifi-update-1.2.3.4-site_id" - assert controller.signal_remove == "unifi-remove-1.2.3.4-site_id" - assert controller.signal_options_update == "unifi-options-1.2.3.4-site_id" + assert controller.signal_reachable == "unifi-reachable-1" + assert controller.signal_update == "unifi-update-1" + assert controller.signal_remove == "unifi-remove-1" + assert controller.signal_options_update == "unifi-options-1" + assert controller.signal_heartbeat_missed == "unifi-heartbeat-missed" async def test_controller_mac(hass): From 285bd3aa917943684ba4e1c1883d3348cb5f20ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Nenz=C3=A9n?= Date: Mon, 1 Feb 2021 18:12:56 +0100 Subject: [PATCH 117/796] Add support for Keg and Airlock to Plaato using polling API (#34760) Co-authored-by: J. Nick Koston --- .coveragerc | 6 +- homeassistant/components/plaato/__init__.py | 236 +++++++++++---- .../components/plaato/binary_sensor.py | 56 ++++ .../components/plaato/config_flow.py | 227 +++++++++++++- homeassistant/components/plaato/const.py | 26 +- homeassistant/components/plaato/entity.py | 103 +++++++ homeassistant/components/plaato/manifest.json | 6 +- homeassistant/components/plaato/sensor.py | 177 ++++------- homeassistant/components/plaato/strings.json | 45 ++- .../components/plaato/translations/en.json | 41 ++- requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/plaato/__init__.py | 1 + tests/components/plaato/test_config_flow.py | 286 ++++++++++++++++++ 14 files changed, 1019 insertions(+), 197 deletions(-) create mode 100644 homeassistant/components/plaato/binary_sensor.py create mode 100644 homeassistant/components/plaato/entity.py create mode 100644 tests/components/plaato/__init__.py create mode 100644 tests/components/plaato/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 0609051f196..3a7f33f6050 100644 --- a/.coveragerc +++ b/.coveragerc @@ -702,7 +702,11 @@ omit = homeassistant/components/ping/device_tracker.py homeassistant/components/pioneer/media_player.py homeassistant/components/pjlink/media_player.py - homeassistant/components/plaato/* + homeassistant/components/plaato/__init__.py + homeassistant/components/plaato/binary_sensor.py + homeassistant/components/plaato/const.py + homeassistant/components/plaato/entity.py + homeassistant/components/plaato/sensor.py homeassistant/components/plex/media_player.py homeassistant/components/plum_lightpad/light.py homeassistant/components/pocketcasts/sensor.py diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py index b365c7e0081..2cf97d5fd9a 100644 --- a/homeassistant/components/plaato/__init__.py +++ b/homeassistant/components/plaato/__init__.py @@ -1,11 +1,34 @@ -"""Support for Plaato Airlock.""" +"""Support for Plaato devices.""" + +import asyncio +from datetime import timedelta import logging from aiohttp import web +from pyplaato.models.airlock import PlaatoAirlock +from pyplaato.plaato import ( + ATTR_ABV, + ATTR_BATCH_VOLUME, + ATTR_BPM, + ATTR_BUBBLES, + ATTR_CO2_VOLUME, + ATTR_DEVICE_ID, + ATTR_DEVICE_NAME, + ATTR_OG, + ATTR_SG, + ATTR_TEMP, + ATTR_TEMP_UNIT, + ATTR_VOLUME_UNIT, + Plaato, + PlaatoDeviceType, +) import voluptuous as vol from homeassistant.components.sensor import DOMAIN as SENSOR +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + CONF_SCAN_INTERVAL, + CONF_TOKEN, CONF_WEBHOOK_ID, HTTP_OK, TEMP_CELSIUS, @@ -13,31 +36,33 @@ from homeassistant.const import ( VOLUME_GALLONS, VOLUME_LITERS, ) +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN +from .const import ( + CONF_DEVICE_NAME, + CONF_DEVICE_TYPE, + CONF_USE_WEBHOOK, + COORDINATOR, + DEFAULT_SCAN_INTERVAL, + DEVICE, + DEVICE_ID, + DEVICE_NAME, + DEVICE_TYPE, + DOMAIN, + PLATFORMS, + SENSOR_DATA, + UNDO_UPDATE_LISTENER, +) _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ["webhook"] -PLAATO_DEVICE_SENSORS = "sensors" -PLAATO_DEVICE_ATTRS = "attrs" - -ATTR_DEVICE_ID = "device_id" -ATTR_DEVICE_NAME = "device_name" -ATTR_TEMP_UNIT = "temp_unit" -ATTR_VOLUME_UNIT = "volume_unit" -ATTR_BPM = "bpm" -ATTR_TEMP = "temp" -ATTR_SG = "sg" -ATTR_OG = "og" -ATTR_BUBBLES = "bubbles" -ATTR_ABV = "abv" -ATTR_CO2_VOLUME = "co2_volume" -ATTR_BATCH_VOLUME = "batch_volume" - SENSOR_UPDATE = f"{DOMAIN}_sensor_update" SENSOR_DATA_KEY = f"{DOMAIN}.{SENSOR}" @@ -60,31 +85,124 @@ WEBHOOK_SCHEMA = vol.Schema( ) -async def async_setup(hass, hass_config): +async def async_setup(hass: HomeAssistant, config: dict): """Set up the Plaato component.""" + hass.data.setdefault(DOMAIN, {}) return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Configure based on config entry.""" - if DOMAIN not in hass.data: - hass.data[DOMAIN] = {} + use_webhook = entry.data[CONF_USE_WEBHOOK] + + if use_webhook: + async_setup_webhook(hass, entry) + else: + await async_setup_coordinator(hass, entry) + + for platform in PLATFORMS: + if entry.options.get(platform, True): + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + return True + + +@callback +def async_setup_webhook(hass: HomeAssistant, entry: ConfigEntry): + """Init webhook based on config entry.""" webhook_id = entry.data[CONF_WEBHOOK_ID] - hass.components.webhook.async_register(DOMAIN, "Plaato", webhook_id, handle_webhook) + device_name = entry.data[CONF_DEVICE_NAME] - hass.async_create_task(hass.config_entries.async_forward_entry_setup(entry, SENSOR)) + _set_entry_data(entry, hass) - return True + hass.components.webhook.async_register( + DOMAIN, f"{DOMAIN}.{device_name}", webhook_id, handle_webhook + ) -async def async_unload_entry(hass, entry): +async def async_setup_coordinator(hass: HomeAssistant, entry: ConfigEntry): + """Init auth token based on config entry.""" + auth_token = entry.data[CONF_TOKEN] + device_type = entry.data[CONF_DEVICE_TYPE] + + if entry.options.get(CONF_SCAN_INTERVAL): + update_interval = timedelta(minutes=entry.options[CONF_SCAN_INTERVAL]) + else: + update_interval = timedelta(minutes=DEFAULT_SCAN_INTERVAL) + + coordinator = PlaatoCoordinator(hass, auth_token, device_type, update_interval) + await coordinator.async_refresh() + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + _set_entry_data(entry, hass, coordinator, auth_token) + + for platform in PLATFORMS: + if entry.options.get(platform, True): + coordinator.platforms.append(platform) + + +def _set_entry_data(entry, hass, coordinator=None, device_id=None): + device = { + DEVICE_NAME: entry.data[CONF_DEVICE_NAME], + DEVICE_TYPE: entry.data[CONF_DEVICE_TYPE], + DEVICE_ID: device_id, + } + + hass.data[DOMAIN][entry.entry_id] = { + COORDINATOR: coordinator, + DEVICE: device, + SENSOR_DATA: None, + UNDO_UPDATE_LISTENER: entry.add_update_listener(_async_update_listener), + } + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) - hass.data[SENSOR_DATA_KEY]() + use_webhook = entry.data[CONF_USE_WEBHOOK] + hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() - await hass.config_entries.async_forward_entry_unload(entry, SENSOR) - return True + if use_webhook: + return await async_unload_webhook(hass, entry) + + return await async_unload_coordinator(hass, entry) + + +async def async_unload_webhook(hass: HomeAssistant, entry: ConfigEntry): + """Unload webhook based entry.""" + if entry.data[CONF_WEBHOOK_ID] is not None: + hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) + return await async_unload_platforms(hass, entry, PLATFORMS) + + +async def async_unload_coordinator(hass: HomeAssistant, entry: ConfigEntry): + """Unload auth token based entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR] + return await async_unload_platforms(hass, entry, coordinator.platforms) + + +async def async_unload_platforms(hass: HomeAssistant, entry: ConfigEntry, platforms): + """Unload platforms.""" + unloaded = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in platforms + ] + ) + ) + if unloaded: + hass.data[DOMAIN].pop(entry.entry_id) + + return unloaded + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) async def handle_webhook(hass, webhook_id, request): @@ -96,31 +214,9 @@ async def handle_webhook(hass, webhook_id, request): return device_id = _device_id(data) + sensor_data = PlaatoAirlock.from_web_hook(data) - attrs = { - ATTR_DEVICE_NAME: data.get(ATTR_DEVICE_NAME), - ATTR_DEVICE_ID: data.get(ATTR_DEVICE_ID), - ATTR_TEMP_UNIT: data.get(ATTR_TEMP_UNIT), - ATTR_VOLUME_UNIT: data.get(ATTR_VOLUME_UNIT), - } - - sensors = { - ATTR_TEMP: data.get(ATTR_TEMP), - ATTR_BPM: data.get(ATTR_BPM), - ATTR_SG: data.get(ATTR_SG), - ATTR_OG: data.get(ATTR_OG), - ATTR_ABV: data.get(ATTR_ABV), - ATTR_CO2_VOLUME: data.get(ATTR_CO2_VOLUME), - ATTR_BATCH_VOLUME: data.get(ATTR_BATCH_VOLUME), - ATTR_BUBBLES: data.get(ATTR_BUBBLES), - } - - hass.data[DOMAIN][device_id] = { - PLAATO_DEVICE_ATTRS: attrs, - PLAATO_DEVICE_SENSORS: sensors, - } - - async_dispatcher_send(hass, SENSOR_UPDATE, device_id) + async_dispatcher_send(hass, SENSOR_UPDATE, *(device_id, sensor_data)) return web.Response(text=f"Saving status for {device_id}", status=HTTP_OK) @@ -128,3 +224,35 @@ async def handle_webhook(hass, webhook_id, request): def _device_id(data): """Return name of device sensor.""" return f"{data.get(ATTR_DEVICE_NAME)}_{data.get(ATTR_DEVICE_ID)}" + + +class PlaatoCoordinator(DataUpdateCoordinator): + """Class to manage fetching data from the API.""" + + def __init__( + self, + hass, + auth_token, + device_type: PlaatoDeviceType, + update_interval: timedelta, + ): + """Initialize.""" + self.api = Plaato(auth_token=auth_token) + self.hass = hass + self.device_type = device_type + self.platforms = [] + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=update_interval, + ) + + async def _async_update_data(self): + """Update data via library.""" + data = await self.api.get_data( + session=aiohttp_client.async_get_clientsession(self.hass), + device_type=self.device_type, + ) + return data diff --git a/homeassistant/components/plaato/binary_sensor.py b/homeassistant/components/plaato/binary_sensor.py new file mode 100644 index 00000000000..0ee61b7668b --- /dev/null +++ b/homeassistant/components/plaato/binary_sensor.py @@ -0,0 +1,56 @@ +"""Support for Plaato Airlock sensors.""" + +import logging + +from pyplaato.plaato import PlaatoKeg + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PROBLEM, + BinarySensorEntity, +) + +from .const import CONF_USE_WEBHOOK, COORDINATOR, DOMAIN +from .entity import PlaatoEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Plaato from a config entry.""" + + if config_entry.data[CONF_USE_WEBHOOK]: + return False + + coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] + async_add_entities( + PlaatoBinarySensor( + hass.data[DOMAIN][config_entry.entry_id], + sensor_type, + coordinator, + ) + for sensor_type in coordinator.data.binary_sensors.keys() + ) + + return True + + +class PlaatoBinarySensor(PlaatoEntity, BinarySensorEntity): + """Representation of a Binary Sensor.""" + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + if self._coordinator is not None: + return self._coordinator.data.binary_sensors.get(self._sensor_type) + return False + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + if self._coordinator is None: + return None + if self._sensor_type is PlaatoKeg.Pins.LEAK_DETECTION: + return DEVICE_CLASS_PROBLEM + if self._sensor_type is PlaatoKeg.Pins.POURING: + return DEVICE_CLASS_OPENING diff --git a/homeassistant/components/plaato/config_flow.py b/homeassistant/components/plaato/config_flow.py index 3c616c822fb..290776f47c1 100644 --- a/homeassistant/components/plaato/config_flow.py +++ b/homeassistant/components/plaato/config_flow.py @@ -1,10 +1,223 @@ -"""Config flow for GPSLogger.""" -from homeassistant.helpers import config_entry_flow +"""Config flow for Plaato.""" +import logging -from .const import DOMAIN +from pyplaato.plaato import PlaatoDeviceType +import voluptuous as vol -config_entry_flow.register_webhook_flow( - DOMAIN, - "Webhook", - {"docs_url": "https://www.home-assistant.io/integrations/plaato/"}, +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_WEBHOOK_ID +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv + +from .const import ( + CONF_CLOUDHOOK, + CONF_DEVICE_NAME, + CONF_DEVICE_TYPE, + CONF_USE_WEBHOOK, + DEFAULT_SCAN_INTERVAL, + DOCS_URL, + PLACEHOLDER_DEVICE_NAME, + PLACEHOLDER_DEVICE_TYPE, + PLACEHOLDER_DOCS_URL, + PLACEHOLDER_WEBHOOK_URL, ) +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__package__) + + +class PlaatoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handles a Plaato config flow.""" + + VERSION = 2 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize.""" + self._init_info = {} + + async def async_step_user(self, user_input=None): + """Handle user step.""" + + if user_input is not None: + self._init_info[CONF_DEVICE_TYPE] = PlaatoDeviceType( + user_input[CONF_DEVICE_TYPE] + ) + self._init_info[CONF_DEVICE_NAME] = user_input[CONF_DEVICE_NAME] + + return await self.async_step_api_method() + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_DEVICE_NAME, + default=self._init_info.get(CONF_DEVICE_NAME, None), + ): str, + vol.Required( + CONF_DEVICE_TYPE, + default=self._init_info.get(CONF_DEVICE_TYPE, None), + ): vol.In(list(PlaatoDeviceType)), + } + ), + ) + + async def async_step_api_method(self, user_input=None): + """Handle device type step.""" + + device_type = self._init_info[CONF_DEVICE_TYPE] + + if user_input is not None: + token = user_input.get(CONF_TOKEN, None) + use_webhook = user_input.get(CONF_USE_WEBHOOK, False) + + if not token and not use_webhook: + errors = {"base": PlaatoConfigFlow._get_error(device_type)} + return await self._show_api_method_form(device_type, errors) + + self._init_info[CONF_USE_WEBHOOK] = use_webhook + self._init_info[CONF_TOKEN] = token + return await self.async_step_webhook() + + return await self._show_api_method_form(device_type) + + async def async_step_webhook(self, user_input=None): + """Validate config step.""" + + use_webhook = self._init_info[CONF_USE_WEBHOOK] + + if use_webhook and user_input is None: + webhook_id, webhook_url, cloudhook = await self._get_webhook_id() + self._init_info[CONF_WEBHOOK_ID] = webhook_id + self._init_info[CONF_CLOUDHOOK] = cloudhook + + return self.async_show_form( + step_id="webhook", + description_placeholders={ + PLACEHOLDER_WEBHOOK_URL: webhook_url, + PLACEHOLDER_DOCS_URL: DOCS_URL, + }, + ) + + return await self._async_create_entry() + + async def _async_create_entry(self): + """Create the entry step.""" + + webhook_id = self._init_info.get(CONF_WEBHOOK_ID, None) + auth_token = self._init_info[CONF_TOKEN] + device_name = self._init_info[CONF_DEVICE_NAME] + device_type = self._init_info[CONF_DEVICE_TYPE] + + unique_id = auth_token if auth_token else webhook_id + + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=device_type.name, + data=self._init_info, + description_placeholders={ + PLACEHOLDER_DEVICE_TYPE: device_type.name, + PLACEHOLDER_DEVICE_NAME: device_name, + }, + ) + + async def _show_api_method_form( + self, device_type: PlaatoDeviceType, errors: dict = None + ): + data_scheme = vol.Schema({vol.Optional(CONF_TOKEN, default=""): str}) + + if device_type == PlaatoDeviceType.Airlock: + data_scheme = data_scheme.extend( + {vol.Optional(CONF_USE_WEBHOOK, default=False): bool} + ) + + return self.async_show_form( + step_id="api_method", + data_schema=data_scheme, + errors=errors, + description_placeholders={PLACEHOLDER_DEVICE_TYPE: device_type.name}, + ) + + async def _get_webhook_id(self): + """Generate webhook ID.""" + webhook_id = self.hass.components.webhook.async_generate_id() + if self.hass.components.cloud.async_active_subscription(): + webhook_url = await self.hass.components.cloud.async_create_cloudhook( + webhook_id + ) + cloudhook = True + else: + webhook_url = self.hass.components.webhook.async_generate_url(webhook_id) + cloudhook = False + + return webhook_id, webhook_url, cloudhook + + @staticmethod + def _get_error(device_type: PlaatoDeviceType): + if device_type == PlaatoDeviceType.Airlock: + return "no_api_method" + return "no_auth_token" + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return PlaatoOptionsFlowHandler(config_entry) + + +class PlaatoOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Plaato options.""" + + def __init__(self, config_entry: ConfigEntry): + """Initialize domain options flow.""" + super().__init__() + + self._config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + use_webhook = self._config_entry.data.get(CONF_USE_WEBHOOK, False) + if use_webhook: + return await self.async_step_webhook() + + return await self.async_step_user() + + async def async_step_user(self, user_input=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="user", + data_schema=vol.Schema( + { + vol.Optional( + CONF_SCAN_INTERVAL, + default=self._config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ), + ): cv.positive_int + } + ), + ) + + async def async_step_webhook(self, user_input=None): + """Manage the options for webhook device.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + webhook_id = self._config_entry.data.get(CONF_WEBHOOK_ID, None) + webhook_url = ( + "" + if webhook_id is None + else self.hass.components.webhook.async_generate_url(webhook_id) + ) + + return self.async_show_form( + step_id="webhook", + description_placeholders={PLACEHOLDER_WEBHOOK_URL: webhook_url}, + ) diff --git a/homeassistant/components/plaato/const.py b/homeassistant/components/plaato/const.py index cbe8fcd2b6d..f50eaaac0ed 100644 --- a/homeassistant/components/plaato/const.py +++ b/homeassistant/components/plaato/const.py @@ -1,3 +1,27 @@ -"""Const for GPSLogger.""" +"""Const for Plaato.""" +from datetime import timedelta DOMAIN = "plaato" +PLAATO_DEVICE_SENSORS = "sensors" +PLAATO_DEVICE_ATTRS = "attrs" +SENSOR_SIGNAL = f"{DOMAIN}_%s_%s" + +CONF_USE_WEBHOOK = "use_webhook" +CONF_DEVICE_TYPE = "device_type" +CONF_DEVICE_NAME = "device_name" +CONF_CLOUDHOOK = "cloudhook" +PLACEHOLDER_WEBHOOK_URL = "webhook_url" +PLACEHOLDER_DOCS_URL = "docs_url" +PLACEHOLDER_DEVICE_TYPE = "device_type" +PLACEHOLDER_DEVICE_NAME = "device_name" +DOCS_URL = "https://www.home-assistant.io/integrations/plaato/" +PLATFORMS = ["sensor", "binary_sensor"] +SENSOR_DATA = "sensor_data" +COORDINATOR = "coordinator" +DEVICE = "device" +DEVICE_NAME = "device_name" +DEVICE_TYPE = "device_type" +DEVICE_ID = "device_id" +UNDO_UPDATE_LISTENER = "undo_update_listener" +DEFAULT_SCAN_INTERVAL = 5 +MIN_UPDATE_INTERVAL = timedelta(minutes=1) diff --git a/homeassistant/components/plaato/entity.py b/homeassistant/components/plaato/entity.py new file mode 100644 index 00000000000..6f72c3419a4 --- /dev/null +++ b/homeassistant/components/plaato/entity.py @@ -0,0 +1,103 @@ +"""PlaatoEntity class.""" +from pyplaato.models.device import PlaatoDevice + +from homeassistant.helpers import entity + +from .const import ( + DEVICE, + DEVICE_ID, + DEVICE_NAME, + DEVICE_TYPE, + DOMAIN, + SENSOR_DATA, + SENSOR_SIGNAL, +) + + +class PlaatoEntity(entity.Entity): + """Representation of a Plaato Entity.""" + + def __init__(self, data, sensor_type, coordinator=None): + """Initialize the sensor.""" + self._coordinator = coordinator + self._entry_data = data + self._sensor_type = sensor_type + self._device_id = data[DEVICE][DEVICE_ID] + self._device_type = data[DEVICE][DEVICE_TYPE] + self._device_name = data[DEVICE][DEVICE_NAME] + self._state = 0 + + @property + def _attributes(self) -> dict: + return PlaatoEntity._to_snake_case(self._sensor_data.attributes) + + @property + def _sensor_name(self) -> str: + return self._sensor_data.get_sensor_name(self._sensor_type) + + @property + def _sensor_data(self) -> PlaatoDevice: + if self._coordinator: + return self._coordinator.data + return self._entry_data[SENSOR_DATA] + + @property + def name(self): + """Return the name of the sensor.""" + return f"{DOMAIN} {self._device_type} {self._device_name} {self._sensor_name}".title() + + @property + def unique_id(self): + """Return the unique ID of this sensor.""" + return f"{self._device_id}_{self._sensor_type}" + + @property + def device_info(self): + """Get device info.""" + device_info = { + "identifiers": {(DOMAIN, self._device_id)}, + "name": self._device_name, + "manufacturer": "Plaato", + "model": self._device_type, + } + + if self._sensor_data.firmware_version != "": + device_info["sw_version"] = self._sensor_data.firmware_version + + return device_info + + @property + def device_state_attributes(self): + """Return the state attributes of the monitored installation.""" + if self._attributes is not None: + return self._attributes + + @property + def available(self): + """Return if sensor is available.""" + if self._coordinator is not None: + return self._coordinator.last_update_success + return True + + @property + def should_poll(self): + """Return the polling state.""" + return False + + async def async_added_to_hass(self): + """When entity is added to hass.""" + if self._coordinator is not None: + self.async_on_remove( + self._coordinator.async_add_listener(self.async_write_ha_state) + ) + else: + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SENSOR_SIGNAL % (self._device_id, self._sensor_type), + self.async_write_ha_state, + ) + ) + + @staticmethod + def _to_snake_case(dictionary: dict): + return {k.lower().replace(" ", "_"): v for k, v in dictionary.items()} diff --git a/homeassistant/components/plaato/manifest.json b/homeassistant/components/plaato/manifest.json index 29e104b13ed..e3291e5a229 100644 --- a/homeassistant/components/plaato/manifest.json +++ b/homeassistant/components/plaato/manifest.json @@ -1,8 +1,10 @@ { "domain": "plaato", - "name": "Plaato Airlock", + "name": "Plaato", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plaato", "dependencies": ["webhook"], - "codeowners": ["@JohNan"] + "after_dependencies": ["cloud"], + "codeowners": ["@JohNan"], + "requirements": ["pyplaato==0.0.15"] } diff --git a/homeassistant/components/plaato/sensor.py b/homeassistant/components/plaato/sensor.py index 3f8034698fd..3f5e467f504 100644 --- a/homeassistant/components/plaato/sensor.py +++ b/homeassistant/components/plaato/sensor.py @@ -1,28 +1,29 @@ """Support for Plaato Airlock sensors.""" import logging +from typing import Optional -from homeassistant.const import PERCENTAGE +from pyplaato.models.device import PlaatoDevice +from pyplaato.plaato import PlaatoKeg + +from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import Entity -from . import ( - ATTR_ABV, - ATTR_BATCH_VOLUME, - ATTR_BPM, - ATTR_CO2_VOLUME, - ATTR_TEMP, - ATTR_TEMP_UNIT, - ATTR_VOLUME_UNIT, - DOMAIN as PLAATO_DOMAIN, - PLAATO_DEVICE_ATTRS, - PLAATO_DEVICE_SENSORS, - SENSOR_DATA_KEY, - SENSOR_UPDATE, +from . import ATTR_TEMP, SENSOR_UPDATE +from ...core import callback +from .const import ( + CONF_USE_WEBHOOK, + COORDINATOR, + DEVICE, + DEVICE_ID, + DOMAIN, + SENSOR_DATA, + SENSOR_SIGNAL, ) +from .entity import PlaatoEntity _LOGGER = logging.getLogger(__name__) @@ -31,134 +32,58 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Set up the Plaato sensor.""" -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry(hass, entry, async_add_entities): """Set up Plaato from a config entry.""" - devices = {} + entry_data = hass.data[DOMAIN][entry.entry_id] - def get_device(device_id): - """Get a device.""" - return hass.data[PLAATO_DOMAIN].get(device_id, False) - - def get_device_sensors(device_id): - """Get device sensors.""" - return hass.data[PLAATO_DOMAIN].get(device_id).get(PLAATO_DEVICE_SENSORS) - - async def _update_sensor(device_id): + @callback + async def _async_update_from_webhook(device_id, sensor_data: PlaatoDevice): """Update/Create the sensors.""" - if device_id not in devices and get_device(device_id): - entities = [] - sensors = get_device_sensors(device_id) + entry_data[SENSOR_DATA] = sensor_data - for sensor_type in sensors: - entities.append(PlaatoSensor(device_id, sensor_type)) - - devices[device_id] = entities - - async_add_entities(entities, True) + if device_id != entry_data[DEVICE][DEVICE_ID]: + entry_data[DEVICE][DEVICE_ID] = device_id + async_add_entities( + [ + PlaatoSensor(entry_data, sensor_type) + for sensor_type in sensor_data.sensors + ] + ) else: - for entity in devices[device_id]: - async_dispatcher_send(hass, f"{PLAATO_DOMAIN}_{entity.unique_id}") + for sensor_type in sensor_data.sensors: + async_dispatcher_send(hass, SENSOR_SIGNAL % (device_id, sensor_type)) - hass.data[SENSOR_DATA_KEY] = async_dispatcher_connect( - hass, SENSOR_UPDATE, _update_sensor - ) + if entry.data[CONF_USE_WEBHOOK]: + async_dispatcher_connect(hass, SENSOR_UPDATE, _async_update_from_webhook) + else: + coordinator = entry_data[COORDINATOR] + async_add_entities( + PlaatoSensor(entry_data, sensor_type, coordinator) + for sensor_type in coordinator.data.sensors.keys() + ) return True -class PlaatoSensor(Entity): - """Representation of a Sensor.""" - - def __init__(self, device_id, sensor_type): - """Initialize the sensor.""" - self._device_id = device_id - self._type = sensor_type - self._state = 0 - self._name = f"{device_id} {sensor_type}" - self._attributes = None +class PlaatoSensor(PlaatoEntity): + """Representation of a Plaato Sensor.""" @property - def name(self): - """Return the name of the sensor.""" - return f"{PLAATO_DOMAIN} {self._name}" - - @property - def unique_id(self): - """Return the unique ID of this sensor.""" - return f"{self._device_id}_{self._type}" - - @property - def device_info(self): - """Get device info.""" - return { - "identifiers": {(PLAATO_DOMAIN, self._device_id)}, - "name": self._device_id, - "manufacturer": "Plaato", - "model": "Airlock", - } - - def get_sensors(self): - """Get device sensors.""" - return ( - self.hass.data[PLAATO_DOMAIN] - .get(self._device_id) - .get(PLAATO_DEVICE_SENSORS, False) - ) - - def get_sensors_unit_of_measurement(self, sensor_type): - """Get unit of measurement for sensor of type.""" - return ( - self.hass.data[PLAATO_DOMAIN] - .get(self._device_id) - .get(PLAATO_DEVICE_ATTRS, []) - .get(sensor_type, "") - ) + def device_class(self) -> Optional[str]: + """Return the class of this device, from component DEVICE_CLASSES.""" + if self._coordinator is not None: + if self._sensor_type == PlaatoKeg.Pins.TEMPERATURE: + return DEVICE_CLASS_TEMPERATURE + if self._sensor_type == ATTR_TEMP: + return DEVICE_CLASS_TEMPERATURE + return None @property def state(self): """Return the state of the sensor.""" - sensors = self.get_sensors() - if sensors is False: - _LOGGER.debug("Device with name %s has no sensors", self.name) - return 0 - - if self._type == ATTR_ABV: - return round(sensors.get(self._type), 2) - if self._type == ATTR_TEMP: - return round(sensors.get(self._type), 1) - if self._type == ATTR_CO2_VOLUME: - return round(sensors.get(self._type), 2) - return sensors.get(self._type) - - @property - def device_state_attributes(self): - """Return the state attributes of the monitored installation.""" - if self._attributes is not None: - return self._attributes + return self._sensor_data.sensors.get(self._sensor_type) @property def unit_of_measurement(self): """Return the unit of measurement.""" - if self._type == ATTR_TEMP: - return self.get_sensors_unit_of_measurement(ATTR_TEMP_UNIT) - if self._type == ATTR_BATCH_VOLUME or self._type == ATTR_CO2_VOLUME: - return self.get_sensors_unit_of_measurement(ATTR_VOLUME_UNIT) - if self._type == ATTR_BPM: - return "bpm" - if self._type == ATTR_ABV: - return PERCENTAGE - - return "" - - @property - def should_poll(self): - """Return the polling state.""" - return False - - async def async_added_to_hass(self): - """Register callbacks.""" - self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - f"{PLAATO_DOMAIN}_{self.unique_id}", self.async_write_ha_state - ) - ) + return self._sensor_data.get_unit_of_measurement(self._sensor_type) diff --git a/homeassistant/components/plaato/strings.json b/homeassistant/components/plaato/strings.json index 087cee13683..852ecc88dde 100644 --- a/homeassistant/components/plaato/strings.json +++ b/homeassistant/components/plaato/strings.json @@ -2,16 +2,53 @@ "config": { "step": { "user": { - "title": "Set up the Plaato Webhook", - "description": "[%key:common::config_flow::description::confirm_setup%]" + "title": "Set up the Plaato devices", + "description": "[%key:common::config_flow::description::confirm_setup%]", + "data": { + "device_name": "Name your device", + "device_type": "Type of Plaato device" + } + }, + "api_method": { + "title": "Select API method", + "description": "To be able to query the API an `auth_token` is required which can be obtained by following [these](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instructions\n\n Selected device: **{device_type}** \n\nIf you rather use the built in webhook method (Airlock only) please check the box below and leave Auth Token blank", + "data": { + "use_webhook": "Use webhook", + "token": "Paste Auth Token here" + } + }, + "webhook": { + "title": "Webhook to use", + "description": "To send events to Home Assistant, you will need to setup the webhook feature in Plaato Airlock.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details." } }, + "error": { + "invalid_webhook_device": "You have selected a device that doesn't not support sending data to a webhook. It is only available for the Airlock", + "no_auth_token": "You need to add an auth token", + "no_api_method": "You need to add an auth token or select webhook" + }, "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", - "webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]" + "webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" }, "create_entry": { - "default": "To send events to Home Assistant, you will need to setup the webhook feature in Plaato Airlock.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details." + "default": "Your Plaato {device_type} with name **{device_name}** was successfully setup!" + } + }, + "options": { + "step": { + "webhook": { + "title": "Options for Plaato Airlock", + "description": "Webhook info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n" + }, + "user": { + "title": "Options for Plaato", + "description": "Set the update interval (minutes)", + "data": { + "update_interval": "Update interval (minutes)" + } + } } } } diff --git a/homeassistant/components/plaato/translations/en.json b/homeassistant/components/plaato/translations/en.json index 6f25c15583c..64d41d0091e 100644 --- a/homeassistant/components/plaato/translations/en.json +++ b/homeassistant/components/plaato/translations/en.json @@ -1,16 +1,53 @@ { "config": { "abort": { + "already_configured": "Account is already configured", "single_instance_allowed": "Already configured. Only a single configuration possible.", "webhook_not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive webhook messages." }, "create_entry": { - "default": "To send events to Home Assistant, you will need to setup the webhook feature in Plaato Airlock.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details." + "default": "Your Plaato {device_type} with name **{device_name}** was successfully setup!" + }, + "error": { + "invalid_webhook_device": "You have selected a device that doesn't not support sending data to a webhook. It is only available for the Airlock", + "no_api_method": "You need to add an auth token or select webhook", + "no_auth_token": "You need to add an auth token" }, "step": { + "api_method": { + "data": { + "token": "Paste Auth Token here", + "use_webhook": "Use webhook" + }, + "description": "To be able to query the API an `auth_token` is required which can be obtained by following [these](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instructions\n\n Selected device: **{device_type}** \n\nIf you rather use the built in webhook method (Airlock only) please check the box below and leave Auth Token blank", + "title": "Select API method" + }, "user": { + "data": { + "device_name": "Name your device", + "device_type": "Type of Plaato device" + }, "description": "Do you want to start set up?", - "title": "Set up the Plaato Webhook" + "title": "Set up the Plaato devices" + }, + "webhook": { + "description": "To send events to Home Assistant, you will need to setup the webhook feature in Plaato Airlock.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details.", + "title": "Webhook to use" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "Update interval (minutes)" + }, + "description": "Set the update interval (minutes)", + "title": "Options for Plaato" + }, + "webhook": { + "description": "Webhook info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n", + "title": "Options for Plaato Airlock" } } } diff --git a/requirements_all.txt b/requirements_all.txt index 804b0495297..c192883c95f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1615,6 +1615,9 @@ pypck==0.7.9 # homeassistant.components.pjlink pypjlink2==1.2.1 +# homeassistant.components.plaato +pyplaato==0.0.15 + # homeassistant.components.point pypoint==2.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f855ab4bacc..5a30ae13e8c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -842,6 +842,9 @@ pyowm==3.1.1 # homeassistant.components.onewire pyownet==0.10.0.post1 +# homeassistant.components.plaato +pyplaato==0.0.15 + # homeassistant.components.point pypoint==2.0.0 diff --git a/tests/components/plaato/__init__.py b/tests/components/plaato/__init__.py new file mode 100644 index 00000000000..dac4d341790 --- /dev/null +++ b/tests/components/plaato/__init__.py @@ -0,0 +1 @@ +"""Tests for the Plaato integration.""" diff --git a/tests/components/plaato/test_config_flow.py b/tests/components/plaato/test_config_flow.py new file mode 100644 index 00000000000..10562b6aa60 --- /dev/null +++ b/tests/components/plaato/test_config_flow.py @@ -0,0 +1,286 @@ +"""Test the Plaato config flow.""" +from unittest.mock import patch + +from pyplaato.models.device import PlaatoDeviceType +import pytest + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.plaato.const import ( + CONF_DEVICE_NAME, + CONF_DEVICE_TYPE, + CONF_USE_WEBHOOK, + DOMAIN, +) +from homeassistant.const import CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_WEBHOOK_ID +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + +from tests.common import MockConfigEntry + +BASE_URL = "http://example.com" +WEBHOOK_ID = "webhook_id" +UNIQUE_ID = "plaato_unique_id" + + +@pytest.fixture(name="webhook_id") +def mock_webhook_id(): + """Mock webhook_id.""" + with patch( + "homeassistant.components.webhook.async_generate_id", return_value=WEBHOOK_ID + ), patch( + "homeassistant.components.webhook.async_generate_url", return_value="hook_id" + ): + yield + + +async def test_show_config_form(hass): + """Test show configuration 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["step_id"] == "user" + + +async def test_show_config_form_device_type_airlock(hass): + """Test show configuration form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={ + CONF_DEVICE_TYPE: PlaatoDeviceType.Airlock, + CONF_DEVICE_NAME: "device_name", + }, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "api_method" + assert result["data_schema"].schema.get(CONF_TOKEN) == str + assert result["data_schema"].schema.get(CONF_USE_WEBHOOK) == bool + + +async def test_show_config_form_device_type_keg(hass): + """Test show configuration form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={CONF_DEVICE_TYPE: PlaatoDeviceType.Keg, CONF_DEVICE_NAME: "device_name"}, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "api_method" + assert result["data_schema"].schema.get(CONF_TOKEN) == str + assert result["data_schema"].schema.get(CONF_USE_WEBHOOK) is None + + +async def test_show_config_form_validate_webhook(hass, webhook_id): + """Test show configuration form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_DEVICE_TYPE: PlaatoDeviceType.Airlock, + CONF_DEVICE_NAME: "device_name", + }, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "api_method" + + async def return_async_value(val): + return val + + hass.config.components.add("cloud") + with patch( + "homeassistant.components.cloud.async_active_subscription", return_value=True + ), patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value=return_async_value("https://hooks.nabu.casa/ABCD"), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_TOKEN: "", + CONF_USE_WEBHOOK: True, + }, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "webhook" + + +async def test_show_config_form_validate_token(hass): + """Test show configuration form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_DEVICE_TYPE: PlaatoDeviceType.Keg, + CONF_DEVICE_NAME: "device_name", + }, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "api_method" + + with patch("homeassistant.components.plaato.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_TOKEN: "valid_token"} + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == PlaatoDeviceType.Keg.name + assert result["data"] == { + CONF_USE_WEBHOOK: False, + CONF_TOKEN: "valid_token", + CONF_DEVICE_TYPE: PlaatoDeviceType.Keg, + CONF_DEVICE_NAME: "device_name", + } + + +async def test_show_config_form_no_cloud_webhook(hass, webhook_id): + """Test show configuration form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_DEVICE_TYPE: PlaatoDeviceType.Airlock, + CONF_DEVICE_NAME: "device_name", + }, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "api_method" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USE_WEBHOOK: True, + CONF_TOKEN: "", + }, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "webhook" + assert result["errors"] is None + + +async def test_show_config_form_api_method_no_auth_token(hass, webhook_id): + """Test show configuration form.""" + + # Using Keg + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_DEVICE_TYPE: PlaatoDeviceType.Keg, + CONF_DEVICE_NAME: "device_name", + }, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "api_method" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_TOKEN: ""} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "api_method" + assert len(result["errors"]) == 1 + assert result["errors"]["base"] == "no_auth_token" + + # Using Airlock + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_DEVICE_TYPE: PlaatoDeviceType.Airlock, + CONF_DEVICE_NAME: "device_name", + }, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "api_method" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_TOKEN: ""} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "api_method" + assert len(result["errors"]) == 1 + assert result["errors"]["base"] == "no_api_method" + + +async def test_options(hass): + """Test updating options.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="NAME", + data={}, + options={CONF_SCAN_INTERVAL: 5}, + ) + + config_entry.add_to_hass(hass) + with patch("homeassistant.components.plaato.async_setup_entry", return_value=True): + 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"] == "user" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_SCAN_INTERVAL: 10}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_SCAN_INTERVAL] == 10 + + +async def test_options_webhook(hass, webhook_id): + """Test updating options.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="NAME", + data={CONF_USE_WEBHOOK: True, CONF_WEBHOOK_ID: None}, + options={CONF_SCAN_INTERVAL: 5}, + ) + + config_entry.add_to_hass(hass) + with patch("homeassistant.components.plaato.async_setup_entry", return_value=True): + 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"] == "webhook" + assert result["description_placeholders"] == {"webhook_url": ""} + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_WEBHOOK_ID: WEBHOOK_ID}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_WEBHOOK_ID] == CONF_WEBHOOK_ID From c90588d35d1ba73f97e11ce3e9dbbb52f2a8d734 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 1 Feb 2021 18:15:34 +0100 Subject: [PATCH 118/796] Correct synology_dsm CPU sensor's naming and measurement unit (#45500) --- .../components/synology_dsm/const.py | 21 ++++++++++--------- .../components/synology_dsm/sensor.py | 5 +++++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index f9bcc8b61b8..1c4e004f749 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -37,6 +37,7 @@ DEFAULT_PORT_SSL = 5001 DEFAULT_SCAN_INTERVAL = 15 # min DEFAULT_TIMEOUT = 10 # sec +ENTITY_UNIT_LOAD = "load" ENTITY_NAME = "name" ENTITY_UNIT = "unit" @@ -95,50 +96,50 @@ STORAGE_DISK_BINARY_SENSORS = { # Sensors UTILISATION_SENSORS = { f"{SynoCoreUtilization.API_KEY}:cpu_other_load": { - ENTITY_NAME: "CPU Load (Other)", + ENTITY_NAME: "CPU Utilization (Other)", ENTITY_UNIT: PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: False, }, f"{SynoCoreUtilization.API_KEY}:cpu_user_load": { - ENTITY_NAME: "CPU Load (User)", + ENTITY_NAME: "CPU Utilization (User)", ENTITY_UNIT: PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, f"{SynoCoreUtilization.API_KEY}:cpu_system_load": { - ENTITY_NAME: "CPU Load (System)", + ENTITY_NAME: "CPU Utilization (System)", ENTITY_UNIT: PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: False, }, f"{SynoCoreUtilization.API_KEY}:cpu_total_load": { - ENTITY_NAME: "CPU Load (Total)", + ENTITY_NAME: "CPU Utilization (Total)", ENTITY_UNIT: PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, f"{SynoCoreUtilization.API_KEY}:cpu_1min_load": { - ENTITY_NAME: "CPU Load (1 min)", - ENTITY_UNIT: PERCENTAGE, + ENTITY_NAME: "CPU Load Averarge (1 min)", + ENTITY_UNIT: ENTITY_UNIT_LOAD, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: False, }, f"{SynoCoreUtilization.API_KEY}:cpu_5min_load": { - ENTITY_NAME: "CPU Load (5 min)", - ENTITY_UNIT: PERCENTAGE, + ENTITY_NAME: "CPU Load Averarge (5 min)", + ENTITY_UNIT: ENTITY_UNIT_LOAD, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, f"{SynoCoreUtilization.API_KEY}:cpu_15min_load": { - ENTITY_NAME: "CPU Load (15 min)", - ENTITY_UNIT: PERCENTAGE, + ENTITY_NAME: "CPU Load Averarge (15 min)", + ENTITY_UNIT: ENTITY_UNIT_LOAD, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: True, diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index dd2df61165d..7dd4e5e9870 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -19,6 +19,7 @@ from . import SynoApi, SynologyDSMDeviceEntity, SynologyDSMDispatcherEntity from .const import ( CONF_VOLUMES, DOMAIN, + ENTITY_UNIT_LOAD, INFORMATION_SENSORS, STORAGE_DISK_SENSORS, STORAGE_VOL_SENSORS, @@ -88,6 +89,10 @@ class SynoDSMUtilSensor(SynologyDSMDispatcherEntity): if self._unit == DATA_RATE_KILOBYTES_PER_SECOND: return round(attr / 1024.0, 1) + # CPU load average + if self._unit == ENTITY_UNIT_LOAD: + return round(attr / 100, 2) + return attr @property From 776b1395de18da6b037eb0aad14512be2a7a13cd Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 1 Feb 2021 20:43:43 +0100 Subject: [PATCH 119/796] Bump brother library to version 0.2.0 (#45832) --- homeassistant/components/brother/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index 9bb9ba00261..52286cd2c68 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -3,7 +3,7 @@ "name": "Brother Printer", "documentation": "https://www.home-assistant.io/integrations/brother", "codeowners": ["@bieniu"], - "requirements": ["brother==0.1.20"], + "requirements": ["brother==0.2.0"], "zeroconf": [{ "type": "_printer._tcp.local.", "name": "brother*" }], "config_flow": true, "quality_scale": "platinum" diff --git a/requirements_all.txt b/requirements_all.txt index c192883c95f..285140101a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -383,7 +383,7 @@ bravia-tv==1.0.8 broadlink==0.16.0 # homeassistant.components.brother -brother==0.1.20 +brother==0.2.0 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5a30ae13e8c..a5dc813c407 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -213,7 +213,7 @@ bravia-tv==1.0.8 broadlink==0.16.0 # homeassistant.components.brother -brother==0.1.20 +brother==0.2.0 # homeassistant.components.bsblan bsblan==0.4.0 From 197c857e1fa1b031195f75f109339b59c34cae37 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 1 Feb 2021 13:46:06 -0600 Subject: [PATCH 120/796] Search all endpoints for value in zwave_js (#45809) Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/climate.py | 1 + homeassistant/components/zwave_js/entity.py | 31 +- tests/components/zwave_js/conftest.py | 26 + tests/components/zwave_js/test_climate.py | 9 + ..._ct100_plus_different_endpoints_state.json | 727 ++++++++++++++++++ 5 files changed, 787 insertions(+), 7 deletions(-) create mode 100644 tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 07d4a3f7d0f..6c0b4a0335e 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -140,6 +140,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): THERMOSTAT_CURRENT_TEMP_PROPERTY, command_class=CommandClass.SENSOR_MULTILEVEL, add_to_watched_value_ids=True, + check_all_endpoints=True, ) self._set_modes_and_presets() diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 5c64ddbc496..84870ba75f4 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -121,6 +121,7 @@ class ZWaveBaseEntity(Entity): endpoint: Optional[int] = None, value_property_key_name: Optional[str] = None, add_to_watched_value_ids: bool = True, + check_all_endpoints: bool = False, ) -> Optional[ZwaveValue]: """Return specific ZwaveValue on this ZwaveNode.""" # use commandclass and endpoint from primary value if omitted @@ -129,17 +130,33 @@ class ZWaveBaseEntity(Entity): command_class = self.info.primary_value.command_class if endpoint is None: endpoint = self.info.primary_value.endpoint + + # Build partial event data dictionary so we can change the endpoint later + partial_evt_data = { + "commandClass": command_class, + "property": value_property, + "propertyKeyName": value_property_key_name, + } + # lookup value by value_id value_id = get_value_id( - self.info.node, - { - "commandClass": command_class, - "endpoint": endpoint, - "property": value_property, - "propertyKeyName": value_property_key_name, - }, + self.info.node, {**partial_evt_data, "endpoint": endpoint} ) return_value = self.info.node.values.get(value_id) + + # If we haven't found a value and check_all_endpoints is True, we should + # return the first value we can find on any other endpoint + if return_value is None and check_all_endpoints: + for endpoint_ in self.info.node.endpoints: + if endpoint_.index != self.info.primary_value.endpoint: + value_id = get_value_id( + self.info.node, + {**partial_evt_data, "endpoint": endpoint_.index}, + ) + return_value = self.info.node.values.get(value_id) + if return_value: + break + # add to watched_ids list so we will be triggered when the value updates if ( return_value diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 470b4c0227b..b6cbc911b6a 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -111,6 +111,22 @@ def climate_radio_thermostat_ct100_plus_state_fixture(): ) +@pytest.fixture( + name="climate_radio_thermostat_ct100_plus_different_endpoints_state", + scope="session", +) +def climate_radio_thermostat_ct100_plus_different_endpoints_state_fixture(): + """Load the thermostat fixture state with values on different endpoints. + + This device is a radio thermostat ct100. + """ + return json.loads( + load_fixture( + "zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json" + ) + ) + + @pytest.fixture(name="nortek_thermostat_state", scope="session") def nortek_thermostat_state_fixture(): """Load the nortek thermostat node state fixture data.""" @@ -231,6 +247,16 @@ def climate_radio_thermostat_ct100_plus_fixture( return node +@pytest.fixture(name="climate_radio_thermostat_ct100_plus_different_endpoints") +def climate_radio_thermostat_ct100_plus_different_endpoints_fixture( + client, climate_radio_thermostat_ct100_plus_different_endpoints_state +): + """Mock a climate radio thermostat ct100 plus node with values on different endpoints.""" + node = Node(client, climate_radio_thermostat_ct100_plus_different_endpoints_state) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="nortek_thermostat") def nortek_thermostat_fixture(client, nortek_thermostat_state): """Mock a nortek thermostat node.""" diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index aca1022f8b0..f7deefc1360 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -324,3 +324,12 @@ async def test_thermostat_v2( }, blocking=True, ) + + +async def test_thermostat_different_endpoints( + hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, integration +): + """Test an entity with values on a different endpoint from the primary value.""" + state = hass.states.get(CLIMATE_RADIO_THERMOSTAT_ENTITY) + + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.5 diff --git a/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json b/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json new file mode 100644 index 00000000000..ea38dfd9d6b --- /dev/null +++ b/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json @@ -0,0 +1,727 @@ +{ + "nodeId": 26, + "index": 0, + "installerIcon": 4608, + "userIcon": 4608, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Routing Slave", + "generic": "Thermostat", + "specific": "Thermostat General V2", + "mandatorySupportedCCs": [ + "Basic", + "Manufacturer Specific", + "Thermostat Mode", + "Thermostat Setpoint", + "Version" + ], + "mandatoryControlCCs": [] + }, + "isListening": true, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 152, + "productId": 256, + "productType": 25602, + "firmwareVersion": "10.7", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 5, + "deviceConfig": { + "manufacturerId": 152, + "manufacturer": "Radio Thermostat Company of America (RTC)", + "label": "CT100 Plus", + "description": "Z-Wave Thermostat", + "devices": [{ "productType": "0x6402", "productId": "0x0100" }], + "firmwareVersion": { "min": "0.0", "max": "255.255" }, + "paramInformation": { "_map": {} } + }, + "label": "CT100 Plus", + "neighbors": [1, 2, 3, 4, 23], + "endpointCountIsDynamic": false, + "endpointsHaveIdenticalCapabilities": false, + "individualEndpointCount": 2, + "aggregatedEndpointCount": 0, + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 26, + "index": 0, + "installerIcon": 4608, + "userIcon": 4608 + }, + { "nodeId": 26, "index": 1 }, + { + "nodeId": 26, + "index": 2, + "installerIcon": 3328, + "userIcon": 3333 + } + ], + "values": [ + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "manufacturerId", + "propertyName": "manufacturerId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 152, + "ccVersion": 2 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productType", + "propertyName": "productType", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 25602, + "ccVersion": 2 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productId", + "propertyName": "productId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 256, + "ccVersion": 2 + }, + { + "commandClassName": "Thermostat Mode", + "commandClass": 64, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 31, + "label": "Thermostat mode", + "states": { + "0": "Off", + "1": "Heat", + "2": "Cool", + "3": "Auto", + "11": "Energy heat", + "12": "Energy cool" + } + }, + "value": 1, + "ccVersion": 2 + }, + { + "commandClassName": "Thermostat Mode", + "commandClass": 64, + "endpoint": 0, + "property": "manufacturerData", + "propertyName": "manufacturerData", + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "ccVersion": 2 + }, + { + "commandClassName": "Thermostat Setpoint", + "commandClass": 67, + "endpoint": 0, + "property": "setpoint", + "propertyKey": 1, + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "unit": "\u00b0F", + "ccSpecific": { "setpointType": 1 } + }, + "value": 72, + "ccVersion": 2 + }, + { + "commandClassName": "Thermostat Setpoint", + "commandClass": 67, + "endpoint": 0, + "property": "setpoint", + "propertyKey": 2, + "propertyName": "setpoint", + "propertyKeyName": "Cooling", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "unit": "\u00b0F", + "ccSpecific": { "setpointType": 2 } + }, + "value": 73, + "ccVersion": 2 + }, + { + "commandClassName": "Thermostat Setpoint", + "commandClass": 67, + "endpoint": 0, + "property": "setpoint", + "propertyKey": 11, + "propertyName": "setpoint", + "propertyKeyName": "Energy Save Heating", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "unit": "\u00b0F", + "ccSpecific": { "setpointType": 11 } + }, + "value": 62, + "ccVersion": 2 + }, + { + "commandClassName": "Thermostat Setpoint", + "commandClass": 67, + "endpoint": 0, + "property": "setpoint", + "propertyKey": 12, + "propertyName": "setpoint", + "propertyKeyName": "Energy Save Cooling", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "unit": "\u00b0F", + "ccSpecific": { "setpointType": 12 } + }, + "value": 85, + "ccVersion": 2 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "libraryType", + "propertyName": "libraryType", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3, + "ccVersion": 2 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "protocolVersion", + "propertyName": "protocolVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.24", + "ccVersion": 2 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["10.7"], + "ccVersion": 2 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + }, + "ccVersion": 2 + }, + { + "commandClassName": "Indicator", + "commandClass": 135, + "endpoint": 0, + "property": "value", + "propertyName": "value", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Indicator value", + "ccSpecific": { "indicatorId": 0 } + }, + "value": 0, + "ccVersion": 1 + }, + { + "commandClassName": "Thermostat Operating State", + "commandClass": 66, + "endpoint": 0, + "property": "state", + "propertyName": "state", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Operating state", + "states": { + "0": "Idle", + "1": "Heating", + "2": "Cooling", + "3": "Fan Only", + "4": "Pending Heat", + "5": "Pending Cool", + "6": "Vent/Economizer", + "7": "Aux Heating", + "8": "2nd Stage Heating", + "9": "2nd Stage Cooling", + "10": "2nd Stage Aux Heat", + "11": "3rd Stage Aux Heat" + } + }, + "value": 0, + "ccVersion": 2 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 1, + "propertyName": "Temperature Reporting Threshold", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 4, + "default": 2, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disabled", + "1": "0.5\u00b0 F", + "2": "1.0\u00b0 F", + "3": "1.5\u00b0 F", + "4": "2.0\u00b0 F" + }, + "label": "Temperature Reporting Threshold", + "description": "Reporting threshold for changes in the ambient temperature", + "isFromConfig": true + }, + "value": 2, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 2, + "propertyName": "HVAC Settings", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "valueSize": 4, + "min": 0, + "max": 0, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "HVAC Settings", + "description": "Configured HVAC settings", + "isFromConfig": true + }, + "value": 17891329, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 4, + "propertyName": "Power Status", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "valueSize": 1, + "min": 0, + "max": 0, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Power Status", + "description": "C-Wire / Battery Status", + "isFromConfig": true + }, + "value": 1, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 5, + "propertyName": "Humidity Reporting Threshold", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Disabled", + "1": "3% RH", + "2": "5% RH", + "3": "10% RH" + }, + "label": "Humidity Reporting Threshold", + "description": "Reporting threshold for changes in the relative humidity", + "isFromConfig": true + }, + "value": 2, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 6, + "propertyName": "Auxiliary/Emergency", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Auxiliary/Emergency heat disabled", + "1": "Auxiliary/Emergency heat enabled" + }, + "label": "Auxiliary/Emergency", + "description": "Enables or disables auxiliary / emergency heating", + "isFromConfig": true + }, + "value": 0, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 7, + "propertyName": "Thermostat Swing Temperature", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 8, + "default": 2, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "0.5\u00b0 F", + "2": "1.0\u00b0 F", + "3": "1.5\u00b0 F", + "4": "2.0\u00b0 F", + "5": "2.5\u00b0 F", + "6": "3.0\u00b0 F", + "7": "3.5\u00b0 F", + "8": "4.0\u00b0 F" + }, + "label": "Thermostat Swing Temperature", + "description": "Variance allowed from setpoint to engage HVAC", + "isFromConfig": true + }, + "value": 2, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 8, + "propertyName": "Thermostat Diff Temperature", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 4, + "max": 12, + "default": 4, + "format": 0, + "allowManualEntry": false, + "states": { + "4": "2.0\u00b0 F", + "8": "4.0\u00b0 F", + "12": "6.0\u00b0 F" + }, + "label": "Thermostat Diff Temperature", + "description": "Configures additional stages", + "isFromConfig": true + }, + "value": 1028, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 9, + "propertyName": "Thermostat Recovery Mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 2, + "default": 2, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "Fast recovery mode", + "2": "Economy recovery mode" + }, + "label": "Thermostat Recovery Mode", + "description": "Fast or Economy recovery mode", + "isFromConfig": true + }, + "value": 2, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 10, + "propertyName": "Temperature Reporting Filter", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 124, + "default": 124, + "format": 0, + "allowManualEntry": true, + "label": "Temperature Reporting Filter", + "description": "Upper/Lower bounds for thermostat temperature reporting", + "isFromConfig": true + }, + "value": 32000, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 11, + "propertyName": "Simple UI Mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Normal mode enabled", + "1": "Simple mode enabled" + }, + "label": "Simple UI Mode", + "description": "Simple mode enable/disable", + "isFromConfig": true + }, + "value": 1, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 12, + "propertyName": "Multicast", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Multicast disabled", + "1": "Multicast enabled" + }, + "label": "Multicast", + "description": "Enable or disables Multicast", + "isFromConfig": true + }, + "value": 0, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 3, + "propertyName": "Utility Lock Enable/Disable", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Utility lock disabled", + "1": "Utility lock enabled" + }, + "label": "Utility Lock Enable/Disable", + "description": "Prevents setpoint changes at thermostat", + "isFromConfig": true + }, + "ccVersion": 1 + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "level", + "propertyName": "level", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 100, + "unit": "%", + "label": "Battery level" + }, + "value": 100, + "ccVersion": 1 + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "isLow", + "propertyName": "isLow", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false, + "ccVersion": 1 + }, + { + "commandClassName": "Multilevel Sensor", + "commandClass": 49, + "endpoint": 2, + "property": "Air temperature", + "propertyName": "Air temperature", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "\u00b0F", + "label": "Air temperature", + "ccSpecific": { "sensorType": 1, "scale": 1 } + }, + "value": 72.5, + "ccVersion": 5 + }, + { + "commandClassName": "Multilevel Sensor", + "commandClass": 49, + "endpoint": 2, + "property": "Humidity", + "propertyName": "Humidity", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "%", + "label": "Humidity", + "ccSpecific": { "sensorType": 5, "scale": 0 } + }, + "value": 20, + "ccVersion": 5 + } + ] +} \ No newline at end of file From 3bdf9628385a381d28f1ee092ab686eed23f8163 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 1 Feb 2021 14:38:03 -0700 Subject: [PATCH 121/796] Add ability to configure AirVisual with city/state/country in UI (#44116) --- .../components/airvisual/__init__.py | 23 +- .../components/airvisual/config_flow.py | 162 +++++++----- homeassistant/components/airvisual/const.py | 3 +- homeassistant/components/airvisual/sensor.py | 45 +++- .../components/airvisual/strings.json | 22 +- .../components/airvisual/translations/en.json | 22 +- .../components/airvisual/test_config_flow.py | 237 +++++++++++++----- 7 files changed, 344 insertions(+), 170 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 956b168a665..3a88243b0b9 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -23,6 +23,7 @@ from homeassistant.const import ( CONF_STATE, ) from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -37,7 +38,7 @@ from .const import ( CONF_INTEGRATION_TYPE, DATA_COORDINATOR, DOMAIN, - INTEGRATION_TYPE_GEOGRAPHY, + INTEGRATION_TYPE_GEOGRAPHY_COORDS, INTEGRATION_TYPE_NODE_PRO, LOGGER, ) @@ -145,7 +146,7 @@ def _standardize_geography_config_entry(hass, config_entry): # If the config entry data doesn't contain the integration type, add it: entry_updates["data"] = { **config_entry.data, - CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY, + CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_COORDS, } if not entry_updates: @@ -232,7 +233,6 @@ async def async_setup_entry(hass, config_entry): update_method=async_update_data, ) - hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator async_sync_geo_coordinator_update_intervals( hass, config_entry.data[CONF_API_KEY] ) @@ -262,9 +262,11 @@ async def async_setup_entry(hass, config_entry): update_method=async_update_data, ) - hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator - await coordinator.async_refresh() + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator for component in PLATFORMS: hass.async_create_task( @@ -299,10 +301,14 @@ async def async_migrate_entry(hass, config_entry): # For any geographies that remain, create a new config entry for each one: for geography in geographies: + if CONF_LATITUDE in geography: + source = "geography_by_coords" + else: + source = "geography_by_name" hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, - context={"source": "geography"}, + context={"source": source}, data={CONF_API_KEY: config_entry.data[CONF_API_KEY], **geography}, ) ) @@ -327,7 +333,10 @@ async def async_unload_entry(hass, config_entry): remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id) remove_listener() - if config_entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_GEOGRAPHY: + if ( + config_entry.data[CONF_INTEGRATION_TYPE] + == INTEGRATION_TYPE_GEOGRAPHY_COORDS + ): # Re-calculate the update interval period for any remaining consumers of # this API key: async_sync_geo_coordinator_update_intervals( diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index b086aeefc27..266f7b7c2c2 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -2,7 +2,12 @@ import asyncio from pyairvisual import CloudAPI, NodeSamba -from pyairvisual.errors import InvalidKeyError, NodeProError +from pyairvisual.errors import ( + AirVisualError, + InvalidKeyError, + NodeProError, + NotFoundError, +) import voluptuous as vol from homeassistant import config_entries @@ -13,20 +18,46 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_PASSWORD, CONF_SHOW_ON_MAP, + CONF_STATE, ) from homeassistant.core import callback from homeassistant.helpers import aiohttp_client, config_validation as cv from . import async_get_geography_id from .const import ( # pylint: disable=unused-import - CONF_GEOGRAPHIES, + CONF_CITY, + CONF_COUNTRY, CONF_INTEGRATION_TYPE, DOMAIN, - INTEGRATION_TYPE_GEOGRAPHY, + INTEGRATION_TYPE_GEOGRAPHY_COORDS, + INTEGRATION_TYPE_GEOGRAPHY_NAME, INTEGRATION_TYPE_NODE_PRO, LOGGER, ) +API_KEY_DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): cv.string}) +GEOGRAPHY_NAME_SCHEMA = API_KEY_DATA_SCHEMA.extend( + { + vol.Required(CONF_CITY): cv.string, + vol.Required(CONF_STATE): cv.string, + vol.Required(CONF_COUNTRY): cv.string, + } +) +NODE_PRO_SCHEMA = vol.Schema( + {vol.Required(CONF_IP_ADDRESS): str, vol.Required(CONF_PASSWORD): cv.string} +) +PICK_INTEGRATION_TYPE_SCHEMA = vol.Schema( + { + vol.Required("type"): vol.In( + [ + INTEGRATION_TYPE_GEOGRAPHY_COORDS, + INTEGRATION_TYPE_GEOGRAPHY_NAME, + INTEGRATION_TYPE_NODE_PRO, + ] + ) + } +) + class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle an AirVisual config flow.""" @@ -36,16 +67,13 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the config flow.""" + self._entry_data_for_reauth = None self._geo_id = None - self._latitude = None - self._longitude = None - - self.api_key_data_schema = vol.Schema({vol.Required(CONF_API_KEY): str}) @property - def geography_schema(self): + def geography_coords_schema(self): """Return the data schema for the cloud API.""" - return self.api_key_data_schema.extend( + return API_KEY_DATA_SCHEMA.extend( { vol.Required( CONF_LATITUDE, default=self.hass.config.latitude @@ -56,24 +84,6 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } ) - @property - def pick_integration_type_schema(self): - """Return the data schema for picking the integration type.""" - return vol.Schema( - { - vol.Required("type"): vol.In( - [INTEGRATION_TYPE_GEOGRAPHY, INTEGRATION_TYPE_NODE_PRO] - ) - } - ) - - @property - def node_pro_schema(self): - """Return the data schema for a Node/Pro.""" - return vol.Schema( - {vol.Required(CONF_IP_ADDRESS): str, vol.Required(CONF_PASSWORD): str} - ) - async def _async_set_unique_id(self, unique_id): """Set the unique ID of the config flow and abort if it already exists.""" await self.async_set_unique_id(unique_id) @@ -85,33 +95,36 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Define the config flow to handle options.""" return AirVisualOptionsFlowHandler(config_entry) - async def async_step_geography(self, user_input=None): + async def async_step_geography(self, user_input, integration_type): """Handle the initialization of the integration via the cloud API.""" - if not user_input: - return self.async_show_form( - step_id="geography", data_schema=self.geography_schema - ) - self._geo_id = async_get_geography_id(user_input) await self._async_set_unique_id(self._geo_id) self._abort_if_unique_id_configured() + return await self.async_step_geography_finish(user_input, integration_type) - # Find older config entries without unique ID: - for entry in self._async_current_entries(): - if entry.version != 1: - continue + async def async_step_geography_by_coords(self, user_input=None): + """Handle the initialization of the cloud API based on latitude/longitude.""" + if not user_input: + return self.async_show_form( + step_id="geography_by_coords", data_schema=self.geography_coords_schema + ) - if any( - self._geo_id == async_get_geography_id(geography) - for geography in entry.data[CONF_GEOGRAPHIES] - ): - return self.async_abort(reason="already_configured") - - return await self.async_step_geography_finish( - user_input, "geography", self.geography_schema + return await self.async_step_geography( + user_input, INTEGRATION_TYPE_GEOGRAPHY_COORDS ) - async def async_step_geography_finish(self, user_input, error_step, error_schema): + async def async_step_geography_by_name(self, user_input=None): + """Handle the initialization of the cloud API based on city/state/country.""" + if not user_input: + return self.async_show_form( + step_id="geography_by_name", data_schema=GEOGRAPHY_NAME_SCHEMA + ) + + return await self.async_step_geography( + user_input, INTEGRATION_TYPE_GEOGRAPHY_NAME + ) + + async def async_step_geography_finish(self, user_input, integration_type): """Validate a Cloud API key.""" websession = aiohttp_client.async_get_clientsession(self.hass) cloud_api = CloudAPI(user_input[CONF_API_KEY], session=websession) @@ -123,16 +136,40 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): "airvisual_checked_api_keys_lock", asyncio.Lock() ) + if integration_type == INTEGRATION_TYPE_GEOGRAPHY_COORDS: + coro = cloud_api.air_quality.nearest_city() + error_schema = self.geography_coords_schema + error_step = "geography_by_coords" + else: + coro = cloud_api.air_quality.city( + user_input[CONF_CITY], user_input[CONF_STATE], user_input[CONF_COUNTRY] + ) + error_schema = GEOGRAPHY_NAME_SCHEMA + error_step = "geography_by_name" + async with valid_keys_lock: if user_input[CONF_API_KEY] not in valid_keys: try: - await cloud_api.air_quality.nearest_city() + await coro except InvalidKeyError: return self.async_show_form( step_id=error_step, data_schema=error_schema, errors={CONF_API_KEY: "invalid_api_key"}, ) + except NotFoundError: + return self.async_show_form( + step_id=error_step, + data_schema=error_schema, + errors={CONF_CITY: "location_not_found"}, + ) + except AirVisualError as err: + LOGGER.error(err) + return self.async_show_form( + step_id=error_step, + data_schema=error_schema, + errors={"base": "unknown"}, + ) valid_keys.add(user_input[CONF_API_KEY]) @@ -143,15 +180,13 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry( title=f"Cloud API ({self._geo_id})", - data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY}, + data={**user_input, CONF_INTEGRATION_TYPE: integration_type}, ) async def async_step_node_pro(self, user_input=None): """Handle the initialization of the integration with a Node/Pro.""" if not user_input: - return self.async_show_form( - step_id="node_pro", data_schema=self.node_pro_schema - ) + return self.async_show_form(step_id="node_pro", data_schema=NODE_PRO_SCHEMA) await self._async_set_unique_id(user_input[CONF_IP_ADDRESS]) @@ -163,7 +198,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): LOGGER.error("Error connecting to Node/Pro unit: %s", err) return self.async_show_form( step_id="node_pro", - data_schema=self.node_pro_schema, + data_schema=NODE_PRO_SCHEMA, errors={CONF_IP_ADDRESS: "cannot_connect"}, ) @@ -176,39 +211,34 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth(self, data): """Handle configuration by re-auth.""" + self._entry_data_for_reauth = data self._geo_id = async_get_geography_id(data) - self._latitude = data[CONF_LATITUDE] - self._longitude = data[CONF_LONGITUDE] - return await self.async_step_reauth_confirm() async def async_step_reauth_confirm(self, user_input=None): """Handle re-auth completion.""" if not user_input: return self.async_show_form( - step_id="reauth_confirm", data_schema=self.api_key_data_schema + step_id="reauth_confirm", data_schema=API_KEY_DATA_SCHEMA ) - conf = { - CONF_API_KEY: user_input[CONF_API_KEY], - CONF_LATITUDE: self._latitude, - CONF_LONGITUDE: self._longitude, - CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY, - } + conf = {CONF_API_KEY: user_input[CONF_API_KEY], **self._entry_data_for_reauth} return await self.async_step_geography_finish( - conf, "reauth_confirm", self.api_key_data_schema + conf, self._entry_data_for_reauth[CONF_INTEGRATION_TYPE] ) 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.pick_integration_type_schema + step_id="user", data_schema=PICK_INTEGRATION_TYPE_SCHEMA ) - if user_input["type"] == INTEGRATION_TYPE_GEOGRAPHY: - return await self.async_step_geography() + if user_input["type"] == INTEGRATION_TYPE_GEOGRAPHY_COORDS: + return await self.async_step_geography_by_coords() + if user_input["type"] == INTEGRATION_TYPE_GEOGRAPHY_NAME: + return await self.async_step_geography_by_name() return await self.async_step_node_pro() diff --git a/homeassistant/components/airvisual/const.py b/homeassistant/components/airvisual/const.py index a98a899b762..510ada2b68c 100644 --- a/homeassistant/components/airvisual/const.py +++ b/homeassistant/components/airvisual/const.py @@ -4,7 +4,8 @@ import logging DOMAIN = "airvisual" LOGGER = logging.getLogger(__package__) -INTEGRATION_TYPE_GEOGRAPHY = "Geographical Location" +INTEGRATION_TYPE_GEOGRAPHY_COORDS = "Geographical Location by Latitude/Longitude" +INTEGRATION_TYPE_GEOGRAPHY_NAME = "Geographical Location by Name" INTEGRATION_TYPE_NODE_PRO = "AirVisual Node/Pro" CONF_CITY = "city" diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index ae9995f36c3..680059af411 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -27,7 +27,8 @@ from .const import ( CONF_INTEGRATION_TYPE, DATA_COORDINATOR, DOMAIN, - INTEGRATION_TYPE_GEOGRAPHY, + INTEGRATION_TYPE_GEOGRAPHY_COORDS, + INTEGRATION_TYPE_GEOGRAPHY_NAME, ) _LOGGER = getLogger(__name__) @@ -115,7 +116,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up AirVisual sensors based on a config entry.""" coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] - if config_entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_GEOGRAPHY: + if config_entry.data[CONF_INTEGRATION_TYPE] in [ + INTEGRATION_TYPE_GEOGRAPHY_COORDS, + INTEGRATION_TYPE_GEOGRAPHY_NAME, + ]: sensors = [ AirVisualGeographySensor( coordinator, @@ -208,17 +212,32 @@ class AirVisualGeographySensor(AirVisualEntity): } ) - if CONF_LATITUDE in self._config_entry.data: - if self._config_entry.options[CONF_SHOW_ON_MAP]: - self._attrs[ATTR_LATITUDE] = self._config_entry.data[CONF_LATITUDE] - self._attrs[ATTR_LONGITUDE] = self._config_entry.data[CONF_LONGITUDE] - self._attrs.pop("lati", None) - self._attrs.pop("long", None) - else: - self._attrs["lati"] = self._config_entry.data[CONF_LATITUDE] - self._attrs["long"] = self._config_entry.data[CONF_LONGITUDE] - self._attrs.pop(ATTR_LATITUDE, None) - self._attrs.pop(ATTR_LONGITUDE, None) + # Displaying the geography on the map relies upon putting the latitude/longitude + # in the entity attributes with "latitude" and "longitude" as the keys. + # Conversely, we can hide the location on the map by using other keys, like + # "lati" and "long". + # + # We use any coordinates in the config entry and, in the case of a geography by + # name, we fall back to the latitude longitude provided in the coordinator data: + latitude = self._config_entry.data.get( + CONF_LATITUDE, + self.coordinator.data["location"]["coordinates"][1], + ) + longitude = self._config_entry.data.get( + CONF_LONGITUDE, + self.coordinator.data["location"]["coordinates"][0], + ) + + if self._config_entry.options[CONF_SHOW_ON_MAP]: + self._attrs[ATTR_LATITUDE] = latitude + self._attrs[ATTR_LONGITUDE] = longitude + self._attrs.pop("lati", None) + self._attrs.pop("long", None) + else: + self._attrs["lati"] = latitude + self._attrs["long"] = longitude + self._attrs.pop(ATTR_LATITUDE, None) + self._attrs.pop(ATTR_LONGITUDE, None) class AirVisualNodeProSensor(AirVisualEntity): diff --git a/homeassistant/components/airvisual/strings.json b/homeassistant/components/airvisual/strings.json index 22f9c80f313..8d2dce85a17 100644 --- a/homeassistant/components/airvisual/strings.json +++ b/homeassistant/components/airvisual/strings.json @@ -1,15 +1,25 @@ { "config": { "step": { - "geography": { + "geography_by_coords": { "title": "Configure a Geography", - "description": "Use the AirVisual cloud API to monitor a geographical location.", + "description": "Use the AirVisual cloud API to monitor a latitude/longitude.", "data": { "api_key": "[%key:common::config_flow::data::api_key%]", "latitude": "[%key:common::config_flow::data::latitude%]", "longitude": "[%key:common::config_flow::data::longitude%]" } }, + "geography_by_name": { + "title": "Configure a Geography", + "description": "Use the AirVisual cloud API to monitor a city/state/country.", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "city": "City", + "country": "Country", + "state": "state" + } + }, "node_pro": { "title": "Configure an AirVisual Node/Pro", "description": "Monitor a personal AirVisual unit. The password can be retrieved from the unit's UI.", @@ -26,17 +36,13 @@ }, "user": { "title": "Configure AirVisual", - "description": "Pick what type of AirVisual data you want to monitor.", - "data": { - "cloud_api": "Geographical Location", - "node_pro": "AirVisual Node Pro", - "type": "Integration Type" - } + "description": "Pick what type of AirVisual data you want to monitor." } }, "error": { "general_error": "[%key:common::config_flow::error::unknown%]", "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "location_not_found": "Location not found", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { diff --git a/homeassistant/components/airvisual/translations/en.json b/homeassistant/components/airvisual/translations/en.json index 129abcc29e5..1e3cb59a520 100644 --- a/homeassistant/components/airvisual/translations/en.json +++ b/homeassistant/components/airvisual/translations/en.json @@ -7,16 +7,27 @@ "error": { "cannot_connect": "Failed to connect", "general_error": "Unexpected error", - "invalid_api_key": "Invalid API key" + "invalid_api_key": "Invalid API key", + "location_not_found": "Location not found" }, "step": { - "geography": { + "geography_by_coords": { "data": { "api_key": "API Key", "latitude": "Latitude", "longitude": "Longitude" }, - "description": "Use the AirVisual cloud API to monitor a geographical location.", + "description": "Use the AirVisual cloud API to monitor a latitude/longitude.", + "title": "Configure a Geography" + }, + "geography_by_name": { + "data": { + "api_key": "API Key", + "city": "City", + "country": "Country", + "state": "state" + }, + "description": "Use the AirVisual cloud API to monitor a city/state/country.", "title": "Configure a Geography" }, "node_pro": { @@ -34,11 +45,6 @@ "title": "Re-authenticate AirVisual" }, "user": { - "data": { - "cloud_api": "Geographical Location", - "node_pro": "AirVisual Node Pro", - "type": "Integration Type" - }, "description": "Pick what type of AirVisual data you want to monitor.", "title": "Configure AirVisual" } diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index 4e550d94b09..248abaf6b5f 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -1,14 +1,22 @@ """Define tests for the AirVisual config flow.""" from unittest.mock import patch -from pyairvisual.errors import InvalidKeyError, NodeProError +from pyairvisual.errors import ( + AirVisualError, + InvalidKeyError, + NodeProError, + NotFoundError, +) from homeassistant import data_entry_flow -from homeassistant.components.airvisual import ( +from homeassistant.components.airvisual.const import ( + CONF_CITY, + CONF_COUNTRY, CONF_GEOGRAPHIES, CONF_INTEGRATION_TYPE, DOMAIN, - INTEGRATION_TYPE_GEOGRAPHY, + INTEGRATION_TYPE_GEOGRAPHY_COORDS, + INTEGRATION_TYPE_GEOGRAPHY_NAME, INTEGRATION_TYPE_NODE_PRO, ) from homeassistant.config_entries import SOURCE_USER @@ -19,6 +27,7 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_PASSWORD, CONF_SHOW_ON_MAP, + CONF_STATE, ) from homeassistant.setup import async_setup_component @@ -38,7 +47,9 @@ async def test_duplicate_error(hass): ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data={"type": "Geographical Location"} + DOMAIN, + context={"source": SOURCE_USER}, + data={"type": INTEGRATION_TYPE_GEOGRAPHY_COORDS}, ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=geography_conf @@ -64,14 +75,8 @@ async def test_duplicate_error(hass): assert result["reason"] == "already_configured" -async def test_invalid_identifier(hass): - """Test that an invalid API key or Node/Pro ID throws an error.""" - geography_conf = { - CONF_API_KEY: "abcde12345", - CONF_LATITUDE: 51.528308, - CONF_LONGITUDE: -0.3817765, - } - +async def test_invalid_identifier_geography_api_key(hass): + """Test that an invalid API key throws an error.""" with patch( "pyairvisual.air_quality.AirQuality.nearest_city", side_effect=InvalidKeyError, @@ -79,64 +84,73 @@ async def test_invalid_identifier(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, - data={"type": "Geographical Location"}, + data={"type": INTEGRATION_TYPE_GEOGRAPHY_COORDS}, ) result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=geography_conf + result["flow_id"], + user_input={ + CONF_API_KEY: "abcde12345", + CONF_LATITUDE: 51.528308, + CONF_LONGITUDE: -0.3817765, + }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} -async def test_migration(hass): - """Test migrating from version 1 to the current version.""" - conf = { - CONF_API_KEY: "abcde12345", - CONF_GEOGRAPHIES: [ - {CONF_LATITUDE: 51.528308, CONF_LONGITUDE: -0.3817765}, - {CONF_LATITUDE: 35.48847, CONF_LONGITUDE: 137.5263065}, - ], - } - - config_entry = MockConfigEntry( - domain=DOMAIN, version=1, unique_id="abcde12345", data=conf - ) - config_entry.add_to_hass(hass) - - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - - with patch("pyairvisual.air_quality.AirQuality.nearest_city"), patch.object( - hass.config_entries, "async_forward_entry_setup" +async def test_invalid_identifier_geography_name(hass): + """Test that an invalid location name throws an error.""" + with patch( + "pyairvisual.air_quality.AirQuality.city", + side_effect=NotFoundError, ): - assert await async_setup_component(hass, DOMAIN, {DOMAIN: conf}) - await hass.async_block_till_done() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={"type": INTEGRATION_TYPE_GEOGRAPHY_NAME}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "abcde12345", + CONF_CITY: "Beijing", + CONF_STATE: "Beijing", + CONF_COUNTRY: "China", + }, + ) - config_entries = hass.config_entries.async_entries(DOMAIN) - - assert len(config_entries) == 2 - - assert config_entries[0].unique_id == "51.528308, -0.3817765" - assert config_entries[0].title == "Cloud API (51.528308, -0.3817765)" - assert config_entries[0].data == { - CONF_API_KEY: "abcde12345", - CONF_LATITUDE: 51.528308, - CONF_LONGITUDE: -0.3817765, - CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY, - } - - assert config_entries[1].unique_id == "35.48847, 137.5263065" - assert config_entries[1].title == "Cloud API (35.48847, 137.5263065)" - assert config_entries[1].data == { - CONF_API_KEY: "abcde12345", - CONF_LATITUDE: 35.48847, - CONF_LONGITUDE: 137.5263065, - CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY, - } + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_CITY: "location_not_found"} -async def test_node_pro_error(hass): - """Test that an invalid Node/Pro ID shows an error.""" +async def test_invalid_identifier_geography_unknown(hass): + """Test that an unknown identifier issue throws an error.""" + with patch( + "pyairvisual.air_quality.AirQuality.city", + side_effect=AirVisualError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={"type": INTEGRATION_TYPE_GEOGRAPHY_NAME}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "abcde12345", + CONF_CITY: "Beijing", + CONF_STATE: "Beijing", + CONF_COUNTRY: "China", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "unknown"} + + +async def test_invalid_identifier_node_pro(hass): + """Test that an invalid Node/Pro identifier shows an error.""" node_pro_conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "my_password"} with patch( @@ -153,6 +167,53 @@ async def test_node_pro_error(hass): assert result["errors"] == {CONF_IP_ADDRESS: "cannot_connect"} +async def test_migration(hass): + """Test migrating from version 1 to the current version.""" + conf = { + CONF_API_KEY: "abcde12345", + CONF_GEOGRAPHIES: [ + {CONF_LATITUDE: 51.528308, CONF_LONGITUDE: -0.3817765}, + {CONF_CITY: "Beijing", CONF_STATE: "Beijing", CONF_COUNTRY: "China"}, + ], + } + + config_entry = MockConfigEntry( + domain=DOMAIN, version=1, unique_id="abcde12345", data=conf + ) + config_entry.add_to_hass(hass) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + with patch("pyairvisual.air_quality.AirQuality.city"), patch( + "pyairvisual.air_quality.AirQuality.nearest_city" + ), patch.object(hass.config_entries, "async_forward_entry_setup"): + assert await async_setup_component(hass, DOMAIN, {DOMAIN: conf}) + await hass.async_block_till_done() + + config_entries = hass.config_entries.async_entries(DOMAIN) + + assert len(config_entries) == 2 + + assert config_entries[0].unique_id == "51.528308, -0.3817765" + assert config_entries[0].title == "Cloud API (51.528308, -0.3817765)" + assert config_entries[0].data == { + CONF_API_KEY: "abcde12345", + CONF_LATITUDE: 51.528308, + CONF_LONGITUDE: -0.3817765, + CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_COORDS, + } + + assert config_entries[1].unique_id == "Beijing, Beijing, China" + assert config_entries[1].title == "Cloud API (Beijing, Beijing, China)" + assert config_entries[1].data == { + CONF_API_KEY: "abcde12345", + CONF_CITY: "Beijing", + CONF_STATE: "Beijing", + CONF_COUNTRY: "China", + CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_NAME, + } + + async def test_options_flow(hass): """Test config flow options.""" geography_conf = { @@ -186,8 +247,8 @@ async def test_options_flow(hass): assert config_entry.options == {CONF_SHOW_ON_MAP: False} -async def test_step_geography(hass): - """Test the geograph (cloud API) step.""" +async def test_step_geography_by_coords(hass): + """Test setting up a geopgraphy entry by latitude/longitude.""" conf = { CONF_API_KEY: "abcde12345", CONF_LATITUDE: 51.528308, @@ -200,7 +261,7 @@ async def test_step_geography(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, - data={"type": "Geographical Location"}, + data={"type": INTEGRATION_TYPE_GEOGRAPHY_COORDS}, ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=conf @@ -212,7 +273,39 @@ async def test_step_geography(hass): CONF_API_KEY: "abcde12345", CONF_LATITUDE: 51.528308, CONF_LONGITUDE: -0.3817765, - CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY, + CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_COORDS, + } + + +async def test_step_geography_by_name(hass): + """Test setting up a geopgraphy entry by city/state/country.""" + conf = { + CONF_API_KEY: "abcde12345", + CONF_CITY: "Beijing", + CONF_STATE: "Beijing", + CONF_COUNTRY: "China", + } + + with patch( + "homeassistant.components.airvisual.async_setup_entry", return_value=True + ), patch("pyairvisual.air_quality.AirQuality.city"): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={"type": INTEGRATION_TYPE_GEOGRAPHY_NAME}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=conf + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Cloud API (Beijing, Beijing, China)" + assert result["data"] == { + CONF_API_KEY: "abcde12345", + CONF_CITY: "Beijing", + CONF_STATE: "Beijing", + CONF_COUNTRY: "China", + CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_NAME, } @@ -244,18 +337,19 @@ async def test_step_node_pro(hass): async def test_step_reauth(hass): """Test that the reauth step works.""" - geography_conf = { + entry_data = { CONF_API_KEY: "abcde12345", CONF_LATITUDE: 51.528308, CONF_LONGITUDE: -0.3817765, + CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_COORDS, } MockConfigEntry( - domain=DOMAIN, unique_id="51.528308, -0.3817765", data=geography_conf + domain=DOMAIN, unique_id="51.528308, -0.3817765", data=entry_data ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=geography_conf + DOMAIN, context={"source": "reauth"}, data=entry_data ) assert result["step_id"] == "reauth_confirm" @@ -287,11 +381,20 @@ async def test_step_user(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, - data={"type": INTEGRATION_TYPE_GEOGRAPHY}, + data={"type": INTEGRATION_TYPE_GEOGRAPHY_COORDS}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "geography" + assert result["step_id"] == "geography_by_coords" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={"type": INTEGRATION_TYPE_GEOGRAPHY_NAME}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "geography_by_name" result = await hass.config_entries.flow.async_init( DOMAIN, From 8222eb5e3efce2f6f73ca447b0505be80b7bc9db Mon Sep 17 00:00:00 2001 From: Alessandro Pilotti Date: Tue, 2 Feb 2021 00:29:31 +0200 Subject: [PATCH 122/796] Allow Influxdb CA path in verify_ssl (#45270) --- homeassistant/components/influxdb/__init__.py | 9 +- homeassistant/components/influxdb/const.py | 4 +- .../components/influxdb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/influxdb/test_init.py | 133 ++++++++++++++++++ 6 files changed, 147 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index 16b6971b11f..e327f34d128 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -62,6 +62,7 @@ from .const import ( CONF_PRECISION, CONF_RETRY_COUNT, CONF_SSL, + CONF_SSL_CA_CERT, CONF_TAGS, CONF_TAGS_ATTRIBUTES, CONF_TOKEN, @@ -335,6 +336,9 @@ def get_influx_connection(conf, test_write=False, test_read=False): kwargs[CONF_URL] = conf[CONF_URL] kwargs[CONF_TOKEN] = conf[CONF_TOKEN] kwargs[INFLUX_CONF_ORG] = conf[CONF_ORG] + kwargs[CONF_VERIFY_SSL] = conf[CONF_VERIFY_SSL] + if CONF_SSL_CA_CERT in conf: + kwargs[CONF_SSL_CA_CERT] = conf[CONF_SSL_CA_CERT] bucket = conf.get(CONF_BUCKET) influx = InfluxDBClientV2(**kwargs) query_api = influx.query_api() @@ -392,7 +396,10 @@ def get_influx_connection(conf, test_write=False, test_read=False): return InfluxClient(buckets, write_v2, query_v2, close_v2) # Else it's a V1 client - kwargs[CONF_VERIFY_SSL] = conf[CONF_VERIFY_SSL] + if CONF_SSL_CA_CERT in conf and conf[CONF_VERIFY_SSL]: + kwargs[CONF_VERIFY_SSL] = conf[CONF_SSL_CA_CERT] + else: + kwargs[CONF_VERIFY_SSL] = conf[CONF_VERIFY_SSL] if CONF_DB_NAME in conf: kwargs[CONF_DB_NAME] = conf[CONF_DB_NAME] diff --git a/homeassistant/components/influxdb/const.py b/homeassistant/components/influxdb/const.py index 1a827c1b63c..e66a0fe10c4 100644 --- a/homeassistant/components/influxdb/const.py +++ b/homeassistant/components/influxdb/const.py @@ -31,6 +31,7 @@ CONF_COMPONENT_CONFIG_DOMAIN = "component_config_domain" CONF_RETRY_COUNT = "max_retries" CONF_IGNORE_ATTRIBUTES = "ignore_attributes" CONF_PRECISION = "precision" +CONF_SSL_CA_CERT = "ssl_ca_cert" CONF_LANGUAGE = "language" CONF_QUERIES = "queries" @@ -139,12 +140,13 @@ COMPONENT_CONFIG_SCHEMA_CONNECTION = { vol.Optional(CONF_PATH): cv.string, vol.Optional(CONF_PORT): cv.port, vol.Optional(CONF_SSL): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + vol.Optional(CONF_SSL_CA_CERT): cv.isfile, vol.Optional(CONF_PRECISION): vol.In(["ms", "s", "us", "ns"]), # Connection config for V1 API only. vol.Inclusive(CONF_USERNAME, "authentication"): cv.string, vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string, vol.Optional(CONF_DB_NAME, default=DEFAULT_DATABASE): cv.string, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, # Connection config for V2 API only. vol.Inclusive(CONF_TOKEN, "v2_authentication"): cv.string, vol.Inclusive(CONF_ORG, "v2_authentication"): cv.string, diff --git a/homeassistant/components/influxdb/manifest.json b/homeassistant/components/influxdb/manifest.json index ec1bd8f9594..c2d6f77e7c1 100644 --- a/homeassistant/components/influxdb/manifest.json +++ b/homeassistant/components/influxdb/manifest.json @@ -2,6 +2,6 @@ "domain": "influxdb", "name": "InfluxDB", "documentation": "https://www.home-assistant.io/integrations/influxdb", - "requirements": ["influxdb==5.2.3", "influxdb-client==1.8.0"], + "requirements": ["influxdb==5.2.3", "influxdb-client==1.14.0"], "codeowners": ["@fabaff", "@mdegat01"] } diff --git a/requirements_all.txt b/requirements_all.txt index 285140101a9..8a74e574f04 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -825,7 +825,7 @@ ihcsdk==2.7.0 incomfort-client==0.4.0 # homeassistant.components.influxdb -influxdb-client==1.8.0 +influxdb-client==1.14.0 # homeassistant.components.influxdb influxdb==5.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a5dc813c407..99f1c997ec1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -436,7 +436,7 @@ iaqualink==0.3.4 icmplib==2.0 # homeassistant.components.influxdb -influxdb-client==1.8.0 +influxdb-client==1.14.0 # homeassistant.components.influxdb influxdb==5.2.3 diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index db22b5c5236..fd43091f457 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -131,6 +131,139 @@ async def test_setup_config_full(hass, mock_client, config_ext, get_write_api): assert get_write_api(mock_client).call_count == 1 +@pytest.mark.parametrize( + "mock_client, config_base, config_ext, expected_client_args", + [ + ( + influxdb.DEFAULT_API_VERSION, + BASE_V1_CONFIG, + { + "ssl": True, + "verify_ssl": False, + }, + { + "ssl": True, + "verify_ssl": False, + }, + ), + ( + influxdb.DEFAULT_API_VERSION, + BASE_V1_CONFIG, + { + "ssl": True, + "verify_ssl": True, + }, + { + "ssl": True, + "verify_ssl": True, + }, + ), + ( + influxdb.DEFAULT_API_VERSION, + BASE_V1_CONFIG, + { + "ssl": True, + "verify_ssl": True, + "ssl_ca_cert": "fake/path/ca.pem", + }, + { + "ssl": True, + "verify_ssl": "fake/path/ca.pem", + }, + ), + ( + influxdb.DEFAULT_API_VERSION, + BASE_V1_CONFIG, + { + "ssl": True, + "ssl_ca_cert": "fake/path/ca.pem", + }, + { + "ssl": True, + "verify_ssl": "fake/path/ca.pem", + }, + ), + ( + influxdb.DEFAULT_API_VERSION, + BASE_V1_CONFIG, + { + "ssl": True, + "verify_ssl": False, + "ssl_ca_cert": "fake/path/ca.pem", + }, + { + "ssl": True, + "verify_ssl": False, + }, + ), + ( + influxdb.API_VERSION_2, + BASE_V2_CONFIG, + { + "api_version": influxdb.API_VERSION_2, + "verify_ssl": False, + }, + { + "verify_ssl": False, + }, + ), + ( + influxdb.API_VERSION_2, + BASE_V2_CONFIG, + { + "api_version": influxdb.API_VERSION_2, + "verify_ssl": True, + }, + { + "verify_ssl": True, + }, + ), + ( + influxdb.API_VERSION_2, + BASE_V2_CONFIG, + { + "api_version": influxdb.API_VERSION_2, + "verify_ssl": True, + "ssl_ca_cert": "fake/path/ca.pem", + }, + { + "verify_ssl": True, + "ssl_ca_cert": "fake/path/ca.pem", + }, + ), + ( + influxdb.API_VERSION_2, + BASE_V2_CONFIG, + { + "api_version": influxdb.API_VERSION_2, + "verify_ssl": False, + "ssl_ca_cert": "fake/path/ca.pem", + }, + { + "verify_ssl": False, + "ssl_ca_cert": "fake/path/ca.pem", + }, + ), + ], + indirect=["mock_client"], +) +async def test_setup_config_ssl( + hass, mock_client, config_base, config_ext, expected_client_args +): + """Test the setup with various verify_ssl values.""" + config = {"influxdb": config_base.copy()} + config["influxdb"].update(config_ext) + + with patch("os.access", return_value=True): + with patch("os.path.isfile", return_value=True): + assert await async_setup_component(hass, influxdb.DOMAIN, config) + await hass.async_block_till_done() + + assert hass.bus.listen.called + assert EVENT_STATE_CHANGED == hass.bus.listen.call_args_list[0][0][0] + assert expected_client_args.items() <= mock_client.call_args.kwargs.items() + + @pytest.mark.parametrize( "mock_client, config_ext, get_write_api", [ From b4559a172c73143e3aca0847ec29491d25937d61 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 1 Feb 2021 23:47:58 +0100 Subject: [PATCH 123/796] Add value notification events to zwave_js integration (#45814) --- homeassistant/components/zwave_js/__init__.py | 52 ++++++- homeassistant/components/zwave_js/const.py | 15 ++ homeassistant/components/zwave_js/entity.py | 16 ++- tests/components/zwave_js/test_events.py | 130 ++++++++++++++++++ 4 files changed, 199 insertions(+), 14 deletions(-) create mode 100644 tests/components/zwave_js/test_events.py diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index c995749f924..2b4b33e9b88 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -1,11 +1,11 @@ """The Z-Wave JS integration.""" import asyncio import logging -from typing import Tuple from async_timeout import timeout from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.node import Node as ZwaveNode +from zwave_js_server.model.value import ValueNotification from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.config_entries import ConfigEntry @@ -18,14 +18,28 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from .api import async_register_api from .const import ( + ATTR_COMMAND_CLASS, + ATTR_COMMAND_CLASS_NAME, + ATTR_DEVICE_ID, + ATTR_DOMAIN, + ATTR_ENDPOINT, + ATTR_HOME_ID, + ATTR_LABEL, + ATTR_NODE_ID, + ATTR_PROPERTY_KEY_NAME, + ATTR_PROPERTY_NAME, + ATTR_TYPE, + ATTR_VALUE, CONF_INTEGRATION_CREATED_ADDON, DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY, PLATFORMS, + ZWAVE_JS_EVENT, ) from .discovery import async_discover_values +from .entity import get_device_id LOGGER = logging.getLogger(__name__) CONNECT_TIMEOUT = 10 @@ -37,12 +51,6 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: return True -@callback -def get_device_id(client: ZwaveClient, node: ZwaveNode) -> Tuple[str, str]: - """Get device registry identifier for Z-Wave node.""" - return (DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}") - - @callback def register_node_in_dev_reg( hass: HomeAssistant, @@ -106,6 +114,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async_dispatcher_send( hass, f"{DOMAIN}_{entry.entry_id}_add_{disc_info.platform}", disc_info ) + # add listener for stateless node events (value notification) + node.on( + "value notification", + lambda event: async_on_value_notification(event["value_notification"]), + ) @callback def async_on_node_added(node: ZwaveNode) -> None: @@ -134,6 +147,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # note: removal of entity registry is handled by core dev_reg.async_remove_device(device.id) + @callback + def async_on_value_notification(notification: ValueNotification) -> None: + """Relay stateless value notification events from Z-Wave nodes to hass.""" + device = dev_reg.async_get_device({get_device_id(client, notification.node)}) + value = notification.value + if notification.metadata.states: + value = notification.metadata.states.get(str(value), value) + hass.bus.async_fire( + ZWAVE_JS_EVENT, + { + ATTR_TYPE: "value_notification", + ATTR_DOMAIN: DOMAIN, + ATTR_NODE_ID: notification.node.node_id, + ATTR_HOME_ID: client.driver.controller.home_id, + ATTR_ENDPOINT: notification.endpoint, + ATTR_DEVICE_ID: device.id, + ATTR_COMMAND_CLASS: notification.command_class, + ATTR_COMMAND_CLASS_NAME: notification.command_class_name, + ATTR_LABEL: notification.metadata.label, + ATTR_PROPERTY_NAME: notification.property_name, + ATTR_PROPERTY_KEY_NAME: notification.property_key_name, + ATTR_VALUE: value, + }, + ) + async def handle_ha_shutdown(event: Event) -> None: """Handle HA shutdown.""" await client.disconnect() diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 526a8429bd4..163f4fff9ac 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -17,3 +17,18 @@ DATA_CLIENT = "client" DATA_UNSUBSCRIBE = "unsubs" EVENT_DEVICE_ADDED_TO_REGISTRY = f"{DOMAIN}_device_added_to_registry" + +# constants for events +ZWAVE_JS_EVENT = f"{DOMAIN}_event" +ATTR_NODE_ID = "node_id" +ATTR_HOME_ID = "home_id" +ATTR_ENDPOINT = "endpoint" +ATTR_LABEL = "label" +ATTR_VALUE = "value" +ATTR_COMMAND_CLASS = "command_class" +ATTR_COMMAND_CLASS_NAME = "command_class_name" +ATTR_TYPE = "type" +ATTR_DOMAIN = "domain" +ATTR_DEVICE_ID = "device_id" +ATTR_PROPERTY_NAME = "property_name" +ATTR_PROPERTY_KEY_NAME = "property_key_name" diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 84870ba75f4..9626ae9a888 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -1,9 +1,10 @@ """Generic Z-Wave Entity Class.""" import logging -from typing import Optional, Union +from typing import Optional, Tuple, Union from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.value import Value as ZwaveValue, get_value_id from homeassistant.config_entries import ConfigEntry @@ -19,6 +20,12 @@ LOGGER = logging.getLogger(__name__) EVENT_VALUE_UPDATED = "value updated" +@callback +def get_device_id(client: ZwaveClient, node: ZwaveNode) -> Tuple[str, str]: + """Get device registry identifier for Z-Wave node.""" + return (DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}") + + class ZWaveBaseEntity(Entity): """Generic Entity Class for a Z-Wave Device.""" @@ -60,12 +67,7 @@ class ZWaveBaseEntity(Entity): """Return device information for the device registry.""" # device is precreated in main handler return { - "identifiers": { - ( - DOMAIN, - f"{self.client.driver.controller.home_id}-{self.info.node.node_id}", - ) - }, + "identifiers": {get_device_id(self.client, self.info.node)}, } @property diff --git a/tests/components/zwave_js/test_events.py b/tests/components/zwave_js/test_events.py new file mode 100644 index 00000000000..c4280ebb50d --- /dev/null +++ b/tests/components/zwave_js/test_events.py @@ -0,0 +1,130 @@ +"""Test Z-Wave JS (value notification) events.""" +from zwave_js_server.event import Event + +from tests.common import async_capture_events + + +async def test_scenes(hass, hank_binary_switch, integration, client): + """Test scene events.""" + # just pick a random node to fake the value notification events + node = hank_binary_switch + events = async_capture_events(hass, "zwave_js_event") + + # Publish fake Basic Set value notification + event = Event( + type="value notification", + data={ + "source": "node", + "event": "value notification", + "nodeId": 32, + "args": { + "commandClassName": "Basic", + "commandClass": 32, + "endpoint": 0, + "property": "event", + "propertyName": "event", + "value": 255, + "metadata": { + "type": "number", + "readable": True, + "writeable": False, + "min": 0, + "max": 255, + "label": "Event value", + }, + "ccVersion": 1, + }, + }, + ) + node.receive_event(event) + # wait for the event + await hass.async_block_till_done() + assert len(events) == 1 + assert events[0].data["home_id"] == client.driver.controller.home_id + assert events[0].data["node_id"] == 32 + assert events[0].data["endpoint"] == 0 + assert events[0].data["command_class"] == 32 + assert events[0].data["command_class_name"] == "Basic" + assert events[0].data["label"] == "Event value" + assert events[0].data["value"] == 255 + + # Publish fake Scene Activation value notification + event = Event( + type="value notification", + data={ + "source": "node", + "event": "value notification", + "nodeId": 32, + "args": { + "commandClassName": "Scene Activation", + "commandClass": 43, + "endpoint": 0, + "property": "SceneID", + "propertyName": "SceneID", + "value": 16, + "metadata": { + "type": "number", + "readable": True, + "writeable": False, + "min": 0, + "max": 255, + "label": "Scene ID", + }, + "ccVersion": 3, + }, + }, + ) + node.receive_event(event) + # wait for the event + await hass.async_block_till_done() + assert len(events) == 2 + assert events[1].data["command_class"] == 43 + assert events[1].data["command_class_name"] == "Scene Activation" + assert events[1].data["label"] == "Scene ID" + assert events[1].data["value"] == 16 + + # Publish fake Central Scene value notification + event = Event( + type="value notification", + data={ + "source": "node", + "event": "value notification", + "nodeId": 32, + "args": { + "commandClassName": "Central Scene", + "commandClass": 91, + "endpoint": 0, + "property": "scene", + "propertyKey": "001", + "propertyName": "scene", + "propertyKeyName": "001", + "value": 4, + "metadata": { + "type": "number", + "readable": True, + "writeable": False, + "min": 0, + "max": 255, + "label": "Scene 001", + "states": { + "0": "KeyPressed", + "1": "KeyReleased", + "2": "KeyHeldDown", + "3": "KeyPressed2x", + "4": "KeyPressed3x", + "5": "KeyPressed4x", + "6": "KeyPressed5x", + }, + }, + "ccVersion": 3, + }, + }, + ) + node.receive_event(event) + # wait for the event + await hass.async_block_till_done() + assert len(events) == 3 + assert events[2].data["command_class"] == 91 + assert events[2].data["command_class_name"] == "Central Scene" + assert events[2].data["label"] == "Scene 001" + assert events[2].data["value"] == "KeyPressed3x" From d38d8a542d5d9648be580b396e975765a88d5bc4 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 2 Feb 2021 01:01:19 +0100 Subject: [PATCH 124/796] Upgrade colorlog to 4.7.2 (#45840) --- homeassistant/scripts/check_config.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 07a6a54e402..992fce2ac87 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -17,7 +17,7 @@ import homeassistant.util.yaml.loader as yaml_loader # mypy: allow-untyped-calls, allow-untyped-defs -REQUIREMENTS = ("colorlog==4.6.2",) +REQUIREMENTS = ("colorlog==4.7.2",) _LOGGER = logging.getLogger(__name__) # pylint: disable=protected-access diff --git a/requirements_all.txt b/requirements_all.txt index 8a74e574f04..41d4c6c3e39 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -431,7 +431,7 @@ coinbase==2.1.0 coinmarketcap==5.0.3 # homeassistant.scripts.check_config -colorlog==4.6.2 +colorlog==4.7.2 # homeassistant.components.color_extractor colorthief==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 99f1c997ec1..164c7df2396 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -228,7 +228,7 @@ caldav==0.7.1 coinmarketcap==5.0.3 # homeassistant.scripts.check_config -colorlog==4.6.2 +colorlog==4.7.2 # homeassistant.components.color_extractor colorthief==0.2.1 From f2286d481150a61428e2335e597bb9953646817f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 2 Feb 2021 01:01:39 +0100 Subject: [PATCH 125/796] Upgrade TwitterAPI to 2.6.5 (#45842) --- homeassistant/components/twitter/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/twitter/manifest.json b/homeassistant/components/twitter/manifest.json index acd47253b82..cd3a12255a2 100644 --- a/homeassistant/components/twitter/manifest.json +++ b/homeassistant/components/twitter/manifest.json @@ -2,6 +2,6 @@ "domain": "twitter", "name": "Twitter", "documentation": "https://www.home-assistant.io/integrations/twitter", - "requirements": ["TwitterAPI==2.6.3"], + "requirements": ["TwitterAPI==2.6.5"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 41d4c6c3e39..80cbcf7cd5f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -78,7 +78,7 @@ RtmAPI==0.7.2 TravisPy==0.3.5 # homeassistant.components.twitter -TwitterAPI==2.6.3 +TwitterAPI==2.6.5 # homeassistant.components.tof # VL53L1X2==0.1.5 From 253ae3f4236e208195379471f80adb9a32c9ce13 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Tue, 2 Feb 2021 00:09:25 +0000 Subject: [PATCH 126/796] Lyric Code Improvements (#45819) Co-authored-by: J. Nick Koston --- homeassistant/components/lyric/__init__.py | 4 +-- homeassistant/components/lyric/climate.py | 35 ++++++++++------------ tests/components/lyric/test_config_flow.py | 14 ++++++--- 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index d29fca09166..0697dfbfd35 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -85,7 +85,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: client = ConfigEntryLyricClient(session, oauth_session) client_id = hass.data[DOMAIN][CONF_CLIENT_ID] - lyric: Lyric = Lyric(client, client_id) + lyric = Lyric(client, client_id) async def async_update_data() -> Lyric: """Fetch data from Lyric.""" @@ -93,7 +93,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async with async_timeout.timeout(60): await lyric.get_locations() return lyric - except (*LYRIC_EXCEPTIONS, TimeoutError) as exception: + except LYRIC_EXCEPTIONS as exception: raise UpdateFailed(exception) from exception coordinator = DataUpdateCoordinator( diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index bcfefef9c93..41e8fa90b67 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -82,7 +82,11 @@ async def async_setup_entry( for location in coordinator.data.locations: for device in location.devices: - entities.append(LyricClimate(hass, coordinator, location, device)) + entities.append( + LyricClimate( + coordinator, location, device, hass.config.units.temperature_unit + ) + ) async_add_entities(entities, True) @@ -100,13 +104,13 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): def __init__( self, - hass: HomeAssistantType, coordinator: DataUpdateCoordinator, location: LyricLocation, device: LyricDevice, + temperature_unit: str, ) -> None: """Initialize Honeywell Lyric climate entity.""" - self._temperature_unit = hass.config.units.temperature_unit + self._temperature_unit = temperature_unit # Setup supported hvac modes self._hvac_modes = [HVAC_MODE_OFF] @@ -161,23 +165,26 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): @property def target_temperature(self) -> Optional[float]: """Return the temperature we try to reach.""" - device: LyricDevice = self.device + device = self.device if not device.hasDualSetpointStatus: return device.changeableValues.heatSetpoint + return None @property def target_temperature_low(self) -> Optional[float]: """Return the upper bound temperature we try to reach.""" - device: LyricDevice = self.device + device = self.device if device.hasDualSetpointStatus: return device.changeableValues.coolSetpoint + return None @property def target_temperature_high(self) -> Optional[float]: """Return the upper bound temperature we try to reach.""" - device: LyricDevice = self.device + device = self.device if device.hasDualSetpointStatus: return device.changeableValues.heatSetpoint + return None @property def preset_mode(self) -> Optional[str]: @@ -198,7 +205,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): @property def min_temp(self) -> float: """Identify min_temp in Lyric API or defaults if not available.""" - device: LyricDevice = self.device + device = self.device if LYRIC_HVAC_MODE_COOL in device.allowedModes: return device.minCoolSetpoint return device.minHeatSetpoint @@ -206,7 +213,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): @property def max_temp(self) -> float: """Identify max_temp in Lyric API or defaults if not available.""" - device: LyricDevice = self.device + device = self.device if LYRIC_HVAC_MODE_HEAT in device.allowedModes: return device.maxHeatSetpoint return device.maxCoolSetpoint @@ -216,7 +223,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) - device: LyricDevice = self.device + device = self.device if device.hasDualSetpointStatus: if target_temp_low is not None and target_temp_high is not None: temp = (target_temp_low, target_temp_high) @@ -255,16 +262,6 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): _LOGGER.error(exception) await self.coordinator.async_refresh() - async def async_set_preset_period(self, period: str) -> None: - """Set preset period (time).""" - try: - await self._update_thermostat( - self.location, self.device, nextPeriodTime=period - ) - except LYRIC_EXCEPTIONS as exception: - _LOGGER.error(exception) - await self.coordinator.async_refresh() - async def async_set_hold_time(self, time_period: str) -> None: """Set the time to hold until.""" _LOGGER.debug("set_hold_time: %s", time_period) diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py index 24b6b68d731..78fd9013466 100644 --- a/tests/components/lyric/test_config_flow.py +++ b/tests/components/lyric/test_config_flow.py @@ -34,9 +34,9 @@ async def mock_impl(hass): async def test_abort_if_no_configuration(hass): """Check flow abort when no configuration.""" - flow = config_flow.OAuth2FlowHandler() - flow.hass = hass - result = await flow.async_step_user() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "missing_configuration" @@ -114,8 +114,14 @@ async def test_full_flow( assert len(mock_setup.mock_calls) == 1 -async def test_abort_if_authorization_timeout(hass, mock_impl): +async def test_abort_if_authorization_timeout( + hass, mock_impl, current_request_with_host +): """Check Somfy authorization timeout.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + flow = config_flow.OAuth2FlowHandler() flow.hass = hass From 3a77ef02e4f17eb320a90e69018afaebec022fa6 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Tue, 2 Feb 2021 01:51:20 +0000 Subject: [PATCH 127/796] Add sensors to Lyric integration (#45791) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + homeassistant/components/lyric/__init__.py | 2 +- homeassistant/components/lyric/sensor.py | 251 +++++++++++++++++++++ 3 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/lyric/sensor.py diff --git a/.coveragerc b/.coveragerc index 3a7f33f6050..47d9c84ba0e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -521,6 +521,7 @@ omit = homeassistant/components/lyric/__init__.py homeassistant/components/lyric/api.py homeassistant/components/lyric/climate.py + homeassistant/components/lyric/sensor.py homeassistant/components/magicseaweed/sensor.py homeassistant/components/mailgun/notify.py homeassistant/components/map/* diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index 0697dfbfd35..12990d66ba9 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -44,7 +44,7 @@ CONFIG_SCHEMA = vol.Schema( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["climate"] +PLATFORMS = ["climate", "sensor"] async def async_setup(hass: HomeAssistant, config: dict): diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py new file mode 100644 index 00000000000..c4950f119d9 --- /dev/null +++ b/homeassistant/components/lyric/sensor.py @@ -0,0 +1,251 @@ +"""Support for Honeywell Lyric sensor platform.""" +from datetime import datetime, timedelta + +from aiolyric.objects.device import LyricDevice +from aiolyric.objects.location import LyricLocation + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, +) +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util import dt as dt_util + +from . import LyricDeviceEntity +from .const import ( + DOMAIN, + PRESET_HOLD_UNTIL, + PRESET_NO_HOLD, + PRESET_PERMANENT_HOLD, + PRESET_TEMPORARY_HOLD, + PRESET_VACATION_HOLD, +) + +LYRIC_SETPOINT_STATUS_NAMES = { + PRESET_NO_HOLD: "Following Schedule", + PRESET_PERMANENT_HOLD: "Held Permanently", + PRESET_TEMPORARY_HOLD: "Held Temporarily", + PRESET_VACATION_HOLD: "Holiday", +} + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Set up the Honeywell Lyric sensor platform based on a config entry.""" + coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + entities = [] + + for location in coordinator.data.locations: + for device in location.devices: + cls_list = [] + if device.indoorTemperature: + cls_list.append(LyricIndoorTemperatureSensor) + if device.outdoorTemperature: + cls_list.append(LyricOutdoorTemperatureSensor) + if device.displayedOutdoorHumidity: + cls_list.append(LyricOutdoorHumiditySensor) + if device.changeableValues: + if device.changeableValues.nextPeriodTime: + cls_list.append(LyricNextPeriodSensor) + if device.changeableValues.thermostatSetpointStatus: + cls_list.append(LyricSetpointStatusSensor) + for cls in cls_list: + entities.append( + cls( + coordinator, + location, + device, + hass.config.units.temperature_unit, + ) + ) + + async_add_entities(entities, True) + + +class LyricSensor(LyricDeviceEntity): + """Defines a Honeywell Lyric sensor.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + location: LyricLocation, + device: LyricDevice, + key: str, + name: str, + icon: str, + device_class: str = None, + unit_of_measurement: str = None, + ) -> None: + """Initialize Honeywell Lyric sensor.""" + self._device_class = device_class + self._unit_of_measurement = unit_of_measurement + + super().__init__(coordinator, location, device, key, name, icon) + + @property + def device_class(self) -> str: + """Return the device class of the sensor.""" + return self._device_class + + @property + def unit_of_measurement(self) -> str: + """Return the unit this state is expressed in.""" + return self._unit_of_measurement + + +class LyricIndoorTemperatureSensor(LyricSensor): + """Defines a Honeywell Lyric sensor.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + location: LyricLocation, + device: LyricDevice, + unit_of_measurement: str = None, + ) -> None: + """Initialize Honeywell Lyric sensor.""" + + super().__init__( + coordinator, + location, + device, + f"{device.macID}_indoor_temperature", + "Indoor Temperature", + None, + DEVICE_CLASS_TEMPERATURE, + unit_of_measurement, + ) + + @property + def state(self) -> str: + """Return the state of the sensor.""" + return self.device.indoorTemperature + + +class LyricOutdoorTemperatureSensor(LyricSensor): + """Defines a Honeywell Lyric sensor.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + location: LyricLocation, + device: LyricDevice, + unit_of_measurement: str = None, + ) -> None: + """Initialize Honeywell Lyric sensor.""" + + super().__init__( + coordinator, + location, + device, + f"{device.macID}_outdoor_temperature", + "Outdoor Temperature", + None, + DEVICE_CLASS_TEMPERATURE, + unit_of_measurement, + ) + + @property + def state(self) -> str: + """Return the state of the sensor.""" + return self.device.outdoorTemperature + + +class LyricOutdoorHumiditySensor(LyricSensor): + """Defines a Honeywell Lyric sensor.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + location: LyricLocation, + device: LyricDevice, + unit_of_measurement: str = None, + ) -> None: + """Initialize Honeywell Lyric sensor.""" + + super().__init__( + coordinator, + location, + device, + f"{device.macID}_outdoor_humidity", + "Outdoor Humidity", + None, + DEVICE_CLASS_HUMIDITY, + "%", + ) + + @property + def state(self) -> str: + """Return the state of the sensor.""" + return self.device.displayedOutdoorHumidity + + +class LyricNextPeriodSensor(LyricSensor): + """Defines a Honeywell Lyric sensor.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + location: LyricLocation, + device: LyricDevice, + unit_of_measurement: str = None, + ) -> None: + """Initialize Honeywell Lyric sensor.""" + + super().__init__( + coordinator, + location, + device, + f"{device.macID}_next_period_time", + "Next Period Time", + None, + DEVICE_CLASS_TIMESTAMP, + ) + + @property + def state(self) -> datetime: + """Return the state of the sensor.""" + device = self.device + time = dt_util.parse_time(device.changeableValues.nextPeriodTime) + now = dt_util.utcnow() + if time <= now.time(): + now = now + timedelta(days=1) + return dt_util.as_utc(datetime.combine(now.date(), time)) + + +class LyricSetpointStatusSensor(LyricSensor): + """Defines a Honeywell Lyric sensor.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + location: LyricLocation, + device: LyricDevice, + unit_of_measurement: str = None, + ) -> None: + """Initialize Honeywell Lyric sensor.""" + + super().__init__( + coordinator, + location, + device, + f"{device.macID}_setpoint_status", + "Setpoint Status", + "mdi:thermostat", + None, + ) + + @property + def state(self) -> str: + """Return the state of the sensor.""" + device = self.device + if device.changeableValues.thermostatSetpointStatus == PRESET_HOLD_UNTIL: + return f"Held until {device.changeableValues.nextPeriodTime}" + return LYRIC_SETPOINT_STATUS_NAMES.get( + device.changeableValues.thermostatSetpointStatus, "Unknown" + ) From 60d4dadcb6b5f806209366892415347129c1ca49 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 2 Feb 2021 03:03:51 +0100 Subject: [PATCH 128/796] Upgrade sqlalchemy to 1.3.23 (#45845) --- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 67d3bdd0f5b..a7e5eb0814d 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.3.22"], + "requirements": ["sqlalchemy==1.3.23"], "codeowners": [], "quality_scale": "internal" } diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 3b21d32b110..7418eb095da 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,6 +2,6 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.3.22"], + "requirements": ["sqlalchemy==1.3.23"], "codeowners": ["@dgomes"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 139f577ff63..e7585a1aaa8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -26,7 +26,7 @@ pyyaml==5.4.1 requests==2.25.1 ruamel.yaml==0.15.100 scapy==2.4.4 -sqlalchemy==1.3.22 +sqlalchemy==1.3.23 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 diff --git a/requirements_all.txt b/requirements_all.txt index 80cbcf7cd5f..2ff45edcda0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2103,7 +2103,7 @@ spotipy==2.16.1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.22 +sqlalchemy==1.3.23 # homeassistant.components.srp_energy srpenergy==1.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 164c7df2396..50b1c6670e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1066,7 +1066,7 @@ spotipy==2.16.1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.22 +sqlalchemy==1.3.23 # homeassistant.components.srp_energy srpenergy==1.3.2 From aea8636c7e0224deb3877e6cb67444609635dfed Mon Sep 17 00:00:00 2001 From: michaeldavie Date: Tue, 2 Feb 2021 02:23:26 -0500 Subject: [PATCH 129/796] Fix environment_canada high/low temperature display in evenings. (#45855) --- .../components/environment_canada/weather.py | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index f4fa96b52d6..dd2252a585f 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -184,23 +184,34 @@ def get_forecast(ec_data, forecast_type): if forecast_type == "daily": half_days = ec_data.daily_forecasts + + today = { + ATTR_FORECAST_TIME: dt.now().isoformat(), + ATTR_FORECAST_CONDITION: icon_code_to_condition( + int(half_days[0]["icon_code"]) + ), + ATTR_FORECAST_PRECIPITATION_PROBABILITY: int( + half_days[0]["precip_probability"] + ), + } + if half_days[0]["temperature_class"] == "high": - forecast_array.append( + today.update( { - ATTR_FORECAST_TIME: dt.now().isoformat(), ATTR_FORECAST_TEMP: int(half_days[0]["temperature"]), ATTR_FORECAST_TEMP_LOW: int(half_days[1]["temperature"]), - ATTR_FORECAST_CONDITION: icon_code_to_condition( - int(half_days[0]["icon_code"]) - ), - ATTR_FORECAST_PRECIPITATION_PROBABILITY: int( - half_days[0]["precip_probability"] - ), } ) - half_days = half_days[2:] else: - half_days = half_days[1:] + today.update( + { + ATTR_FORECAST_TEMP_LOW: int(half_days[0]["temperature"]), + ATTR_FORECAST_TEMP: int(half_days[1]["temperature"]), + } + ) + + forecast_array.append(today) + half_days = half_days[2:] for day, high, low in zip(range(1, 6), range(0, 9, 2), range(1, 10, 2)): forecast_array.append( From 411c0a968559073184fe9c3fc95ad0a20f90ce97 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Feb 2021 09:36:00 +0100 Subject: [PATCH 130/796] Improve MQTT JSON light to allow non-ambiguous states (#45522) --- .../components/mqtt/light/schema_json.py | 78 +++++++++++-------- tests/components/mqtt/test_light_json.py | 22 ++++++ 2 files changed, 68 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index c6622578a6f..489b424f4eb 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -175,6 +175,43 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._supported_features |= config[CONF_XY] and SUPPORT_COLOR self._supported_features |= config[CONF_HS] and SUPPORT_COLOR + def _parse_color(self, values): + try: + red = int(values["color"]["r"]) + green = int(values["color"]["g"]) + blue = int(values["color"]["b"]) + + return color_util.color_RGB_to_hs(red, green, blue) + except KeyError: + pass + except ValueError: + _LOGGER.warning("Invalid RGB color value received") + return self._hs + + try: + x_color = float(values["color"]["x"]) + y_color = float(values["color"]["y"]) + + return color_util.color_xy_to_hs(x_color, y_color) + except KeyError: + pass + except ValueError: + _LOGGER.warning("Invalid XY color value received") + return self._hs + + try: + hue = float(values["color"]["h"]) + saturation = float(values["color"]["s"]) + + return (hue, saturation) + except KeyError: + pass + except ValueError: + _LOGGER.warning("Invalid HS color value received") + return self._hs + + return self._hs + async def _subscribe_topics(self): """(Re)Subscribe to topics.""" last_state = await self.async_get_last_state() @@ -190,37 +227,11 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): elif values["state"] == "OFF": self._state = False - if self._supported_features and SUPPORT_COLOR: - try: - red = int(values["color"]["r"]) - green = int(values["color"]["g"]) - blue = int(values["color"]["b"]) - - self._hs = color_util.color_RGB_to_hs(red, green, blue) - except KeyError: - pass - except ValueError: - _LOGGER.warning("Invalid RGB color value received") - - try: - x_color = float(values["color"]["x"]) - y_color = float(values["color"]["y"]) - - self._hs = color_util.color_xy_to_hs(x_color, y_color) - except KeyError: - pass - except ValueError: - _LOGGER.warning("Invalid XY color value received") - - try: - hue = float(values["color"]["h"]) - saturation = float(values["color"]["s"]) - - self._hs = (hue, saturation) - except KeyError: - pass - except ValueError: - _LOGGER.warning("Invalid HS color value received") + if self._supported_features and SUPPORT_COLOR and "color" in values: + if values["color"] is None: + self._hs = None + else: + self._hs = self._parse_color(values) if self._supported_features and SUPPORT_BRIGHTNESS: try: @@ -236,7 +247,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): if self._supported_features and SUPPORT_COLOR_TEMP: try: - self._color_temp = int(values["color_temp"]) + if values["color_temp"] is None: + self._color_temp = None + else: + self._color_temp = int(values["color_temp"]) except KeyError: pass except ValueError: diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 1c9eed0e404..022df109f38 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -295,11 +295,21 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): light_state = hass.states.get("light.test") assert light_state.attributes.get("hs_color") == (180.0, 50.0) + async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON", "color":null}') + + light_state = hass.states.get("light.test") + assert "hs_color" not in light_state.attributes + async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON", "color_temp":155}') light_state = hass.states.get("light.test") assert light_state.attributes.get("color_temp") == 155 + async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON", "color_temp":null}') + + light_state = hass.states.get("light.test") + assert "color_temp" not in light_state.attributes + async_fire_mqtt_message( hass, "test_light_rgb", '{"state":"ON", "effect":"colorloop"}' ) @@ -1004,6 +1014,18 @@ async def test_invalid_values(hass, mqtt_mock): assert state.attributes.get("white_value") == 255 assert state.attributes.get("color_temp") == 100 + # Empty color value + async_fire_mqtt_message( + hass, + "test_light_rgb", + '{"state":"ON",' '"color":{}}', + ) + + # Color should not have changed + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (255, 255, 255) + # Bad HS color values async_fire_mqtt_message( hass, From 3ef7bd6b73d518bae6eefe7cfddd7d483d336b5f Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 2 Feb 2021 02:37:42 -0600 Subject: [PATCH 131/796] Add notification events to zwave_js integration (#45827) Co-authored-by: Paulus Schoutsen --- homeassistant/components/zwave_js/__init__.py | 25 ++++++++++++++++- homeassistant/components/zwave_js/const.py | 1 + tests/components/zwave_js/test_events.py | 28 +++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 2b4b33e9b88..a4eb466fe87 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -5,6 +5,7 @@ import logging from async_timeout import timeout from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.node import Node as ZwaveNode +from zwave_js_server.model.notification import Notification from zwave_js_server.model.value import ValueNotification from homeassistant.components.hassio.handler import HassioAPIError @@ -26,6 +27,7 @@ from .const import ( ATTR_HOME_ID, ATTR_LABEL, ATTR_NODE_ID, + ATTR_PARAMETERS, ATTR_PROPERTY_KEY_NAME, ATTR_PROPERTY_NAME, ATTR_TYPE, @@ -114,11 +116,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async_dispatcher_send( hass, f"{DOMAIN}_{entry.entry_id}_add_{disc_info.platform}", disc_info ) - # add listener for stateless node events (value notification) + # add listener for stateless node value notification events node.on( "value notification", lambda event: async_on_value_notification(event["value_notification"]), ) + # add listener for stateless node notification events + node.on( + "notification", lambda event: async_on_notification(event["notification"]) + ) @callback def async_on_node_added(node: ZwaveNode) -> None: @@ -172,6 +178,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: }, ) + @callback + def async_on_notification(notification: Notification) -> None: + """Relay stateless notification events from Z-Wave nodes to hass.""" + device = dev_reg.async_get_device({get_device_id(client, notification.node)}) + hass.bus.async_fire( + ZWAVE_JS_EVENT, + { + ATTR_TYPE: "notification", + ATTR_DOMAIN: DOMAIN, + ATTR_NODE_ID: notification.node.node_id, + ATTR_HOME_ID: client.driver.controller.home_id, + ATTR_DEVICE_ID: device.id, + ATTR_LABEL: notification.notification_label, + ATTR_PARAMETERS: notification.parameters, + }, + ) + async def handle_ha_shutdown(event: Event) -> None: """Handle HA shutdown.""" await client.disconnect() diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 163f4fff9ac..dc2ffaeaa20 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -32,3 +32,4 @@ ATTR_DOMAIN = "domain" ATTR_DEVICE_ID = "device_id" ATTR_PROPERTY_NAME = "property_name" ATTR_PROPERTY_KEY_NAME = "property_key_name" +ATTR_PARAMETERS = "parameters" diff --git a/tests/components/zwave_js/test_events.py b/tests/components/zwave_js/test_events.py index c4280ebb50d..2a347f6afea 100644 --- a/tests/components/zwave_js/test_events.py +++ b/tests/components/zwave_js/test_events.py @@ -128,3 +128,31 @@ async def test_scenes(hass, hank_binary_switch, integration, client): assert events[2].data["command_class_name"] == "Central Scene" assert events[2].data["label"] == "Scene 001" assert events[2].data["value"] == "KeyPressed3x" + + +async def test_notifications(hass, hank_binary_switch, integration, client): + """Test notification events.""" + # just pick a random node to fake the value notification events + node = hank_binary_switch + events = async_capture_events(hass, "zwave_js_event") + + # Publish fake Basic Set value notification + event = Event( + type="notification", + data={ + "source": "node", + "event": "notification", + "nodeId": 23, + "notificationLabel": "Keypad lock operation", + "parameters": {"userId": 1}, + }, + ) + node.receive_event(event) + # wait for the event + await hass.async_block_till_done() + assert len(events) == 1 + assert events[0].data["type"] == "notification" + assert events[0].data["home_id"] == client.driver.controller.home_id + assert events[0].data["node_id"] == 32 + assert events[0].data["label"] == "Keypad lock operation" + assert events[0].data["parameters"]["userId"] == 1 From 8d9b66e23daba8526d6778f70d0f29ce0beb34f9 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 2 Feb 2021 02:41:00 -0600 Subject: [PATCH 132/796] Add current humidity to zwave_js climate platform (#45857) --- homeassistant/components/zwave_js/climate.py | 11 +++++++++++ tests/components/zwave_js/test_climate.py | 2 ++ 2 files changed, 13 insertions(+) diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 6c0b4a0335e..417f5aa5e5d 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -142,6 +142,12 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): add_to_watched_value_ids=True, check_all_endpoints=True, ) + self._current_humidity = self.get_zwave_value( + "Humidity", + command_class=CommandClass.SENSOR_MULTILEVEL, + add_to_watched_value_ids=True, + check_all_endpoints=True, + ) self._set_modes_and_presets() def _setpoint_value(self, setpoint_type: ThermostatSetpointType) -> ZwaveValue: @@ -207,6 +213,11 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): return None return HVAC_CURRENT_MAP.get(int(self._operating_state.value)) + @property + def current_humidity(self) -> Optional[int]: + """Return the current humidity level.""" + return self._current_humidity.value if self._current_humidity else None + @property def current_temperature(self) -> Optional[float]: """Return the current temperature.""" diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index f7deefc1360..bede37e6959 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -3,6 +3,7 @@ import pytest from zwave_js_server.event import Event from homeassistant.components.climate.const import ( + ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, ATTR_HVAC_ACTION, ATTR_HVAC_MODE, @@ -42,6 +43,7 @@ async def test_thermostat_v2( HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, ] + assert state.attributes[ATTR_CURRENT_HUMIDITY] == 30 assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.2 assert state.attributes[ATTR_TEMPERATURE] == 22.2 assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE From 0feda9ce63aa04a3aa31c8d34f2a671d8c3e6610 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 2 Feb 2021 10:06:09 +0100 Subject: [PATCH 133/796] Fix sensor discovery for zwave_js integration (#45834) Co-authored-by: Raman Gupta <7243222+raman325@users.noreply.github.com> --- .../components/zwave_js/binary_sensor.py | 173 +++++++----------- .../components/zwave_js/discovery.py | 112 ++++++------ homeassistant/components/zwave_js/entity.py | 3 + homeassistant/components/zwave_js/sensor.py | 56 ++++-- tests/components/zwave_js/common.py | 3 +- tests/components/zwave_js/test_sensor.py | 36 +++- 6 files changed, 204 insertions(+), 179 deletions(-) diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index 42394fe127c..f17d893e371 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -14,7 +14,6 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_LOCK, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, - DEVICE_CLASS_POWER, DEVICE_CLASS_PROBLEM, DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, @@ -57,201 +56,144 @@ class NotificationSensorMapping(TypedDict, total=False): """Represent a notification sensor mapping dict type.""" type: int # required - states: List[int] # required + states: List[str] device_class: str enabled: bool # Mappings for Notification sensors +# https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/notifications.json NOTIFICATION_SENSOR_MAPPINGS: List[NotificationSensorMapping] = [ { - # NotificationType 1: Smoke Alarm - State Id's 1 and 2 - # Assuming here that Value 1 and 2 are not present at the same time + # NotificationType 1: Smoke Alarm - State Id's 1 and 2 - Smoke detected "type": NOTIFICATION_SMOKE_ALARM, - "states": [1, 2], + "states": ["1", "2"], "device_class": DEVICE_CLASS_SMOKE, }, { # NotificationType 1: Smoke Alarm - All other State Id's - # Create as disabled sensors "type": NOTIFICATION_SMOKE_ALARM, - "states": [3, 4, 5, 6, 7, 8], - "device_class": DEVICE_CLASS_SMOKE, - "enabled": False, + "device_class": DEVICE_CLASS_PROBLEM, }, { # NotificationType 2: Carbon Monoxide - State Id's 1 and 2 "type": NOTIFICATION_CARBON_MONOOXIDE, - "states": [1, 2], + "states": ["1", "2"], "device_class": DEVICE_CLASS_GAS, }, { # NotificationType 2: Carbon Monoxide - All other State Id's "type": NOTIFICATION_CARBON_MONOOXIDE, - "states": [4, 5, 7], - "device_class": DEVICE_CLASS_GAS, - "enabled": False, + "device_class": DEVICE_CLASS_PROBLEM, }, { # NotificationType 3: Carbon Dioxide - State Id's 1 and 2 "type": NOTIFICATION_CARBON_DIOXIDE, - "states": [1, 2], + "states": ["1", "2"], "device_class": DEVICE_CLASS_GAS, }, { # NotificationType 3: Carbon Dioxide - All other State Id's "type": NOTIFICATION_CARBON_DIOXIDE, - "states": [4, 5, 7], - "device_class": DEVICE_CLASS_GAS, - "enabled": False, + "device_class": DEVICE_CLASS_PROBLEM, }, { # NotificationType 4: Heat - State Id's 1, 2, 5, 6 (heat/underheat) "type": NOTIFICATION_HEAT, - "states": [1, 2, 5, 6], + "states": ["1", "2", "5", "6"], "device_class": DEVICE_CLASS_HEAT, }, { # NotificationType 4: Heat - All other State Id's "type": NOTIFICATION_HEAT, - "states": [3, 4, 8, 10, 11], - "device_class": DEVICE_CLASS_HEAT, - "enabled": False, + "device_class": DEVICE_CLASS_PROBLEM, }, { # NotificationType 5: Water - State Id's 1, 2, 3, 4 "type": NOTIFICATION_WATER, - "states": [1, 2, 3, 4], + "states": ["1", "2", "3", "4"], "device_class": DEVICE_CLASS_MOISTURE, }, { # NotificationType 5: Water - All other State Id's "type": NOTIFICATION_WATER, - "states": [5], - "device_class": DEVICE_CLASS_MOISTURE, - "enabled": False, + "device_class": DEVICE_CLASS_PROBLEM, }, { # NotificationType 6: Access Control - State Id's 1, 2, 3, 4 (Lock) "type": NOTIFICATION_ACCESS_CONTROL, - "states": [1, 2, 3, 4], + "states": ["1", "2", "3", "4"], "device_class": DEVICE_CLASS_LOCK, }, { - # NotificationType 6: Access Control - State Id 22 (door/window open) + # NotificationType 6: Access Control - State Id 16 (door/window open) "type": NOTIFICATION_ACCESS_CONTROL, - "states": [22], + "states": ["22"], "device_class": DEVICE_CLASS_DOOR, }, + { + # NotificationType 6: Access Control - State Id 17 (door/window closed) + "type": NOTIFICATION_ACCESS_CONTROL, + "states": ["23"], + "enabled": False, + }, { # NotificationType 7: Home Security - State Id's 1, 2 (intrusion) - # Assuming that value 1 and 2 are not present at the same time "type": NOTIFICATION_HOME_SECURITY, - "states": [1, 2], + "states": ["1", "2"], "device_class": DEVICE_CLASS_SAFETY, }, { # NotificationType 7: Home Security - State Id's 3, 4, 9 (tampering) "type": NOTIFICATION_HOME_SECURITY, - "states": [3, 4, 9], + "states": ["3", "4", "9"], "device_class": DEVICE_CLASS_SAFETY, }, { # NotificationType 7: Home Security - State Id's 5, 6 (glass breakage) - # Assuming that value 5 and 6 are not present at the same time "type": NOTIFICATION_HOME_SECURITY, - "states": [5, 6], + "states": ["5", "6"], "device_class": DEVICE_CLASS_SAFETY, }, { # NotificationType 7: Home Security - State Id's 7, 8 (motion) "type": NOTIFICATION_HOME_SECURITY, - "states": [7, 8], + "states": ["7", "8"], "device_class": DEVICE_CLASS_MOTION, }, - { - # NotificationType 8: Power management - Values 1...9 - "type": NOTIFICATION_POWER_MANAGEMENT, - "states": [1, 2, 3, 4, 5, 6, 7, 8, 9], - "device_class": DEVICE_CLASS_POWER, - "enabled": False, - }, - { - # NotificationType 8: Power management - Values 10...15 - # Battery values (mutually exclusive) - "type": NOTIFICATION_POWER_MANAGEMENT, - "states": [10, 11, 12, 13, 14, 15], - "device_class": DEVICE_CLASS_BATTERY, - "enabled": False, - }, { # NotificationType 9: System - State Id's 1, 2, 6, 7 "type": NOTIFICATION_SYSTEM, - "states": [1, 2, 6, 7], + "states": ["1", "2", "6", "7"], "device_class": DEVICE_CLASS_PROBLEM, - "enabled": False, }, { # NotificationType 10: Emergency - State Id's 1, 2, 3 "type": NOTIFICATION_EMERGENCY, - "states": [1, 2, 3], + "states": ["1", "2", "3"], "device_class": DEVICE_CLASS_PROBLEM, }, - { - # NotificationType 11: Clock - State Id's 1, 2 - "type": NOTIFICATION_CLOCK, - "states": [1, 2], - "enabled": False, - }, - { - # NotificationType 12: Appliance - All State Id's - "type": NOTIFICATION_APPLIANCE, - "states": list(range(1, 22)), - }, - { - # NotificationType 13: Home Health - State Id's 1,2,3,4,5 - "type": NOTIFICATION_APPLIANCE, - "states": [1, 2, 3, 4, 5], - }, { # NotificationType 14: Siren "type": NOTIFICATION_SIREN, - "states": [1], + "states": ["1"], "device_class": DEVICE_CLASS_SOUND, }, - { - # NotificationType 15: Water valve - # ignore non-boolean values - "type": NOTIFICATION_WATER_VALVE, - "states": [3, 4], - "device_class": DEVICE_CLASS_PROBLEM, - }, - { - # NotificationType 16: Weather - "type": NOTIFICATION_WEATHER, - "states": [1, 2], - "device_class": DEVICE_CLASS_PROBLEM, - }, - { - # NotificationType 17: Irrigation - # ignore non-boolean values - "type": NOTIFICATION_IRRIGATION, - "states": [1, 2, 3, 4, 5], - }, { # NotificationType 18: Gas "type": NOTIFICATION_GAS, - "states": [1, 2, 3, 4], + "states": ["1", "2", "3", "4"], "device_class": DEVICE_CLASS_GAS, }, { # NotificationType 18: Gas "type": NOTIFICATION_GAS, - "states": [6], + "states": ["6"], "device_class": DEVICE_CLASS_PROBLEM, }, ] + PROPERTY_DOOR_STATUS = "doorStatus" @@ -284,10 +226,17 @@ async def async_setup_entry( @callback def async_add_binary_sensor(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave Binary Sensor.""" - entities: List[ZWaveBaseEntity] = [] + entities: List[BinarySensorEntity] = [] if info.platform_hint == "notification": - entities.append(ZWaveNotificationBinarySensor(config_entry, client, info)) + # Get all sensors from Notification CC states + for state_key in info.primary_value.metadata.states: + # ignore idle key (0) + if state_key == "0": + continue + entities.append( + ZWaveNotificationBinarySensor(config_entry, client, info, state_key) + ) elif info.platform_hint == "property": entities.append(ZWavePropertyBinarySensor(config_entry, client, info)) else: @@ -335,58 +284,60 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity): """Representation of a Z-Wave binary_sensor from Notification CommandClass.""" def __init__( - self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + self, + config_entry: ConfigEntry, + client: ZwaveClient, + info: ZwaveDiscoveryInfo, + state_key: str, ) -> None: """Initialize a ZWaveNotificationBinarySensor entity.""" super().__init__(config_entry, client, info) + self.state_key = state_key # check if we have a custom mapping for this value self._mapping_info = self._get_sensor_mapping() @property def is_on(self) -> bool: """Return if the sensor is on or off.""" - if self._mapping_info: - return self.info.primary_value.value in self._mapping_info["states"] - return bool(self.info.primary_value.value != 0) + return int(self.info.primary_value.value) == int(self.state_key) @property def name(self) -> str: """Return default name from device name and value name combination.""" node_name = self.info.node.name or self.info.node.device_config.description - property_name = self.info.primary_value.property_name - property_key_name = self.info.primary_value.property_key_name - return f"{node_name}: {property_name}: {property_key_name}" + value_name = self.info.primary_value.property_name + state_label = self.info.primary_value.metadata.states[self.state_key] + return f"{node_name}: {value_name} - {state_label}" @property def device_class(self) -> Optional[str]: """Return device class.""" return self._mapping_info.get("device_class") + @property + def unique_id(self) -> str: + """Return unique id for this entity.""" + return f"{super().unique_id}.{self.state_key}" + @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" - # We hide some more advanced sensors by default to not overwhelm users if not self._mapping_info: - # consider value for which we do not have a mapping as advanced. - return False + return True return self._mapping_info.get("enabled", True) @callback def _get_sensor_mapping(self) -> NotificationSensorMapping: """Try to get a device specific mapping for this sensor.""" for mapping in NOTIFICATION_SENSOR_MAPPINGS: - if mapping["type"] != int( - self.info.primary_value.metadata.cc_specific["notificationType"] + if ( + mapping["type"] + != self.info.primary_value.metadata.cc_specific["notificationType"] ): continue - for state_key in self.info.primary_value.metadata.states: - # make sure the key is int - state_key = int(state_key) - if state_key not in mapping["states"]: - continue + if not mapping.get("states") or self.state_key in mapping["states"]: # match found - mapping_info = mapping.copy() - return mapping_info + return mapping return {} diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 1fdd8e12fd6..0720c28aceb 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -143,10 +143,8 @@ DISCOVERY_SCHEMAS = [ platform="sensor", hint="string_sensor", command_class={ - CommandClass.ALARM, CommandClass.SENSOR_ALARM, CommandClass.INDICATOR, - CommandClass.NOTIFICATION, }, type={"string"}, ), @@ -157,14 +155,30 @@ DISCOVERY_SCHEMAS = [ command_class={ CommandClass.SENSOR_MULTILEVEL, CommandClass.METER, - CommandClass.ALARM, CommandClass.SENSOR_ALARM, CommandClass.INDICATOR, CommandClass.BATTERY, + }, + type={"number"}, + ), + # special list sensors (Notification CC) + ZWaveDiscoverySchema( + platform="sensor", + hint="list_sensor", + command_class={ CommandClass.NOTIFICATION, + }, + type={"number"}, + ), + # sensor for basic CC + ZWaveDiscoverySchema( + platform="sensor", + hint="numeric_sensor", + command_class={ CommandClass.BASIC, }, type={"number"}, + property={"currentValue"}, ), # binary switches ZWaveDiscoverySchema( @@ -204,54 +218,44 @@ DISCOVERY_SCHEMAS = [ def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None, None]: """Run discovery on ZWave node and return matching (primary) values.""" for value in node.values.values(): - disc_val = async_discover_value(value) - if disc_val: - yield disc_val - - -@callback -def async_discover_value(value: ZwaveValue) -> Optional[ZwaveDiscoveryInfo]: - """Run discovery on Z-Wave value and return ZwaveDiscoveryInfo if match found.""" - for schema in DISCOVERY_SCHEMAS: - # check device_class_basic - if ( - schema.device_class_basic is not None - and value.node.device_class.basic not in schema.device_class_basic - ): - continue - # check device_class_generic - if ( - schema.device_class_generic is not None - and value.node.device_class.generic not in schema.device_class_generic - ): - continue - # check device_class_specific - if ( - schema.device_class_specific is not None - and value.node.device_class.specific not in schema.device_class_specific - ): - continue - # check command_class - if ( - schema.command_class is not None - and value.command_class not in schema.command_class - ): - continue - # check endpoint - if schema.endpoint is not None and value.endpoint not in schema.endpoint: - continue - # check property - if schema.property is not None and value.property_ not in schema.property: - continue - # check metadata_type - if schema.type is not None and value.metadata.type not in schema.type: - continue - # all checks passed, this value belongs to an entity - return ZwaveDiscoveryInfo( - node=value.node, - primary_value=value, - platform=schema.platform, - platform_hint=schema.hint, - ) - - return None + for schema in DISCOVERY_SCHEMAS: + # check device_class_basic + if ( + schema.device_class_basic is not None + and value.node.device_class.basic not in schema.device_class_basic + ): + continue + # check device_class_generic + if ( + schema.device_class_generic is not None + and value.node.device_class.generic not in schema.device_class_generic + ): + continue + # check device_class_specific + if ( + schema.device_class_specific is not None + and value.node.device_class.specific not in schema.device_class_specific + ): + continue + # check command_class + if ( + schema.command_class is not None + and value.command_class not in schema.command_class + ): + continue + # check endpoint + if schema.endpoint is not None and value.endpoint not in schema.endpoint: + continue + # check property + if schema.property is not None and value.property_ not in schema.property: + continue + # check metadata_type + if schema.type is not None and value.metadata.type not in schema.type: + continue + # all checks passed, this value belongs to an entity + yield ZwaveDiscoveryInfo( + node=value.node, + primary_value=value, + platform=schema.platform, + platform_hint=schema.hint, + ) diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 9626ae9a888..285824ef2f1 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -79,6 +79,9 @@ class ZWaveBaseEntity(Entity): or self.info.primary_value.property_key_name or self.info.primary_value.property_name ) + # append endpoint if > 1 + if self.info.primary_value.endpoint > 1: + value_name += f" ({self.info.primary_value.endpoint})" return f"{node_name}: {value_name}" @property diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index ff48790b5f3..d5c34742c49 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -9,6 +9,7 @@ from zwave_js_server.const import CommandClass from homeassistant.components.sensor import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DOMAIN as SENSOR_DOMAIN, ) @@ -39,6 +40,8 @@ async def async_setup_entry( entities.append(ZWaveStringSensor(config_entry, client, info)) elif info.platform_hint == "numeric_sensor": entities.append(ZWaveNumericSensor(config_entry, client, info)) + elif info.platform_hint == "list_sensor": + entities.append(ZWaveListSensor(config_entry, client, info)) else: LOGGER.warning( "Sensor not implemented for %s/%s", @@ -67,11 +70,15 @@ class ZwaveSensorBase(ZWaveBaseEntity): if self.info.primary_value.command_class == CommandClass.BATTERY: return DEVICE_CLASS_BATTERY if self.info.primary_value.command_class == CommandClass.METER: - if self.info.primary_value.property_key_name == "kWh_Consumed": + if self.info.primary_value.metadata.unit == "kWh": return DEVICE_CLASS_ENERGY return DEVICE_CLASS_POWER - if self.info.primary_value.property_ == "Air temperature": + if "temperature" in self.info.primary_value.property_.lower(): return DEVICE_CLASS_TEMPERATURE + if self.info.primary_value.metadata.unit == "W": + return DEVICE_CLASS_POWER + if self.info.primary_value.metadata.unit == "Lux": + return DEVICE_CLASS_ILLUMINANCE return None @property @@ -133,17 +140,42 @@ class ZWaveNumericSensor(ZwaveSensorBase): return str(self.info.primary_value.metadata.unit) @property - def device_state_attributes(self) -> Optional[Dict[str, str]]: - """Return the device specific state attributes.""" + def name(self) -> str: + """Return default name from device name and value name combination.""" + if self.info.primary_value.command_class == CommandClass.BASIC: + node_name = self.info.node.name or self.info.node.device_config.description + label = self.info.primary_value.command_class_name + return f"{node_name}: {label}" + return super().name + + +class ZWaveListSensor(ZwaveSensorBase): + """Representation of a Z-Wave Numeric sensor with multiple states.""" + + @property + def state(self) -> Optional[str]: + """Return state of the sensor.""" + if self.info.primary_value.value is None: + return None if ( - self.info.primary_value.value is None - or not self.info.primary_value.metadata.states + not str(self.info.primary_value.value) + in self.info.primary_value.metadata.states ): return None - # add the value's label as property for multi-value (list) items - label = self.info.primary_value.metadata.states.get( - self.info.primary_value.value - ) or self.info.primary_value.metadata.states.get( - str(self.info.primary_value.value) + return str( + self.info.primary_value.metadata.states[str(self.info.primary_value.value)] ) - return {"label": label} + + @property + def device_state_attributes(self) -> Optional[Dict[str, str]]: + """Return the device specific state attributes.""" + # add the value's int value as property for multi-value (list) items + return {"value": self.info.primary_value.value} + + @property + def name(self) -> str: + """Return default name from device name and value name combination.""" + node_name = self.info.node.name or self.info.node.device_config.description + prop_name = self.info.primary_value.property_name + prop_key_name = self.info.primary_value.property_key_name + return f"{node_name}: {prop_name} - {prop_key_name}" diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index 399b009f4c2..63ec9013fa3 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -7,8 +7,9 @@ LOW_BATTERY_BINARY_SENSOR = "binary_sensor.multisensor_6_low_battery_level" ENABLED_LEGACY_BINARY_SENSOR = "binary_sensor.z_wave_door_window_sensor_any" DISABLED_LEGACY_BINARY_SENSOR = "binary_sensor.multisensor_6_any" NOTIFICATION_MOTION_BINARY_SENSOR = ( - "binary_sensor.multisensor_6_home_security_motion_sensor_status" + "binary_sensor.multisensor_6_home_security_motion_detection" ) +NOTIFICATION_MOTION_SENSOR = "sensor.multisensor_6_home_security_motion_sensor_status" PROPERTY_DOOR_STATUS_BINARY_SENSOR = ( "binary_sensor.august_smart_lock_pro_3rd_gen_the_current_status_of_the_door" ) diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index 284d2e1a84f..bd6fb9f2569 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -7,8 +7,17 @@ from homeassistant.const import ( POWER_WATT, TEMP_CELSIUS, ) +from homeassistant.helpers.entity_registry import ( + DISABLED_INTEGRATION, + async_get_registry, +) -from .common import AIR_TEMPERATURE_SENSOR, ENERGY_SENSOR, POWER_SENSOR +from .common import ( + AIR_TEMPERATURE_SENSOR, + ENERGY_SENSOR, + NOTIFICATION_MOTION_SENSOR, + POWER_SENSOR, +) async def test_numeric_sensor(hass, multisensor_6, integration): @@ -36,3 +45,28 @@ async def test_energy_sensors(hass, hank_binary_switch, integration): assert state.state == "0.16" assert state.attributes["unit_of_measurement"] == ENERGY_KILO_WATT_HOUR assert state.attributes["device_class"] == DEVICE_CLASS_ENERGY + + +async def test_disabled_notification_sensor(hass, multisensor_6, integration): + """Test sensor is created from Notification CC and is disabled.""" + ent_reg = await async_get_registry(hass) + entity_entry = ent_reg.async_get(NOTIFICATION_MOTION_SENSOR) + + assert entity_entry + assert entity_entry.disabled + assert entity_entry.disabled_by == DISABLED_INTEGRATION + + # Test enabling entity + updated_entry = ent_reg.async_update_entity( + entity_entry.entity_id, **{"disabled_by": None} + ) + assert updated_entry != entity_entry + assert updated_entry.disabled is False + + # reload integration and check if entity is correctly there + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(NOTIFICATION_MOTION_SENSOR) + assert state.state == "Motion detection" + assert state.attributes["value"] == 8 From b3e2f8f9049076c7c0d884a2dfded939400bb5c0 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Tue, 2 Feb 2021 04:17:17 -0500 Subject: [PATCH 134/796] Add Insteon entities in event loop (#45829) --- homeassistant/components/insteon/binary_sensor.py | 8 +++++--- homeassistant/components/insteon/climate.py | 8 +++++--- homeassistant/components/insteon/cover.py | 8 +++++--- homeassistant/components/insteon/fan.py | 8 +++++--- homeassistant/components/insteon/light.py | 8 +++++--- homeassistant/components/insteon/switch.py | 8 +++++--- 6 files changed, 30 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/insteon/binary_sensor.py b/homeassistant/components/insteon/binary_sensor.py index ad87c69bd0f..69a4a5f5280 100644 --- a/homeassistant/components/insteon/binary_sensor.py +++ b/homeassistant/components/insteon/binary_sensor.py @@ -27,6 +27,7 @@ from homeassistant.components.binary_sensor import ( DOMAIN as BINARY_SENSOR_DOMAIN, BinarySensorEntity, ) +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import SIGNAL_ADD_ENTITIES @@ -51,7 +52,8 @@ SENSOR_TYPES = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Insteon binary sensors from a config entry.""" - def add_entities(discovery_info=None): + @callback + def async_add_insteon_binary_sensor_entities(discovery_info=None): """Add the Insteon entities for the platform.""" async_add_insteon_entities( hass, @@ -62,8 +64,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) signal = f"{SIGNAL_ADD_ENTITIES}_{BINARY_SENSOR_DOMAIN}" - async_dispatcher_connect(hass, signal, add_entities) - add_entities() + async_dispatcher_connect(hass, signal, async_add_insteon_binary_sensor_entities) + async_add_insteon_binary_sensor_entities() class InsteonBinarySensorEntity(InsteonEntity, BinarySensorEntity): diff --git a/homeassistant/components/insteon/climate.py b/homeassistant/components/insteon/climate.py index 7d4d9543c3f..c699e76c4f3 100644 --- a/homeassistant/components/insteon/climate.py +++ b/homeassistant/components/insteon/climate.py @@ -25,6 +25,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE_RANGE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import SIGNAL_ADD_ENTITIES @@ -64,7 +65,8 @@ SUPPORTED_FEATURES = ( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Insteon climate entities from a config entry.""" - def add_entities(discovery_info=None): + @callback + def async_add_insteon_climate_entities(discovery_info=None): """Add the Insteon entities for the platform.""" async_add_insteon_entities( hass, @@ -75,8 +77,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) signal = f"{SIGNAL_ADD_ENTITIES}_{CLIMATE_DOMAIN}" - async_dispatcher_connect(hass, signal, add_entities) - add_entities() + async_dispatcher_connect(hass, signal, async_add_insteon_climate_entities) + async_add_insteon_climate_entities() class InsteonClimateEntity(InsteonEntity, ClimateEntity): diff --git a/homeassistant/components/insteon/cover.py b/homeassistant/components/insteon/cover.py index 498d194667c..fd20637b174 100644 --- a/homeassistant/components/insteon/cover.py +++ b/homeassistant/components/insteon/cover.py @@ -9,6 +9,7 @@ from homeassistant.components.cover import ( SUPPORT_SET_POSITION, CoverEntity, ) +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import SIGNAL_ADD_ENTITIES @@ -21,15 +22,16 @@ SUPPORTED_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Insteon covers from a config entry.""" - def add_entities(discovery_info=None): + @callback + def async_add_insteon_cover_entities(discovery_info=None): """Add the Insteon entities for the platform.""" async_add_insteon_entities( hass, COVER_DOMAIN, InsteonCoverEntity, async_add_entities, discovery_info ) signal = f"{SIGNAL_ADD_ENTITIES}_{COVER_DOMAIN}" - async_dispatcher_connect(hass, signal, add_entities) - add_entities() + async_dispatcher_connect(hass, signal, async_add_insteon_cover_entities) + async_add_insteon_cover_entities() class InsteonCoverEntity(InsteonEntity, CoverEntity): diff --git a/homeassistant/components/insteon/fan.py b/homeassistant/components/insteon/fan.py index 3327f9df5eb..a641d353450 100644 --- a/homeassistant/components/insteon/fan.py +++ b/homeassistant/components/insteon/fan.py @@ -8,6 +8,7 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, FanEntity, ) +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.percentage import ( percentage_to_ranged_value, @@ -24,15 +25,16 @@ SPEED_RANGE = (1, FanSpeed.HIGH) # off is not included async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Insteon fans from a config entry.""" - def add_entities(discovery_info=None): + @callback + def async_add_insteon_fan_entities(discovery_info=None): """Add the Insteon entities for the platform.""" async_add_insteon_entities( hass, FAN_DOMAIN, InsteonFanEntity, async_add_entities, discovery_info ) signal = f"{SIGNAL_ADD_ENTITIES}_{FAN_DOMAIN}" - async_dispatcher_connect(hass, signal, add_entities) - add_entities() + async_dispatcher_connect(hass, signal, async_add_insteon_fan_entities) + async_add_insteon_fan_entities() class InsteonFanEntity(InsteonEntity, FanEntity): diff --git a/homeassistant/components/insteon/light.py b/homeassistant/components/insteon/light.py index f49dafed2fe..206aa078dc3 100644 --- a/homeassistant/components/insteon/light.py +++ b/homeassistant/components/insteon/light.py @@ -6,6 +6,7 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, LightEntity, ) +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import SIGNAL_ADD_ENTITIES @@ -18,15 +19,16 @@ MAX_BRIGHTNESS = 255 async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Insteon lights from a config entry.""" - def add_entities(discovery_info=None): + @callback + def async_add_insteon_light_entities(discovery_info=None): """Add the Insteon entities for the platform.""" async_add_insteon_entities( hass, LIGHT_DOMAIN, InsteonDimmerEntity, async_add_entities, discovery_info ) signal = f"{SIGNAL_ADD_ENTITIES}_{LIGHT_DOMAIN}" - async_dispatcher_connect(hass, signal, add_entities) - add_entities() + async_dispatcher_connect(hass, signal, async_add_insteon_light_entities) + async_add_insteon_light_entities() class InsteonDimmerEntity(InsteonEntity, LightEntity): diff --git a/homeassistant/components/insteon/switch.py b/homeassistant/components/insteon/switch.py index 43430ceb7a0..0a1a0253b1d 100644 --- a/homeassistant/components/insteon/switch.py +++ b/homeassistant/components/insteon/switch.py @@ -1,5 +1,6 @@ """Support for INSTEON dimmers via PowerLinc Modem.""" from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import SIGNAL_ADD_ENTITIES @@ -10,15 +11,16 @@ from .utils import async_add_insteon_entities async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Insteon switches from a config entry.""" - def add_entities(discovery_info=None): + @callback + def async_add_insteon_switch_entities(discovery_info=None): """Add the Insteon entities for the platform.""" async_add_insteon_entities( hass, SWITCH_DOMAIN, InsteonSwitchEntity, async_add_entities, discovery_info ) signal = f"{SIGNAL_ADD_ENTITIES}_{SWITCH_DOMAIN}" - async_dispatcher_connect(hass, signal, add_entities) - add_entities() + async_dispatcher_connect(hass, signal, async_add_insteon_switch_entities) + async_add_insteon_switch_entities() class InsteonSwitchEntity(InsteonEntity, SwitchEntity): From 0e3ba532c721095288b6eb6e8fe6efa74b16395d Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 2 Feb 2021 10:18:44 +0100 Subject: [PATCH 135/796] Update zwave_js discovery schema for light platform (#45861) --- homeassistant/components/zwave_js/discovery.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 0720c28aceb..88717d9fc83 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -115,6 +115,7 @@ DISCOVERY_SCHEMAS = [ "Binary Tunable Color Light", "Multilevel Remote Switch", "Multilevel Power Switch", + "Multilevel Scene Switch", }, command_class={CommandClass.SWITCH_MULTILEVEL}, property={"currentValue"}, From a64ad50b27d2004c1aa7f43605994548d220b03e Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 2 Feb 2021 03:30:12 -0600 Subject: [PATCH 136/796] Remove zwave_js devices that the controller is no longer connected to on initialization (#45853) * Remove zwave_js devices that the controller is no longer connected to on initialization * remove extra line break * fix test * Clean up Co-authored-by: Paulus Schoutsen * Lint Co-authored-by: Martin Hjelmare Co-authored-by: Paulus Schoutsen --- homeassistant/components/zwave_js/__init__.py | 14 +++++++ tests/components/zwave_js/conftest.py | 12 ++++++ tests/components/zwave_js/test_init.py | 41 +++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index a4eb466fe87..82e79b83659 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -102,6 +102,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # update entity availability async_dispatcher_send(hass, f"{DOMAIN}_{entry.entry_id}_connection_state") + # Check for nodes that no longer exist and remove them + stored_devices = device_registry.async_entries_for_config_entry( + dev_reg, entry.entry_id + ) + known_devices = [ + dev_reg.async_get_device({get_device_id(client, node)}) + for node in client.driver.controller.nodes.values() + ] + + # Devices that are in the device registry that are not known by the controller can be removed + for device in stored_devices: + if device not in known_devices: + dev_reg.async_remove_device(device.id) + @callback def async_on_node_ready(node: ZwaveNode) -> None: """Handle node ready event.""" diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index b6cbc911b6a..0e0ebdee3c6 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -308,3 +308,15 @@ def in_wall_smart_fan_control_fixture(client, in_wall_smart_fan_control_state): node = Node(client, in_wall_smart_fan_control_state) client.driver.controller.nodes[node.node_id] = node return node + + +@pytest.fixture(name="multiple_devices") +def multiple_devices_fixture( + client, climate_radio_thermostat_ct100_plus_state, lock_schlage_be469_state +): + """Mock a client with multiple devices.""" + node = Node(client, climate_radio_thermostat_ct100_plus_state) + client.driver.controller.nodes[node.node_id] = node + node = Node(client, lock_schlage_be469_state) + client.driver.controller.nodes[node.node_id] = node + return client.driver.controller.nodes diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index b17945d05c6..86fbf27ab4f 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -7,6 +7,7 @@ from zwave_js_server.model.node import Node from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.zwave_js.const import DOMAIN +from homeassistant.components.zwave_js.entity import get_device_id from homeassistant.config_entries import ( CONN_CLASS_LOCAL_PUSH, ENTRY_STATE_LOADED, @@ -14,6 +15,7 @@ from homeassistant.config_entries import ( ENTRY_STATE_SETUP_RETRY, ) from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.helpers import device_registry, entity_registry from .common import AIR_TEMPERATURE_SENSOR @@ -290,3 +292,42 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog): assert entry.state == ENTRY_STATE_NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert "Failed to uninstall the Z-Wave JS add-on" in caplog.text + + +async def test_removed_device(hass, client, multiple_devices, integration): + """Test that the device registry gets updated when a device gets removed.""" + nodes = multiple_devices + + # Verify how many nodes are available + assert len(client.driver.controller.nodes) == 2 + + # Make sure there are the same number of devices + dev_reg = await device_registry.async_get_registry(hass) + device_entries = device_registry.async_entries_for_config_entry( + dev_reg, integration.entry_id + ) + assert len(device_entries) == 2 + + # Check how many entities there are + ent_reg = await entity_registry.async_get_registry(hass) + entity_entries = entity_registry.async_entries_for_config_entry( + ent_reg, integration.entry_id + ) + assert len(entity_entries) == 18 + + # Remove a node and reload the entry + old_node = nodes.pop(13) + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + # Assert that the node and all of it's entities were removed from the device and + # entity registry + device_entries = device_registry.async_entries_for_config_entry( + dev_reg, integration.entry_id + ) + assert len(device_entries) == 1 + entity_entries = entity_registry.async_entries_for_config_entry( + ent_reg, integration.entry_id + ) + assert len(entity_entries) == 9 + assert dev_reg.async_get_device({get_device_id(client, old_node)}) is None From 463a32819c0ae8f5c84e75a16fdc99aef0a11cae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Feb 2021 03:30:38 -1000 Subject: [PATCH 137/796] Ensure homekit never picks a port that another config entry uses (#45433) --- homeassistant/components/homekit/__init__.py | 17 ++++++----- .../components/homekit/config_flow.py | 12 +++----- homeassistant/components/homekit/util.py | 21 ++++++++++++-- tests/components/homekit/test_config_flow.py | 6 ++-- tests/components/homekit/test_homekit.py | 5 +--- tests/components/homekit/test_util.py | 28 +++++++++++++++---- 6 files changed, 59 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index e92c35ffac8..568804ef081 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -238,12 +238,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): port = conf[CONF_PORT] _LOGGER.debug("Begin setup HomeKit for %s", name) - # If the previous instance hasn't cleaned up yet - # we need to wait a bit - if not await hass.async_add_executor_job(port_is_available, port): - _LOGGER.warning("The local port %s is in use", port) - raise ConfigEntryNotReady - if CONF_ENTRY_INDEX in conf and conf[CONF_ENTRY_INDEX] == 0: _LOGGER.debug("Migrating legacy HomeKit data for %s", name) hass.async_add_executor_job( @@ -275,7 +269,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): entry.entry_id, ) zeroconf_instance = await zeroconf.async_get_instance(hass) - await hass.async_add_executor_job(homekit.setup, zeroconf_instance) + + # If the previous instance hasn't cleaned up yet + # we need to wait a bit + try: + await hass.async_add_executor_job(homekit.setup, zeroconf_instance) + except (OSError, AttributeError) as ex: + _LOGGER.warning( + "%s could not be setup because the local port %s is in use", name, port + ) + raise ConfigEntryNotReady from ex undo_listener = entry.add_update_listener(_async_update_listener) diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index d8708168e12..a20d9a5b843 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -40,7 +40,7 @@ from .const import ( VIDEO_CODEC_COPY, ) from .const import DOMAIN # pylint:disable=unused-import -from .util import find_next_available_port +from .util import async_find_next_available_port CONF_CAMERA_COPY = "camera_copy" CONF_INCLUDE_EXCLUDE_MODE = "include_exclude_mode" @@ -162,7 +162,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: return self.async_create_entry(title=self.entry_title, data=self.hk_data) - self.hk_data[CONF_PORT] = await self._async_available_port() + self.hk_data[CONF_PORT] = await async_find_next_available_port( + self.hass, DEFAULT_CONFIG_FLOW_PORT + ) self.hk_data[CONF_NAME] = self._async_available_name( self.hk_data[CONF_HOMEKIT_MODE] ) @@ -205,12 +207,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=f"{user_input[CONF_NAME]}:{user_input[CONF_PORT]}", data=user_input ) - async def _async_available_port(self): - """Return an available port the bridge.""" - return await self.hass.async_add_executor_job( - find_next_available_port, DEFAULT_CONFIG_FLOW_PORT - ) - @callback def _async_current_names(self): """Return a set of bridge names.""" diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 98374b73f40..83a9a0a0353 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -15,6 +15,7 @@ from homeassistant.const import ( ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, + CONF_PORT, CONF_TYPE, TEMP_CELSIUS, ) @@ -445,7 +446,7 @@ def _get_test_socket(): return test_socket -def port_is_available(port: int): +def port_is_available(port: int) -> bool: """Check to see if a port is available.""" test_socket = _get_test_socket() try: @@ -456,10 +457,24 @@ def port_is_available(port: int): return True -def find_next_available_port(start_port: int): +async def async_find_next_available_port(hass: HomeAssistant, start_port: int) -> int: + """Find the next available port not assigned to a config entry.""" + exclude_ports = set() + for entry in hass.config_entries.async_entries(DOMAIN): + if CONF_PORT in entry.data: + exclude_ports.add(entry.data[CONF_PORT]) + + return await hass.async_add_executor_job( + _find_next_available_port, start_port, exclude_ports + ) + + +def _find_next_available_port(start_port: int, exclude_ports: set) -> int: """Find the next available port starting with the given port.""" test_socket = _get_test_socket() for port in range(start_port, MAX_PORT): + if port in exclude_ports: + continue try: test_socket.bind(("", port)) return port @@ -469,7 +484,7 @@ def find_next_available_port(start_port: int): continue -def pid_is_alive(pid): +def pid_is_alive(pid) -> bool: """Check to see if a process is alive.""" try: os.kill(pid, 0) diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index 1dd628af18d..0e4114d566d 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -49,7 +49,7 @@ async def test_setup_in_bridge_mode(hass): assert result2["step_id"] == "bridge_mode" with patch( - "homeassistant.components.homekit.config_flow.find_next_available_port", + "homeassistant.components.homekit.config_flow.async_find_next_available_port", return_value=12345, ): result3 = await hass.config_entries.flow.async_configure( @@ -108,7 +108,7 @@ async def test_setup_in_accessory_mode(hass): assert result2["step_id"] == "accessory_mode" with patch( - "homeassistant.components.homekit.config_flow.find_next_available_port", + "homeassistant.components.homekit.config_flow.async_find_next_available_port", return_value=12345, ): result3 = await hass.config_entries.flow.async_configure( @@ -629,7 +629,7 @@ async def test_converting_bridge_to_accessory_mode(hass): assert result2["step_id"] == "bridge_mode" with patch( - "homeassistant.components.homekit.config_flow.find_next_available_port", + "homeassistant.components.homekit.config_flow.async_find_next_available_port", return_value=12345, ): result3 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index a8c3c81595e..f0d6a8b365f 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1020,10 +1020,7 @@ async def test_raise_config_entry_not_ready(hass, mock_zeroconf): ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.homekit.port_is_available", - return_value=False, - ): + with patch(f"{PATH_HOMEKIT}.HomeKit.setup", side_effect=OSError): assert not await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index e0f10a94d69..afa1408a06b 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -3,6 +3,7 @@ import pytest import voluptuous as vol from homeassistant.components.homekit.const import ( + BRIDGE_NAME, CONF_FEATURE, CONF_FEATURE_LIST, CONF_LINKED_BATTERY_SENSOR, @@ -21,11 +22,11 @@ from homeassistant.components.homekit.const import ( TYPE_VALVE, ) from homeassistant.components.homekit.util import ( + async_find_next_available_port, cleanup_name_for_homekit, convert_to_float, density_to_air_quality, dismiss_setup_message, - find_next_available_port, format_sw_version, port_is_available, show_setup_message, @@ -43,6 +44,7 @@ from homeassistant.const import ( ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, + CONF_PORT, CONF_TYPE, STATE_UNKNOWN, TEMP_CELSIUS, @@ -52,7 +54,7 @@ from homeassistant.core import State from .util import async_init_integration -from tests.common import async_mock_service +from tests.common import MockConfigEntry, async_mock_service def test_validate_entity_config(): @@ -251,14 +253,30 @@ async def test_dismiss_setup_msg(hass): async def test_port_is_available(hass): """Test we can get an available port and it is actually available.""" - next_port = await hass.async_add_executor_job( - find_next_available_port, DEFAULT_CONFIG_FLOW_PORT - ) + next_port = await async_find_next_available_port(hass, DEFAULT_CONFIG_FLOW_PORT) + assert next_port assert await hass.async_add_executor_job(port_is_available, next_port) +async def test_port_is_available_skips_existing_entries(hass): + """Test we can get an available port and it is actually available.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_CONFIG_FLOW_PORT}, + options={}, + ) + entry.add_to_hass(hass) + + next_port = await async_find_next_available_port(hass, DEFAULT_CONFIG_FLOW_PORT) + + assert next_port + assert next_port != DEFAULT_CONFIG_FLOW_PORT + + assert await hass.async_add_executor_job(port_is_available, next_port) + + async def test_format_sw_version(): """Test format_sw_version method.""" assert format_sw_version("soho+3.6.8+soho-release-rt120+10") == "3.6.8" From a96a80e78d032cc6d77077f4d8590ba169dc240a Mon Sep 17 00:00:00 2001 From: Daniel Pereira Date: Tue, 2 Feb 2021 10:38:13 -0300 Subject: [PATCH 138/796] Update alexa/const.py to reflect docs (#45806) The current docs say the Alexa integration is compatible with languages not currently present in the conf validator. See: https://www.home-assistant.io/integrations/alexa.smart_home/#alexa-locale --- homeassistant/components/alexa/const.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index a5a1cde2e15..402cb9e1fb2 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -53,10 +53,13 @@ CONF_SUPPORTED_LOCALES = ( "en-US", "es-ES", "es-MX", + "es-US", "fr-CA", "fr-FR", + "hi-IN", "it-IT", "ja-JP", + "pt-BR", ) API_TEMP_UNITS = {TEMP_FAHRENHEIT: "FAHRENHEIT", TEMP_CELSIUS: "CELSIUS"} From d417ee27324d569f1197aa6ba61eabc7cf099daf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Feb 2021 03:39:07 -1000 Subject: [PATCH 139/796] Add fan speed percentage support to google assistant (#45835) --- .../components/google_assistant/trait.py | 20 ++++++++++++++----- .../components/google_assistant/test_trait.py | 12 +++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index b5dc2afd3e2..f2a2274d8a8 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1276,6 +1276,7 @@ class FanSpeedTrait(_Trait): return { "availableFanSpeeds": {"speeds": speeds, "ordered": True}, "reversible": reversible, + "supportsFanSpeedPercent": True, } def query_attributes(self): @@ -1289,9 +1290,11 @@ class FanSpeedTrait(_Trait): response["currentFanSpeedSetting"] = speed if domain == fan.DOMAIN: speed = attrs.get(fan.ATTR_SPEED) + percent = attrs.get(fan.ATTR_PERCENTAGE) or 0 if speed is not None: response["on"] = speed != fan.SPEED_OFF response["currentFanSpeedSetting"] = speed + response["currentFanSpeedPercent"] = percent return response async def execute(self, command, data, params, challenge): @@ -1309,13 +1312,20 @@ class FanSpeedTrait(_Trait): context=data.context, ) if domain == fan.DOMAIN: + service_params = { + ATTR_ENTITY_ID: self.state.entity_id, + } + if "fanSpeedPercent" in params: + service = fan.SERVICE_SET_PERCENTAGE + service_params[fan.ATTR_PERCENTAGE] = params["fanSpeedPercent"] + else: + service = fan.SERVICE_SET_SPEED + service_params[fan.ATTR_SPEED] = params["fanSpeed"] + await self.hass.services.async_call( fan.DOMAIN, - fan.SERVICE_SET_SPEED, - { - ATTR_ENTITY_ID: self.state.entity_id, - fan.ATTR_SPEED: params["fanSpeed"], - }, + service, + service_params, blocking=True, context=data.context, ) diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 9b573f1cf71..74e8ab21eb0 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1391,6 +1391,7 @@ async def test_fan_speed(hass): fan.SPEED_HIGH, ], "speed": "low", + "percentage": 33, }, ), BASIC_CONFIG, @@ -1438,11 +1439,13 @@ async def test_fan_speed(hass): ], }, "reversible": False, + "supportsFanSpeedPercent": True, } assert trt.query_attributes() == { "currentFanSpeedSetting": "low", "on": True, + "currentFanSpeedPercent": 33, } assert trt.can_execute(trait.COMMAND_FANSPEED, params={"fanSpeed": "medium"}) @@ -1453,6 +1456,14 @@ async def test_fan_speed(hass): assert len(calls) == 1 assert calls[0].data == {"entity_id": "fan.living_room_fan", "speed": "medium"} + assert trt.can_execute(trait.COMMAND_FANSPEED, params={"fanSpeedPercent": 10}) + + calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_PERCENTAGE) + await trt.execute(trait.COMMAND_FANSPEED, BASIC_DATA, {"fanSpeedPercent": 10}, {}) + + assert len(calls) == 1 + assert calls[0].data == {"entity_id": "fan.living_room_fan", "percentage": 10} + async def test_climate_fan_speed(hass): """Test FanSpeed trait speed control support for climate domain.""" @@ -1495,6 +1506,7 @@ async def test_climate_fan_speed(hass): ], }, "reversible": False, + "supportsFanSpeedPercent": True, } assert trt.query_attributes() == { From 0382c932836ff58854fb2076a16b825928f95566 Mon Sep 17 00:00:00 2001 From: Thomas Friedel Date: Tue, 2 Feb 2021 14:45:02 +0100 Subject: [PATCH 140/796] Enable Osramlightify again (#45849) --- homeassistant/components/osramlightify/manifest.json | 3 +-- requirements_all.txt | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/osramlightify/manifest.json b/homeassistant/components/osramlightify/manifest.json index 7e4b810b223..80cfeff6e12 100644 --- a/homeassistant/components/osramlightify/manifest.json +++ b/homeassistant/components/osramlightify/manifest.json @@ -1,8 +1,7 @@ { - "disabled": "Upstream package has been removed from PyPi", "domain": "osramlightify", "name": "Osramlightify", "documentation": "https://www.home-assistant.io/integrations/osramlightify", - "requirements": ["lightify==1.0.7.2"], + "requirements": ["lightify==1.0.7.3"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 2ff45edcda0..77ac5fb106b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -879,6 +879,9 @@ life360==4.1.1 # homeassistant.components.lifx_legacy liffylights==0.9.4 +# homeassistant.components.osramlightify +lightify==1.0.7.3 + # homeassistant.components.lightwave lightwave==0.19 From 7ff4281b6d43f5177d783ded8c93b4f1544e5726 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 2 Feb 2021 15:10:54 +0100 Subject: [PATCH 141/796] Upgrade jinja2 to >=2.11.3 (#45843) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e7585a1aaa8..5b4ff1dfcd0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ emoji==0.5.4 hass-nabucasa==0.41.0 home-assistant-frontend==20210127.6 httpx==0.16.1 -jinja2>=2.11.2 +jinja2>=2.11.3 netdisco==2.8.2 paho-mqtt==1.5.1 pillow==8.1.0 diff --git a/requirements.txt b/requirements.txt index ef7d73e1cb5..4b7ff6d7ef8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ bcrypt==3.1.7 certifi>=2020.12.5 ciso8601==2.1.3 httpx==0.16.1 -jinja2>=2.11.2 +jinja2>=2.11.3 PyJWT==1.7.1 cryptography==3.3.1 pip>=8.0.3,<20.3 diff --git a/setup.py b/setup.py index 5998a40a24e..125776ea4b0 100755 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ REQUIRES = [ "certifi>=2020.12.5", "ciso8601==2.1.3", "httpx==0.16.1", - "jinja2>=2.11.2", + "jinja2>=2.11.3", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. "cryptography==3.3.1", From e9b2d33ad85a036a6f30119951b7af0e6bbbd3c1 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 2 Feb 2021 15:18:58 +0100 Subject: [PATCH 142/796] Upgrade pytz to >=2021.1 (#45839) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5b4ff1dfcd0..570a79a3bbd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ paho-mqtt==1.5.1 pillow==8.1.0 pip>=8.0.3,<20.3 python-slugify==4.0.1 -pytz>=2020.5 +pytz>=2021.1 pyyaml==5.4.1 requests==2.25.1 ruamel.yaml==0.15.100 diff --git a/requirements.txt b/requirements.txt index 4b7ff6d7ef8..c094efe3e46 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ PyJWT==1.7.1 cryptography==3.3.1 pip>=8.0.3,<20.3 python-slugify==4.0.1 -pytz>=2020.5 +pytz>=2021.1 pyyaml==5.4.1 requests==2.25.1 ruamel.yaml==0.15.100 diff --git a/setup.py b/setup.py index 125776ea4b0..7e0df7f95c9 100755 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ REQUIRES = [ "cryptography==3.3.1", "pip>=8.0.3,<20.3", "python-slugify==4.0.1", - "pytz>=2020.5", + "pytz>=2021.1", "pyyaml==5.4.1", "requests==2.25.1", "ruamel.yaml==0.15.100", From 6e205965eea87e5af6698b41856d84812a3cd5c7 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 2 Feb 2021 15:28:21 +0100 Subject: [PATCH 143/796] Fix zwave_js device remove test (#45864) --- tests/components/zwave_js/test_init.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 86fbf27ab4f..fa61b3deb27 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -313,7 +313,7 @@ async def test_removed_device(hass, client, multiple_devices, integration): entity_entries = entity_registry.async_entries_for_config_entry( ent_reg, integration.entry_id ) - assert len(entity_entries) == 18 + assert len(entity_entries) == 24 # Remove a node and reload the entry old_node = nodes.pop(13) @@ -329,5 +329,5 @@ async def test_removed_device(hass, client, multiple_devices, integration): entity_entries = entity_registry.async_entries_for_config_entry( ent_reg, integration.entry_id ) - assert len(entity_entries) == 9 + assert len(entity_entries) == 15 assert dev_reg.async_get_device({get_device_id(client, old_node)}) is None From 63cc2517dd0827be8383831e9d8aabfa4ebb6a64 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 2 Feb 2021 15:53:03 +0100 Subject: [PATCH 144/796] Upgrade watchdog to 1.0.2 (#45848) --- homeassistant/components/folder_watcher/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/folder_watcher/manifest.json b/homeassistant/components/folder_watcher/manifest.json index 722b60a952d..60239aeb0d1 100644 --- a/homeassistant/components/folder_watcher/manifest.json +++ b/homeassistant/components/folder_watcher/manifest.json @@ -2,7 +2,7 @@ "domain": "folder_watcher", "name": "Folder Watcher", "documentation": "https://www.home-assistant.io/integrations/folder_watcher", - "requirements": ["watchdog==0.8.3"], + "requirements": ["watchdog==1.0.2"], "codeowners": [], "quality_scale": "internal" } diff --git a/requirements_all.txt b/requirements_all.txt index 77ac5fb106b..71a9eb7edf2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2281,7 +2281,7 @@ wakeonlan==1.1.6 waqiasync==1.0.0 # homeassistant.components.folder_watcher -watchdog==0.8.3 +watchdog==1.0.2 # homeassistant.components.waterfurnace waterfurnace==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 50b1c6670e6..25f8d3d45b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1148,7 +1148,7 @@ vultr==0.1.2 wakeonlan==1.1.6 # homeassistant.components.folder_watcher -watchdog==0.8.3 +watchdog==1.0.2 # homeassistant.components.wiffi wiffi==1.0.1 From 811bbb7acb6657300fcc6c531a99a4d6d62ac871 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 2 Feb 2021 15:56:56 +0100 Subject: [PATCH 145/796] Upgrade emoji to 1.2.0 (#45847) --- homeassistant/components/mobile_app/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/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index 758df70c3d0..bd8ed771348 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -3,7 +3,7 @@ "name": "Mobile App", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mobile_app", - "requirements": ["PyNaCl==1.3.0", "emoji==0.5.4"], + "requirements": ["PyNaCl==1.3.0", "emoji==1.2.0"], "dependencies": ["http", "webhook", "person", "tag"], "after_dependencies": ["cloud", "camera", "notify"], "codeowners": ["@robbiet480"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 570a79a3bbd..8ae31a0bbd2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ ciso8601==2.1.3 cryptography==3.3.1 defusedxml==0.6.0 distro==1.5.0 -emoji==0.5.4 +emoji==1.2.0 hass-nabucasa==0.41.0 home-assistant-frontend==20210127.6 httpx==0.16.1 diff --git a/requirements_all.txt b/requirements_all.txt index 71a9eb7edf2..2b8efbff9e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -541,7 +541,7 @@ eliqonline==1.2.2 elkm1-lib==0.8.10 # homeassistant.components.mobile_app -emoji==0.5.4 +emoji==1.2.0 # homeassistant.components.emulated_roku emulated_roku==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 25f8d3d45b7..1998d1baf0c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -290,7 +290,7 @@ elgato==1.0.0 elkm1-lib==0.8.10 # homeassistant.components.mobile_app -emoji==0.5.4 +emoji==1.2.0 # homeassistant.components.emulated_roku emulated_roku==0.2.1 From c93fec34b3a75de0f19003aab723afbf0fced076 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 2 Feb 2021 16:25:43 +0100 Subject: [PATCH 146/796] Fix zwave_js sensor device class attribute error (#45863) --- homeassistant/components/zwave_js/sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index d5c34742c49..3d3f782bc1b 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -73,7 +73,10 @@ class ZwaveSensorBase(ZWaveBaseEntity): if self.info.primary_value.metadata.unit == "kWh": return DEVICE_CLASS_ENERGY return DEVICE_CLASS_POWER - if "temperature" in self.info.primary_value.property_.lower(): + if ( + isinstance(self.info.primary_value.property_, str) + and "temperature" in self.info.primary_value.property_.lower() + ): return DEVICE_CLASS_TEMPERATURE if self.info.primary_value.metadata.unit == "W": return DEVICE_CLASS_POWER From 2e98cfb9ab8c73dc398037a1ac8bedaa7cfb2e3b Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 2 Feb 2021 19:57:08 +0100 Subject: [PATCH 147/796] Guard for missing value (#45867) * guard for missing value * update comment --- homeassistant/components/zwave_js/entity.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 285824ef2f1..b039113270d 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -92,7 +92,13 @@ class ZWaveBaseEntity(Entity): @property def available(self) -> bool: """Return entity availability.""" - return self.client.connected and bool(self.info.node.ready) + return ( + self.client.connected + and bool(self.info.node.ready) + # a None value indicates something wrong with the device, + # or the value is simply not yet there (it will arrive later). + and self.info.primary_value.value is not None + ) @callback def _value_changed(self, event_data: dict) -> None: From 524b9e7b1ff8b4aeed6dfd49a39a8d92cca807dc Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 2 Feb 2021 20:59:56 +0100 Subject: [PATCH 148/796] Use new zwave_js client (#45872) * Use new zwave_js client * Remove client callbacks * Clean up on connect and on disconnect * Clean log * Add stop listen to unsubscribe callbacks * Fix most tests * Adapt to new listen interface * Fix most tests * Remove stale connection state feature * Bump zwave-js-server-python to 0.16.0 * Clean up disconnect --- homeassistant/components/zwave_js/__init__.py | 146 ++++++++++-------- homeassistant/components/zwave_js/entity.py | 9 -- .../components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/conftest.py | 35 ++--- tests/components/zwave_js/test_init.py | 38 ----- 7 files changed, 104 insertions(+), 130 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 82e79b83659..1a2cdfa7017 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -1,9 +1,11 @@ """The Z-Wave JS integration.""" import asyncio import logging +from typing import Callable, List from async_timeout import timeout from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.exceptions import BaseZwaveJSServerError from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.notification import Notification from zwave_js_server.model.value import ValueNotification @@ -45,6 +47,8 @@ from .entity import get_device_id LOGGER = logging.getLogger(__name__) CONNECT_TIMEOUT = 10 +DATA_CLIENT_LISTEN_TASK = "client_listen_task" +DATA_START_PLATFORM_TASK = "start_platform_task" async def async_setup(hass: HomeAssistant, config: dict) -> bool: @@ -77,45 +81,8 @@ def register_node_in_dev_reg( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Z-Wave JS from a config entry.""" client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass)) - connected = asyncio.Event() - initialized = asyncio.Event() dev_reg = await device_registry.async_get_registry(hass) - async def async_on_connect() -> None: - """Handle websocket is (re)connected.""" - LOGGER.info("Connected to Zwave JS Server") - connected.set() - - async def async_on_disconnect() -> None: - """Handle websocket is disconnected.""" - LOGGER.info("Disconnected from Zwave JS Server") - connected.clear() - if initialized.is_set(): - initialized.clear() - # update entity availability - async_dispatcher_send(hass, f"{DOMAIN}_{entry.entry_id}_connection_state") - - async def async_on_initialized() -> None: - """Handle initial full state received.""" - LOGGER.info("Connection to Zwave JS Server initialized.") - initialized.set() - # update entity availability - async_dispatcher_send(hass, f"{DOMAIN}_{entry.entry_id}_connection_state") - - # Check for nodes that no longer exist and remove them - stored_devices = device_registry.async_entries_for_config_entry( - dev_reg, entry.entry_id - ) - known_devices = [ - dev_reg.async_get_device({get_device_id(client, node)}) - for node in client.driver.controller.nodes.values() - ] - - # Devices that are in the device registry that are not known by the controller can be removed - for device in stored_devices: - if device not in known_devices: - dev_reg.async_remove_device(device.id) - @callback def async_on_node_ready(node: ZwaveNode) -> None: """Handle node ready event.""" @@ -209,32 +176,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: }, ) - async def handle_ha_shutdown(event: Event) -> None: - """Handle HA shutdown.""" - await client.disconnect() - - # register main event callbacks. - unsubs = [ - client.register_on_initialized(async_on_initialized), - client.register_on_disconnect(async_on_disconnect), - client.register_on_connect(async_on_connect), - hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, handle_ha_shutdown), - ] - # connect and throw error if connection failed - asyncio.create_task(client.connect()) try: async with timeout(CONNECT_TIMEOUT): - await connected.wait() - except asyncio.TimeoutError as err: - for unsub in unsubs: - unsub() - await client.disconnect() + await client.connect() + except (asyncio.TimeoutError, BaseZwaveJSServerError) as err: raise ConfigEntryNotReady from err + else: + LOGGER.info("Connected to Zwave JS Server") + unsubscribe_callbacks: List[Callable] = [] hass.data[DOMAIN][entry.entry_id] = { DATA_CLIENT: client, - DATA_UNSUBSCRIBE: unsubs, + DATA_UNSUBSCRIBE: unsubscribe_callbacks, } # Set up websocket API @@ -250,9 +204,37 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ] ) - # Wait till we're initialized - LOGGER.info("Waiting for Z-Wave to be fully initialized") - await initialized.wait() + driver_ready = asyncio.Event() + + async def handle_ha_shutdown(event: Event) -> None: + """Handle HA shutdown.""" + await disconnect_client(hass, entry, client, listen_task, platform_task) + + listen_task = asyncio.create_task( + client_listen(hass, entry, client, driver_ready) + ) + hass.data[DOMAIN][entry.entry_id][DATA_CLIENT_LISTEN_TASK] = listen_task + unsubscribe_callbacks.append( + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, handle_ha_shutdown) + ) + + await driver_ready.wait() + + LOGGER.info("Connection to Zwave JS Server initialized") + + # Check for nodes that no longer exist and remove them + stored_devices = device_registry.async_entries_for_config_entry( + dev_reg, entry.entry_id + ) + known_devices = [ + dev_reg.async_get_device({get_device_id(client, node)}) + for node in client.driver.controller.nodes.values() + ] + + # Devices that are in the device registry that are not known by the controller can be removed + for device in stored_devices: + if device not in known_devices: + dev_reg.async_remove_device(device.id) # run discovery on all ready nodes for node in client.driver.controller.nodes.values(): @@ -268,11 +250,46 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "node removed", lambda event: async_on_node_removed(event["node"]) ) - hass.async_create_task(start_platforms()) + platform_task = hass.async_create_task(start_platforms()) + hass.data[DOMAIN][entry.entry_id][DATA_START_PLATFORM_TASK] = platform_task return True +async def client_listen( + hass: HomeAssistant, + entry: ConfigEntry, + client: ZwaveClient, + driver_ready: asyncio.Event, +) -> None: + """Listen with the client.""" + try: + await client.listen(driver_ready) + except BaseZwaveJSServerError: + # The entry needs to be reloaded since a new driver state + # will be acquired on reconnect. + # All model instances will be replaced when the new state is acquired. + hass.async_create_task(hass.config_entries.async_reload(entry.entry_id)) + + +async def disconnect_client( + hass: HomeAssistant, + entry: ConfigEntry, + client: ZwaveClient, + listen_task: asyncio.Task, + platform_task: asyncio.Task, +) -> None: + """Disconnect client.""" + await client.disconnect() + + listen_task.cancel() + platform_task.cancel() + + await asyncio.gather(listen_task, platform_task) + + LOGGER.info("Disconnected from Zwave JS Server") + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = all( @@ -291,7 +308,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for unsub in info[DATA_UNSUBSCRIBE]: unsub() - await info[DATA_CLIENT].disconnect() + if DATA_CLIENT_LISTEN_TASK in info: + await disconnect_client( + hass, + entry, + info[DATA_CLIENT], + info[DATA_CLIENT_LISTEN_TASK], + platform_task=info[DATA_START_PLATFORM_TASK], + ) return True diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index b039113270d..334a2cccd4f 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -9,7 +9,6 @@ from zwave_js_server.model.value import Value as ZwaveValue, get_value_id from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from .const import DOMAIN @@ -54,14 +53,6 @@ class ZWaveBaseEntity(Entity): self.info.node.on(EVENT_VALUE_UPDATED, self._value_changed) ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{DOMAIN}_{self.config_entry.entry_id}_connection_state", - self.async_write_ha_state, - ) - ) - @property def device_info(self) -> dict: """Return device information for the device registry.""" diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 586a6492a1a..de77ebbf5e0 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.15.0"], + "requirements": ["zwave-js-server-python==0.16.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2b8efbff9e1..21211668779 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2384,4 +2384,4 @@ zigpy==0.32.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.15.0 +zwave-js-server-python==0.16.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1998d1baf0c..c2d39fd320e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1203,4 +1203,4 @@ zigpy-znp==0.3.0 zigpy==0.32.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.15.0 +zwave-js-server-python==0.16.0 diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 0e0ebdee3c6..b5301f4cd2f 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -1,6 +1,7 @@ """Provide common Z-Wave JS fixtures.""" +import asyncio import json -from unittest.mock import DEFAULT, Mock, patch +from unittest.mock import DEFAULT, AsyncMock, patch import pytest from zwave_js_server.event import Event @@ -149,35 +150,31 @@ def in_wall_smart_fan_control_state_fixture(): def mock_client_fixture(controller_state, version_state): """Mock a client.""" - def mock_callback(): - callbacks = [] - - def add_callback(cb): - callbacks.append(cb) - return DEFAULT - - return callbacks, Mock(side_effect=add_callback) - with patch( "homeassistant.components.zwave_js.ZwaveClient", autospec=True ) as client_class: client = client_class.return_value - connect_callback, client.register_on_connect = mock_callback() - initialized_callback, client.register_on_initialized = mock_callback() - async def connect(): - for cb in connect_callback: - await cb() + await asyncio.sleep(0) + client.state = "connected" + client.connected = True - for cb in initialized_callback: - await cb() + async def listen(driver_ready: asyncio.Event) -> None: + driver_ready.set() - client.connect = Mock(side_effect=connect) + async def disconnect(): + client.state = "disconnected" + client.connected = False + + client.connect = AsyncMock(side_effect=connect) + client.listen = AsyncMock(side_effect=listen) + client.disconnect = AsyncMock(side_effect=disconnect) client.driver = Driver(client, controller_state) + client.version = VersionInfo.from_message(version_state) client.ws_server_url = "ws://test:3000/zjs" - client.state = "connected" + yield client diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index fa61b3deb27..1aad07400ad 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -50,17 +50,11 @@ async def test_entry_setup_unload(hass, client, integration): entry = integration assert client.connect.call_count == 1 - assert client.register_on_initialized.call_count == 1 - assert client.register_on_disconnect.call_count == 1 - assert client.register_on_connect.call_count == 1 assert entry.state == ENTRY_STATE_LOADED await hass.config_entries.async_unload(entry.entry_id) assert client.disconnect.call_count == 1 - assert client.register_on_initialized.return_value.call_count == 1 - assert client.register_on_disconnect.return_value.call_count == 1 - assert client.register_on_connect.return_value.call_count == 1 assert entry.state == ENTRY_STATE_NOT_LOADED @@ -71,38 +65,6 @@ async def test_home_assistant_stop(hass, client, integration): assert client.disconnect.call_count == 1 -async def test_availability_reflect_connection_status( - hass, client, multisensor_6, integration -): - """Test we handle disconnect and reconnect.""" - on_initialized = client.register_on_initialized.call_args[0][0] - on_disconnect = client.register_on_disconnect.call_args[0][0] - state = hass.states.get(AIR_TEMPERATURE_SENSOR) - - assert state - assert state.state != STATE_UNAVAILABLE - - client.connected = False - - await on_disconnect() - await hass.async_block_till_done() - - state = hass.states.get(AIR_TEMPERATURE_SENSOR) - - assert state - assert state.state == STATE_UNAVAILABLE - - client.connected = True - - await on_initialized() - await hass.async_block_till_done() - - state = hass.states.get(AIR_TEMPERATURE_SENSOR) - - assert state - assert state.state != STATE_UNAVAILABLE - - async def test_initialized_timeout(hass, client, connect_timeout): """Test we handle a timeout during client initialization.""" entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"}) From bf9b3bf9dbca6926dd846cc31dc08e6751ee8204 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 2 Feb 2021 22:45:51 +0100 Subject: [PATCH 149/796] Update frontend to 20210127.7 (#45874) --- 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 9d21be79912..65a5497d1f9 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==20210127.6"], + "requirements": ["home-assistant-frontend==20210127.7"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8ae31a0bbd2..b280c23982d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.41.0 -home-assistant-frontend==20210127.6 +home-assistant-frontend==20210127.7 httpx==0.16.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 21211668779..c29204f21ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.10.4 # homeassistant.components.frontend -home-assistant-frontend==20210127.6 +home-assistant-frontend==20210127.7 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2d39fd320e..05565349d0b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -405,7 +405,7 @@ hole==0.5.1 holidays==0.10.4 # homeassistant.components.frontend -home-assistant-frontend==20210127.6 +home-assistant-frontend==20210127.7 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From d9dba1b7ab105d3213aa4cba9d5eaa373bf21772 Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 2 Feb 2021 22:57:06 +0100 Subject: [PATCH 150/796] Bump Freebox to 0.0.9 (#45837) * Bump Freebox to 0.0.9 * Remove @SNoof85 from code owners * Module is now freebox_api --- CODEOWNERS | 2 +- homeassistant/components/freebox/config_flow.py | 2 +- homeassistant/components/freebox/manifest.json | 4 ++-- homeassistant/components/freebox/router.py | 8 ++++---- homeassistant/components/freebox/switch.py | 2 +- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- tests/components/freebox/test_config_flow.py | 2 +- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index dc0129c8b8e..499b7e131f7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -158,7 +158,7 @@ homeassistant/components/flunearyou/* @bachya homeassistant/components/forked_daapd/* @uvjustin homeassistant/components/fortios/* @kimfrellsen homeassistant/components/foscam/* @skgsergio -homeassistant/components/freebox/* @snoof85 @Quentame +homeassistant/components/freebox/* @hacf-fr @Quentame homeassistant/components/fronius/* @nielstron homeassistant/components/frontend/* @home-assistant/frontend homeassistant/components/garmin_connect/* @cyberjunky diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py index d776c34c4f9..2ee52884c88 100644 --- a/homeassistant/components/freebox/config_flow.py +++ b/homeassistant/components/freebox/config_flow.py @@ -1,7 +1,7 @@ """Config flow to configure the Freebox integration.""" import logging -from aiofreepybox.exceptions import AuthorizationError, HttpRequestError +from freebox_api.exceptions import AuthorizationError, HttpRequestError import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/freebox/manifest.json b/homeassistant/components/freebox/manifest.json index ae96f7f6510..2739849b547 100644 --- a/homeassistant/components/freebox/manifest.json +++ b/homeassistant/components/freebox/manifest.json @@ -3,7 +3,7 @@ "name": "Freebox", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/freebox", - "requirements": ["aiofreepybox==0.0.8"], + "requirements": ["freebox-api==0.0.9"], "after_dependencies": ["discovery"], - "codeowners": ["@snoof85", "@Quentame"] + "codeowners": ["@hacf-fr", "@Quentame"] } diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index daa57a89c47..2511280f719 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -4,9 +4,9 @@ import logging from pathlib import Path from typing import Any, Dict, List, Optional -from aiofreepybox import Freepybox -from aiofreepybox.api.wifi import Wifi -from aiofreepybox.exceptions import HttpRequestError +from freebox_api import Freepybox +from freebox_api.api.wifi import Wifi +from freebox_api.exceptions import HttpRequestError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT @@ -138,7 +138,7 @@ class FreeboxRouter: "serial": syst_datas["serial"], } - self.call_list = await self._api.call.get_call_list() + self.call_list = await self._api.call.get_calls_log() async_dispatcher_send(self.hass, self.signal_sensor_update) diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index 00f87e21f47..b1cfc93eb53 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -2,7 +2,7 @@ import logging from typing import Dict -from aiofreepybox.exceptions import InsufficientPermissionsError +from freebox_api.exceptions import InsufficientPermissionsError from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry diff --git a/requirements_all.txt b/requirements_all.txt index c29204f21ef..d776522992d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -156,9 +156,6 @@ aioesphomeapi==2.6.4 # homeassistant.components.flo aioflo==0.4.1 -# homeassistant.components.freebox -aiofreepybox==0.0.8 - # homeassistant.components.yi aioftp==0.12.0 @@ -613,6 +610,9 @@ foobot_async==1.0.0 # homeassistant.components.fortios fortiosapi==0.10.8 +# homeassistant.components.freebox +freebox-api==0.0.9 + # homeassistant.components.free_mobile freesms==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 05565349d0b..95fb6e3dd96 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -93,9 +93,6 @@ aioesphomeapi==2.6.4 # homeassistant.components.flo aioflo==0.4.1 -# homeassistant.components.freebox -aiofreepybox==0.0.8 - # homeassistant.components.guardian aioguardian==1.0.4 @@ -313,6 +310,9 @@ fnvhash==0.1.0 # homeassistant.components.foobot foobot_async==1.0.0 +# homeassistant.components.freebox +freebox-api==0.0.9 + # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor # homeassistant.components.fritzbox_netmonitor diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index f7150df7efc..197be7bd3a6 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -1,7 +1,7 @@ """Tests for the Freebox config flow.""" from unittest.mock import AsyncMock, patch -from aiofreepybox.exceptions import ( +from freebox_api.exceptions import ( AuthorizationError, HttpRequestError, InvalidTokenError, From 048f36c77ec488eee058df93efe76929054204ca Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 3 Feb 2021 02:44:34 -0500 Subject: [PATCH 151/796] Bump plexapi to 3.4.1 (#45878) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index f0f1e09a15c..913f405cfcd 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==4.3.0", + "plexapi==4.3.1", "plexauth==0.0.6", "plexwebsocket==0.0.12" ], diff --git a/requirements_all.txt b/requirements_all.txt index d776522992d..01d6fe0a92a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1137,7 +1137,7 @@ pillow==8.1.0 pizzapi==0.0.3 # homeassistant.components.plex -plexapi==4.3.0 +plexapi==4.3.1 # homeassistant.components.plex plexauth==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 95fb6e3dd96..95eab6a7440 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -574,7 +574,7 @@ pilight==0.1.1 pillow==8.1.0 # homeassistant.components.plex -plexapi==4.3.0 +plexapi==4.3.1 # homeassistant.components.plex plexauth==0.0.6 From 45ac6df76f9425ffed98cb1fdbfe83096321203f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 3 Feb 2021 10:41:02 +0100 Subject: [PATCH 152/796] Update docker base image 2021.02.0 (#45889) --- build.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.json b/build.json index 1cf4217146d..0183b61c67c 100644 --- a/build.json +++ b/build.json @@ -1,11 +1,11 @@ { "image": "homeassistant/{arch}-homeassistant", "build_from": { - "aarch64": "homeassistant/aarch64-homeassistant-base:2021.01.1", - "armhf": "homeassistant/armhf-homeassistant-base:2021.01.1", - "armv7": "homeassistant/armv7-homeassistant-base:2021.01.1", - "amd64": "homeassistant/amd64-homeassistant-base:2021.01.1", - "i386": "homeassistant/i386-homeassistant-base:2021.01.1" + "aarch64": "homeassistant/aarch64-homeassistant-base:2021.02.0", + "armhf": "homeassistant/armhf-homeassistant-base:2021.02.0", + "armv7": "homeassistant/armv7-homeassistant-base:2021.02.0", + "amd64": "homeassistant/amd64-homeassistant-base:2021.02.0", + "i386": "homeassistant/i386-homeassistant-base:2021.02.0" }, "labels": { "io.hass.type": "core" From 959ed6d077fba573f71cab5e8299464b372b1997 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 3 Feb 2021 11:46:49 +0100 Subject: [PATCH 153/796] Update translations --- .../components/abode/translations/de.json | 2 +- .../components/abode/translations/es.json | 4 +- .../components/abode/translations/fr.json | 21 +++- .../components/abode/translations/tr.json | 34 ++++++ .../components/abode/translations/uk.json | 35 ++++++ .../accuweather/translations/ca.json | 2 +- .../accuweather/translations/cs.json | 2 +- .../accuweather/translations/de.json | 9 +- .../accuweather/translations/en.json | 2 +- .../accuweather/translations/et.json | 2 +- .../accuweather/translations/fr.json | 3 +- .../accuweather/translations/it.json | 2 +- .../accuweather/translations/no.json | 2 +- .../accuweather/translations/pl.json | 2 +- .../accuweather/translations/ru.json | 2 +- .../accuweather/translations/sensor.uk.json | 9 ++ .../accuweather/translations/tr.json | 38 ++++++ .../accuweather/translations/uk.json | 21 +++- .../accuweather/translations/zh-Hant.json | 2 +- .../components/acmeda/translations/de.json | 5 +- .../components/acmeda/translations/tr.json | 11 ++ .../components/acmeda/translations/uk.json | 15 +++ .../components/adguard/translations/de.json | 6 +- .../components/adguard/translations/no.json | 4 +- .../components/adguard/translations/ru.json | 4 +- .../components/adguard/translations/tr.json | 20 ++++ .../components/adguard/translations/uk.json | 28 +++++ .../advantage_air/translations/de.json | 5 +- .../advantage_air/translations/tr.json | 19 +++ .../advantage_air/translations/uk.json | 20 ++++ .../components/agent_dvr/translations/de.json | 4 +- .../components/agent_dvr/translations/tr.json | 20 ++++ .../components/agent_dvr/translations/uk.json | 20 ++++ .../components/airly/translations/de.json | 7 +- .../components/airly/translations/tr.json | 17 +++ .../components/airly/translations/uk.json | 28 +++++ .../components/airnow/translations/ca.json | 26 +++++ .../components/airnow/translations/cs.json | 23 ++++ .../components/airnow/translations/de.json | 24 ++++ .../components/airnow/translations/en.json | 9 +- .../components/airnow/translations/es.json | 26 +++++ .../components/airnow/translations/et.json | 26 +++++ .../components/airnow/translations/fr.json | 26 +++++ .../components/airnow/translations/it.json | 26 +++++ .../components/airnow/translations/lb.json | 24 ++++ .../components/airnow/translations/no.json | 26 +++++ .../components/airnow/translations/pl.json | 26 +++++ .../components/airnow/translations/pt.json | 23 ++++ .../components/airnow/translations/ru.json | 26 +++++ .../components/airnow/translations/tr.json | 25 ++++ .../components/airnow/translations/uk.json | 26 +++++ .../airnow/translations/zh-Hant.json | 26 +++++ .../components/airvisual/translations/ar.json | 11 ++ .../components/airvisual/translations/ca.json | 24 +++- .../components/airvisual/translations/de.json | 11 +- .../components/airvisual/translations/en.json | 14 +++ .../components/airvisual/translations/et.json | 22 +++- .../components/airvisual/translations/fr.json | 10 +- .../components/airvisual/translations/no.json | 22 +++- .../components/airvisual/translations/pl.json | 22 +++- .../components/airvisual/translations/sv.json | 3 +- .../components/airvisual/translations/tr.json | 59 ++++++++++ .../components/airvisual/translations/uk.json | 57 +++++++++ .../airvisual/translations/zh-Hant.json | 22 +++- .../alarm_control_panel/translations/tr.json | 6 + .../alarm_control_panel/translations/uk.json | 29 ++++- .../alarmdecoder/translations/de.json | 7 +- .../alarmdecoder/translations/tr.json | 48 ++++++++ .../alarmdecoder/translations/uk.json | 74 ++++++++++++ .../components/almond/translations/de.json | 6 +- .../components/almond/translations/no.json | 4 +- .../components/almond/translations/ru.json | 4 +- .../components/almond/translations/tr.json | 8 ++ .../components/almond/translations/uk.json | 19 +++ .../ambiclimate/translations/de.json | 4 +- .../ambiclimate/translations/fr.json | 2 +- .../ambiclimate/translations/tr.json | 7 ++ .../ambiclimate/translations/uk.json | 22 ++++ .../ambient_station/translations/de.json | 4 +- .../ambient_station/translations/tr.json | 17 +++ .../ambient_station/translations/uk.json | 20 ++++ .../components/apple_tv/translations/fr.json | 15 ++- .../components/apple_tv/translations/lb.json | 8 ++ .../components/apple_tv/translations/tr.json | 2 + .../components/apple_tv/translations/uk.json | 64 ++++++++++ .../apple_tv/translations/zh-Hans.json | 17 +++ .../components/arcam_fmj/translations/de.json | 3 +- .../components/arcam_fmj/translations/ro.json | 9 ++ .../components/arcam_fmj/translations/tr.json | 17 +++ .../components/arcam_fmj/translations/uk.json | 27 +++++ .../components/atag/translations/de.json | 6 +- .../components/atag/translations/tr.json | 21 ++++ .../components/atag/translations/uk.json | 21 ++++ .../components/august/translations/de.json | 5 +- .../components/august/translations/tr.json | 31 +++++ .../components/august/translations/uk.json | 32 +++++ .../components/aurora/translations/de.json | 14 ++- .../components/aurora/translations/fr.json | 26 +++++ .../components/aurora/translations/tr.json | 16 +++ .../components/aurora/translations/uk.json | 26 +++++ .../components/auth/translations/de.json | 2 +- .../components/auth/translations/tr.json | 22 ++++ .../components/auth/translations/uk.json | 23 +++- .../components/awair/translations/de.json | 12 +- .../components/awair/translations/tr.json | 27 +++++ .../components/awair/translations/uk.json | 29 +++++ .../components/axis/translations/ca.json | 2 +- .../components/axis/translations/cs.json | 2 +- .../components/axis/translations/de.json | 5 +- .../components/axis/translations/en.json | 2 +- .../components/axis/translations/et.json | 2 +- .../components/axis/translations/it.json | 2 +- .../components/axis/translations/no.json | 2 +- .../components/axis/translations/pl.json | 2 +- .../components/axis/translations/ru.json | 2 +- .../components/axis/translations/tr.json | 23 ++++ .../components/axis/translations/uk.json | 37 ++++++ .../components/axis/translations/zh-Hant.json | 2 +- .../azure_devops/translations/de.json | 7 +- .../azure_devops/translations/fr.json | 2 +- .../azure_devops/translations/tr.json | 28 +++++ .../azure_devops/translations/uk.json | 16 ++- .../binary_sensor/translations/de.json | 16 +++ .../binary_sensor/translations/en.json | 4 +- .../binary_sensor/translations/pl.json | 2 +- .../binary_sensor/translations/tr.json | 22 ++++ .../binary_sensor/translations/uk.json | 108 +++++++++++++++-- .../components/blebox/translations/de.json | 6 +- .../components/blebox/translations/tr.json | 20 ++++ .../components/blebox/translations/uk.json | 24 ++++ .../components/blink/translations/de.json | 5 +- .../components/blink/translations/tr.json | 24 ++++ .../components/blink/translations/uk.json | 40 +++++++ .../bmw_connected_drive/translations/ca.json | 30 +++++ .../bmw_connected_drive/translations/cs.json | 19 +++ .../bmw_connected_drive/translations/de.json | 19 +++ .../bmw_connected_drive/translations/en.json | 1 - .../bmw_connected_drive/translations/es.json | 30 +++++ .../bmw_connected_drive/translations/et.json | 30 +++++ .../bmw_connected_drive/translations/fr.json | 29 +++++ .../bmw_connected_drive/translations/it.json | 30 +++++ .../bmw_connected_drive/translations/lb.json | 19 +++ .../bmw_connected_drive/translations/no.json | 30 +++++ .../bmw_connected_drive/translations/pl.json | 30 +++++ .../bmw_connected_drive/translations/pt.json | 19 +++ .../bmw_connected_drive/translations/ru.json | 30 +++++ .../bmw_connected_drive/translations/tr.json | 19 +++ .../bmw_connected_drive/translations/uk.json | 30 +++++ .../translations/zh-Hant.json | 30 +++++ .../components/bond/translations/de.json | 6 +- .../components/bond/translations/tr.json | 25 ++++ .../components/bond/translations/uk.json | 23 +++- .../components/braviatv/translations/de.json | 7 +- .../components/braviatv/translations/tr.json | 29 +++++ .../components/braviatv/translations/uk.json | 39 +++++++ .../components/broadlink/translations/de.json | 6 +- .../components/broadlink/translations/tr.json | 39 +++++++ .../components/broadlink/translations/uk.json | 47 ++++++++ .../components/brother/translations/de.json | 2 +- .../components/brother/translations/tr.json | 14 +++ .../components/brother/translations/uk.json | 30 +++++ .../components/bsblan/translations/de.json | 2 +- .../components/bsblan/translations/tr.json | 8 ++ .../components/bsblan/translations/uk.json | 24 ++++ .../components/canary/translations/de.json | 3 +- .../components/canary/translations/tr.json | 19 +++ .../components/canary/translations/uk.json | 31 +++++ .../components/cast/translations/de.json | 4 +- .../components/cast/translations/tr.json | 12 ++ .../components/cast/translations/uk.json | 6 +- .../cert_expiry/translations/tr.json | 15 +++ .../cert_expiry/translations/uk.json | 24 ++++ .../components/climate/translations/tr.json | 6 + .../components/climate/translations/uk.json | 14 +-- .../components/cloud/translations/ca.json | 6 +- .../components/cloud/translations/de.json | 15 +++ .../components/cloud/translations/fr.json | 16 +++ .../components/cloud/translations/pl.json | 2 +- .../components/cloud/translations/tr.json | 3 + .../components/cloud/translations/uk.json | 16 +++ .../cloudflare/translations/de.json | 8 ++ .../cloudflare/translations/tr.json | 9 ++ .../cloudflare/translations/uk.json | 35 ++++++ .../components/control4/translations/de.json | 6 +- .../components/control4/translations/tr.json | 21 ++++ .../components/control4/translations/uk.json | 15 ++- .../coolmaster/translations/de.json | 2 +- .../coolmaster/translations/tr.json | 15 +++ .../coolmaster/translations/uk.json | 22 ++++ .../coronavirus/translations/tr.json | 15 +++ .../coronavirus/translations/uk.json | 15 +++ .../components/cover/translations/tr.json | 6 + .../components/cover/translations/uk.json | 21 +++- .../components/daikin/translations/de.json | 6 +- .../components/daikin/translations/pt.json | 2 +- .../components/daikin/translations/tr.json | 22 ++++ .../components/daikin/translations/uk.json | 24 ++++ .../components/deconz/translations/cs.json | 11 +- .../components/deconz/translations/de.json | 12 +- .../components/deconz/translations/no.json | 4 +- .../components/deconz/translations/pl.json | 6 +- .../components/deconz/translations/ru.json | 4 +- .../components/deconz/translations/tr.json | 43 +++++++ .../components/deconz/translations/uk.json | 105 +++++++++++++++++ .../components/demo/translations/tr.json | 12 ++ .../components/demo/translations/uk.json | 21 ++++ .../components/denonavr/translations/de.json | 4 + .../components/denonavr/translations/tr.json | 16 +++ .../components/denonavr/translations/uk.json | 48 ++++++++ .../device_tracker/translations/de.json | 8 +- .../device_tracker/translations/uk.json | 12 +- .../devolo_home_control/translations/de.json | 7 +- .../devolo_home_control/translations/tr.json | 18 +++ .../devolo_home_control/translations/uk.json | 20 ++++ .../components/dexcom/translations/de.json | 3 +- .../components/dexcom/translations/fr.json | 2 +- .../components/dexcom/translations/tr.json | 22 ++++ .../components/dexcom/translations/uk.json | 32 +++++ .../dialogflow/translations/de.json | 4 + .../dialogflow/translations/tr.json | 8 ++ .../dialogflow/translations/uk.json | 17 +++ .../components/directv/translations/tr.json | 21 ++++ .../components/directv/translations/uk.json | 22 ++++ .../components/doorbird/translations/de.json | 2 +- .../components/doorbird/translations/tr.json | 22 ++++ .../components/doorbird/translations/uk.json | 36 ++++++ .../components/dsmr/translations/de.json | 7 ++ .../components/dsmr/translations/fr.json | 10 ++ .../components/dsmr/translations/tr.json | 5 + .../components/dsmr/translations/uk.json | 17 +++ .../components/dunehd/translations/tr.json | 19 +++ .../components/dunehd/translations/uk.json | 21 ++++ .../components/eafm/translations/de.json | 7 ++ .../components/eafm/translations/tr.json | 16 +++ .../components/eafm/translations/uk.json | 17 +++ .../components/ecobee/translations/de.json | 3 + .../components/ecobee/translations/tr.json | 14 +++ .../components/ecobee/translations/uk.json | 24 ++++ .../components/econet/translations/ca.json | 22 ++++ .../components/econet/translations/en.json | 1 + .../components/econet/translations/es.json | 21 ++++ .../components/econet/translations/et.json | 22 ++++ .../components/econet/translations/it.json | 22 ++++ .../components/econet/translations/no.json | 22 ++++ .../components/econet/translations/pl.json | 22 ++++ .../components/econet/translations/ru.json | 22 ++++ .../components/econet/translations/tr.json | 22 ++++ .../econet/translations/zh-Hant.json | 22 ++++ .../components/elgato/translations/de.json | 4 +- .../components/elgato/translations/tr.json | 19 +++ .../components/elgato/translations/uk.json | 25 ++++ .../components/elkm1/translations/de.json | 2 +- .../components/elkm1/translations/tr.json | 21 ++++ .../components/elkm1/translations/uk.json | 27 +++++ .../emulated_roku/translations/de.json | 2 +- .../emulated_roku/translations/tr.json | 7 ++ .../emulated_roku/translations/uk.json | 21 ++++ .../components/enocean/translations/tr.json | 23 ++++ .../components/enocean/translations/uk.json | 25 ++++ .../components/epson/translations/de.json | 6 +- .../components/epson/translations/tr.json | 3 + .../components/epson/translations/uk.json | 16 +++ .../components/esphome/translations/de.json | 3 +- .../components/esphome/translations/pt.json | 2 +- .../components/esphome/translations/tr.json | 19 +++ .../components/esphome/translations/uk.json | 11 +- .../components/fan/translations/tr.json | 10 ++ .../components/fan/translations/uk.json | 12 +- .../fireservicerota/translations/fr.json | 27 +++++ .../fireservicerota/translations/tr.json | 10 ++ .../fireservicerota/translations/uk.json | 29 +++++ .../components/firmata/translations/tr.json | 7 ++ .../components/firmata/translations/uk.json | 7 ++ .../flick_electric/translations/de.json | 2 +- .../flick_electric/translations/tr.json | 20 ++++ .../flick_electric/translations/uk.json | 23 ++++ .../components/flo/translations/de.json | 7 +- .../components/flo/translations/tr.json | 21 ++++ .../components/flo/translations/uk.json | 21 ++++ .../components/flume/translations/de.json | 4 +- .../components/flume/translations/tr.json | 20 ++++ .../components/flume/translations/uk.json | 24 ++++ .../flunearyou/translations/de.json | 2 +- .../flunearyou/translations/tr.json | 18 +++ .../flunearyou/translations/uk.json | 20 ++++ .../forked_daapd/translations/de.json | 2 +- .../forked_daapd/translations/tr.json | 21 ++++ .../forked_daapd/translations/uk.json | 42 +++++++ .../components/foscam/translations/af.json | 11 ++ .../components/foscam/translations/ca.json | 24 ++++ .../components/foscam/translations/cs.json | 23 ++++ .../components/foscam/translations/de.json | 23 ++++ .../components/foscam/translations/en.json | 44 +++---- .../components/foscam/translations/es.json | 24 ++++ .../components/foscam/translations/et.json | 24 ++++ .../components/foscam/translations/fr.json | 24 ++++ .../components/foscam/translations/it.json | 24 ++++ .../components/foscam/translations/lb.json | 16 +++ .../components/foscam/translations/no.json | 24 ++++ .../components/foscam/translations/pl.json | 24 ++++ .../components/foscam/translations/pt.json | 11 ++ .../components/foscam/translations/ru.json | 24 ++++ .../components/foscam/translations/tr.json | 24 ++++ .../foscam/translations/zh-Hant.json | 24 ++++ .../components/freebox/translations/de.json | 6 +- .../components/freebox/translations/tr.json | 20 ++++ .../components/freebox/translations/uk.json | 25 ++++ .../components/fritzbox/translations/ca.json | 10 +- .../components/fritzbox/translations/cs.json | 9 +- .../components/fritzbox/translations/de.json | 12 +- .../components/fritzbox/translations/en.json | 10 +- .../components/fritzbox/translations/et.json | 10 +- .../components/fritzbox/translations/it.json | 10 +- .../components/fritzbox/translations/no.json | 10 +- .../components/fritzbox/translations/pl.json | 10 +- .../components/fritzbox/translations/ru.json | 10 +- .../components/fritzbox/translations/tr.json | 35 ++++++ .../components/fritzbox/translations/uk.json | 31 +++++ .../fritzbox/translations/zh-Hant.json | 10 +- .../fritzbox_callmonitor/translations/ca.json | 41 +++++++ .../fritzbox_callmonitor/translations/cs.json | 21 ++++ .../fritzbox_callmonitor/translations/et.json | 41 +++++++ .../fritzbox_callmonitor/translations/it.json | 41 +++++++ .../fritzbox_callmonitor/translations/lb.json | 33 ++++++ .../fritzbox_callmonitor/translations/no.json | 41 +++++++ .../fritzbox_callmonitor/translations/pl.json | 41 +++++++ .../fritzbox_callmonitor/translations/ru.json | 41 +++++++ .../fritzbox_callmonitor/translations/tr.json | 41 +++++++ .../translations/zh-Hant.json | 41 +++++++ .../garmin_connect/translations/de.json | 8 +- .../garmin_connect/translations/tr.json | 20 ++++ .../garmin_connect/translations/uk.json | 23 ++++ .../components/gdacs/translations/de.json | 2 +- .../components/gdacs/translations/tr.json | 14 +++ .../components/gdacs/translations/uk.json | 15 +++ .../components/geofency/translations/de.json | 4 + .../components/geofency/translations/tr.json | 8 ++ .../components/geofency/translations/uk.json | 17 +++ .../geonetnz_quakes/translations/tr.json | 7 ++ .../geonetnz_quakes/translations/uk.json | 16 +++ .../geonetnz_volcano/translations/de.json | 3 + .../geonetnz_volcano/translations/tr.json | 14 +++ .../geonetnz_volcano/translations/uk.json | 15 +++ .../components/gios/translations/de.json | 4 +- .../components/gios/translations/fr.json | 5 + .../components/gios/translations/it.json | 2 +- .../components/gios/translations/lb.json | 5 + .../components/gios/translations/tr.json | 10 ++ .../components/gios/translations/uk.json | 27 +++++ .../components/glances/translations/de.json | 4 +- .../components/glances/translations/tr.json | 29 +++++ .../components/glances/translations/uk.json | 36 ++++++ .../components/goalzero/translations/de.json | 13 ++- .../components/goalzero/translations/fr.json | 2 +- .../components/goalzero/translations/tr.json | 18 +++ .../components/goalzero/translations/uk.json | 22 ++++ .../components/gogogate2/translations/tr.json | 20 ++++ .../components/gogogate2/translations/uk.json | 22 ++++ .../components/gpslogger/translations/de.json | 4 + .../components/gpslogger/translations/tr.json | 8 ++ .../components/gpslogger/translations/uk.json | 17 +++ .../components/gree/translations/de.json | 13 +++ .../components/gree/translations/tr.json | 12 ++ .../components/gree/translations/uk.json | 13 +++ .../components/griddy/translations/de.json | 4 +- .../components/griddy/translations/tr.json | 3 + .../components/griddy/translations/uk.json | 20 ++++ .../components/group/translations/uk.json | 2 +- .../components/guardian/translations/de.json | 3 +- .../components/guardian/translations/tr.json | 17 +++ .../components/guardian/translations/uk.json | 21 ++++ .../components/hangouts/translations/de.json | 2 +- .../components/hangouts/translations/tr.json | 16 +++ .../components/hangouts/translations/uk.json | 31 +++++ .../components/harmony/translations/de.json | 2 +- .../components/harmony/translations/tr.json | 18 +++ .../components/harmony/translations/uk.json | 36 ++++++ .../components/hassio/translations/ca.json | 2 +- .../components/hassio/translations/de.json | 15 +++ .../components/hassio/translations/en.json | 4 +- .../components/hassio/translations/fr.json | 15 +++ .../components/hassio/translations/tr.json | 8 +- .../components/hassio/translations/uk.json | 16 +++ .../components/heos/translations/de.json | 5 +- .../components/heos/translations/tr.json | 17 +++ .../components/heos/translations/uk.json | 19 +++ .../hisense_aehw4a1/translations/de.json | 2 +- .../hisense_aehw4a1/translations/tr.json | 12 ++ .../hisense_aehw4a1/translations/uk.json | 13 +++ .../components/hlk_sw16/translations/de.json | 6 + .../components/hlk_sw16/translations/tr.json | 21 ++++ .../components/hlk_sw16/translations/uk.json | 21 ++++ .../home_connect/translations/de.json | 7 +- .../home_connect/translations/uk.json | 16 +++ .../homeassistant/translations/de.json | 13 ++- .../homeassistant/translations/fr.json | 21 ++++ .../homeassistant/translations/tr.json | 2 + .../homeassistant/translations/uk.json | 21 ++++ .../components/homekit/translations/ca.json | 31 +++-- .../components/homekit/translations/cs.json | 8 +- .../components/homekit/translations/de.json | 7 +- .../components/homekit/translations/en.json | 9 +- .../components/homekit/translations/et.json | 27 ++++- .../components/homekit/translations/it.json | 25 +++- .../components/homekit/translations/no.json | 29 +++-- .../components/homekit/translations/pl.json | 25 +++- .../components/homekit/translations/ro.json | 9 ++ .../components/homekit/translations/ru.json | 11 +- .../components/homekit/translations/sv.json | 21 ++++ .../components/homekit/translations/tr.json | 60 ++++++++++ .../components/homekit/translations/uk.json | 51 +++++++- .../homekit/translations/zh-Hant.json | 31 +++-- .../homekit_controller/translations/cs.json | 6 +- .../homekit_controller/translations/de.json | 4 +- .../homekit_controller/translations/pl.json | 6 +- .../homekit_controller/translations/tr.json | 35 ++++++ .../homekit_controller/translations/uk.json | 71 ++++++++++++ .../homematicip_cloud/translations/de.json | 8 +- .../homematicip_cloud/translations/tr.json | 9 ++ .../homematicip_cloud/translations/uk.json | 29 +++++ .../huawei_lte/translations/de.json | 5 +- .../huawei_lte/translations/tr.json | 27 +++++ .../huawei_lte/translations/uk.json | 42 +++++++ .../components/hue/translations/cs.json | 2 +- .../components/hue/translations/de.json | 20 ++-- .../components/hue/translations/pl.json | 10 +- .../components/hue/translations/pt.json | 8 +- .../components/hue/translations/tr.json | 48 ++++++++ .../components/hue/translations/uk.json | 67 +++++++++++ .../huisbaasje/translations/ca.json | 21 ++++ .../huisbaasje/translations/cs.json | 21 ++++ .../huisbaasje/translations/en.json | 21 ++++ .../huisbaasje/translations/et.json | 21 ++++ .../huisbaasje/translations/it.json | 21 ++++ .../huisbaasje/translations/no.json | 21 ++++ .../huisbaasje/translations/pl.json | 21 ++++ .../huisbaasje/translations/ru.json | 21 ++++ .../huisbaasje/translations/tr.json | 21 ++++ .../huisbaasje/translations/zh-Hant.json | 21 ++++ .../humidifier/translations/tr.json | 25 ++++ .../humidifier/translations/uk.json | 24 +++- .../translations/tr.json | 18 +++ .../translations/uk.json | 23 ++++ .../hvv_departures/translations/tr.json | 20 ++++ .../hvv_departures/translations/uk.json | 47 ++++++++ .../components/hyperion/translations/fr.json | 4 + .../components/hyperion/translations/it.json | 2 +- .../components/hyperion/translations/tr.json | 12 +- .../components/hyperion/translations/uk.json | 53 +++++++++ .../components/iaqualink/translations/de.json | 6 + .../components/iaqualink/translations/tr.json | 19 +++ .../components/iaqualink/translations/uk.json | 20 ++++ .../components/icloud/translations/de.json | 9 +- .../components/icloud/translations/tr.json | 23 +++- .../components/icloud/translations/uk.json | 46 ++++++++ .../components/ifttt/translations/de.json | 4 + .../components/ifttt/translations/tr.json | 8 ++ .../components/ifttt/translations/uk.json | 17 +++ .../input_boolean/translations/uk.json | 2 +- .../input_datetime/translations/uk.json | 2 +- .../input_number/translations/uk.json | 2 +- .../input_select/translations/uk.json | 2 +- .../input_text/translations/uk.json | 2 +- .../components/insteon/translations/de.json | 6 +- .../components/insteon/translations/tr.json | 89 ++++++++++++++ .../components/insteon/translations/uk.json | 109 ++++++++++++++++++ .../components/ios/translations/de.json | 2 +- .../components/ios/translations/tr.json | 12 ++ .../components/ios/translations/uk.json | 12 ++ .../components/ipma/translations/lb.json | 5 + .../components/ipma/translations/tr.json | 11 ++ .../components/ipma/translations/uk.json | 17 ++- .../components/ipp/translations/de.json | 8 +- .../components/ipp/translations/tr.json | 17 +++ .../components/ipp/translations/uk.json | 35 ++++++ .../components/iqvia/translations/tr.json | 7 ++ .../components/iqvia/translations/uk.json | 19 +++ .../islamic_prayer_times/translations/de.json | 5 + .../islamic_prayer_times/translations/tr.json | 7 ++ .../islamic_prayer_times/translations/uk.json | 23 ++++ .../components/isy994/translations/de.json | 2 +- .../components/isy994/translations/tr.json | 30 +++++ .../components/isy994/translations/uk.json | 40 +++++++ .../components/izone/translations/de.json | 2 +- .../components/izone/translations/tr.json | 12 ++ .../components/izone/translations/uk.json | 13 +++ .../components/juicenet/translations/de.json | 10 +- .../components/juicenet/translations/tr.json | 21 ++++ .../components/juicenet/translations/uk.json | 21 ++++ .../components/kodi/translations/de.json | 4 + .../components/kodi/translations/tr.json | 35 ++++++ .../components/kodi/translations/uk.json | 50 ++++++++ .../components/konnected/translations/de.json | 12 +- .../components/konnected/translations/pl.json | 4 +- .../components/konnected/translations/tr.json | 60 ++++++++++ .../components/konnected/translations/uk.json | 108 +++++++++++++++++ .../components/kulersky/translations/de.json | 2 +- .../components/kulersky/translations/lb.json | 13 +++ .../components/kulersky/translations/tr.json | 8 +- .../components/kulersky/translations/uk.json | 13 +++ .../components/life360/translations/de.json | 2 + .../components/life360/translations/fr.json | 2 +- .../components/life360/translations/tr.json | 11 ++ .../components/life360/translations/uk.json | 27 +++++ .../components/lifx/translations/de.json | 4 +- .../components/lifx/translations/tr.json | 12 ++ .../components/lifx/translations/uk.json | 13 +++ .../components/light/translations/uk.json | 12 ++ .../components/local_ip/translations/de.json | 3 +- .../components/local_ip/translations/es.json | 2 +- .../components/local_ip/translations/tr.json | 17 +++ .../components/local_ip/translations/uk.json | 17 +++ .../components/locative/translations/de.json | 6 +- .../components/locative/translations/tr.json | 8 ++ .../components/locative/translations/uk.json | 17 +++ .../components/lock/translations/pt.json | 3 + .../components/lock/translations/tr.json | 6 + .../components/lock/translations/uk.json | 15 +++ .../logi_circle/translations/de.json | 8 +- .../logi_circle/translations/fr.json | 2 +- .../logi_circle/translations/lb.json | 1 + .../logi_circle/translations/tr.json | 10 ++ .../logi_circle/translations/uk.json | 28 +++++ .../components/lovelace/translations/de.json | 3 + .../components/lovelace/translations/fr.json | 10 ++ .../components/lovelace/translations/tr.json | 3 +- .../components/lovelace/translations/uk.json | 10 ++ .../components/luftdaten/translations/de.json | 1 + .../components/luftdaten/translations/tr.json | 8 ++ .../components/luftdaten/translations/uk.json | 18 +++ .../lutron_caseta/translations/ca.json | 61 +++++++++- .../lutron_caseta/translations/cs.json | 7 ++ .../lutron_caseta/translations/es.json | 36 +++++- .../lutron_caseta/translations/et.json | 61 +++++++++- .../lutron_caseta/translations/it.json | 61 +++++++++- .../lutron_caseta/translations/no.json | 61 +++++++++- .../lutron_caseta/translations/pl.json | 61 +++++++++- .../lutron_caseta/translations/ru.json | 42 ++++++- .../lutron_caseta/translations/tr.json | 72 ++++++++++++ .../lutron_caseta/translations/uk.json | 17 +++ .../lutron_caseta/translations/zh-Hant.json | 61 +++++++++- .../components/lyric/translations/ca.json | 16 +++ .../components/lyric/translations/cs.json | 16 +++ .../components/lyric/translations/en.json | 3 +- .../components/lyric/translations/et.json | 16 +++ .../components/lyric/translations/it.json | 16 +++ .../components/lyric/translations/no.json | 16 +++ .../components/lyric/translations/pl.json | 16 +++ .../components/lyric/translations/tr.json | 16 +++ .../lyric/translations/zh-Hant.json | 16 +++ .../components/mailgun/translations/de.json | 6 +- .../components/mailgun/translations/tr.json | 8 ++ .../components/mailgun/translations/uk.json | 17 +++ .../media_player/translations/tr.json | 6 + .../media_player/translations/uk.json | 13 ++- .../components/melcloud/translations/de.json | 2 +- .../components/melcloud/translations/tr.json | 20 ++++ .../components/melcloud/translations/uk.json | 22 ++++ .../components/met/translations/de.json | 3 + .../components/met/translations/tr.json | 15 +++ .../components/met/translations/uk.json | 19 +++ .../meteo_france/translations/de.json | 4 +- .../meteo_france/translations/tr.json | 21 ++++ .../meteo_france/translations/uk.json | 36 ++++++ .../components/metoffice/translations/de.json | 1 + .../components/metoffice/translations/tr.json | 21 ++++ .../components/metoffice/translations/uk.json | 22 ++++ .../components/mikrotik/translations/de.json | 5 +- .../components/mikrotik/translations/tr.json | 21 ++++ .../components/mikrotik/translations/uk.json | 36 ++++++ .../components/mill/translations/de.json | 3 + .../components/mill/translations/fr.json | 2 +- .../components/mill/translations/tr.json | 18 +++ .../components/mill/translations/uk.json | 18 +++ .../minecraft_server/translations/de.json | 2 +- .../minecraft_server/translations/tr.json | 2 +- .../minecraft_server/translations/uk.json | 22 ++++ .../mobile_app/translations/tr.json | 7 ++ .../mobile_app/translations/uk.json | 10 +- .../components/monoprice/translations/de.json | 2 +- .../components/monoprice/translations/tr.json | 25 ++++ .../components/monoprice/translations/uk.json | 40 +++++++ .../moon/translations/sensor.uk.json | 6 +- .../motion_blinds/translations/ca.json | 19 ++- .../motion_blinds/translations/cs.json | 11 +- .../motion_blinds/translations/de.json | 14 ++- .../motion_blinds/translations/en.json | 19 ++- .../motion_blinds/translations/es.json | 19 ++- .../motion_blinds/translations/et.json | 19 ++- .../motion_blinds/translations/fr.json | 17 +++ .../motion_blinds/translations/it.json | 19 ++- .../motion_blinds/translations/lb.json | 14 +++ .../motion_blinds/translations/no.json | 19 ++- .../motion_blinds/translations/pl.json | 19 ++- .../motion_blinds/translations/pt.json | 15 ++- .../motion_blinds/translations/ru.json | 19 ++- .../motion_blinds/translations/tr.json | 18 ++- .../motion_blinds/translations/uk.json | 37 ++++++ .../motion_blinds/translations/zh-Hant.json | 19 ++- .../components/mqtt/translations/cs.json | 10 +- .../components/mqtt/translations/de.json | 11 +- .../components/mqtt/translations/no.json | 4 +- .../components/mqtt/translations/pl.json | 16 +-- .../components/mqtt/translations/ru.json | 4 +- .../components/mqtt/translations/tr.json | 41 +++++++ .../components/mqtt/translations/uk.json | 70 ++++++++++- .../components/myq/translations/de.json | 4 +- .../components/myq/translations/tr.json | 21 ++++ .../components/myq/translations/uk.json | 21 ++++ .../components/neato/translations/de.json | 4 +- .../components/neato/translations/it.json | 14 +-- .../components/neato/translations/lb.json | 14 ++- .../components/neato/translations/pt.json | 15 ++- .../components/neato/translations/tr.json | 25 ++++ .../components/neato/translations/uk.json | 37 ++++++ .../components/nest/translations/de.json | 21 +++- .../components/nest/translations/fr.json | 3 +- .../components/nest/translations/it.json | 4 +- .../components/nest/translations/lb.json | 7 +- .../components/nest/translations/pl.json | 5 + .../components/nest/translations/pt.json | 7 +- .../components/nest/translations/tr.json | 13 ++- .../components/nest/translations/uk.json | 53 +++++++++ .../components/netatmo/translations/de.json | 6 +- .../components/netatmo/translations/tr.json | 32 +++++ .../components/netatmo/translations/uk.json | 43 +++++++ .../components/nexia/translations/de.json | 4 +- .../components/nexia/translations/tr.json | 21 ++++ .../components/nexia/translations/uk.json | 21 ++++ .../nightscout/translations/de.json | 6 + .../nightscout/translations/no.json | 2 +- .../nightscout/translations/tr.json | 11 +- .../nightscout/translations/uk.json | 23 ++++ .../components/notify/translations/uk.json | 2 +- .../components/notion/translations/de.json | 3 +- .../components/notion/translations/tr.json | 12 ++ .../components/notion/translations/uk.json | 20 ++++ .../components/nuheat/translations/de.json | 4 +- .../components/nuheat/translations/tr.json | 22 ++++ .../components/nuheat/translations/uk.json | 24 ++++ .../components/nuki/translations/ca.json | 18 +++ .../components/nuki/translations/cs.json | 18 +++ .../components/nuki/translations/en.json | 8 +- .../components/nuki/translations/et.json | 18 +++ .../components/nuki/translations/it.json | 18 +++ .../components/nuki/translations/no.json | 18 +++ .../components/nuki/translations/pl.json | 18 +++ .../components/nuki/translations/ru.json | 18 +++ .../components/nuki/translations/tr.json | 18 +++ .../components/nuki/translations/zh-Hant.json | 18 +++ .../components/number/translations/ca.json | 8 ++ .../components/number/translations/cs.json | 8 ++ .../components/number/translations/en.json | 8 ++ .../components/number/translations/et.json | 8 ++ .../components/number/translations/it.json | 8 ++ .../components/number/translations/no.json | 8 ++ .../components/number/translations/pl.json | 8 ++ .../components/number/translations/ru.json | 8 ++ .../components/number/translations/tr.json | 8 ++ .../number/translations/zh-Hant.json | 8 ++ .../components/nut/translations/de.json | 2 +- .../components/nut/translations/tr.json | 41 +++++++ .../components/nut/translations/uk.json | 46 ++++++++ .../components/nws/translations/de.json | 4 +- .../components/nws/translations/tr.json | 21 ++++ .../components/nws/translations/uk.json | 23 ++++ .../components/nzbget/translations/de.json | 8 +- .../components/nzbget/translations/tr.json | 29 +++++ .../components/nzbget/translations/uk.json | 35 ++++++ .../components/omnilogic/translations/de.json | 6 +- .../components/omnilogic/translations/tr.json | 20 ++++ .../components/omnilogic/translations/uk.json | 29 +++++ .../onboarding/translations/uk.json | 7 ++ .../ondilo_ico/translations/ca.json | 17 +++ .../ondilo_ico/translations/cs.json | 17 +++ .../ondilo_ico/translations/de.json | 16 +++ .../ondilo_ico/translations/en.json | 3 +- .../ondilo_ico/translations/es.json | 17 +++ .../ondilo_ico/translations/et.json | 17 +++ .../ondilo_ico/translations/it.json | 17 +++ .../ondilo_ico/translations/lb.json | 11 ++ .../ondilo_ico/translations/no.json | 17 +++ .../ondilo_ico/translations/pl.json | 17 +++ .../ondilo_ico/translations/ru.json | 17 +++ .../ondilo_ico/translations/tr.json | 10 ++ .../ondilo_ico/translations/uk.json | 17 +++ .../ondilo_ico/translations/zh-Hant.json | 17 +++ .../components/onewire/translations/de.json | 3 + .../components/onewire/translations/tr.json | 24 ++++ .../components/onewire/translations/uk.json | 26 +++++ .../components/onvif/translations/de.json | 7 +- .../components/onvif/translations/tr.json | 36 +++++- .../components/onvif/translations/uk.json | 59 ++++++++++ .../opentherm_gw/translations/de.json | 2 +- .../opentherm_gw/translations/tr.json | 16 +++ .../opentherm_gw/translations/uk.json | 30 +++++ .../components/openuv/translations/de.json | 2 +- .../components/openuv/translations/tr.json | 19 +++ .../components/openuv/translations/uk.json | 9 +- .../openweathermap/translations/de.json | 6 +- .../openweathermap/translations/tr.json | 30 +++++ .../openweathermap/translations/uk.json | 35 ++++++ .../ovo_energy/translations/de.json | 8 +- .../ovo_energy/translations/fr.json | 7 +- .../ovo_energy/translations/lb.json | 5 + .../ovo_energy/translations/tr.json | 11 ++ .../ovo_energy/translations/uk.json | 27 +++++ .../components/owntracks/translations/de.json | 3 + .../components/owntracks/translations/tr.json | 7 ++ .../components/owntracks/translations/uk.json | 6 + .../components/ozw/translations/ca.json | 2 +- .../components/ozw/translations/de.json | 12 +- .../components/ozw/translations/lb.json | 9 ++ .../components/ozw/translations/no.json | 18 +-- .../components/ozw/translations/tr.json | 19 ++- .../components/ozw/translations/uk.json | 41 +++++++ .../panasonic_viera/translations/de.json | 16 +-- .../panasonic_viera/translations/tr.json | 19 +++ .../panasonic_viera/translations/uk.json | 30 +++++ .../components/person/translations/uk.json | 2 +- .../components/pi_hole/translations/ca.json | 6 + .../components/pi_hole/translations/cs.json | 5 + .../components/pi_hole/translations/de.json | 6 +- .../components/pi_hole/translations/en.json | 6 + .../components/pi_hole/translations/es.json | 6 + .../components/pi_hole/translations/et.json | 6 + .../components/pi_hole/translations/it.json | 6 + .../components/pi_hole/translations/no.json | 6 + .../components/pi_hole/translations/pl.json | 6 + .../components/pi_hole/translations/ru.json | 6 + .../components/pi_hole/translations/tr.json | 26 +++++ .../components/pi_hole/translations/uk.json | 23 ++++ .../pi_hole/translations/zh-Hant.json | 6 + .../components/plaato/translations/ca.json | 41 ++++++- .../components/plaato/translations/de.json | 6 +- .../components/plaato/translations/et.json | 41 ++++++- .../components/plaato/translations/no.json | 32 +++++ .../components/plaato/translations/pl.json | 41 ++++++- .../components/plaato/translations/tr.json | 43 +++++++ .../components/plaato/translations/uk.json | 17 +++ .../plaato/translations/zh-Hant.json | 41 ++++++- .../components/plant/translations/uk.json | 4 +- .../components/plex/translations/de.json | 5 +- .../components/plex/translations/tr.json | 27 +++++ .../components/plex/translations/uk.json | 62 ++++++++++ .../components/plugwise/translations/de.json | 5 +- .../components/plugwise/translations/lb.json | 3 +- .../components/plugwise/translations/tr.json | 15 ++- .../components/plugwise/translations/uk.json | 42 +++++++ .../plum_lightpad/translations/de.json | 5 +- .../plum_lightpad/translations/tr.json | 18 +++ .../plum_lightpad/translations/uk.json | 18 +++ .../components/point/translations/de.json | 10 +- .../components/point/translations/fr.json | 3 +- .../components/point/translations/tr.json | 11 ++ .../components/point/translations/uk.json | 32 +++++ .../components/poolsense/translations/de.json | 5 +- .../components/poolsense/translations/tr.json | 18 +++ .../components/poolsense/translations/uk.json | 20 ++++ .../components/powerwall/translations/ca.json | 1 + .../components/powerwall/translations/cs.json | 1 + .../components/powerwall/translations/de.json | 5 +- .../components/powerwall/translations/en.json | 36 +++--- .../components/powerwall/translations/es.json | 1 + .../components/powerwall/translations/et.json | 1 + .../components/powerwall/translations/it.json | 1 + .../components/powerwall/translations/no.json | 1 + .../components/powerwall/translations/pl.json | 1 + .../components/powerwall/translations/ru.json | 1 + .../components/powerwall/translations/tr.json | 19 +++ .../components/powerwall/translations/uk.json | 20 ++++ .../powerwall/translations/zh-Hant.json | 1 + .../components/profiler/translations/de.json | 12 ++ .../components/profiler/translations/tr.json | 7 ++ .../components/profiler/translations/uk.json | 12 ++ .../progettihwsw/translations/de.json | 5 +- .../progettihwsw/translations/tr.json | 41 +++++++ .../progettihwsw/translations/uk.json | 41 +++++++ .../components/ps4/translations/de.json | 7 +- .../components/ps4/translations/tr.json | 22 ++++ .../components/ps4/translations/uk.json | 41 +++++++ .../pvpc_hourly_pricing/translations/de.json | 2 +- .../pvpc_hourly_pricing/translations/tr.json | 14 +++ .../pvpc_hourly_pricing/translations/uk.json | 17 +++ .../components/rachio/translations/de.json | 6 +- .../components/rachio/translations/tr.json | 19 +++ .../components/rachio/translations/uk.json | 30 +++++ .../rainmachine/translations/de.json | 5 +- .../rainmachine/translations/tr.json | 17 +++ .../rainmachine/translations/uk.json | 30 +++++ .../recollect_waste/translations/de.json | 22 ++++ .../recollect_waste/translations/es.json | 3 +- .../recollect_waste/translations/lb.json | 18 +++ .../recollect_waste/translations/pl.json | 10 ++ .../recollect_waste/translations/tr.json | 7 ++ .../recollect_waste/translations/uk.json | 28 +++++ .../components/remote/translations/tr.json | 10 ++ .../components/remote/translations/uk.json | 9 ++ .../components/rfxtrx/translations/ca.json | 3 +- .../components/rfxtrx/translations/de.json | 19 ++- .../components/rfxtrx/translations/en.json | 4 +- .../components/rfxtrx/translations/es.json | 3 +- .../components/rfxtrx/translations/et.json | 3 +- .../components/rfxtrx/translations/it.json | 3 +- .../components/rfxtrx/translations/no.json | 3 +- .../components/rfxtrx/translations/pl.json | 3 +- .../components/rfxtrx/translations/ru.json | 3 +- .../components/rfxtrx/translations/tr.json | 32 +++++ .../components/rfxtrx/translations/uk.json | 74 ++++++++++++ .../rfxtrx/translations/zh-Hant.json | 3 +- .../components/ring/translations/tr.json | 19 +++ .../components/ring/translations/uk.json | 26 +++++ .../components/risco/translations/de.json | 14 ++- .../components/risco/translations/lb.json | 4 +- .../components/risco/translations/tr.json | 27 +++++ .../components/risco/translations/uk.json | 55 +++++++++ .../components/roku/translations/ca.json | 4 + .../components/roku/translations/cs.json | 4 + .../components/roku/translations/de.json | 4 + .../components/roku/translations/en.json | 4 + .../components/roku/translations/es.json | 4 + .../components/roku/translations/et.json | 4 + .../components/roku/translations/it.json | 4 + .../components/roku/translations/lb.json | 4 + .../components/roku/translations/no.json | 4 + .../components/roku/translations/pl.json | 10 ++ .../components/roku/translations/ru.json | 4 + .../components/roku/translations/tr.json | 25 ++++ .../components/roku/translations/uk.json | 28 +++++ .../components/roku/translations/zh-Hant.json | 4 + .../components/roomba/translations/ca.json | 32 +++++ .../components/roomba/translations/cs.json | 20 ++++ .../components/roomba/translations/de.json | 35 +++++- .../components/roomba/translations/en.json | 103 +++++++++-------- .../components/roomba/translations/es.json | 32 +++++ .../components/roomba/translations/et.json | 32 +++++ .../components/roomba/translations/fr.json | 29 +++++ .../components/roomba/translations/it.json | 32 +++++ .../components/roomba/translations/lb.json | 26 +++++ .../components/roomba/translations/no.json | 32 +++++ .../components/roomba/translations/pl.json | 32 +++++ .../components/roomba/translations/pt.json | 7 ++ .../components/roomba/translations/ru.json | 32 +++++ .../components/roomba/translations/tr.json | 60 ++++++++++ .../components/roomba/translations/uk.json | 30 +++++ .../roomba/translations/zh-Hant.json | 32 +++++ .../components/roon/translations/ca.json | 2 +- .../components/roon/translations/cs.json | 3 +- .../components/roon/translations/de.json | 11 ++ .../components/roon/translations/en.json | 2 +- .../components/roon/translations/et.json | 2 +- .../components/roon/translations/it.json | 2 +- .../components/roon/translations/no.json | 2 +- .../components/roon/translations/pl.json | 2 +- .../components/roon/translations/ru.json | 2 +- .../components/roon/translations/tr.json | 23 ++++ .../components/roon/translations/uk.json | 24 ++++ .../components/roon/translations/zh-Hant.json | 2 +- .../components/rpi_power/translations/de.json | 13 +++ .../components/rpi_power/translations/lb.json | 5 + .../components/rpi_power/translations/tr.json | 13 +++ .../components/rpi_power/translations/uk.json | 14 +++ .../ruckus_unleashed/translations/de.json | 4 +- .../ruckus_unleashed/translations/tr.json | 21 ++++ .../ruckus_unleashed/translations/uk.json | 21 ++++ .../components/samsungtv/translations/de.json | 4 +- .../components/samsungtv/translations/tr.json | 6 +- .../components/samsungtv/translations/uk.json | 25 ++++ .../components/script/translations/uk.json | 2 +- .../season/translations/sensor.uk.json | 6 + .../components/sense/translations/de.json | 2 +- .../components/sense/translations/tr.json | 20 ++++ .../components/sense/translations/uk.json | 21 ++++ .../components/sensor/translations/tr.json | 27 +++++ .../components/sensor/translations/uk.json | 31 ++++- .../components/sentry/translations/de.json | 3 + .../components/sentry/translations/tr.json | 27 +++++ .../components/sentry/translations/uk.json | 36 ++++++ .../components/sharkiq/translations/de.json | 7 +- .../components/sharkiq/translations/fr.json | 2 +- .../components/sharkiq/translations/tr.json | 29 +++++ .../components/sharkiq/translations/uk.json | 29 +++++ .../components/shelly/translations/ca.json | 16 +++ .../components/shelly/translations/cs.json | 16 +++ .../components/shelly/translations/da.json | 17 +++ .../components/shelly/translations/de.json | 9 +- .../components/shelly/translations/en.json | 18 +-- .../components/shelly/translations/es.json | 16 +++ .../components/shelly/translations/et.json | 16 +++ .../components/shelly/translations/it.json | 16 +++ .../components/shelly/translations/lb.json | 8 ++ .../components/shelly/translations/no.json | 16 +++ .../components/shelly/translations/pl.json | 16 +++ .../components/shelly/translations/ru.json | 16 +++ .../components/shelly/translations/tr.json | 41 +++++++ .../components/shelly/translations/uk.json | 47 ++++++++ .../shelly/translations/zh-Hant.json | 16 +++ .../shopping_list/translations/de.json | 2 +- .../shopping_list/translations/tr.json | 14 +++ .../shopping_list/translations/uk.json | 14 +++ .../simplisafe/translations/de.json | 8 +- .../simplisafe/translations/tr.json | 20 ++++ .../simplisafe/translations/uk.json | 29 ++++- .../components/smappee/translations/de.json | 14 ++- .../components/smappee/translations/tr.json | 26 +++++ .../components/smappee/translations/uk.json | 35 ++++++ .../smart_meter_texas/translations/de.json | 6 +- .../smart_meter_texas/translations/tr.json | 20 ++++ .../smart_meter_texas/translations/uk.json | 20 ++++ .../components/smarthab/translations/de.json | 1 + .../components/smarthab/translations/tr.json | 17 +++ .../components/smarthab/translations/uk.json | 19 +++ .../smartthings/translations/tr.json | 17 +++ .../smartthings/translations/uk.json | 38 ++++++ .../components/smhi/translations/uk.json | 18 +++ .../components/sms/translations/de.json | 6 +- .../components/sms/translations/tr.json | 17 +++ .../components/sms/translations/uk.json | 20 ++++ .../components/solaredge/translations/fr.json | 4 +- .../components/solaredge/translations/lb.json | 5 +- .../components/solaredge/translations/tr.json | 13 +++ .../components/solaredge/translations/uk.json | 25 ++++ .../components/solarlog/translations/de.json | 2 +- .../components/solarlog/translations/tr.json | 18 +++ .../components/solarlog/translations/uk.json | 20 ++++ .../components/soma/translations/tr.json | 12 ++ .../components/soma/translations/uk.json | 24 ++++ .../components/somfy/translations/de.json | 6 +- .../components/somfy/translations/tr.json | 7 ++ .../components/somfy/translations/uk.json | 18 +++ .../somfy_mylink/translations/ca.json | 53 +++++++++ .../somfy_mylink/translations/cs.json | 26 +++++ .../somfy_mylink/translations/de.json | 39 +++++++ .../somfy_mylink/translations/en.json | 87 +++++++------- .../somfy_mylink/translations/es.json | 53 +++++++++ .../somfy_mylink/translations/et.json | 53 +++++++++ .../somfy_mylink/translations/fr.json | 43 +++++++ .../somfy_mylink/translations/it.json | 53 +++++++++ .../somfy_mylink/translations/lb.json | 27 +++++ .../somfy_mylink/translations/no.json | 53 +++++++++ .../somfy_mylink/translations/pl.json | 53 +++++++++ .../somfy_mylink/translations/ru.json | 53 +++++++++ .../somfy_mylink/translations/tr.json | 53 +++++++++ .../somfy_mylink/translations/uk.json | 40 +++++++ .../somfy_mylink/translations/zh-Hant.json | 53 +++++++++ .../components/sonarr/translations/de.json | 12 +- .../components/sonarr/translations/tr.json | 22 ++++ .../components/sonarr/translations/uk.json | 40 +++++++ .../components/songpal/translations/tr.json | 17 +++ .../components/songpal/translations/uk.json | 22 ++++ .../components/sonos/translations/de.json | 2 +- .../components/sonos/translations/tr.json | 12 ++ .../components/sonos/translations/uk.json | 13 +++ .../speedtestdotnet/translations/de.json | 9 +- .../speedtestdotnet/translations/tr.json | 24 ++++ .../speedtestdotnet/translations/uk.json | 24 ++++ .../components/spider/translations/de.json | 7 ++ .../components/spider/translations/tr.json | 19 +++ .../components/spider/translations/uk.json | 20 ++++ .../components/spotify/translations/de.json | 8 +- .../components/spotify/translations/lb.json | 5 + .../components/spotify/translations/uk.json | 27 +++++ .../squeezebox/translations/de.json | 8 +- .../squeezebox/translations/tr.json | 27 +++++ .../squeezebox/translations/uk.json | 31 +++++ .../srp_energy/translations/de.json | 13 ++- .../srp_energy/translations/es.json | 6 +- .../srp_energy/translations/fr.json | 14 +++ .../srp_energy/translations/lb.json | 20 ++++ .../srp_energy/translations/tr.json | 8 +- .../srp_energy/translations/uk.json | 24 ++++ .../components/starline/translations/tr.json | 33 ++++++ .../components/starline/translations/uk.json | 41 +++++++ .../components/sun/translations/pl.json | 2 +- .../components/switch/translations/uk.json | 9 ++ .../components/syncthru/translations/tr.json | 20 ++++ .../components/syncthru/translations/uk.json | 27 +++++ .../synology_dsm/translations/de.json | 11 +- .../synology_dsm/translations/tr.json | 18 ++- .../synology_dsm/translations/uk.json | 55 +++++++++ .../system_health/translations/uk.json | 2 +- .../components/tado/translations/de.json | 8 +- .../components/tado/translations/tr.json | 29 +++++ .../components/tado/translations/uk.json | 33 ++++++ .../components/tag/translations/uk.json | 3 + .../components/tasmota/translations/de.json | 16 +++ .../components/tasmota/translations/tr.json | 16 +++ .../components/tasmota/translations/uk.json | 22 ++++ .../tellduslive/translations/de.json | 5 +- .../tellduslive/translations/fr.json | 3 +- .../tellduslive/translations/lb.json | 3 +- .../tellduslive/translations/tr.json | 19 +++ .../tellduslive/translations/uk.json | 27 +++++ .../components/tesla/translations/de.json | 5 +- .../components/tesla/translations/fr.json | 2 +- .../components/tesla/translations/tr.json | 18 +++ .../components/tesla/translations/uk.json | 29 +++++ .../components/tibber/translations/de.json | 4 +- .../components/tibber/translations/tr.json | 18 +++ .../components/tibber/translations/uk.json | 21 ++++ .../components/tile/translations/de.json | 3 + .../components/tile/translations/tr.json | 28 +++++ .../components/tile/translations/uk.json | 29 +++++ .../components/timer/translations/uk.json | 6 +- .../components/toon/translations/de.json | 3 + .../components/toon/translations/fr.json | 3 +- .../components/toon/translations/lb.json | 3 +- .../components/toon/translations/tr.json | 20 ++++ .../components/toon/translations/uk.json | 25 ++++ .../totalconnect/translations/de.json | 5 +- .../totalconnect/translations/tr.json | 18 +++ .../totalconnect/translations/uk.json | 19 +++ .../components/tplink/translations/de.json | 2 +- .../components/tplink/translations/tr.json | 12 ++ .../components/tplink/translations/uk.json | 13 +++ .../components/traccar/translations/de.json | 6 +- .../components/traccar/translations/lb.json | 3 +- .../components/traccar/translations/tr.json | 4 + .../components/traccar/translations/uk.json | 17 +++ .../components/tradfri/translations/de.json | 4 +- .../components/tradfri/translations/tr.json | 18 +++ .../components/tradfri/translations/uk.json | 12 +- .../transmission/translations/de.json | 5 +- .../transmission/translations/tr.json | 21 ++++ .../transmission/translations/uk.json | 36 ++++++ .../components/tuya/translations/de.json | 8 +- .../components/tuya/translations/lb.json | 3 + .../components/tuya/translations/tr.json | 28 +++++ .../components/tuya/translations/uk.json | 63 ++++++++++ .../twentemilieu/translations/de.json | 5 +- .../twentemilieu/translations/tr.json | 10 ++ .../twentemilieu/translations/uk.json | 22 ++++ .../components/twilio/translations/de.json | 8 +- .../components/twilio/translations/lb.json | 3 +- .../components/twilio/translations/tr.json | 8 ++ .../components/twilio/translations/uk.json | 17 +++ .../components/twinkly/translations/de.json | 5 +- .../components/twinkly/translations/fr.json | 18 +++ .../components/twinkly/translations/lb.json | 7 ++ .../components/twinkly/translations/tr.json | 6 + .../components/twinkly/translations/uk.json | 19 +++ .../components/unifi/translations/ca.json | 5 +- .../components/unifi/translations/cs.json | 4 +- .../components/unifi/translations/da.json | 1 + .../components/unifi/translations/de.json | 8 +- .../components/unifi/translations/en.json | 1 + .../components/unifi/translations/es.json | 4 +- .../components/unifi/translations/et.json | 5 +- .../components/unifi/translations/it.json | 5 +- .../components/unifi/translations/no.json | 5 +- .../components/unifi/translations/pl.json | 5 +- .../components/unifi/translations/ru.json | 4 +- .../components/unifi/translations/tr.json | 13 +++ .../components/unifi/translations/uk.json | 66 +++++++++++ .../unifi/translations/zh-Hant.json | 5 +- .../components/upb/translations/de.json | 11 +- .../components/upb/translations/tr.json | 11 ++ .../components/upb/translations/uk.json | 23 ++++ .../components/upcloud/translations/de.json | 3 +- .../components/upcloud/translations/tr.json | 16 +++ .../components/upcloud/translations/uk.json | 25 ++++ .../components/upnp/translations/ro.json | 7 ++ .../components/upnp/translations/tr.json | 19 +++ .../components/upnp/translations/uk.json | 16 ++- .../components/vacuum/translations/de.json | 2 +- .../components/vacuum/translations/uk.json | 16 ++- .../components/velbus/translations/de.json | 6 +- .../components/velbus/translations/tr.json | 11 ++ .../components/velbus/translations/uk.json | 20 ++++ .../components/vera/translations/tr.json | 14 +++ .../components/vera/translations/uk.json | 30 +++++ .../components/vesync/translations/de.json | 6 + .../components/vesync/translations/tr.json | 19 +++ .../components/vesync/translations/uk.json | 19 +++ .../components/vilfo/translations/de.json | 6 +- .../components/vilfo/translations/tr.json | 20 ++++ .../components/vilfo/translations/uk.json | 22 ++++ .../components/vizio/translations/de.json | 13 ++- .../components/vizio/translations/tr.json | 19 +++ .../components/vizio/translations/uk.json | 54 +++++++++ .../components/volumio/translations/de.json | 14 ++- .../components/volumio/translations/tr.json | 20 ++++ .../components/volumio/translations/uk.json | 6 +- .../water_heater/translations/uk.json | 8 ++ .../components/wemo/translations/de.json | 2 +- .../components/wemo/translations/tr.json | 8 +- .../components/wemo/translations/uk.json | 13 +++ .../components/wiffi/translations/tr.json | 24 ++++ .../components/wiffi/translations/uk.json | 25 ++++ .../components/wilight/translations/de.json | 3 + .../components/wilight/translations/tr.json | 7 ++ .../components/wilight/translations/uk.json | 16 +++ .../components/withings/translations/de.json | 16 ++- .../components/withings/translations/fr.json | 2 +- .../components/withings/translations/tr.json | 18 +++ .../components/withings/translations/uk.json | 33 ++++++ .../components/wled/translations/de.json | 6 +- .../components/wled/translations/tr.json | 23 ++++ .../components/wled/translations/uk.json | 24 ++++ .../components/wolflink/translations/de.json | 8 ++ .../wolflink/translations/sensor.de.json | 1 + .../wolflink/translations/sensor.tr.json | 11 +- .../wolflink/translations/sensor.uk.json | 78 ++++++++++++- .../components/wolflink/translations/tr.json | 20 ++++ .../components/wolflink/translations/uk.json | 13 ++- .../components/xbox/translations/de.json | 5 + .../components/xbox/translations/lb.json | 4 + .../components/xbox/translations/tr.json | 7 ++ .../components/xbox/translations/uk.json | 17 +++ .../xiaomi_aqara/translations/de.json | 15 ++- .../xiaomi_aqara/translations/tr.json | 31 +++++ .../xiaomi_aqara/translations/uk.json | 43 +++++++ .../xiaomi_miio/translations/de.json | 15 +-- .../xiaomi_miio/translations/tr.json | 29 +++++ .../xiaomi_miio/translations/uk.json | 31 +++++ .../components/yeelight/translations/de.json | 10 +- .../components/yeelight/translations/tr.json | 34 ++++++ .../components/yeelight/translations/uk.json | 38 ++++++ .../components/zerproc/translations/tr.json | 8 +- .../components/zerproc/translations/uk.json | 13 +++ .../components/zha/translations/cs.json | 20 ++-- .../components/zha/translations/de.json | 4 +- .../components/zha/translations/pl.json | 32 ++--- .../components/zha/translations/tr.json | 26 +++++ .../components/zha/translations/uk.json | 91 +++++++++++++++ .../zodiac/translations/sensor.tr.json | 18 +++ .../zodiac/translations/sensor.uk.json | 18 +++ .../components/zone/translations/tr.json | 12 ++ .../zoneminder/translations/de.json | 9 ++ .../zoneminder/translations/tr.json | 22 ++++ .../zoneminder/translations/uk.json | 34 ++++++ .../components/zwave/translations/de.json | 3 +- .../components/zwave/translations/tr.json | 4 + .../components/zwave/translations/uk.json | 27 ++++- .../components/zwave_js/translations/ca.json | 56 +++++++++ .../components/zwave_js/translations/cs.json | 30 +++++ .../components/zwave_js/translations/de.json | 18 +++ .../components/zwave_js/translations/en.json | 5 + .../components/zwave_js/translations/es.json | 36 ++++++ .../components/zwave_js/translations/et.json | 56 +++++++++ .../components/zwave_js/translations/fr.json | 20 ++++ .../components/zwave_js/translations/it.json | 56 +++++++++ .../components/zwave_js/translations/lb.json | 20 ++++ .../components/zwave_js/translations/no.json | 56 +++++++++ .../components/zwave_js/translations/pl.json | 56 +++++++++ .../zwave_js/translations/pt-BR.json | 7 ++ .../components/zwave_js/translations/ru.json | 56 +++++++++ .../components/zwave_js/translations/tr.json | 56 +++++++++ .../components/zwave_js/translations/uk.json | 20 ++++ .../zwave_js/translations/zh-Hant.json | 56 +++++++++ 1150 files changed, 19484 insertions(+), 880 deletions(-) create mode 100644 homeassistant/components/abode/translations/tr.json create mode 100644 homeassistant/components/abode/translations/uk.json create mode 100644 homeassistant/components/accuweather/translations/sensor.uk.json create mode 100644 homeassistant/components/accuweather/translations/tr.json create mode 100644 homeassistant/components/acmeda/translations/tr.json create mode 100644 homeassistant/components/acmeda/translations/uk.json create mode 100644 homeassistant/components/adguard/translations/tr.json create mode 100644 homeassistant/components/adguard/translations/uk.json create mode 100644 homeassistant/components/advantage_air/translations/tr.json create mode 100644 homeassistant/components/advantage_air/translations/uk.json create mode 100644 homeassistant/components/agent_dvr/translations/tr.json create mode 100644 homeassistant/components/agent_dvr/translations/uk.json create mode 100644 homeassistant/components/airly/translations/uk.json create mode 100644 homeassistant/components/airnow/translations/ca.json create mode 100644 homeassistant/components/airnow/translations/cs.json create mode 100644 homeassistant/components/airnow/translations/de.json create mode 100644 homeassistant/components/airnow/translations/es.json create mode 100644 homeassistant/components/airnow/translations/et.json create mode 100644 homeassistant/components/airnow/translations/fr.json create mode 100644 homeassistant/components/airnow/translations/it.json create mode 100644 homeassistant/components/airnow/translations/lb.json create mode 100644 homeassistant/components/airnow/translations/no.json create mode 100644 homeassistant/components/airnow/translations/pl.json create mode 100644 homeassistant/components/airnow/translations/pt.json create mode 100644 homeassistant/components/airnow/translations/ru.json create mode 100644 homeassistant/components/airnow/translations/tr.json create mode 100644 homeassistant/components/airnow/translations/uk.json create mode 100644 homeassistant/components/airnow/translations/zh-Hant.json create mode 100644 homeassistant/components/airvisual/translations/ar.json create mode 100644 homeassistant/components/airvisual/translations/tr.json create mode 100644 homeassistant/components/airvisual/translations/uk.json create mode 100644 homeassistant/components/alarmdecoder/translations/tr.json create mode 100644 homeassistant/components/alarmdecoder/translations/uk.json create mode 100644 homeassistant/components/almond/translations/tr.json create mode 100644 homeassistant/components/almond/translations/uk.json create mode 100644 homeassistant/components/ambiclimate/translations/tr.json create mode 100644 homeassistant/components/ambiclimate/translations/uk.json create mode 100644 homeassistant/components/ambient_station/translations/tr.json create mode 100644 homeassistant/components/ambient_station/translations/uk.json create mode 100644 homeassistant/components/apple_tv/translations/uk.json create mode 100644 homeassistant/components/arcam_fmj/translations/ro.json create mode 100644 homeassistant/components/arcam_fmj/translations/tr.json create mode 100644 homeassistant/components/arcam_fmj/translations/uk.json create mode 100644 homeassistant/components/atag/translations/tr.json create mode 100644 homeassistant/components/atag/translations/uk.json create mode 100644 homeassistant/components/august/translations/tr.json create mode 100644 homeassistant/components/august/translations/uk.json create mode 100644 homeassistant/components/aurora/translations/fr.json create mode 100644 homeassistant/components/aurora/translations/tr.json create mode 100644 homeassistant/components/aurora/translations/uk.json create mode 100644 homeassistant/components/auth/translations/tr.json create mode 100644 homeassistant/components/awair/translations/tr.json create mode 100644 homeassistant/components/awair/translations/uk.json create mode 100644 homeassistant/components/axis/translations/tr.json create mode 100644 homeassistant/components/axis/translations/uk.json create mode 100644 homeassistant/components/azure_devops/translations/tr.json create mode 100644 homeassistant/components/blebox/translations/tr.json create mode 100644 homeassistant/components/blebox/translations/uk.json create mode 100644 homeassistant/components/blink/translations/tr.json create mode 100644 homeassistant/components/blink/translations/uk.json create mode 100644 homeassistant/components/bmw_connected_drive/translations/ca.json create mode 100644 homeassistant/components/bmw_connected_drive/translations/cs.json create mode 100644 homeassistant/components/bmw_connected_drive/translations/de.json create mode 100644 homeassistant/components/bmw_connected_drive/translations/es.json create mode 100644 homeassistant/components/bmw_connected_drive/translations/et.json create mode 100644 homeassistant/components/bmw_connected_drive/translations/fr.json create mode 100644 homeassistant/components/bmw_connected_drive/translations/it.json create mode 100644 homeassistant/components/bmw_connected_drive/translations/lb.json create mode 100644 homeassistant/components/bmw_connected_drive/translations/no.json create mode 100644 homeassistant/components/bmw_connected_drive/translations/pl.json create mode 100644 homeassistant/components/bmw_connected_drive/translations/pt.json create mode 100644 homeassistant/components/bmw_connected_drive/translations/ru.json create mode 100644 homeassistant/components/bmw_connected_drive/translations/tr.json create mode 100644 homeassistant/components/bmw_connected_drive/translations/uk.json create mode 100644 homeassistant/components/bmw_connected_drive/translations/zh-Hant.json create mode 100644 homeassistant/components/bond/translations/tr.json create mode 100644 homeassistant/components/braviatv/translations/tr.json create mode 100644 homeassistant/components/braviatv/translations/uk.json create mode 100644 homeassistant/components/broadlink/translations/tr.json create mode 100644 homeassistant/components/broadlink/translations/uk.json create mode 100644 homeassistant/components/brother/translations/uk.json create mode 100644 homeassistant/components/bsblan/translations/uk.json create mode 100644 homeassistant/components/canary/translations/tr.json create mode 100644 homeassistant/components/canary/translations/uk.json create mode 100644 homeassistant/components/cast/translations/tr.json create mode 100644 homeassistant/components/cert_expiry/translations/tr.json create mode 100644 homeassistant/components/cert_expiry/translations/uk.json create mode 100644 homeassistant/components/cloud/translations/de.json create mode 100644 homeassistant/components/cloud/translations/fr.json create mode 100644 homeassistant/components/cloud/translations/uk.json create mode 100644 homeassistant/components/cloudflare/translations/uk.json create mode 100644 homeassistant/components/control4/translations/tr.json create mode 100644 homeassistant/components/coolmaster/translations/tr.json create mode 100644 homeassistant/components/coolmaster/translations/uk.json create mode 100644 homeassistant/components/coronavirus/translations/tr.json create mode 100644 homeassistant/components/coronavirus/translations/uk.json create mode 100644 homeassistant/components/daikin/translations/tr.json create mode 100644 homeassistant/components/daikin/translations/uk.json create mode 100644 homeassistant/components/deconz/translations/uk.json create mode 100644 homeassistant/components/demo/translations/tr.json create mode 100644 homeassistant/components/demo/translations/uk.json create mode 100644 homeassistant/components/denonavr/translations/tr.json create mode 100644 homeassistant/components/denonavr/translations/uk.json create mode 100644 homeassistant/components/devolo_home_control/translations/tr.json create mode 100644 homeassistant/components/devolo_home_control/translations/uk.json create mode 100644 homeassistant/components/dexcom/translations/uk.json create mode 100644 homeassistant/components/dialogflow/translations/tr.json create mode 100644 homeassistant/components/dialogflow/translations/uk.json create mode 100644 homeassistant/components/directv/translations/tr.json create mode 100644 homeassistant/components/directv/translations/uk.json create mode 100644 homeassistant/components/doorbird/translations/tr.json create mode 100644 homeassistant/components/doorbird/translations/uk.json create mode 100644 homeassistant/components/dsmr/translations/de.json create mode 100644 homeassistant/components/dsmr/translations/uk.json create mode 100644 homeassistant/components/dunehd/translations/tr.json create mode 100644 homeassistant/components/dunehd/translations/uk.json create mode 100644 homeassistant/components/eafm/translations/de.json create mode 100644 homeassistant/components/eafm/translations/tr.json create mode 100644 homeassistant/components/eafm/translations/uk.json create mode 100644 homeassistant/components/ecobee/translations/tr.json create mode 100644 homeassistant/components/ecobee/translations/uk.json create mode 100644 homeassistant/components/econet/translations/ca.json create mode 100644 homeassistant/components/econet/translations/es.json create mode 100644 homeassistant/components/econet/translations/et.json create mode 100644 homeassistant/components/econet/translations/it.json create mode 100644 homeassistant/components/econet/translations/no.json create mode 100644 homeassistant/components/econet/translations/pl.json create mode 100644 homeassistant/components/econet/translations/ru.json create mode 100644 homeassistant/components/econet/translations/tr.json create mode 100644 homeassistant/components/econet/translations/zh-Hant.json create mode 100644 homeassistant/components/elgato/translations/tr.json create mode 100644 homeassistant/components/elgato/translations/uk.json create mode 100644 homeassistant/components/elkm1/translations/tr.json create mode 100644 homeassistant/components/elkm1/translations/uk.json create mode 100644 homeassistant/components/emulated_roku/translations/tr.json create mode 100644 homeassistant/components/emulated_roku/translations/uk.json create mode 100644 homeassistant/components/enocean/translations/tr.json create mode 100644 homeassistant/components/enocean/translations/uk.json create mode 100644 homeassistant/components/epson/translations/uk.json create mode 100644 homeassistant/components/fireservicerota/translations/fr.json create mode 100644 homeassistant/components/fireservicerota/translations/uk.json create mode 100644 homeassistant/components/firmata/translations/tr.json create mode 100644 homeassistant/components/firmata/translations/uk.json create mode 100644 homeassistant/components/flick_electric/translations/tr.json create mode 100644 homeassistant/components/flick_electric/translations/uk.json create mode 100644 homeassistant/components/flo/translations/tr.json create mode 100644 homeassistant/components/flo/translations/uk.json create mode 100644 homeassistant/components/flume/translations/tr.json create mode 100644 homeassistant/components/flume/translations/uk.json create mode 100644 homeassistant/components/flunearyou/translations/tr.json create mode 100644 homeassistant/components/flunearyou/translations/uk.json create mode 100644 homeassistant/components/forked_daapd/translations/tr.json create mode 100644 homeassistant/components/forked_daapd/translations/uk.json create mode 100644 homeassistant/components/foscam/translations/af.json create mode 100644 homeassistant/components/foscam/translations/ca.json create mode 100644 homeassistant/components/foscam/translations/cs.json create mode 100644 homeassistant/components/foscam/translations/de.json create mode 100644 homeassistant/components/foscam/translations/es.json create mode 100644 homeassistant/components/foscam/translations/et.json create mode 100644 homeassistant/components/foscam/translations/fr.json create mode 100644 homeassistant/components/foscam/translations/it.json create mode 100644 homeassistant/components/foscam/translations/lb.json create mode 100644 homeassistant/components/foscam/translations/no.json create mode 100644 homeassistant/components/foscam/translations/pl.json create mode 100644 homeassistant/components/foscam/translations/pt.json create mode 100644 homeassistant/components/foscam/translations/ru.json create mode 100644 homeassistant/components/foscam/translations/tr.json create mode 100644 homeassistant/components/foscam/translations/zh-Hant.json create mode 100644 homeassistant/components/freebox/translations/tr.json create mode 100644 homeassistant/components/freebox/translations/uk.json create mode 100644 homeassistant/components/fritzbox/translations/tr.json create mode 100644 homeassistant/components/fritzbox/translations/uk.json create mode 100644 homeassistant/components/fritzbox_callmonitor/translations/ca.json create mode 100644 homeassistant/components/fritzbox_callmonitor/translations/cs.json create mode 100644 homeassistant/components/fritzbox_callmonitor/translations/et.json create mode 100644 homeassistant/components/fritzbox_callmonitor/translations/it.json create mode 100644 homeassistant/components/fritzbox_callmonitor/translations/lb.json create mode 100644 homeassistant/components/fritzbox_callmonitor/translations/no.json create mode 100644 homeassistant/components/fritzbox_callmonitor/translations/pl.json create mode 100644 homeassistant/components/fritzbox_callmonitor/translations/ru.json create mode 100644 homeassistant/components/fritzbox_callmonitor/translations/tr.json create mode 100644 homeassistant/components/fritzbox_callmonitor/translations/zh-Hant.json create mode 100644 homeassistant/components/garmin_connect/translations/tr.json create mode 100644 homeassistant/components/garmin_connect/translations/uk.json create mode 100644 homeassistant/components/gdacs/translations/tr.json create mode 100644 homeassistant/components/gdacs/translations/uk.json create mode 100644 homeassistant/components/geofency/translations/tr.json create mode 100644 homeassistant/components/geofency/translations/uk.json create mode 100644 homeassistant/components/geonetnz_quakes/translations/tr.json create mode 100644 homeassistant/components/geonetnz_quakes/translations/uk.json create mode 100644 homeassistant/components/geonetnz_volcano/translations/tr.json create mode 100644 homeassistant/components/geonetnz_volcano/translations/uk.json create mode 100644 homeassistant/components/gios/translations/tr.json create mode 100644 homeassistant/components/gios/translations/uk.json create mode 100644 homeassistant/components/glances/translations/tr.json create mode 100644 homeassistant/components/glances/translations/uk.json create mode 100644 homeassistant/components/goalzero/translations/tr.json create mode 100644 homeassistant/components/goalzero/translations/uk.json create mode 100644 homeassistant/components/gogogate2/translations/tr.json create mode 100644 homeassistant/components/gogogate2/translations/uk.json create mode 100644 homeassistant/components/gpslogger/translations/tr.json create mode 100644 homeassistant/components/gpslogger/translations/uk.json create mode 100644 homeassistant/components/gree/translations/de.json create mode 100644 homeassistant/components/gree/translations/tr.json create mode 100644 homeassistant/components/gree/translations/uk.json create mode 100644 homeassistant/components/griddy/translations/uk.json create mode 100644 homeassistant/components/guardian/translations/tr.json create mode 100644 homeassistant/components/guardian/translations/uk.json create mode 100644 homeassistant/components/hangouts/translations/tr.json create mode 100644 homeassistant/components/hangouts/translations/uk.json create mode 100644 homeassistant/components/harmony/translations/tr.json create mode 100644 homeassistant/components/harmony/translations/uk.json create mode 100644 homeassistant/components/heos/translations/tr.json create mode 100644 homeassistant/components/heos/translations/uk.json create mode 100644 homeassistant/components/hisense_aehw4a1/translations/tr.json create mode 100644 homeassistant/components/hisense_aehw4a1/translations/uk.json create mode 100644 homeassistant/components/hlk_sw16/translations/tr.json create mode 100644 homeassistant/components/hlk_sw16/translations/uk.json create mode 100644 homeassistant/components/home_connect/translations/uk.json create mode 100644 homeassistant/components/homeassistant/translations/fr.json create mode 100644 homeassistant/components/homeassistant/translations/uk.json create mode 100644 homeassistant/components/homekit/translations/ro.json create mode 100644 homeassistant/components/homekit/translations/tr.json create mode 100644 homeassistant/components/homekit_controller/translations/tr.json create mode 100644 homeassistant/components/homekit_controller/translations/uk.json create mode 100644 homeassistant/components/homematicip_cloud/translations/tr.json create mode 100644 homeassistant/components/homematicip_cloud/translations/uk.json create mode 100644 homeassistant/components/huawei_lte/translations/uk.json create mode 100644 homeassistant/components/hue/translations/tr.json create mode 100644 homeassistant/components/hue/translations/uk.json create mode 100644 homeassistant/components/huisbaasje/translations/ca.json create mode 100644 homeassistant/components/huisbaasje/translations/cs.json create mode 100644 homeassistant/components/huisbaasje/translations/en.json create mode 100644 homeassistant/components/huisbaasje/translations/et.json create mode 100644 homeassistant/components/huisbaasje/translations/it.json create mode 100644 homeassistant/components/huisbaasje/translations/no.json create mode 100644 homeassistant/components/huisbaasje/translations/pl.json create mode 100644 homeassistant/components/huisbaasje/translations/ru.json create mode 100644 homeassistant/components/huisbaasje/translations/tr.json create mode 100644 homeassistant/components/huisbaasje/translations/zh-Hant.json create mode 100644 homeassistant/components/humidifier/translations/tr.json create mode 100644 homeassistant/components/hunterdouglas_powerview/translations/tr.json create mode 100644 homeassistant/components/hunterdouglas_powerview/translations/uk.json create mode 100644 homeassistant/components/hvv_departures/translations/tr.json create mode 100644 homeassistant/components/hvv_departures/translations/uk.json create mode 100644 homeassistant/components/hyperion/translations/uk.json create mode 100644 homeassistant/components/iaqualink/translations/tr.json create mode 100644 homeassistant/components/iaqualink/translations/uk.json create mode 100644 homeassistant/components/icloud/translations/uk.json create mode 100644 homeassistant/components/ifttt/translations/tr.json create mode 100644 homeassistant/components/ifttt/translations/uk.json create mode 100644 homeassistant/components/insteon/translations/tr.json create mode 100644 homeassistant/components/insteon/translations/uk.json create mode 100644 homeassistant/components/ios/translations/tr.json create mode 100644 homeassistant/components/ios/translations/uk.json create mode 100644 homeassistant/components/ipp/translations/uk.json create mode 100644 homeassistant/components/iqvia/translations/tr.json create mode 100644 homeassistant/components/iqvia/translations/uk.json create mode 100644 homeassistant/components/islamic_prayer_times/translations/tr.json create mode 100644 homeassistant/components/islamic_prayer_times/translations/uk.json create mode 100644 homeassistant/components/isy994/translations/tr.json create mode 100644 homeassistant/components/isy994/translations/uk.json create mode 100644 homeassistant/components/izone/translations/tr.json create mode 100644 homeassistant/components/izone/translations/uk.json create mode 100644 homeassistant/components/juicenet/translations/tr.json create mode 100644 homeassistant/components/juicenet/translations/uk.json create mode 100644 homeassistant/components/kodi/translations/tr.json create mode 100644 homeassistant/components/kodi/translations/uk.json create mode 100644 homeassistant/components/konnected/translations/tr.json create mode 100644 homeassistant/components/konnected/translations/uk.json create mode 100644 homeassistant/components/kulersky/translations/lb.json create mode 100644 homeassistant/components/kulersky/translations/uk.json create mode 100644 homeassistant/components/life360/translations/uk.json create mode 100644 homeassistant/components/lifx/translations/tr.json create mode 100644 homeassistant/components/lifx/translations/uk.json create mode 100644 homeassistant/components/local_ip/translations/tr.json create mode 100644 homeassistant/components/local_ip/translations/uk.json create mode 100644 homeassistant/components/locative/translations/tr.json create mode 100644 homeassistant/components/locative/translations/uk.json create mode 100644 homeassistant/components/logi_circle/translations/tr.json create mode 100644 homeassistant/components/logi_circle/translations/uk.json create mode 100644 homeassistant/components/lovelace/translations/fr.json create mode 100644 homeassistant/components/lovelace/translations/uk.json create mode 100644 homeassistant/components/luftdaten/translations/tr.json create mode 100644 homeassistant/components/luftdaten/translations/uk.json create mode 100644 homeassistant/components/lutron_caseta/translations/tr.json create mode 100644 homeassistant/components/lutron_caseta/translations/uk.json create mode 100644 homeassistant/components/lyric/translations/ca.json create mode 100644 homeassistant/components/lyric/translations/cs.json create mode 100644 homeassistant/components/lyric/translations/et.json create mode 100644 homeassistant/components/lyric/translations/it.json create mode 100644 homeassistant/components/lyric/translations/no.json create mode 100644 homeassistant/components/lyric/translations/pl.json create mode 100644 homeassistant/components/lyric/translations/tr.json create mode 100644 homeassistant/components/lyric/translations/zh-Hant.json create mode 100644 homeassistant/components/mailgun/translations/tr.json create mode 100644 homeassistant/components/mailgun/translations/uk.json create mode 100644 homeassistant/components/melcloud/translations/tr.json create mode 100644 homeassistant/components/melcloud/translations/uk.json create mode 100644 homeassistant/components/met/translations/tr.json create mode 100644 homeassistant/components/met/translations/uk.json create mode 100644 homeassistant/components/meteo_france/translations/uk.json create mode 100644 homeassistant/components/metoffice/translations/tr.json create mode 100644 homeassistant/components/metoffice/translations/uk.json create mode 100644 homeassistant/components/mikrotik/translations/tr.json create mode 100644 homeassistant/components/mikrotik/translations/uk.json create mode 100644 homeassistant/components/mill/translations/tr.json create mode 100644 homeassistant/components/mill/translations/uk.json create mode 100644 homeassistant/components/minecraft_server/translations/uk.json create mode 100644 homeassistant/components/mobile_app/translations/tr.json create mode 100644 homeassistant/components/monoprice/translations/tr.json create mode 100644 homeassistant/components/monoprice/translations/uk.json create mode 100644 homeassistant/components/motion_blinds/translations/fr.json create mode 100644 homeassistant/components/motion_blinds/translations/uk.json create mode 100644 homeassistant/components/myq/translations/tr.json create mode 100644 homeassistant/components/myq/translations/uk.json create mode 100644 homeassistant/components/neato/translations/tr.json create mode 100644 homeassistant/components/neato/translations/uk.json create mode 100644 homeassistant/components/nest/translations/uk.json create mode 100644 homeassistant/components/netatmo/translations/tr.json create mode 100644 homeassistant/components/netatmo/translations/uk.json create mode 100644 homeassistant/components/nexia/translations/tr.json create mode 100644 homeassistant/components/nexia/translations/uk.json create mode 100644 homeassistant/components/nightscout/translations/uk.json create mode 100644 homeassistant/components/notion/translations/uk.json create mode 100644 homeassistant/components/nuheat/translations/tr.json create mode 100644 homeassistant/components/nuheat/translations/uk.json create mode 100644 homeassistant/components/nuki/translations/ca.json create mode 100644 homeassistant/components/nuki/translations/cs.json create mode 100644 homeassistant/components/nuki/translations/et.json create mode 100644 homeassistant/components/nuki/translations/it.json create mode 100644 homeassistant/components/nuki/translations/no.json create mode 100644 homeassistant/components/nuki/translations/pl.json create mode 100644 homeassistant/components/nuki/translations/ru.json create mode 100644 homeassistant/components/nuki/translations/tr.json create mode 100644 homeassistant/components/nuki/translations/zh-Hant.json create mode 100644 homeassistant/components/number/translations/ca.json create mode 100644 homeassistant/components/number/translations/cs.json create mode 100644 homeassistant/components/number/translations/en.json create mode 100644 homeassistant/components/number/translations/et.json create mode 100644 homeassistant/components/number/translations/it.json create mode 100644 homeassistant/components/number/translations/no.json create mode 100644 homeassistant/components/number/translations/pl.json create mode 100644 homeassistant/components/number/translations/ru.json create mode 100644 homeassistant/components/number/translations/tr.json create mode 100644 homeassistant/components/number/translations/zh-Hant.json create mode 100644 homeassistant/components/nut/translations/tr.json create mode 100644 homeassistant/components/nut/translations/uk.json create mode 100644 homeassistant/components/nws/translations/tr.json create mode 100644 homeassistant/components/nws/translations/uk.json create mode 100644 homeassistant/components/nzbget/translations/tr.json create mode 100644 homeassistant/components/nzbget/translations/uk.json create mode 100644 homeassistant/components/omnilogic/translations/tr.json create mode 100644 homeassistant/components/omnilogic/translations/uk.json create mode 100644 homeassistant/components/onboarding/translations/uk.json create mode 100644 homeassistant/components/ondilo_ico/translations/ca.json create mode 100644 homeassistant/components/ondilo_ico/translations/cs.json create mode 100644 homeassistant/components/ondilo_ico/translations/de.json create mode 100644 homeassistant/components/ondilo_ico/translations/es.json create mode 100644 homeassistant/components/ondilo_ico/translations/et.json create mode 100644 homeassistant/components/ondilo_ico/translations/it.json create mode 100644 homeassistant/components/ondilo_ico/translations/lb.json create mode 100644 homeassistant/components/ondilo_ico/translations/no.json create mode 100644 homeassistant/components/ondilo_ico/translations/pl.json create mode 100644 homeassistant/components/ondilo_ico/translations/ru.json create mode 100644 homeassistant/components/ondilo_ico/translations/tr.json create mode 100644 homeassistant/components/ondilo_ico/translations/uk.json create mode 100644 homeassistant/components/ondilo_ico/translations/zh-Hant.json create mode 100644 homeassistant/components/onewire/translations/tr.json create mode 100644 homeassistant/components/onewire/translations/uk.json create mode 100644 homeassistant/components/onvif/translations/uk.json create mode 100644 homeassistant/components/opentherm_gw/translations/tr.json create mode 100644 homeassistant/components/opentherm_gw/translations/uk.json create mode 100644 homeassistant/components/openuv/translations/tr.json create mode 100644 homeassistant/components/openweathermap/translations/tr.json create mode 100644 homeassistant/components/openweathermap/translations/uk.json create mode 100644 homeassistant/components/ovo_energy/translations/uk.json create mode 100644 homeassistant/components/owntracks/translations/tr.json create mode 100644 homeassistant/components/ozw/translations/uk.json create mode 100644 homeassistant/components/panasonic_viera/translations/tr.json create mode 100644 homeassistant/components/panasonic_viera/translations/uk.json create mode 100644 homeassistant/components/pi_hole/translations/tr.json create mode 100644 homeassistant/components/pi_hole/translations/uk.json create mode 100644 homeassistant/components/plaato/translations/tr.json create mode 100644 homeassistant/components/plaato/translations/uk.json create mode 100644 homeassistant/components/plex/translations/tr.json create mode 100644 homeassistant/components/plex/translations/uk.json create mode 100644 homeassistant/components/plugwise/translations/uk.json create mode 100644 homeassistant/components/plum_lightpad/translations/tr.json create mode 100644 homeassistant/components/plum_lightpad/translations/uk.json create mode 100644 homeassistant/components/point/translations/tr.json create mode 100644 homeassistant/components/point/translations/uk.json create mode 100644 homeassistant/components/poolsense/translations/tr.json create mode 100644 homeassistant/components/poolsense/translations/uk.json create mode 100644 homeassistant/components/powerwall/translations/tr.json create mode 100644 homeassistant/components/powerwall/translations/uk.json create mode 100644 homeassistant/components/profiler/translations/de.json create mode 100644 homeassistant/components/profiler/translations/tr.json create mode 100644 homeassistant/components/profiler/translations/uk.json create mode 100644 homeassistant/components/progettihwsw/translations/tr.json create mode 100644 homeassistant/components/progettihwsw/translations/uk.json create mode 100644 homeassistant/components/ps4/translations/tr.json create mode 100644 homeassistant/components/ps4/translations/uk.json create mode 100644 homeassistant/components/pvpc_hourly_pricing/translations/tr.json create mode 100644 homeassistant/components/pvpc_hourly_pricing/translations/uk.json create mode 100644 homeassistant/components/rachio/translations/tr.json create mode 100644 homeassistant/components/rachio/translations/uk.json create mode 100644 homeassistant/components/rainmachine/translations/uk.json create mode 100644 homeassistant/components/recollect_waste/translations/de.json create mode 100644 homeassistant/components/recollect_waste/translations/lb.json create mode 100644 homeassistant/components/recollect_waste/translations/tr.json create mode 100644 homeassistant/components/recollect_waste/translations/uk.json create mode 100644 homeassistant/components/rfxtrx/translations/tr.json create mode 100644 homeassistant/components/rfxtrx/translations/uk.json create mode 100644 homeassistant/components/ring/translations/tr.json create mode 100644 homeassistant/components/ring/translations/uk.json create mode 100644 homeassistant/components/risco/translations/tr.json create mode 100644 homeassistant/components/risco/translations/uk.json create mode 100644 homeassistant/components/roku/translations/tr.json create mode 100644 homeassistant/components/roku/translations/uk.json create mode 100644 homeassistant/components/roomba/translations/tr.json create mode 100644 homeassistant/components/roomba/translations/uk.json create mode 100644 homeassistant/components/roon/translations/tr.json create mode 100644 homeassistant/components/roon/translations/uk.json create mode 100644 homeassistant/components/rpi_power/translations/de.json create mode 100644 homeassistant/components/rpi_power/translations/tr.json create mode 100644 homeassistant/components/rpi_power/translations/uk.json create mode 100644 homeassistant/components/ruckus_unleashed/translations/tr.json create mode 100644 homeassistant/components/ruckus_unleashed/translations/uk.json create mode 100644 homeassistant/components/samsungtv/translations/uk.json create mode 100644 homeassistant/components/sense/translations/tr.json create mode 100644 homeassistant/components/sense/translations/uk.json create mode 100644 homeassistant/components/sentry/translations/tr.json create mode 100644 homeassistant/components/sentry/translations/uk.json create mode 100644 homeassistant/components/sharkiq/translations/tr.json create mode 100644 homeassistant/components/sharkiq/translations/uk.json create mode 100644 homeassistant/components/shelly/translations/da.json create mode 100644 homeassistant/components/shelly/translations/tr.json create mode 100644 homeassistant/components/shelly/translations/uk.json create mode 100644 homeassistant/components/shopping_list/translations/tr.json create mode 100644 homeassistant/components/shopping_list/translations/uk.json create mode 100644 homeassistant/components/smappee/translations/tr.json create mode 100644 homeassistant/components/smappee/translations/uk.json create mode 100644 homeassistant/components/smart_meter_texas/translations/tr.json create mode 100644 homeassistant/components/smart_meter_texas/translations/uk.json create mode 100644 homeassistant/components/smarthab/translations/tr.json create mode 100644 homeassistant/components/smarthab/translations/uk.json create mode 100644 homeassistant/components/smartthings/translations/tr.json create mode 100644 homeassistant/components/smartthings/translations/uk.json create mode 100644 homeassistant/components/smhi/translations/uk.json create mode 100644 homeassistant/components/sms/translations/tr.json create mode 100644 homeassistant/components/sms/translations/uk.json create mode 100644 homeassistant/components/solaredge/translations/uk.json create mode 100644 homeassistant/components/solarlog/translations/tr.json create mode 100644 homeassistant/components/solarlog/translations/uk.json create mode 100644 homeassistant/components/soma/translations/tr.json create mode 100644 homeassistant/components/soma/translations/uk.json create mode 100644 homeassistant/components/somfy/translations/tr.json create mode 100644 homeassistant/components/somfy/translations/uk.json create mode 100644 homeassistant/components/somfy_mylink/translations/ca.json create mode 100644 homeassistant/components/somfy_mylink/translations/cs.json create mode 100644 homeassistant/components/somfy_mylink/translations/de.json create mode 100644 homeassistant/components/somfy_mylink/translations/es.json create mode 100644 homeassistant/components/somfy_mylink/translations/et.json create mode 100644 homeassistant/components/somfy_mylink/translations/fr.json create mode 100644 homeassistant/components/somfy_mylink/translations/it.json create mode 100644 homeassistant/components/somfy_mylink/translations/lb.json create mode 100644 homeassistant/components/somfy_mylink/translations/no.json create mode 100644 homeassistant/components/somfy_mylink/translations/pl.json create mode 100644 homeassistant/components/somfy_mylink/translations/ru.json create mode 100644 homeassistant/components/somfy_mylink/translations/tr.json create mode 100644 homeassistant/components/somfy_mylink/translations/uk.json create mode 100644 homeassistant/components/somfy_mylink/translations/zh-Hant.json create mode 100644 homeassistant/components/sonarr/translations/tr.json create mode 100644 homeassistant/components/sonarr/translations/uk.json create mode 100644 homeassistant/components/songpal/translations/tr.json create mode 100644 homeassistant/components/songpal/translations/uk.json create mode 100644 homeassistant/components/sonos/translations/tr.json create mode 100644 homeassistant/components/sonos/translations/uk.json create mode 100644 homeassistant/components/speedtestdotnet/translations/tr.json create mode 100644 homeassistant/components/speedtestdotnet/translations/uk.json create mode 100644 homeassistant/components/spider/translations/tr.json create mode 100644 homeassistant/components/spider/translations/uk.json create mode 100644 homeassistant/components/spotify/translations/uk.json create mode 100644 homeassistant/components/squeezebox/translations/tr.json create mode 100644 homeassistant/components/squeezebox/translations/uk.json create mode 100644 homeassistant/components/srp_energy/translations/fr.json create mode 100644 homeassistant/components/srp_energy/translations/lb.json create mode 100644 homeassistant/components/srp_energy/translations/uk.json create mode 100644 homeassistant/components/starline/translations/tr.json create mode 100644 homeassistant/components/starline/translations/uk.json create mode 100644 homeassistant/components/syncthru/translations/tr.json create mode 100644 homeassistant/components/syncthru/translations/uk.json create mode 100644 homeassistant/components/synology_dsm/translations/uk.json create mode 100644 homeassistant/components/tado/translations/tr.json create mode 100644 homeassistant/components/tado/translations/uk.json create mode 100644 homeassistant/components/tag/translations/uk.json create mode 100644 homeassistant/components/tasmota/translations/de.json create mode 100644 homeassistant/components/tasmota/translations/tr.json create mode 100644 homeassistant/components/tasmota/translations/uk.json create mode 100644 homeassistant/components/tellduslive/translations/tr.json create mode 100644 homeassistant/components/tellduslive/translations/uk.json create mode 100644 homeassistant/components/tesla/translations/tr.json create mode 100644 homeassistant/components/tesla/translations/uk.json create mode 100644 homeassistant/components/tibber/translations/tr.json create mode 100644 homeassistant/components/tibber/translations/uk.json create mode 100644 homeassistant/components/tile/translations/tr.json create mode 100644 homeassistant/components/tile/translations/uk.json create mode 100644 homeassistant/components/toon/translations/tr.json create mode 100644 homeassistant/components/toon/translations/uk.json create mode 100644 homeassistant/components/totalconnect/translations/tr.json create mode 100644 homeassistant/components/totalconnect/translations/uk.json create mode 100644 homeassistant/components/tplink/translations/tr.json create mode 100644 homeassistant/components/tplink/translations/uk.json create mode 100644 homeassistant/components/traccar/translations/uk.json create mode 100644 homeassistant/components/tradfri/translations/tr.json create mode 100644 homeassistant/components/transmission/translations/tr.json create mode 100644 homeassistant/components/transmission/translations/uk.json create mode 100644 homeassistant/components/tuya/translations/uk.json create mode 100644 homeassistant/components/twentemilieu/translations/tr.json create mode 100644 homeassistant/components/twentemilieu/translations/uk.json create mode 100644 homeassistant/components/twilio/translations/tr.json create mode 100644 homeassistant/components/twilio/translations/uk.json create mode 100644 homeassistant/components/twinkly/translations/fr.json create mode 100644 homeassistant/components/twinkly/translations/lb.json create mode 100644 homeassistant/components/twinkly/translations/uk.json create mode 100644 homeassistant/components/unifi/translations/uk.json create mode 100644 homeassistant/components/upb/translations/tr.json create mode 100644 homeassistant/components/upb/translations/uk.json create mode 100644 homeassistant/components/upcloud/translations/tr.json create mode 100644 homeassistant/components/upcloud/translations/uk.json create mode 100644 homeassistant/components/upnp/translations/tr.json create mode 100644 homeassistant/components/velbus/translations/tr.json create mode 100644 homeassistant/components/velbus/translations/uk.json create mode 100644 homeassistant/components/vera/translations/tr.json create mode 100644 homeassistant/components/vera/translations/uk.json create mode 100644 homeassistant/components/vesync/translations/tr.json create mode 100644 homeassistant/components/vesync/translations/uk.json create mode 100644 homeassistant/components/vilfo/translations/tr.json create mode 100644 homeassistant/components/vilfo/translations/uk.json create mode 100644 homeassistant/components/vizio/translations/tr.json create mode 100644 homeassistant/components/vizio/translations/uk.json create mode 100644 homeassistant/components/volumio/translations/tr.json create mode 100644 homeassistant/components/water_heater/translations/uk.json create mode 100644 homeassistant/components/wemo/translations/uk.json create mode 100644 homeassistant/components/wiffi/translations/tr.json create mode 100644 homeassistant/components/wiffi/translations/uk.json create mode 100644 homeassistant/components/wilight/translations/tr.json create mode 100644 homeassistant/components/wilight/translations/uk.json create mode 100644 homeassistant/components/withings/translations/tr.json create mode 100644 homeassistant/components/withings/translations/uk.json create mode 100644 homeassistant/components/wled/translations/tr.json create mode 100644 homeassistant/components/wled/translations/uk.json create mode 100644 homeassistant/components/wolflink/translations/tr.json create mode 100644 homeassistant/components/xbox/translations/tr.json create mode 100644 homeassistant/components/xbox/translations/uk.json create mode 100644 homeassistant/components/xiaomi_aqara/translations/uk.json create mode 100644 homeassistant/components/xiaomi_miio/translations/tr.json create mode 100644 homeassistant/components/xiaomi_miio/translations/uk.json create mode 100644 homeassistant/components/yeelight/translations/tr.json create mode 100644 homeassistant/components/yeelight/translations/uk.json create mode 100644 homeassistant/components/zerproc/translations/uk.json create mode 100644 homeassistant/components/zha/translations/tr.json create mode 100644 homeassistant/components/zha/translations/uk.json create mode 100644 homeassistant/components/zodiac/translations/sensor.tr.json create mode 100644 homeassistant/components/zodiac/translations/sensor.uk.json create mode 100644 homeassistant/components/zone/translations/tr.json create mode 100644 homeassistant/components/zoneminder/translations/tr.json create mode 100644 homeassistant/components/zoneminder/translations/uk.json create mode 100644 homeassistant/components/zwave_js/translations/ca.json create mode 100644 homeassistant/components/zwave_js/translations/cs.json create mode 100644 homeassistant/components/zwave_js/translations/de.json create mode 100644 homeassistant/components/zwave_js/translations/es.json create mode 100644 homeassistant/components/zwave_js/translations/et.json create mode 100644 homeassistant/components/zwave_js/translations/fr.json create mode 100644 homeassistant/components/zwave_js/translations/it.json create mode 100644 homeassistant/components/zwave_js/translations/lb.json create mode 100644 homeassistant/components/zwave_js/translations/no.json create mode 100644 homeassistant/components/zwave_js/translations/pl.json create mode 100644 homeassistant/components/zwave_js/translations/pt-BR.json create mode 100644 homeassistant/components/zwave_js/translations/ru.json create mode 100644 homeassistant/components/zwave_js/translations/tr.json create mode 100644 homeassistant/components/zwave_js/translations/uk.json create mode 100644 homeassistant/components/zwave_js/translations/zh-Hant.json diff --git a/homeassistant/components/abode/translations/de.json b/homeassistant/components/abode/translations/de.json index 43d6ba21ca5..307f5f45065 100644 --- a/homeassistant/components/abode/translations/de.json +++ b/homeassistant/components/abode/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "reauth_successful": "Die erneute Authentifizierung war erfolgreich", - "single_instance_allowed": "Es ist nur eine einzige Konfiguration von Abode erlaubt." + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/abode/translations/es.json b/homeassistant/components/abode/translations/es.json index 9fa8cd8b06b..66cb5d13f22 100644 --- a/homeassistant/components/abode/translations/es.json +++ b/homeassistant/components/abode/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "La reautenticaci\u00f3n fue exitosa", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { @@ -19,7 +19,7 @@ "reauth_confirm": { "data": { "password": "Contrase\u00f1a", - "username": "Correo electronico" + "username": "Correo electr\u00f3nico" }, "title": "Rellene su informaci\u00f3n de inicio de sesi\u00f3n de Abode" }, diff --git a/homeassistant/components/abode/translations/fr.json b/homeassistant/components/abode/translations/fr.json index 87be79571a4..2ab158cca57 100644 --- a/homeassistant/components/abode/translations/fr.json +++ b/homeassistant/components/abode/translations/fr.json @@ -1,17 +1,32 @@ { "config": { "abort": { - "single_instance_allowed": "Une seule configuration d'Abode est autoris\u00e9e." + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "single_instance_allowed": "D\u00e9ja configur\u00e9. Une seule configuration possible." }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification invalide", + "invalid_mfa_code": "Code MFA non valide" }, "step": { + "mfa": { + "data": { + "mfa_code": "Code MFA (6 chiffres)" + }, + "title": "Entrez votre code MFA pour Abode" + }, + "reauth_confirm": { + "data": { + "password": "Mot de passe", + "username": "Email" + }, + "title": "Remplissez vos informations de connexion Abode" + }, "user": { "data": { "password": "Mot de passe", - "username": "Adresse e-mail" + "username": "Email" }, "title": "Remplissez vos informations de connexion Abode" } diff --git a/homeassistant/components/abode/translations/tr.json b/homeassistant/components/abode/translations/tr.json new file mode 100644 index 00000000000..d469e43f1f4 --- /dev/null +++ b/homeassistant/components/abode/translations/tr.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_mfa_code": "Ge\u00e7ersiz MFA kodu" + }, + "step": { + "mfa": { + "data": { + "mfa_code": "MFA kodu (6 basamakl\u0131)" + }, + "title": "Abode i\u00e7in MFA kodunuzu girin" + }, + "reauth_confirm": { + "data": { + "password": "Parola", + "username": "E-posta" + }, + "title": "Abode giri\u015f bilgilerinizi doldurun" + }, + "user": { + "data": { + "password": "Parola", + "username": "E-posta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/translations/uk.json b/homeassistant/components/abode/translations/uk.json new file mode 100644 index 00000000000..7ad57a0ec68 --- /dev/null +++ b/homeassistant/components/abode/translations/uk.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "invalid_mfa_code": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439 \u043a\u043e\u0434 MFA." + }, + "step": { + "mfa": { + "data": { + "mfa_code": "\u041a\u043e\u0434 MFA (6 \u0446\u0438\u0444\u0440)" + }, + "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043e\u0434 MFA \u0434\u043b\u044f Abode" + }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + }, + "title": "Abode" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + }, + "title": "Abode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/ca.json b/homeassistant/components/accuweather/translations/ca.json index 9c33637baa8..8178a5caef0 100644 --- a/homeassistant/components/accuweather/translations/ca.json +++ b/homeassistant/components/accuweather/translations/ca.json @@ -27,7 +27,7 @@ "data": { "forecast": "Previsi\u00f3 meteorol\u00f2gica" }, - "description": "Per culpa de les limitacions de la versi\u00f3 gratu\u00efta l'API d'AccuWeather, quan habilitis la previsi\u00f3 meteorol\u00f2gica, les actualitzacions es realitzaran cada 64 minuts en comptes de 32.", + "description": "Per culpa de les limitacions de la versi\u00f3 gratu\u00efta l'API d'AccuWeather, quan habilitis la previsi\u00f3 meteorol\u00f2gica, les actualitzacions de dades es faran cada 80 minuts en comptes de cada 40.", "title": "Opcions d'AccuWeather" } } diff --git a/homeassistant/components/accuweather/translations/cs.json b/homeassistant/components/accuweather/translations/cs.json index ea954b9f0db..1cf34a42695 100644 --- a/homeassistant/components/accuweather/translations/cs.json +++ b/homeassistant/components/accuweather/translations/cs.json @@ -27,7 +27,7 @@ "data": { "forecast": "P\u0159edpov\u011b\u010f po\u010das\u00ed" }, - "description": "Kdy\u017e povol\u00edte p\u0159edpov\u011b\u010f po\u010das\u00ed, budou aktualizace dat prov\u00e1d\u011bny ka\u017ed\u00fdch 64 minut nam\u00edsto 32 minut z d\u016fvodu omezen\u00ed bezplatn\u00e9 verze AccuWeather.", + "description": "Kdy\u017e povol\u00edte p\u0159edpov\u011b\u010f po\u010das\u00ed, budou aktualizace dat prov\u00e1d\u011bny ka\u017ed\u00fdch 80 minut nam\u00edsto 40 minut z d\u016fvodu omezen\u00ed bezplatn\u00e9 verze AccuWeather.", "title": "Mo\u017enosti AccuWeather" } } diff --git a/homeassistant/components/accuweather/translations/de.json b/homeassistant/components/accuweather/translations/de.json index fe0319764a7..814e57d1d6c 100644 --- a/homeassistant/components/accuweather/translations/de.json +++ b/homeassistant/components/accuweather/translations/de.json @@ -1,11 +1,16 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" }, "step": { "user": { "data": { + "api_key": "API-Schl\u00fcssel", "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad", "name": "Name" @@ -25,7 +30,7 @@ }, "system_health": { "info": { - "can_reach_server": "AccuWeather Server erreichen", + "can_reach_server": "AccuWeather-Server erreichen", "remaining_requests": "Verbleibende erlaubte Anfragen" } } diff --git a/homeassistant/components/accuweather/translations/en.json b/homeassistant/components/accuweather/translations/en.json index b737c420a2d..8f2261b93c7 100644 --- a/homeassistant/components/accuweather/translations/en.json +++ b/homeassistant/components/accuweather/translations/en.json @@ -27,7 +27,7 @@ "data": { "forecast": "Weather forecast" }, - "description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 64 minutes instead of every 32 minutes.", + "description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 80 minutes instead of every 40 minutes.", "title": "AccuWeather Options" } } diff --git a/homeassistant/components/accuweather/translations/et.json b/homeassistant/components/accuweather/translations/et.json index bed28b62975..6e2dc1ffd96 100644 --- a/homeassistant/components/accuweather/translations/et.json +++ b/homeassistant/components/accuweather/translations/et.json @@ -27,7 +27,7 @@ "data": { "forecast": "Ilmateade" }, - "description": "AccuWeather API tasuta versioonis toimub ilmaennustuse lubamisel andmete v\u00e4rskendamine iga 32 minuti asemel iga 64 minuti j\u00e4rel.", + "description": "AccuWeather API tasuta versioonis toimub ilmaennustuse lubamisel andmete v\u00e4rskendamine iga 80 minuti j\u00e4rel (muidu 40 minutit).", "title": "AccuWeatheri valikud" } } diff --git a/homeassistant/components/accuweather/translations/fr.json b/homeassistant/components/accuweather/translations/fr.json index 8e638205417..a083ed09bdf 100644 --- a/homeassistant/components/accuweather/translations/fr.json +++ b/homeassistant/components/accuweather/translations/fr.json @@ -34,7 +34,8 @@ }, "system_health": { "info": { - "can_reach_server": "Acc\u00e8s au serveur AccuWeather" + "can_reach_server": "Acc\u00e8s au serveur AccuWeather", + "remaining_requests": "Demandes restantes autoris\u00e9es" } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/it.json b/homeassistant/components/accuweather/translations/it.json index 86aaa213a15..8a1f9b96463 100644 --- a/homeassistant/components/accuweather/translations/it.json +++ b/homeassistant/components/accuweather/translations/it.json @@ -27,7 +27,7 @@ "data": { "forecast": "Previsioni meteo" }, - "description": "A causa delle limitazioni della versione gratuita della chiave API AccuWeather, quando si abilitano le previsioni del tempo, gli aggiornamenti dei dati verranno eseguiti ogni 64 minuti invece che ogni 32 minuti.", + "description": "A causa delle limitazioni della versione gratuita della chiave API AccuWeather, quando si abilitano le previsioni del tempo, gli aggiornamenti dei dati verranno eseguiti ogni 80 minuti invece che ogni 40.", "title": "Opzioni AccuWeather" } } diff --git a/homeassistant/components/accuweather/translations/no.json b/homeassistant/components/accuweather/translations/no.json index 50482cb3e61..be87b1ab244 100644 --- a/homeassistant/components/accuweather/translations/no.json +++ b/homeassistant/components/accuweather/translations/no.json @@ -27,7 +27,7 @@ "data": { "forecast": "V\u00e6rmelding" }, - "description": "P\u00e5 grunn av begrensningene i gratisversjonen av AccuWeather API-n\u00f8kkelen, n\u00e5r du aktiverer v\u00e6rmelding, vil dataoppdateringer bli utf\u00f8rt hvert 64. minutt i stedet for hvert 32. minutt.", + "description": "P\u00e5 grunn av begrensningene i den gratis versjonen av AccuWeather API-n\u00f8kkelen, vil dataoppdateringer utf\u00f8res hvert 80. minutt i stedet for hvert 40. minutt n\u00e5r du aktiverer v\u00e6rmelding.", "title": "AccuWeather-alternativer" } } diff --git a/homeassistant/components/accuweather/translations/pl.json b/homeassistant/components/accuweather/translations/pl.json index c6e4fb3ba82..2794bc8b7b6 100644 --- a/homeassistant/components/accuweather/translations/pl.json +++ b/homeassistant/components/accuweather/translations/pl.json @@ -27,7 +27,7 @@ "data": { "forecast": "Prognoza pogody" }, - "description": "Ze wzgl\u0119du na ograniczenia darmowej wersji klucza API AccuWeather po w\u0142\u0105czeniu prognozy pogody aktualizacje danych b\u0119d\u0105 wykonywane co 64 minuty zamiast co 32 minuty.", + "description": "Ze wzgl\u0119du na ograniczenia darmowej wersji klucza API AccuWeather po w\u0142\u0105czeniu prognozy pogody aktualizacje danych b\u0119d\u0105 wykonywane co 80 minut zamiast co 40 minut.", "title": "Opcje AccuWeather" } } diff --git a/homeassistant/components/accuweather/translations/ru.json b/homeassistant/components/accuweather/translations/ru.json index 6a675c17248..7bc767b1baf 100644 --- a/homeassistant/components/accuweather/translations/ru.json +++ b/homeassistant/components/accuweather/translations/ru.json @@ -27,7 +27,7 @@ "data": { "forecast": "\u041f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u044b" }, - "description": "\u0412 \u0441\u0432\u044f\u0437\u0438 \u0441 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f\u043c\u0438 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u043a\u043b\u044e\u0447\u0430 API AccuWeather, \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 \u043f\u043e\u0433\u043e\u0434\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442\u044c \u043a\u0430\u0436\u0434\u044b\u0435 64 \u043c\u0438\u043d\u0443\u0442\u044b, \u0430 \u043d\u0435 \u043a\u0430\u0436\u0434\u044b\u0435 32 \u043c\u0438\u043d\u0443\u0442\u044b.", + "description": "\u0412 \u0441\u0432\u044f\u0437\u0438 \u0441 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f\u043c\u0438 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u043a\u043b\u044e\u0447\u0430 API AccuWeather, \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 \u043f\u043e\u0433\u043e\u0434\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442\u044c \u043a\u0430\u0436\u0434\u044b\u0435 80 \u043c\u0438\u043d\u0443\u0442, \u0430 \u043d\u0435 \u043a\u0430\u0436\u0434\u044b\u0435 40 \u043c\u0438\u043d\u0443\u0442.", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AccuWeather" } } diff --git a/homeassistant/components/accuweather/translations/sensor.uk.json b/homeassistant/components/accuweather/translations/sensor.uk.json new file mode 100644 index 00000000000..81243e0b05d --- /dev/null +++ b/homeassistant/components/accuweather/translations/sensor.uk.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "\u0417\u043d\u0438\u0436\u0435\u043d\u043d\u044f", + "rising": "\u0417\u0440\u043e\u0441\u0442\u0430\u043d\u043d\u044f", + "steady": "\u0421\u0442\u0430\u0431\u0456\u043b\u044c\u043d\u0438\u0439" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/tr.json b/homeassistant/components/accuweather/translations/tr.json new file mode 100644 index 00000000000..f79f9a0e327 --- /dev/null +++ b/homeassistant/components/accuweather/translations/tr.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam", + "name": "Ad" + }, + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Hava Durumu tahmini" + }, + "title": "AccuWeather Se\u00e7enekleri" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "AccuWeather sunucusuna ula\u015f\u0131n", + "remaining_requests": "Kalan izin verilen istekler" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/uk.json b/homeassistant/components/accuweather/translations/uk.json index 8c3f282b350..7432d0df484 100644 --- a/homeassistant/components/accuweather/translations/uk.json +++ b/homeassistant/components/accuweather/translations/uk.json @@ -1,15 +1,22 @@ { "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, "error": { - "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API" + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API", + "requests_exceeded": "\u041f\u0435\u0440\u0435\u0432\u0438\u0449\u0435\u043d\u043e \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u0443 \u043a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u0437\u0430\u043f\u0438\u0442\u0456\u0432 \u0434\u043e API Accuweather. \u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u043f\u043e\u0447\u0435\u043a\u0430\u0442\u0438 \u0430\u0431\u043e \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u043a\u043b\u044e\u0447 API." }, "step": { "user": { "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", - "name": "\u041d\u0430\u0437\u0432\u0430 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457" + "name": "\u041d\u0430\u0437\u0432\u0430" }, + "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438, \u044f\u043a\u0449\u043e \u0412\u0430\u043c \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u0430 \u0437 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u043c:\n https://www.home-assistant.io/integrations/accuweather/ \n\n\u0417\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0434\u0435\u044f\u043a\u0456 \u0441\u0435\u043d\u0441\u043e\u0440\u0438 \u043f\u0440\u0438\u0445\u043e\u0432\u0430\u043d\u0456 \u0456 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 \u043f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u0438. \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0430\u043a\u0442\u0438\u0432\u0443\u0432\u0430\u0442\u0438 \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u043d\u044f \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u0438\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u0456\u0432 \u0432 \u0440\u0435\u0454\u0441\u0442\u0440\u0456 \u043e\u0431'\u0454\u043a\u0442\u0456\u0432 \u0456 \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u0438 \u0432 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u0445 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457.", "title": "AccuWeather" } } @@ -19,8 +26,16 @@ "user": { "data": { "forecast": "\u041f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u0438" - } + }, + "description": "\u0423 \u0437\u0432'\u044f\u0437\u043a\u0443 \u0437 \u043e\u0431\u043c\u0435\u0436\u0435\u043d\u043d\u044f\u043c\u0438 \u0431\u0435\u0437\u043a\u043e\u0448\u0442\u043e\u0432\u043d\u043e\u0457 \u0432\u0435\u0440\u0441\u0456\u0457 \u043a\u043b\u044e\u0447\u0430 API AccuWeather, \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u0456 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0443 \u043f\u043e\u0433\u043e\u0434\u0438 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0434\u0430\u043d\u0438\u0445 \u0431\u0443\u0434\u0435 \u0432\u0456\u0434\u0431\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u043a\u043e\u0436\u043d\u0456 64 \u0445\u0432\u0438\u043b\u0438\u043d\u0438, \u0430 \u043d\u0435 \u043a\u043e\u0436\u043d\u0456 32 \u0445\u0432\u0438\u043b\u0438\u043d\u0438.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AccuWeather" } } + }, + "system_health": { + "info": { + "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 AccuWeather", + "remaining_requests": "\u0417\u0430\u043f\u0438\u0442\u0456\u0432 \u0437\u0430\u043b\u0438\u0448\u0438\u043b\u043e\u0441\u044c" + } } } \ 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 ed5fa26f0c0..eb3729fd2c4 100644 --- a/homeassistant/components/accuweather/translations/zh-Hant.json +++ b/homeassistant/components/accuweather/translations/zh-Hant.json @@ -27,7 +27,7 @@ "data": { "forecast": "\u5929\u6c23\u9810\u5831" }, - "description": "\u7531\u65bc AccuWeather API \u5bc6\u9470\u514d\u8cbb\u7248\u672c\u9650\u5236\uff0c\u7576\u958b\u555f\u5929\u6c23\u9810\u5831\u6642\u3001\u6578\u64da\u6703\u6bcf 64 \u5206\u9418\u66f4\u65b0\u4e00\u6b21\uff0c\u800c\u975e 32 \u5206\u9418\u3002", + "description": "\u7531\u65bc AccuWeather API \u5bc6\u9470\u514d\u8cbb\u7248\u672c\u9650\u5236\uff0c\u7576\u958b\u555f\u5929\u6c23\u9810\u5831\u6642\u3001\u6578\u64da\u6703\u6bcf 80 \u5206\u9418\u66f4\u65b0\u4e00\u6b21\uff0c\u800c\u975e 40 \u5206\u9418\u3002", "title": "AccuWeather \u9078\u9805" } } diff --git a/homeassistant/components/acmeda/translations/de.json b/homeassistant/components/acmeda/translations/de.json index 86b22e47cda..94834cde427 100644 --- a/homeassistant/components/acmeda/translations/de.json +++ b/homeassistant/components/acmeda/translations/de.json @@ -1,11 +1,14 @@ { "config": { + "abort": { + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, "step": { "user": { "data": { "id": "Host-ID" }, - "title": "W\u00e4hlen Sie einen Hub zum Hinzuf\u00fcgen aus" + "title": "W\u00e4hle einen Hub zum Hinzuf\u00fcgen aus" } } } diff --git a/homeassistant/components/acmeda/translations/tr.json b/homeassistant/components/acmeda/translations/tr.json new file mode 100644 index 00000000000..aea81abdcba --- /dev/null +++ b/homeassistant/components/acmeda/translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "id": "Ana bilgisayar kimli\u011fi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/uk.json b/homeassistant/components/acmeda/translations/uk.json new file mode 100644 index 00000000000..245428e9c73 --- /dev/null +++ b/homeassistant/components/acmeda/translations/uk.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456." + }, + "step": { + "user": { + "data": { + "id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0445\u043e\u0441\u0442\u0430" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0445\u0430\u0431, \u044f\u043a\u0438\u0439 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0434\u043e\u0434\u0430\u0442\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/de.json b/homeassistant/components/adguard/translations/de.json index a02601759be..67746b3abcf 100644 --- a/homeassistant/components/adguard/translations/de.json +++ b/homeassistant/components/adguard/translations/de.json @@ -2,10 +2,10 @@ "config": { "abort": { "existing_instance_updated": "Bestehende Konfiguration wurde aktualisiert.", - "single_instance_allowed": "Es ist nur eine einzige Konfiguration von AdGuard Home zul\u00e4ssig." + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "hassio_confirm": { @@ -19,7 +19,7 @@ "port": "Port", "ssl": "AdGuard Home verwendet ein SSL-Zertifikat", "username": "Benutzername", - "verify_ssl": "AdGuard Home verwendet ein richtiges Zertifikat" + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, "description": "Richte deine AdGuard Home-Instanz ein um sie zu \u00dcberwachen und zu Steuern." } diff --git a/homeassistant/components/adguard/translations/no.json b/homeassistant/components/adguard/translations/no.json index f5aeea990c3..25046c8d38f 100644 --- a/homeassistant/components/adguard/translations/no.json +++ b/homeassistant/components/adguard/translations/no.json @@ -9,8 +9,8 @@ }, "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til \u00e5 koble til AdGuard Hjem gitt av hass.io tillegget {addon}?", - "title": "AdGuard Hjem via Hass.io tillegg" + "description": "Vil du konfigurere Home Assistant til \u00e5 koble til AdGuard Home gitt av Hass.io-tillegg {addon}?", + "title": "AdGuard Home via Hass.io-tillegg" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/ru.json b/homeassistant/components/adguard/translations/ru.json index 34c56342b5b..5e8483047f8 100644 --- a/homeassistant/components/adguard/translations/ru.json +++ b/homeassistant/components/adguard/translations/ru.json @@ -9,8 +9,8 @@ }, "step": { "hassio_confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a AdGuard Home (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io \"{addon}\")?", - "title": "AdGuard Home (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io)" + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a AdGuard Home (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant \"{addon}\")?", + "title": "AdGuard Home (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/tr.json b/homeassistant/components/adguard/translations/tr.json new file mode 100644 index 00000000000..26bef46408a --- /dev/null +++ b/homeassistant/components/adguard/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/uk.json b/homeassistant/components/adguard/translations/uk.json new file mode 100644 index 00000000000..8c24fb0a877 --- /dev/null +++ b/homeassistant/components/adguard/translations/uk.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "existing_instance_updated": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u0430.", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "hassio_confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e AdGuard Home (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io \"{addon}\")?", + "title": "AdGuard Home (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io)" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443 \u0456 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044e AdGuard Home." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/advantage_air/translations/de.json b/homeassistant/components/advantage_air/translations/de.json index 0d8a0052406..3b4066996eb 100644 --- a/homeassistant/components/advantage_air/translations/de.json +++ b/homeassistant/components/advantage_air/translations/de.json @@ -1,7 +1,10 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/advantage_air/translations/tr.json b/homeassistant/components/advantage_air/translations/tr.json new file mode 100644 index 00000000000..db639c59376 --- /dev/null +++ b/homeassistant/components/advantage_air/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "ip_address": "\u0130p Adresi", + "port": "Port" + }, + "title": "Ba\u011flan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/advantage_air/translations/uk.json b/homeassistant/components/advantage_air/translations/uk.json new file mode 100644 index 00000000000..14ac18395e2 --- /dev/null +++ b/homeassistant/components/advantage_air/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e API \u0412\u0430\u0448\u043e\u0433\u043e \u043d\u0430\u0441\u0442\u0456\u043d\u043d\u043e\u0433\u043e \u043f\u043b\u0430\u043d\u0448\u0435\u0442\u0430 Advantage Air.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/de.json b/homeassistant/components/agent_dvr/translations/de.json index 6ea40d0fd00..10a8307ada1 100644 --- a/homeassistant/components/agent_dvr/translations/de.json +++ b/homeassistant/components/agent_dvr/translations/de.json @@ -4,8 +4,8 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "already_in_progress": "Der Konfigurationsfluss f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt.", - "cannot_connect": "Verbindungsfehler" + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/agent_dvr/translations/tr.json b/homeassistant/components/agent_dvr/translations/tr.json new file mode 100644 index 00000000000..31dddab7795 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + }, + "title": "Agent DVR'\u0131 kurun" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/uk.json b/homeassistant/components/agent_dvr/translations/uk.json new file mode 100644 index 00000000000..fef8d45d5a4 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "Agent DVR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/translations/de.json b/homeassistant/components/airly/translations/de.json index 743a68a010e..8004444fdb9 100644 --- a/homeassistant/components/airly/translations/de.json +++ b/homeassistant/components/airly/translations/de.json @@ -1,9 +1,10 @@ { "config": { "abort": { - "already_configured": "Die Airly-Integration ist f\u00fcr diese Koordinaten bereits konfiguriert." + "already_configured": "Standort ist bereits konfiguriert" }, "error": { + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", "wrong_location": "Keine Airly Luftmessstation an diesem Ort" }, "step": { @@ -12,7 +13,7 @@ "api_key": "API-Schl\u00fcssel", "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad", - "name": "Name der Integration" + "name": "Name" }, "description": "Einrichtung der Airly-Luftqualit\u00e4t Integration. Um einen API-Schl\u00fcssel zu generieren, registriere dich auf https://developer.airly.eu/register", "title": "Airly" @@ -21,7 +22,7 @@ }, "system_health": { "info": { - "can_reach_server": "Airly Server erreichen" + "can_reach_server": "Airly-Server erreichen" } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/tr.json b/homeassistant/components/airly/translations/tr.json index 1b6e9caa24c..144acc1e1ae 100644 --- a/homeassistant/components/airly/translations/tr.json +++ b/homeassistant/components/airly/translations/tr.json @@ -1,4 +1,21 @@ { + "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam" + } + } + } + }, "system_health": { "info": { "can_reach_server": "Airly sunucusuna eri\u015fin" diff --git a/homeassistant/components/airly/translations/uk.json b/homeassistant/components/airly/translations/uk.json new file mode 100644 index 00000000000..51bcf5195df --- /dev/null +++ b/homeassistant/components/airly/translations/uk.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + }, + "error": { + "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API", + "wrong_location": "\u0423 \u0446\u0456\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u0456 \u043d\u0435\u043c\u0430\u0454 \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043b\u044c\u043d\u0438\u0445 \u0441\u0442\u0430\u043d\u0446\u0456\u0439 Airly." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f \u0441\u0435\u0440\u0432\u0456\u0441\u0443 \u0437 \u0430\u043d\u0430\u043b\u0456\u0437\u0443 \u044f\u043a\u043e\u0441\u0442\u0456 \u043f\u043e\u0432\u0456\u0442\u0440\u044f Airly. \u0429\u043e\u0431 \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0437\u0430 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c https://developer.airly.eu/register.", + "title": "Airly" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Airly" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/ca.json b/homeassistant/components/airnow/translations/ca.json new file mode 100644 index 00000000000..2db3cfad563 --- /dev/null +++ b/homeassistant/components/airnow/translations/ca.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_location": "No s'ha trobat cap resultat per a aquesta ubicaci\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API", + "latitude": "Latitud", + "longitude": "Longitud", + "radius": "Radi de l'estaci\u00f3 (milles; opcional)" + }, + "description": "Configura la integraci\u00f3 de qualitat d'aire AirNow. Per generar la clau API, v\u00e9s a https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/cs.json b/homeassistant/components/airnow/translations/cs.json new file mode 100644 index 00000000000..d978e44c70a --- /dev/null +++ b/homeassistant/components/airnow/translations/cs.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka" + }, + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/de.json b/homeassistant/components/airnow/translations/de.json new file mode 100644 index 00000000000..c98fc6d7415 --- /dev/null +++ b/homeassistant/components/airnow/translations/de.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_location": "F\u00fcr diesen Standort wurden keine Ergebnisse gefunden", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad" + }, + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/en.json b/homeassistant/components/airnow/translations/en.json index 5c5259c74e2..371bb270ac1 100644 --- a/homeassistant/components/airnow/translations/en.json +++ b/homeassistant/components/airnow/translations/en.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "Device is already configured" }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", "invalid_location": "No results found for that location", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "Unexpected error" }, "step": { "user": { @@ -15,7 +15,6 @@ "api_key": "API Key", "latitude": "Latitude", "longitude": "Longitude", - "name": "Name of the Entity", "radius": "Station Radius (miles; optional)" }, "description": "Set up AirNow air quality integration. To generate API key go to https://docs.airnowapi.org/account/request/", diff --git a/homeassistant/components/airnow/translations/es.json b/homeassistant/components/airnow/translations/es.json new file mode 100644 index 00000000000..d6a228a6e27 --- /dev/null +++ b/homeassistant/components/airnow/translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "invalid_location": "No se han encontrado resultados para esa ubicaci\u00f3n", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Clave API", + "latitude": "Latitud", + "longitude": "Longitud", + "radius": "Radio de la estaci\u00f3n (millas; opcional)" + }, + "description": "Configurar la integraci\u00f3n de calidad del aire de AirNow. Para generar una clave API, ve a https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/et.json b/homeassistant/components/airnow/translations/et.json new file mode 100644 index 00000000000..52b2bb618e0 --- /dev/null +++ b/homeassistant/components/airnow/translations/et.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine", + "invalid_location": "Selle asukoha jaoks ei leitud andmeid", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad", + "radius": "Jaama raadius (miilid; valikuline)" + }, + "description": "Seadista AirNow \u00f5hukvaliteedi sidumine. API-v\u00f5tme loomiseks mine aadressile https://docs.airnowapi.org/account/request/", + "title": "" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/fr.json b/homeassistant/components/airnow/translations/fr.json new file mode 100644 index 00000000000..ff85d9318e9 --- /dev/null +++ b/homeassistant/components/airnow/translations/fr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec \u00e0 la connexion", + "invalid_auth": "Authentification invalide", + "invalid_location": "Aucun r\u00e9sultat trouv\u00e9 pour cet emplacement", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 API", + "latitude": "Latitude", + "longitude": "Longitude", + "radius": "Rayon d'action de la station (en miles, facultatif)" + }, + "description": "Configurez l'int\u00e9gration de la qualit\u00e9 de l'air AirNow. Pour g\u00e9n\u00e9rer la cl\u00e9 API, acc\u00e9dez \u00e0 https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/it.json b/homeassistant/components/airnow/translations/it.json new file mode 100644 index 00000000000..9dda15dfbd2 --- /dev/null +++ b/homeassistant/components/airnow/translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "invalid_location": "Nessun risultato trovato per quella localit\u00e0", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "latitude": "Latitudine", + "longitude": "Logitudine", + "radius": "Raggio stazione (miglia; opzionale)" + }, + "description": "Configura l'integrazione per la qualit\u00e0 dell'aria AirNow. Per generare la chiave API, vai su https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/lb.json b/homeassistant/components/airnow/translations/lb.json new file mode 100644 index 00000000000..a62bd0bf478 --- /dev/null +++ b/homeassistant/components/airnow/translations/lb.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "invalid_location": "Keng Resultater fonnt fir d\u00ebse Standuert", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "api_key": "API Schl\u00ebssel", + "latitude": "L\u00e4ngegrad", + "longitude": "Breedegrag" + }, + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/no.json b/homeassistant/components/airnow/translations/no.json new file mode 100644 index 00000000000..19fa7e12207 --- /dev/null +++ b/homeassistant/components/airnow/translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "invalid_location": "Ingen resultater funnet for den plasseringen", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "radius": "Stasjonsradius (miles; valgfritt)" + }, + "description": "Konfigurer integrering av luftkvalitet i AirNow. For \u00e5 generere en API-n\u00f8kkel, g\u00e5r du til https://docs.airnowapi.org/account/request/", + "title": "" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/pl.json b/homeassistant/components/airnow/translations/pl.json new file mode 100644 index 00000000000..fe4310607b9 --- /dev/null +++ b/homeassistant/components/airnow/translations/pl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_location": "Brak wynik\u00f3w dla tej lokalizacji", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "radius": "Promie\u0144 od stacji (w milach; opcjonalnie)" + }, + "description": "Konfiguracja integracji jako\u015bci powietrza AirNow. Aby wygenerowa\u0107 klucz API, przejd\u017a do https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/pt.json b/homeassistant/components/airnow/translations/pt.json new file mode 100644 index 00000000000..3aa509dd6e8 --- /dev/null +++ b/homeassistant/components/airnow/translations/pt.json @@ -0,0 +1,23 @@ +{ + "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": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude" + }, + "title": "" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/ru.json b/homeassistant/components/airnow/translations/ru.json new file mode 100644 index 00000000000..650633cc816 --- /dev/null +++ b/homeassistant/components/airnow/translations/ru.json @@ -0,0 +1,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." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_location": "\u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u043e\u0432 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "radius": "\u0420\u0430\u0434\u0438\u0443\u0441 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 (\u0432 \u043c\u0438\u043b\u044f\u0445; \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" + }, + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u043f\u043e \u0430\u043d\u0430\u043b\u0438\u0437\u0443 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u0432\u043e\u0437\u0434\u0443\u0445\u0430 AirNow. \u0427\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 https://docs.airnowapi.org/account/request/.", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/tr.json b/homeassistant/components/airnow/translations/tr.json new file mode 100644 index 00000000000..06af714dc87 --- /dev/null +++ b/homeassistant/components/airnow/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_location": "Bu konum i\u00e7in hi\u00e7bir sonu\u00e7 bulunamad\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam", + "radius": "\u0130stasyon Yar\u0131\u00e7ap\u0131 (mil; iste\u011fe ba\u011fl\u0131)" + }, + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/uk.json b/homeassistant/components/airnow/translations/uk.json new file mode 100644 index 00000000000..bb872123f54 --- /dev/null +++ b/homeassistant/components/airnow/translations/uk.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "invalid_location": "\u041d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0456\u0432 \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", + "radius": "\u0420\u0430\u0434\u0456\u0443\u0441 \u0441\u0442\u0430\u043d\u0446\u0456\u0457 (\u043c\u0438\u043b\u0456; \u043d\u0435\u043e\u0431\u043e\u0432\u2019\u044f\u0437\u043a\u043e\u0432\u043e)" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e \u044f\u043a\u043e\u0441\u0442\u0456 \u043f\u043e\u0432\u0456\u0442\u0440\u044f AirNow. \u0429\u043e\u0431 \u0437\u0433\u0435\u043d\u0435\u0440\u0443\u0432\u0430\u0442\u0438 \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043d\u0430 \u0441\u0442\u043e\u0440\u0456\u043d\u043a\u0443 https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/zh-Hant.json b/homeassistant/components/airnow/translations/zh-Hant.json new file mode 100644 index 00000000000..0f6008e75a6 --- /dev/null +++ b/homeassistant/components/airnow/translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_location": "\u627e\u4e0d\u5230\u8a72\u4f4d\u7f6e\u7684\u7d50\u679c", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "radius": "\u89c0\u6e2c\u7ad9\u534a\u5f91\uff08\u82f1\u91cc\uff1b\u9078\u9805\uff09" + }, + "description": "\u6b32\u8a2d\u5b9a AirNow \u7a7a\u6c23\u54c1\u8cea\u6574\u5408\u3002\u8acb\u81f3 https://docs.airnowapi.org/account/request/ \u7522\u751f API \u5bc6\u9470", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/ar.json b/homeassistant/components/airvisual/translations/ar.json new file mode 100644 index 00000000000..771d88e8434 --- /dev/null +++ b/homeassistant/components/airvisual/translations/ar.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "geography_by_name": { + "data": { + "country": "\u0627\u0644\u062f\u0648\u0644\u0629" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/ca.json b/homeassistant/components/airvisual/translations/ca.json index d7a0ec2bd99..29df3dc7ca2 100644 --- a/homeassistant/components/airvisual/translations/ca.json +++ b/homeassistant/components/airvisual/translations/ca.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "general_error": "Error inesperat", - "invalid_api_key": "Clau API inv\u00e0lida" + "invalid_api_key": "Clau API inv\u00e0lida", + "location_not_found": "No s'ha trobat la ubicaci\u00f3" }, "step": { "geography": { @@ -17,7 +18,26 @@ "longitude": "Longitud" }, "description": "Utilitza l'API d'AirVisual per monitoritzar una ubicaci\u00f3 geogr\u00e0fica.", - "title": "Configuraci\u00f3 localitzaci\u00f3 geogr\u00e0fica" + "title": "Configura una ubicaci\u00f3 geogr\u00e0fica" + }, + "geography_by_coords": { + "data": { + "api_key": "Clau API", + "latitude": "Latitud", + "longitude": "Longitud" + }, + "description": "Utilitza l'API d'AirVisual per monitoritzar una latitud/longitud.", + "title": "Configura una ubicaci\u00f3 geogr\u00e0fica" + }, + "geography_by_name": { + "data": { + "api_key": "Clau API", + "city": "Ciutat", + "country": "Pa\u00eds", + "state": "Estat" + }, + "description": "Utilitza l'API d'AirVisual per monitoritzar un/a ciutat/estat/pa\u00eds", + "title": "Configura una ubicaci\u00f3 geogr\u00e0fica" }, "node_pro": { "data": { diff --git a/homeassistant/components/airvisual/translations/de.json b/homeassistant/components/airvisual/translations/de.json index 63012e23da1..a16b02915ee 100644 --- a/homeassistant/components/airvisual/translations/de.json +++ b/homeassistant/components/airvisual/translations/de.json @@ -1,12 +1,13 @@ { "config": { "abort": { - "already_configured": "Diese Koordinaten oder Node/Pro ID sind bereits registriert." + "already_configured": "Diese Koordinaten oder Node/Pro ID sind bereits registriert.", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { - "cannot_connect": "Verbindungsfehler", - "general_error": "Es gab einen unbekannten Fehler.", - "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel bereitgestellt." + "cannot_connect": "Verbindung fehlgeschlagen", + "general_error": "Unerwarteter Fehler", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" }, "step": { "geography": { @@ -19,7 +20,7 @@ }, "node_pro": { "data": { - "ip_address": "IP-Adresse/Hostname des Ger\u00e4ts", + "ip_address": "Host", "password": "Passwort" }, "description": "\u00dcberwachen Sie eine pers\u00f6nliche AirVisual-Einheit. Das Passwort kann von der Benutzeroberfl\u00e4che des Ger\u00e4ts abgerufen werden.", diff --git a/homeassistant/components/airvisual/translations/en.json b/homeassistant/components/airvisual/translations/en.json index 1e3cb59a520..64eb11f902c 100644 --- a/homeassistant/components/airvisual/translations/en.json +++ b/homeassistant/components/airvisual/translations/en.json @@ -11,6 +11,15 @@ "location_not_found": "Location not found" }, "step": { + "geography": { + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude" + }, + "description": "Use the AirVisual cloud API to monitor a geographical location.", + "title": "Configure a Geography" + }, "geography_by_coords": { "data": { "api_key": "API Key", @@ -45,6 +54,11 @@ "title": "Re-authenticate AirVisual" }, "user": { + "data": { + "cloud_api": "Geographical Location", + "node_pro": "AirVisual Node Pro", + "type": "Integration Type" + }, "description": "Pick what type of AirVisual data you want to monitor.", "title": "Configure AirVisual" } diff --git a/homeassistant/components/airvisual/translations/et.json b/homeassistant/components/airvisual/translations/et.json index 4bbf04817f9..9912dbce035 100644 --- a/homeassistant/components/airvisual/translations/et.json +++ b/homeassistant/components/airvisual/translations/et.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "\u00dchendamine nurjus", "general_error": "Tundmatu viga", - "invalid_api_key": "Vale API v\u00f5ti" + "invalid_api_key": "Vale API v\u00f5ti", + "location_not_found": "Asukohta ei leitud" }, "step": { "geography": { @@ -19,6 +20,25 @@ "description": "Kasutage AirVisual pilve API-t geograafilise asukoha j\u00e4lgimiseks.", "title": "Seadista Geography" }, + "geography_by_coords": { + "data": { + "api_key": "API v\u00f5ti", + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad" + }, + "description": "Kasuta AirVisual pilve API-t pikkus/laiuskraadi j\u00e4lgimiseks.", + "title": "Seadista Geography sidumine" + }, + "geography_by_name": { + "data": { + "api_key": "API v\u00f5ti", + "city": "Linn", + "country": "Riik", + "state": "olek" + }, + "description": "Kasuta AirVisual pilve API-t linna/osariigi/riigi j\u00e4lgimiseks.", + "title": "Seadista Geography sidumine" + }, "node_pro": { "data": { "ip_address": "\u00dcksuse IP-aadress / hostinimi", diff --git a/homeassistant/components/airvisual/translations/fr.json b/homeassistant/components/airvisual/translations/fr.json index 90857d826ea..d1a0d3d511a 100644 --- a/homeassistant/components/airvisual/translations/fr.json +++ b/homeassistant/components/airvisual/translations/fr.json @@ -6,8 +6,8 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "general_error": "Une erreur inconnue est survenue.", - "invalid_api_key": "La cl\u00e9 API fournie n'est pas valide." + "general_error": "Erreur inattendue", + "invalid_api_key": "Cl\u00e9 API invalide" }, "step": { "geography": { @@ -21,11 +21,11 @@ }, "node_pro": { "data": { - "ip_address": "Adresse IP / nom d'h\u00f4te de l'unit\u00e9", + "ip_address": "H\u00f4te", "password": "Mot de passe" }, - "description": "Surveillez une unit\u00e9 AirVisual personnelle. Le mot de passe peut \u00eatre r\u00e9cup\u00e9r\u00e9 dans l'interface utilisateur de l'unit\u00e9.", - "title": "Configurer un AirVisual Node/Pro" + "description": "Surveillez une unit\u00e9 personnelle AirVisual. Le mot de passe peut \u00eatre r\u00e9cup\u00e9r\u00e9 dans l'interface utilisateur de l'unit\u00e9.", + "title": "Configurer un noeud AirVisual Pro" }, "reauth_confirm": { "data": { diff --git a/homeassistant/components/airvisual/translations/no.json b/homeassistant/components/airvisual/translations/no.json index abf4a9f62e4..7c5b0333652 100644 --- a/homeassistant/components/airvisual/translations/no.json +++ b/homeassistant/components/airvisual/translations/no.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "Tilkobling mislyktes", "general_error": "Uventet feil", - "invalid_api_key": "Ugyldig API-n\u00f8kkel" + "invalid_api_key": "Ugyldig API-n\u00f8kkel", + "location_not_found": "Stedet ble ikke funnet" }, "step": { "geography": { @@ -19,6 +20,25 @@ "description": "Bruk AirVisual cloud API til \u00e5 overv\u00e5ke en geografisk plassering.", "title": "Konfigurer en Geography" }, + "geography_by_coords": { + "data": { + "api_key": "API-n\u00f8kkel", + "latitude": "Breddegrad", + "longitude": "Lengdegrad" + }, + "description": "Bruk AirVisual cloud API til \u00e5 overv\u00e5ke en breddegrad/lengdegrad.", + "title": "Konfigurer en Geography" + }, + "geography_by_name": { + "data": { + "api_key": "API-n\u00f8kkel", + "city": "By", + "country": "Land", + "state": "stat" + }, + "description": "Bruk AirVisual cloud API til \u00e5 overv\u00e5ke en by/stat/land.", + "title": "Konfigurer en Geography" + }, "node_pro": { "data": { "ip_address": "Vert", diff --git a/homeassistant/components/airvisual/translations/pl.json b/homeassistant/components/airvisual/translations/pl.json index 10af1fc2ee0..5590a951641 100644 --- a/homeassistant/components/airvisual/translations/pl.json +++ b/homeassistant/components/airvisual/translations/pl.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "general_error": "Nieoczekiwany b\u0142\u0105d", - "invalid_api_key": "Nieprawid\u0142owy klucz API" + "invalid_api_key": "Nieprawid\u0142owy klucz API", + "location_not_found": "Nie znaleziono lokalizacji" }, "step": { "geography": { @@ -19,6 +20,25 @@ "description": "U\u017cyj interfejsu API chmury AirVisual do monitorowania lokalizacji geograficznej.", "title": "Konfiguracja Geography" }, + "geography_by_coords": { + "data": { + "api_key": "Klucz API", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna" + }, + "description": "U\u017cyj API chmury AirVisual do monitorowania szeroko\u015bci/d\u0142ugo\u015bci geograficznej.", + "title": "Konfiguracja Geography" + }, + "geography_by_name": { + "data": { + "api_key": "Klucz API", + "city": "Miasto", + "country": "Kraj", + "state": "Stan" + }, + "description": "U\u017cyj API chmury AirVisual do monitorowania miasta/stanu/kraju.", + "title": "Konfiguracja Geography" + }, "node_pro": { "data": { "ip_address": "Nazwa hosta lub adres IP", diff --git a/homeassistant/components/airvisual/translations/sv.json b/homeassistant/components/airvisual/translations/sv.json index 4c4e1271d72..f375b4fc598 100644 --- a/homeassistant/components/airvisual/translations/sv.json +++ b/homeassistant/components/airvisual/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "error": { - "general_error": "Ett ok\u00e4nt fel intr\u00e4ffade." + "general_error": "Ett ok\u00e4nt fel intr\u00e4ffade.", + "invalid_api_key": "Ogiltig API-nyckel" }, "step": { "geography": { diff --git a/homeassistant/components/airvisual/translations/tr.json b/homeassistant/components/airvisual/translations/tr.json new file mode 100644 index 00000000000..3d20c8ea9fc --- /dev/null +++ b/homeassistant/components/airvisual/translations/tr.json @@ -0,0 +1,59 @@ +{ + "config": { + "abort": { + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "general_error": "Beklenmeyen hata", + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", + "location_not_found": "Konum bulunamad\u0131" + }, + "step": { + "geography": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam" + } + }, + "geography_by_coords": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam" + }, + "description": "Bir enlem / boylam\u0131 izlemek i\u00e7in AirVisual bulut API'sini kullan\u0131n.", + "title": "Bir Co\u011frafyay\u0131 Yap\u0131land\u0131rma" + }, + "geography_by_name": { + "data": { + "api_key": "API Anahtar\u0131", + "city": "\u015eehir", + "country": "\u00dclke", + "state": "durum" + }, + "title": "Bir Co\u011frafyay\u0131 Yap\u0131land\u0131rma" + }, + "node_pro": { + "data": { + "ip_address": "Ana Bilgisayar", + "password": "Parola" + }, + "description": "Ki\u015fisel bir AirVisual \u00fcnitesini izleyin. Parola, \u00fcnitenin kullan\u0131c\u0131 aray\u00fcz\u00fcnden al\u0131nabilir." + }, + "reauth_confirm": { + "data": { + "api_key": "API Anahtar\u0131" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "AirVisual'\u0131 yap\u0131land\u0131r\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/uk.json b/homeassistant/components/airvisual/translations/uk.json new file mode 100644 index 00000000000..d99c58de7c0 --- /dev/null +++ b/homeassistant/components/airvisual/translations/uk.json @@ -0,0 +1,57 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435. \u0410\u0431\u043e \u0446\u0435\u0439 Node / Pro ID \u0432\u0436\u0435 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u043e\u0432\u0430\u043d\u0438\u0439.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "general_error": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430", + "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API" + }, + "step": { + "geography": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430" + }, + "description": "\u041c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433 \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0445\u043c\u0430\u0440\u043d\u043e\u0433\u043e API AirVisual.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f" + }, + "node_pro": { + "data": { + "ip_address": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e AirVisual. \u041f\u0430\u0440\u043e\u043b\u044c \u043c\u043e\u0436\u043d\u0430 \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AirVisual Node / Pro" + }, + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0444\u0456\u043b\u044e" + }, + "user": { + "data": { + "cloud_api": "\u041c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f", + "node_pro": "AirVisual Node Pro", + "type": "\u0422\u0438\u043f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0438\u043f \u0434\u0430\u043d\u0438\u0445 AirVisual, \u044f\u043a\u0438\u0439 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0443\u0432\u0430\u0442\u0438.", + "title": "AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0443\u0432\u0430\u043d\u0443 \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u043d\u0430 \u043a\u0430\u0440\u0442\u0456" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AirVisual" + } + } + } +} \ 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 4bdc2959047..3767d41b519 100644 --- a/homeassistant/components/airvisual/translations/zh-Hant.json +++ b/homeassistant/components/airvisual/translations/zh-Hant.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "general_error": "\u672a\u9810\u671f\u932f\u8aa4", - "invalid_api_key": "API \u5bc6\u9470\u7121\u6548" + "invalid_api_key": "API \u5bc6\u9470\u7121\u6548", + "location_not_found": "\u627e\u4e0d\u5230\u5730\u9ede" }, "step": { "geography": { @@ -19,6 +20,25 @@ "description": "\u4f7f\u7528 AirVisual \u96f2\u7aef API \u4ee5\u76e3\u63a7\u5730\u7406\u5ea7\u6a19\u3002", "title": "\u8a2d\u5b9a\u5730\u7406\u5ea7\u6a19" }, + "geography_by_coords": { + "data": { + "api_key": "API \u5bc6\u9470", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6" + }, + "description": "\u4f7f\u7528 AirVisual \u96f2\u7aef API \u4ee5\u76e3\u63a7\u7d93\u5ea6/\u7def\u5ea6\u3002", + "title": "\u8a2d\u5b9a\u5730\u7406\u5ea7\u6a19" + }, + "geography_by_name": { + "data": { + "api_key": "API \u5bc6\u9470", + "city": "\u57ce\u5e02", + "country": "\u570b\u5bb6", + "state": "\u5dde" + }, + "description": "\u4f7f\u7528 AirVisual \u96f2\u7aef API \u4ee5\u76e3\u63a7\u57ce\u5e02/\u5dde/\u570b\u5bb6\u3002", + "title": "\u8a2d\u5b9a\u5730\u7406\u5ea7\u6a19" + }, "node_pro": { "data": { "ip_address": "\u4e3b\u6a5f\u7aef", diff --git a/homeassistant/components/alarm_control_panel/translations/tr.json b/homeassistant/components/alarm_control_panel/translations/tr.json index ebbcf568338..cc509430436 100644 --- a/homeassistant/components/alarm_control_panel/translations/tr.json +++ b/homeassistant/components/alarm_control_panel/translations/tr.json @@ -1,4 +1,10 @@ { + "device_automation": { + "trigger_type": { + "disarmed": "{entity_name} b\u0131rak\u0131ld\u0131", + "triggered": "{entity_name} tetiklendi" + } + }, "state": { "_": { "armed": "Etkin", diff --git a/homeassistant/components/alarm_control_panel/translations/uk.json b/homeassistant/components/alarm_control_panel/translations/uk.json index e618e297019..b50fd9f459d 100644 --- a/homeassistant/components/alarm_control_panel/translations/uk.json +++ b/homeassistant/components/alarm_control_panel/translations/uk.json @@ -1,13 +1,36 @@ { + "device_automation": { + "action_type": { + "arm_away": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0435 \u0432\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "arm_home": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u0412\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "arm_night": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0456\u0447\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "disarm": "\u0412\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043e\u0445\u043e\u0440\u043e\u043d\u0443 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "trigger": "{entity_name} \u0441\u043f\u0440\u0430\u0446\u044c\u043e\u0432\u0443\u0454" + }, + "condition_type": { + "is_armed_away": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0435 \u0432\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "is_armed_home": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u0412\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "is_armed_night": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0456\u0447\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "is_disarmed": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0430 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "is_triggered": "{entity_name} \u0441\u043f\u0440\u0430\u0446\u044c\u043e\u0432\u0443\u0454" + }, + "trigger_type": { + "armed_away": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0435 \u0432\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "armed_home": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u0412\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "armed_night": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0456\u0447\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "disarmed": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0430 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "triggered": "{entity_name} \u0441\u043f\u0440\u0430\u0446\u044c\u043e\u0432\u0443\u0454" + } + }, "state": { "_": { "armed": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430", - "armed_away": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (\u043d\u0435 \u0432\u0434\u043e\u043c\u0430)", + "armed_away": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (\u041d\u0435 \u0432\u0434\u043e\u043c\u0430)", "armed_custom_bypass": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 \u0437 \u0432\u0438\u043d\u044f\u0442\u043a\u0430\u043c\u0438", - "armed_home": "\u0411\u0443\u0434\u0438\u043d\u043a\u043e\u0432\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0430", + "armed_home": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (\u0412\u0434\u043e\u043c\u0430)", "armed_night": "\u041d\u0456\u0447\u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0430", "arming": "\u0421\u0442\u0430\u0432\u043b\u044e \u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0443", - "disarmed": "\u0417\u043d\u044f\u0442\u043e", + "disarmed": "\u0417\u043d\u044f\u0442\u043e \u0437 \u043e\u0445\u043e\u0440\u043e\u043d\u0438", "disarming": "\u0417\u043d\u044f\u0442\u0442\u044f", "pending": "\u041e\u0447\u0456\u043a\u0443\u044e", "triggered": "\u0422\u0440\u0438\u0432\u043e\u0433\u0430" diff --git a/homeassistant/components/alarmdecoder/translations/de.json b/homeassistant/components/alarmdecoder/translations/de.json index 3f1b7ef816e..c37fb7b4390 100644 --- a/homeassistant/components/alarmdecoder/translations/de.json +++ b/homeassistant/components/alarmdecoder/translations/de.json @@ -1,7 +1,10 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "protocol": { @@ -42,7 +45,7 @@ "data": { "zone_number": "Zonennummer" }, - "description": "Geben Sie die Zonennummer ein, die Sie hinzuf\u00fcgen, bearbeiten oder entfernen m\u00f6chten." + "description": "Gib die die Zonennummer ein, die du hinzuf\u00fcgen, bearbeiten oder entfernen m\u00f6chtest." } } } diff --git a/homeassistant/components/alarmdecoder/translations/tr.json b/homeassistant/components/alarmdecoder/translations/tr.json new file mode 100644 index 00000000000..276b733b31f --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/tr.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "protocol": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + } + } + }, + "options": { + "error": { + "relay_inclusive": "R\u00f6le Adresi ve R\u00f6le Kanal\u0131 birbirine ba\u011fl\u0131d\u0131r ve birlikte eklenmelidir." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Alternatif Gece Modu" + } + }, + "init": { + "data": { + "edit_select": "D\u00fczenle" + } + }, + "zone_details": { + "data": { + "zone_name": "B\u00f6lge Ad\u0131", + "zone_relayaddr": "R\u00f6le Adresi", + "zone_relaychan": "R\u00f6le Kanal\u0131" + } + }, + "zone_select": { + "data": { + "zone_number": "B\u00f6lge Numaras\u0131" + }, + "title": "AlarmDecoder'\u0131 yap\u0131land\u0131r\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/uk.json b/homeassistant/components/alarmdecoder/translations/uk.json new file mode 100644 index 00000000000..c19d00c0eca --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/uk.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0456\u0448\u043d\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e AlarmDecoder." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "\u0428\u0432\u0438\u0434\u043a\u0456\u0441\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0456 \u0434\u0430\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", + "device_path": "\u0428\u043b\u044f\u0445 \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + }, + "user": { + "data": { + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b AlarmDecoder" + } + } + }, + "options": { + "error": { + "int": "\u041f\u043e\u043b\u0435 \u043d\u0438\u0436\u0447\u0435 \u043c\u0430\u0454 \u0431\u0443\u0442\u0438 \u0446\u0456\u043b\u0438\u043c \u0447\u0438\u0441\u043b\u043e\u043c.", + "loop_range": "RF Loop \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0446\u0456\u043b\u0438\u043c \u0447\u0438\u0441\u043b\u043e\u043c \u0432\u0456\u0434 1 \u0434\u043e 4.", + "loop_rfid": "RF Loop \u043d\u0435 \u043c\u043e\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0431\u0435\u0437 RF Serial.", + "relay_inclusive": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0440\u0435\u043b\u0435 \u0456 \u043a\u0430\u043d\u0430\u043b \u0440\u0435\u043b\u0435 \u0432\u0437\u0430\u0454\u043c\u043e\u0437\u0430\u043b\u0435\u0436\u043d\u0456 \u0456 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0431\u0443\u0442\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0456 \u0440\u0430\u0437\u043e\u043c." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u043d\u0456\u0447\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c", + "auto_bypass": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0438\u0439 \u0432\u043a\u043b\u044e\u0447\u0430\u0442\u0438 \u0432\u0438\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438 \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u0446\u0456 \u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0443", + "code_arm_required": "\u041a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0438\u0439 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0443" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "\u0417\u043c\u0456\u043d\u0438\u0442\u0438" + }, + "description": "\u0429\u043e \u0431 \u0412\u0438 \u0445\u043e\u0442\u0456\u043b\u0438 \u0440\u0435\u0434\u0430\u0433\u0443\u0432\u0430\u0442\u0438?", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "RF Loop", + "zone_name": "\u041d\u0430\u0437\u0432\u0430 \u0437\u043e\u043d\u0438", + "zone_relayaddr": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0440\u0435\u043b\u0435", + "zone_relaychan": "\u041a\u0430\u043d\u0430\u043b \u0440\u0435\u043b\u0435", + "zone_rfid": "RF Serial", + "zone_type": "\u0422\u0438\u043f \u0437\u043e\u043d\u0438" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0434\u0430\u043d\u0456 \u0434\u043b\u044f \u0437\u043e\u043d\u0438 {zone_number}. \u0429\u043e\u0431 \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u0437\u043e\u043d\u0443 {zone_number}, \u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u043b\u0435 \"\u041d\u0430\u0437\u0432\u0430 \u0437\u043e\u043d\u0438\" \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "\u041d\u043e\u043c\u0435\u0440 \u0437\u043e\u043d\u0438" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043d\u043e\u043c\u0435\u0440 \u0437\u043e\u043d\u0438, \u044f\u043a\u0443 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438, \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u0430\u0431\u043e \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/de.json b/homeassistant/components/almond/translations/de.json index e3a61026774..5eb8c4940aa 100644 --- a/homeassistant/components/almond/translations/de.json +++ b/homeassistant/components/almond/translations/de.json @@ -1,8 +1,10 @@ { "config": { "abort": { - "cannot_connect": "Verbindung zum Almond-Server nicht m\u00f6glich.", - "missing_configuration": "Bitte \u00fcberpr\u00fcfe die Dokumentation zur Einrichtung von Almond." + "cannot_connect": "Verbindung fehlgeschlagen", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/almond/translations/no.json b/homeassistant/components/almond/translations/no.json index 1b0f03b8018..9cd22ca5bc5 100644 --- a/homeassistant/components/almond/translations/no.json +++ b/homeassistant/components/almond/translations/no.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til \u00e5 koble til Almond levert av Hass.io tillegget: {addon}?", - "title": "Almond via Hass.io tillegg" + "description": "Vil du konfigurere Home Assistant til \u00e5 koble til Almond levert av Hass.io-tillegg: {addon}?", + "title": "Almond via Hass.io-tillegg" }, "pick_implementation": { "title": "Velg godkjenningsmetode" diff --git a/homeassistant/components/almond/translations/ru.json b/homeassistant/components/almond/translations/ru.json index 27870a46e95..e671651f65d 100644 --- a/homeassistant/components/almond/translations/ru.json +++ b/homeassistant/components/almond/translations/ru.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io \"{addon}\")?", - "title": "Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io)" + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant \"{addon}\")?", + "title": "Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)" }, "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" diff --git a/homeassistant/components/almond/translations/tr.json b/homeassistant/components/almond/translations/tr.json new file mode 100644 index 00000000000..dc270099fcd --- /dev/null +++ b/homeassistant/components/almond/translations/tr.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/uk.json b/homeassistant/components/almond/translations/uk.json new file mode 100644 index 00000000000..7f8c12917bb --- /dev/null +++ b/homeassistant/components/almond/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "step": { + "hassio_confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e Almond (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io \"{addon}\")?", + "title": "Almond (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io)" + }, + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/translations/de.json b/homeassistant/components/ambiclimate/translations/de.json index e5988f76103..d91fc15f37d 100644 --- a/homeassistant/components/ambiclimate/translations/de.json +++ b/homeassistant/components/ambiclimate/translations/de.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "access_token": "Unbekannter Fehler beim Generieren eines Zugriffstokens." + "access_token": "Unbekannter Fehler beim Generieren eines Zugriffstokens.", + "already_configured": "Konto wurde bereits konfiguriert", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen." }, "create_entry": { "default": "Erfolgreiche Authentifizierung mit Ambiclimate" diff --git a/homeassistant/components/ambiclimate/translations/fr.json b/homeassistant/components/ambiclimate/translations/fr.json index bdbfaea20ef..37ef9549686 100644 --- a/homeassistant/components/ambiclimate/translations/fr.json +++ b/homeassistant/components/ambiclimate/translations/fr.json @@ -2,7 +2,7 @@ "config": { "abort": { "access_token": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'un jeton d'acc\u00e8s.", - "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9", + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation." }, "create_entry": { diff --git a/homeassistant/components/ambiclimate/translations/tr.json b/homeassistant/components/ambiclimate/translations/tr.json new file mode 100644 index 00000000000..bcaeba84558 --- /dev/null +++ b/homeassistant/components/ambiclimate/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/translations/uk.json b/homeassistant/components/ambiclimate/translations/uk.json new file mode 100644 index 00000000000..398665ab667 --- /dev/null +++ b/homeassistant/components/ambiclimate/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "access_token": "\u041f\u0440\u0438 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u0456 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u0441\u0442\u0430\u043b\u0430\u0441\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430.", + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438." + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "error": { + "follow_link": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0437\u0430 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c \u0456 \u043f\u0440\u043e\u0439\u0434\u0456\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e, \u043f\u0435\u0440\u0448 \u043d\u0456\u0436 \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0438 \"\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438\".", + "no_token": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043d\u0435 \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430." + }, + "step": { + "auth": { + "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043f\u043e [\u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c]({authorization_url}) \u0456 ** \u0414\u043e\u0437\u0432\u043e\u043b\u044c\u0442\u0435 ** \u0434\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0432\u0430\u0448\u043e\u0433\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 Ambi Climate, \u043f\u043e\u0442\u0456\u043c \u043f\u043e\u0432\u0435\u0440\u043d\u0456\u0442\u044c\u0441\u044f \u0441\u044e\u0434\u0438 \u0456 \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **.\n(\u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0432\u043a\u0430\u0437\u0430\u043d\u0438\u0439 URL \u0437\u0432\u043e\u0440\u043e\u0442\u043d\u043e\u0433\u043e \u0432\u0438\u043a\u043b\u0438\u043a\u0443 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u0430\u0454 {cb_url} )", + "title": "Ambi Climate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/translations/de.json b/homeassistant/components/ambient_station/translations/de.json index 53e6b1f69d6..c6570fee0e3 100644 --- a/homeassistant/components/ambient_station/translations/de.json +++ b/homeassistant/components/ambient_station/translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Dieser App-Schl\u00fcssel wird bereits verwendet." + "already_configured": "Der Dienst ist bereits konfiguriert" }, "error": { - "invalid_key": "Ung\u00fcltiger API Key und / oder Anwendungsschl\u00fcssel", + "invalid_key": "Ung\u00fcltiger API-Schl\u00fcssel", "no_devices": "Keine Ger\u00e4te im Konto gefunden" }, "step": { diff --git a/homeassistant/components/ambient_station/translations/tr.json b/homeassistant/components/ambient_station/translations/tr.json new file mode 100644 index 00000000000..908d97f5758 --- /dev/null +++ b/homeassistant/components/ambient_station/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_key": "Ge\u00e7ersiz API anahtar\u0131" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/translations/uk.json b/homeassistant/components/ambient_station/translations/uk.json new file mode 100644 index 00000000000..722cf99af7e --- /dev/null +++ b/homeassistant/components/ambient_station/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "invalid_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API", + "no_devices": "\u0412 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441\u0456 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "app_key": "\u041a\u043b\u044e\u0447 \u0434\u043e\u0434\u0430\u0442\u043a\u0443" + }, + "title": "Ambient PWS" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/fr.json b/homeassistant/components/apple_tv/translations/fr.json index a55d37ed588..e1a719b31c9 100644 --- a/homeassistant/components/apple_tv/translations/fr.json +++ b/homeassistant/components/apple_tv/translations/fr.json @@ -1,7 +1,17 @@ { "config": { + "abort": { + "already_configured_device": "Le p\u00e9riph\u00e9rique est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "backoff": "L'appareil n'accepte pas les demandes d'appariement pour le moment (vous avez peut-\u00eatre saisi un code PIN non valide trop de fois), r\u00e9essayez plus tard.", + "device_did_not_pair": "Aucune tentative pour terminer l'appairage n'a \u00e9t\u00e9 effectu\u00e9e \u00e0 partir de l'appareil.", + "invalid_config": "La configuration de cet appareil est incompl\u00e8te. Veuillez r\u00e9essayer de l'ajouter.", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "unknown": "Erreur inattendue" + }, "error": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "invalid_auth": "Autentification invalide", "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" @@ -19,9 +29,12 @@ "pair_with_pin": { "data": { "pin": "Code PIN" - } + }, + "description": "L'appairage est requis pour le protocole `{protocol}`. Veuillez saisir le code PIN affich\u00e9 \u00e0 l'\u00e9cran. Les z\u00e9ros doivent \u00eatre omis, c'est-\u00e0-dire entrer 123 si le code affich\u00e9 est 0123.", + "title": "Appairage" }, "reconfigure": { + "description": "Cette Apple TV rencontre des difficult\u00e9s de connexion et doit \u00eatre reconfigur\u00e9e.", "title": "Reconfiguration de l'appareil" }, "service_problem": { diff --git a/homeassistant/components/apple_tv/translations/lb.json b/homeassistant/components/apple_tv/translations/lb.json index 945f467c4cf..2354033b577 100644 --- a/homeassistant/components/apple_tv/translations/lb.json +++ b/homeassistant/components/apple_tv/translations/lb.json @@ -3,9 +3,14 @@ "abort": { "already_configured_device": "Apparat ass scho konfigur\u00e9iert", "already_in_progress": "Konfiguratioun's Oflaf ass schon am gaang", + "invalid_config": "Konfiguratioun fir d\u00ebsen Apparat ass net komplett. Prob\u00e9ier fir et nach emol dob\u00e4i ze setzen.", + "no_devices_found": "Keng Apparater am Netzwierk fonnt", "unknown": "Onerwaarte Feeler" }, "error": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "no_devices_found": "Keng Apparater am Netzwierk fonnt", "unknown": "Onerwaarte Feeler" }, "flow_title": "Apple TV: {name}", @@ -29,6 +34,9 @@ "description": "D\u00ebsen Apple TV huet e puer Verbindungsschwieregkeeten a muss nei konfigur\u00e9iert ginn.", "title": "Apparat Rekonfiguratioun" }, + "service_problem": { + "title": "Feeler beim dob\u00e4isetze vum Service" + }, "user": { "data": { "device_input": "Apparat" diff --git a/homeassistant/components/apple_tv/translations/tr.json b/homeassistant/components/apple_tv/translations/tr.json index 0ddc466a6f7..f33e3998af6 100644 --- a/homeassistant/components/apple_tv/translations/tr.json +++ b/homeassistant/components/apple_tv/translations/tr.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "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" diff --git a/homeassistant/components/apple_tv/translations/uk.json b/homeassistant/components/apple_tv/translations/uk.json new file mode 100644 index 00000000000..a1ae2259ada --- /dev/null +++ b/homeassistant/components/apple_tv/translations/uk.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "backoff": "\u0412 \u0434\u0430\u043d\u0438\u0439 \u0447\u0430\u0441 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0440\u0438\u0439\u043c\u0430\u0454 \u0437\u0430\u043f\u0438\u0442\u0438 \u043d\u0430 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438 (\u043c\u043e\u0436\u043b\u0438\u0432\u043e, \u0412\u0438 \u0437\u0430\u043d\u0430\u0434\u0442\u043e \u0431\u0430\u0433\u0430\u0442\u043e \u0440\u0430\u0437 \u0432\u0432\u043e\u0434\u0438\u043b\u0438 \u043d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 PIN-\u043a\u043e\u0434), \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437 \u043f\u0456\u0437\u043d\u0456\u0448\u0435.", + "device_did_not_pair": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043d\u0430\u043c\u0430\u0433\u0430\u0432\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043f\u0440\u043e\u0446\u0435\u0441 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438.", + "invalid_config": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u0446\u044c\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u043d\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430. \u0421\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 \u0439\u043e\u0433\u043e \u0449\u0435 \u0440\u0430\u0437.", + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "no_usable_service": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u0432\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438 \u0441\u043f\u043e\u0441\u0456\u0431 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e. \u042f\u043a\u0449\u043e \u0412\u0438 \u0432\u0436\u0435 \u0431\u0430\u0447\u0438\u043b\u0438 \u0446\u0435 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u043d\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0432\u043a\u0430\u0437\u0430\u0442\u0438 IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0430\u0431\u043e \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0456\u0442\u044c \u0439\u043e\u0433\u043e.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "\u0412\u0438 \u0437\u0431\u0438\u0440\u0430\u0454\u0442\u0435\u0441\u044f \u0434\u043e\u0434\u0430\u0442\u0438 Apple TV `{name}` \u0432 Home Assistant. \n\n ** \u0414\u043b\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044f \u043f\u0440\u043e\u0446\u0435\u0441\u0443 \u0412\u0430\u043c \u043c\u043e\u0436\u0435 \u0437\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0438\u0441\u044f \u0432\u0432\u0435\u0441\u0442\u0438 \u043a\u0456\u043b\u044c\u043a\u0430 PIN-\u043a\u043e\u0434\u0456\u0432. ** \n\n\u0417\u0432\u0435\u0440\u043d\u0456\u0442\u044c \u0443\u0432\u0430\u0433\u0443, \u0449\u043e \u0412\u0438 *\u043d\u0435* \u0437\u043c\u043e\u0436\u0435\u0442\u0435 \u0432\u0438\u043c\u0438\u043a\u0430\u0442\u0438 Apple TV \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0446\u0456\u0454\u0457 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457. \u0412 Home Assistant \u043c\u043e\u0436\u043b\u0438\u0432\u043e \u0432\u0438\u043c\u043a\u043d\u0443\u0442\u0438 \u0442\u0456\u043b\u044c\u043a\u0438 \u043c\u0435\u0434\u0456\u0430\u043f\u0440\u043e\u0433\u0440\u0430\u0432\u0430\u0447!", + "title": "\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0456\u0442\u044c \u0434\u043e\u0434\u0430\u0432\u0430\u043d\u043d\u044f Apple TV" + }, + "pair_no_pin": { + "description": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0434\u043b\u044f \u0441\u043b\u0443\u0436\u0431\u0438 `{protocol}`. \u0414\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u0432\u0436\u0435\u043d\u043d\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0432\u0435\u0434\u0456\u0442\u044c PIN-\u043a\u043e\u0434 {pin} \u043d\u0430 \u0412\u0430\u0448\u043e\u043c\u0443 Apple TV.", + "title": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "pair_with_pin": { + "data": { + "pin": "PIN-\u043a\u043e\u0434" + }, + "description": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 `{protocol}`. \u0412\u0432\u0435\u0434\u0456\u0442\u044c PIN-\u043a\u043e\u0434, \u044f\u043a\u0438\u0439 \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0454\u0442\u044c\u0441\u044f \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456. \u041f\u0435\u0440\u0448\u0456 \u043d\u0443\u043b\u0456 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0431\u0443\u0442\u0438 \u043e\u043f\u0443\u0449\u0435\u043d\u0456, \u0442\u043e\u0431\u0442\u043e \u0432\u0432\u0435\u0434\u0456\u0442\u044c 123, \u044f\u043a\u0449\u043e \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0454\u0442\u044c\u0441\u044f \u043a\u043e\u0434 0123.", + "title": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "reconfigure": { + "description": "\u0423 \u0446\u044c\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Apple TV \u0432\u0438\u043d\u0438\u043a\u0430\u044e\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u043f\u0440\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u0456, \u0439\u043e\u0433\u043e \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u043f\u0435\u0440\u0435\u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438.", + "title": "\u041f\u0435\u0440\u0435\u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "service_problem": { + "description": "\u0412\u0438\u043d\u0438\u043a\u043b\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u043f\u0440\u0438 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u0456 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 `{protocol}`. \u0426\u0435 \u0431\u0443\u0434\u0435 \u043f\u0440\u043e\u0456\u0433\u043d\u043e\u0440\u043e\u0432\u0430\u043d\u043e.", + "title": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0434\u043e\u0434\u0430\u0442\u0438 \u0441\u043b\u0443\u0436\u0431\u0443" + }, + "user": { + "data": { + "device_input": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + }, + "description": "\u041f\u043e\u0447\u043d\u0456\u0442\u044c \u0437 \u0432\u0432\u0435\u0434\u0435\u043d\u043d\u044f \u043d\u0430\u0437\u0432\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434, \u041a\u0443\u0445\u043d\u044f \u0430\u0431\u043e \u0421\u043f\u0430\u043b\u044c\u043d\u044f) \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0438 Apple TV, \u044f\u043a\u0443 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438. \u042f\u043a\u0449\u043e \u0431\u0443\u0434\u044c-\u044f\u043a\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u0431\u0443\u043b\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u0456 \u0443 \u0412\u0430\u0448\u0456\u0439 \u043c\u0435\u0440\u0435\u0436\u0456, \u0432\u043e\u043d\u0438 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u0456 \u043d\u0438\u0436\u0447\u0435. \n\n \u042f\u043a\u0449\u043e \u0412\u0438 \u043d\u0435 \u0431\u0430\u0447\u0438\u0442\u0435 \u0441\u0432\u0456\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0430\u0431\u043e \u0432\u0438\u043d\u0438\u043a\u0430\u044e\u0442\u044c \u0431\u0443\u0434\u044c-\u044f\u043a\u0456 \u0456\u043d\u0448\u0456 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u043f\u0456\u0434 \u0447\u0430\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0432\u043a\u0430\u0437\u0430\u0442\u0438 IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e. \n\n {devices}", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043d\u043e\u0432\u043e\u0433\u043e Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "\u041d\u0435 \u0432\u043c\u0438\u043a\u0430\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0443 Home Assistant" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/zh-Hans.json b/homeassistant/components/apple_tv/translations/zh-Hans.json index bb1f8e025ca..54095a0a633 100644 --- a/homeassistant/components/apple_tv/translations/zh-Hans.json +++ b/homeassistant/components/apple_tv/translations/zh-Hans.json @@ -1,6 +1,9 @@ { "config": { "step": { + "confirm": { + "description": "\u60a8\u5373\u5c06\u6dfb\u52a0 Apple TV (\u540d\u79f0\u4e3a\u201c{name}\u201d)\u5230 Home Assistant\u3002 \n\n **\u8981\u5b8c\u6210\u6b64\u8fc7\u7a0b\uff0c\u53ef\u80fd\u9700\u8981\u8f93\u5165\u591a\u4e2a PIN \u7801\u3002** \n\n\u8bf7\u6ce8\u610f\uff0c\u6b64\u96c6\u6210*\u4e0d\u80fd*\u5173\u95ed Apple TV \u7684\u7535\u6e90\uff0c\u53ea\u4f1a\u5173\u95ed Home Assistant \u4e2d\u7684\u5a92\u4f53\u64ad\u653e\u5668\uff01" + }, "pair_no_pin": { "title": "\u914d\u5bf9\u4e2d" }, @@ -8,6 +11,20 @@ "data": { "pin": "PIN\u7801" } + }, + "user": { + "description": "\u8981\u5f00\u59cb\uff0c\u8bf7\u8f93\u5165\u8981\u6dfb\u52a0\u7684 Apple TV \u7684\u8bbe\u5907\u540d\u79f0\u6216 IP \u5730\u5740\u3002\u5728\u7f51\u7edc\u4e0a\u81ea\u52a8\u53d1\u73b0\u7684\u8bbe\u5907\u4f1a\u663e\u793a\u5728\u4e0b\u65b9\u3002 \n\n\u5982\u679c\u6ca1\u6709\u53d1\u73b0\u8bbe\u5907\u6216\u9047\u5230\u4efb\u4f55\u95ee\u9898\uff0c\u8bf7\u5c1d\u8bd5\u6307\u5b9a\u8bbe\u5907 IP \u5730\u5740\u3002 \n\n {devices}", + "title": "\u8bbe\u7f6e\u65b0\u7684 Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "\u542f\u52a8 Home Assistant \u65f6\u4e0d\u6253\u5f00\u8bbe\u5907" + }, + "description": "\u914d\u7f6e\u8bbe\u5907\u901a\u7528\u8bbe\u7f6e" } } }, diff --git a/homeassistant/components/arcam_fmj/translations/de.json b/homeassistant/components/arcam_fmj/translations/de.json index 92ad0e22663..b7270e730bb 100644 --- a/homeassistant/components/arcam_fmj/translations/de.json +++ b/homeassistant/components/arcam_fmj/translations/de.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "cannot_connect": "Verbindungsfehler" + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/arcam_fmj/translations/ro.json b/homeassistant/components/arcam_fmj/translations/ro.json new file mode 100644 index 00000000000..a8008f1e8bc --- /dev/null +++ b/homeassistant/components/arcam_fmj/translations/ro.json @@ -0,0 +1,9 @@ +{ + "config": { + "error": { + "few": "Pu\u021bine", + "one": "Unul", + "other": "Altele" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/tr.json b/homeassistant/components/arcam_fmj/translations/tr.json new file mode 100644 index 00000000000..dd15f57212c --- /dev/null +++ b/homeassistant/components/arcam_fmj/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/uk.json b/homeassistant/components/arcam_fmj/translations/uk.json new file mode 100644 index 00000000000..4d33a5bc0d9 --- /dev/null +++ b/homeassistant/components/arcam_fmj/translations/uk.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "flow_title": "Arcam FMJ {host}", + "step": { + "confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 Arcam FMJ `{host}`?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "\u0437\u0430\u043f\u0438\u0442\u0430\u043d\u043e \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u044f {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/de.json b/homeassistant/components/atag/translations/de.json index 2ced7577fdf..b94103d898b 100644 --- a/homeassistant/components/atag/translations/de.json +++ b/homeassistant/components/atag/translations/de.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "already_configured": "Dieses Ger\u00e4t wurde bereits zu HomeAssistant hinzugef\u00fcgt" + "already_configured": "Dieses Ger\u00e4t wurde bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { "data": { - "email": "Email (Optional)", + "email": "E-Mail", "host": "Host", "port": "Port" }, diff --git a/homeassistant/components/atag/translations/tr.json b/homeassistant/components/atag/translations/tr.json new file mode 100644 index 00000000000..f7c94d0a976 --- /dev/null +++ b/homeassistant/components/atag/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unauthorized": "E\u015fle\u015ftirme reddedildi, kimlik do\u011frulama iste\u011fi i\u00e7in cihaz\u0131 kontrol edin" + }, + "step": { + "user": { + "data": { + "email": "E-posta", + "host": "Ana Bilgisayar", + "port": "Port" + }, + "title": "Cihaza ba\u011flan\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/uk.json b/homeassistant/components/atag/translations/uk.json new file mode 100644 index 00000000000..ee0a077d900 --- /dev/null +++ b/homeassistant/components/atag/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unauthorized": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u0437\u0430\u0431\u043e\u0440\u043e\u043d\u0435\u043d\u043e, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0430 \u0437\u0430\u043f\u0438\u0442 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457." + }, + "step": { + "user": { + "data": { + "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438", + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/de.json b/homeassistant/components/august/translations/de.json index d46be650e2c..3a5bd70f1af 100644 --- a/homeassistant/components/august/translations/de.json +++ b/homeassistant/components/august/translations/de.json @@ -1,10 +1,11 @@ { "config": { "abort": { - "already_configured": "Konto ist bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/august/translations/tr.json b/homeassistant/components/august/translations/tr.json new file mode 100644 index 00000000000..ccb9e200c82 --- /dev/null +++ b/homeassistant/components/august/translations/tr.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "login_method": "Giri\u015f Y\u00f6ntemi", + "password": "Parola", + "timeout": "Zaman a\u015f\u0131m\u0131 (saniye)", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Giri\u015f Y\u00f6ntemi 'e-posta' ise, Kullan\u0131c\u0131 Ad\u0131 e-posta adresidir. Giri\u015f Y\u00f6ntemi 'telefon' ise, Kullan\u0131c\u0131 Ad\u0131 '+ NNNNNNNNN' bi\u00e7imindeki telefon numaras\u0131d\u0131r." + }, + "validation": { + "data": { + "code": "Do\u011frulama kodu" + }, + "description": "L\u00fctfen {login_method} ( {username} ) bilgilerinizi kontrol edin ve a\u015fa\u011f\u0131ya do\u011frulama kodunu girin", + "title": "\u0130ki fakt\u00f6rl\u00fc kimlik do\u011frulama" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/uk.json b/homeassistant/components/august/translations/uk.json new file mode 100644 index 00000000000..e06c5347d73 --- /dev/null +++ b/homeassistant/components/august/translations/uk.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "login_method": "\u0421\u043f\u043e\u0441\u0456\u0431 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u042f\u043a\u0449\u043e \u0441\u043f\u043e\u0441\u043e\u0431\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457 \u0432\u0438\u0431\u0440\u0430\u043d\u043e 'email', \u0442\u043e \u043b\u043e\u0433\u0456\u043d\u043e\u043c \u0454 \u0430\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438. \u042f\u043a\u0449\u043e \u0441\u043f\u043e\u0441\u043e\u0431\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457 \u0432\u0438\u0431\u0440\u0430\u043d\u043e 'phone', \u0442\u043e \u043b\u043e\u0433\u0456\u043d\u043e\u043c \u0454 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0443 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 '+ NNNNNNNNN'.", + "title": "August" + }, + "validation": { + "data": { + "code": "\u041a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f" + }, + "description": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 {login_method} ({username}) \u0456 \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u0438\u0439 \u043a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f.", + "title": "\u0414\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/de.json b/homeassistant/components/aurora/translations/de.json index 95312fe7943..838673e8d60 100644 --- a/homeassistant/components/aurora/translations/de.json +++ b/homeassistant/components/aurora/translations/de.json @@ -1,7 +1,16 @@ { "config": { "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "name": "Name" + } + } } }, "options": { @@ -12,5 +21,6 @@ } } } - } + }, + "title": "NOAA Aurora-Sensor" } \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/fr.json b/homeassistant/components/aurora/translations/fr.json new file mode 100644 index 00000000000..473ecefdbd9 --- /dev/null +++ b/homeassistant/components/aurora/translations/fr.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00c9chec \u00e0 la connexion" + }, + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nom" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Seuil (%)" + } + } + } + }, + "title": "Capteur NOAA Aurora" +} \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/tr.json b/homeassistant/components/aurora/translations/tr.json new file mode 100644 index 00000000000..0c3bb75ed6e --- /dev/null +++ b/homeassistant/components/aurora/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "latitude": "Enlem", + "longitude": "Boylam", + "name": "Ad" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/uk.json b/homeassistant/components/aurora/translations/uk.json new file mode 100644 index 00000000000..0cb3c4fcbce --- /dev/null +++ b/homeassistant/components/aurora/translations/uk.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "\u041f\u043e\u0440\u0456\u0433 (%)" + } + } + } + }, + "title": "NOAA Aurora Sensor" +} \ No newline at end of file diff --git a/homeassistant/components/auth/translations/de.json b/homeassistant/components/auth/translations/de.json index 06da3cde1a1..93cbf1073cc 100644 --- a/homeassistant/components/auth/translations/de.json +++ b/homeassistant/components/auth/translations/de.json @@ -25,7 +25,7 @@ }, "step": { "init": { - "description": "Um die Zwei-Faktor-Authentifizierung mit zeitbasierten Einmalpassw\u00f6rtern zu aktivieren, scanne den QR-Code mit Ihrer Authentifizierungs-App. Wenn du keine hast, empfehlen wir entweder [Google Authenticator] (https://support.google.com/accounts/answer/1066447) oder [Authy] (https://authy.com/). \n\n {qr_code} \n \nNachdem du den Code gescannt hast, gebe den sechsstelligen Code aus der App ein, um das Setup zu \u00fcberpr\u00fcfen. Wenn es Probleme beim Scannen des QR-Codes gibt, f\u00fchre ein manuelles Setup mit dem Code ** ` {code} ` ** durch.", + "description": "Um die Zwei-Faktor-Authentifizierung mit zeitbasierten Einmalpassw\u00f6rtern zu aktivieren, scanne den QR-Code mit deiner Authentifizierungs-App. Wenn du keine hast, empfehlen wir entweder [Google Authenticator] (https://support.google.com/accounts/answer/1066447) oder [Authy] (https://authy.com/). \n\n {qr_code} \n \nNachdem du den Code gescannt hast, gibst du den sechsstelligen Code aus der App ein, um das Setup zu \u00fcberpr\u00fcfen. Wenn es Probleme beim Scannen des QR-Codes gibt, f\u00fchre ein manuelles Setup mit dem Code ** ` {code} ` ** durch.", "title": "Richte die Zwei-Faktor-Authentifizierung mit TOTP ein" } }, diff --git a/homeassistant/components/auth/translations/tr.json b/homeassistant/components/auth/translations/tr.json new file mode 100644 index 00000000000..7d273214574 --- /dev/null +++ b/homeassistant/components/auth/translations/tr.json @@ -0,0 +1,22 @@ +{ + "mfa_setup": { + "notify": { + "step": { + "init": { + "title": "Bilgilendirme bile\u015feni taraf\u0131ndan verilen tek seferlik parolay\u0131 ayarlay\u0131n" + }, + "setup": { + "description": "**bildirim yoluyla tek seferlik bir parola g\u00f6nderildi. {notify_service}**. L\u00fctfen a\u015fa\u011f\u0131da girin:" + } + }, + "title": "Tek Seferlik Parolay\u0131 Bildir" + }, + "totp": { + "step": { + "init": { + "description": "Zamana dayal\u0131 tek seferlik parolalar\u0131 kullanarak iki fakt\u00f6rl\u00fc kimlik do\u011frulamay\u0131 etkinle\u015ftirmek i\u00e7in kimlik do\u011frulama uygulaman\u0131zla QR kodunu taray\u0131n. Hesab\u0131n\u0131z yoksa, [Google Authenticator] (https://support.google.com/accounts/answer/1066447) veya [Authy] (https://authy.com/) \u00f6neririz. \n\n {qr_code}\n\n Kodu tarad\u0131ktan sonra, kurulumu do\u011frulamak i\u00e7in uygulaman\u0131zdan alt\u0131 haneli kodu girin. QR kodunu taramayla ilgili sorun ya\u015f\u0131yorsan\u0131z, ** ` {code} ` manuel kurulum yap\u0131n." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/translations/uk.json b/homeassistant/components/auth/translations/uk.json index f826075078e..eeb8f1ee7c7 100644 --- a/homeassistant/components/auth/translations/uk.json +++ b/homeassistant/components/auth/translations/uk.json @@ -1,14 +1,35 @@ { "mfa_setup": { "notify": { + "abort": { + "no_available_service": "\u041d\u0435\u043c\u0430\u0454 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u0441\u043b\u0443\u0436\u0431 \u0441\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u044c." + }, "error": { "invalid_code": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043e\u0434, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437." }, "step": { + "init": { + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u0434\u043d\u0443 \u0456\u0437 \u0441\u043b\u0443\u0436\u0431 \u0441\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u044c:", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0438 \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u0438\u0445 \u043f\u0430\u0440\u043e\u043b\u0456\u0432 \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0441\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u044c" + }, "setup": { + "description": "\u041e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u0438\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0439 \u0447\u0435\u0440\u0435\u0437 ** notify.{notify_service} **. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u0439\u043e\u0433\u043e \u043d\u0438\u0436\u0447\u0435:", "title": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f" } - } + }, + "title": "\u0414\u043e\u0441\u0442\u0430\u0432\u043a\u0430 \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u0438\u0445 \u043f\u0430\u0440\u043e\u043b\u0456\u0432" + }, + "totp": { + "error": { + "invalid_code": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043e\u0434. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443. \u042f\u043a\u0449\u043e \u0412\u0438 \u043f\u043e\u0441\u0442\u0456\u0439\u043d\u043e \u043e\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u0435 \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443, \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0433\u043e\u0434\u0438\u043d\u043d\u0438\u043a \u0443 \u0412\u0430\u0448\u0456\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0456 Home Assistant \u043f\u043e\u043a\u0430\u0437\u0443\u0454 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u0447\u0430\u0441." + }, + "step": { + "init": { + "description": "\u0429\u043e\u0431 \u0430\u043a\u0442\u0438\u0432\u0443\u0432\u0430\u0442\u0438 \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0443 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0437 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f\u043c \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u0438\u0445 \u043f\u0430\u0440\u043e\u043b\u0456\u0432, \u0437\u0430\u0441\u043d\u043e\u0432\u0430\u043d\u0438\u0445 \u043d\u0430 \u0447\u0430\u0441\u0456, \u0432\u0456\u0434\u0441\u043a\u0430\u043d\u0443\u0439\u0442\u0435 QR-\u043a\u043e\u0434 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0438 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0438 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0447\u043d\u043e\u0441\u0442\u0456. \u042f\u043a\u0449\u043e \u0443 \u0412\u0430\u0441 \u0457\u0457 \u043d\u0435\u043c\u0430\u0454, \u043c\u0438 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0454\u043c\u043e \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0430\u0431\u043e [Google Authenticator](https://support.google.com/accounts/answer/1066447), \u0430\u0431\u043e [Authy](https://authy.com/). \n\n{qr_code}\n\n\u041f\u0456\u0441\u043b\u044f \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f QR-\u043a\u043e\u0434\u0443 \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u0448\u0435\u0441\u0442\u0438\u0437\u043d\u0430\u0447\u043d\u0438\u0439 \u043a\u043e\u0434 \u0437 \u0412\u0430\u0448\u043e\u0433\u043e \u0437\u0430\u0441\u0442\u043e\u0441\u0443\u0432\u0430\u043d\u043d\u044f, \u0449\u043e\u0431 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u042f\u043a\u0449\u043e \u0443 \u0412\u0430\u0441 \u0454 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0437\u0456 \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f\u043c QR-\u043a\u043e\u0434\u0443, \u0432\u0438\u043a\u043e\u043d\u0430\u0439\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u043a\u043e\u0434\u0443 ** `{code}` **.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457 \u0437 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f\u043c TOTP" + } + }, + "title": "TOTP" } } } \ No newline at end of file diff --git a/homeassistant/components/awair/translations/de.json b/homeassistant/components/awair/translations/de.json index fcdcd0190e3..5b65ece083b 100644 --- a/homeassistant/components/awair/translations/de.json +++ b/homeassistant/components/awair/translations/de.json @@ -1,17 +1,25 @@ { "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, "error": { - "unknown": "Unbekannter Awair-API-Fehler." + "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token", + "unknown": "Unerwarteter Fehler" }, "step": { "reauth": { "data": { + "access_token": "Zugangstoken", "email": "E-Mail" }, - "description": "Bitte geben Sie Ihr Awair-Entwicklerzugriffstoken erneut ein." + "description": "Bitte gib dein Awair-Entwicklerzugriffstoken erneut ein." }, "user": { "data": { + "access_token": "Zugangstoken", "email": "E-Mail" } } diff --git a/homeassistant/components/awair/translations/tr.json b/homeassistant/components/awair/translations/tr.json new file mode 100644 index 00000000000..84da92b97d3 --- /dev/null +++ b/homeassistant/components/awair/translations/tr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "invalid_access_token": "Ge\u00e7ersiz eri\u015fim belirteci", + "unknown": "Beklenmeyen hata" + }, + "step": { + "reauth": { + "data": { + "access_token": "Eri\u015fim Belirteci", + "email": "E-posta" + } + }, + "user": { + "data": { + "access_token": "Eri\u015fim Belirteci", + "email": "E-posta" + }, + "description": "Awair geli\u015ftirici eri\u015fim belirteci i\u00e7in \u015fu adresten kaydolmal\u0131s\u0131n\u0131z: https://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/uk.json b/homeassistant/components/awair/translations/uk.json new file mode 100644 index 00000000000..f8150ad7faf --- /dev/null +++ b/homeassistant/components/awair/translations/uk.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "error": { + "invalid_access_token": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "reauth": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443", + "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + }, + "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443." + }, + "user": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443", + "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + }, + "description": "\u0414\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u0434\u043e Awair \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e: https://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/translations/ca.json b/homeassistant/components/axis/translations/ca.json index 26da6057dc1..3e104c1005e 100644 --- a/homeassistant/components/axis/translations/ca.json +++ b/homeassistant/components/axis/translations/ca.json @@ -11,7 +11,7 @@ "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, - "flow_title": "Dispositiu d'eix: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/translations/cs.json b/homeassistant/components/axis/translations/cs.json index fd99c68ab35..4f7c3016235 100644 --- a/homeassistant/components/axis/translations/cs.json +++ b/homeassistant/components/axis/translations/cs.json @@ -11,7 +11,7 @@ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, - "flow_title": "Za\u0159\u00edzen\u00ed Axis: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/translations/de.json b/homeassistant/components/axis/translations/de.json index 4706350cdb3..1f6aedf5d9c 100644 --- a/homeassistant/components/axis/translations/de.json +++ b/homeassistant/components/axis/translations/de.json @@ -7,8 +7,9 @@ }, "error": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "already_in_progress": "Der Konfigurationsablauf f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt.", - "cannot_connect": "Verbindungsfehler" + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "flow_title": "Achsenger\u00e4t: {name} ({host})", "step": { diff --git a/homeassistant/components/axis/translations/en.json b/homeassistant/components/axis/translations/en.json index 6b01533aefa..f71e91f6280 100644 --- a/homeassistant/components/axis/translations/en.json +++ b/homeassistant/components/axis/translations/en.json @@ -11,7 +11,7 @@ "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication" }, - "flow_title": "Axis device: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/translations/et.json b/homeassistant/components/axis/translations/et.json index 6a27e74b287..f6f9a523cb6 100644 --- a/homeassistant/components/axis/translations/et.json +++ b/homeassistant/components/axis/translations/et.json @@ -11,7 +11,7 @@ "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamise viga" }, - "flow_title": "Axise seade: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/translations/it.json b/homeassistant/components/axis/translations/it.json index 6461b2a6619..7e7aeb1d1b2 100644 --- a/homeassistant/components/axis/translations/it.json +++ b/homeassistant/components/axis/translations/it.json @@ -11,7 +11,7 @@ "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida" }, - "flow_title": "Dispositivo Axis: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/translations/no.json b/homeassistant/components/axis/translations/no.json index 984d522eba9..1fc0640eb9b 100644 --- a/homeassistant/components/axis/translations/no.json +++ b/homeassistant/components/axis/translations/no.json @@ -11,7 +11,7 @@ "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning" }, - "flow_title": "Axis enhet: {name} ({host})", + "flow_title": "{name} ( {host} )", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/translations/pl.json b/homeassistant/components/axis/translations/pl.json index 84af845ab31..e44816bc2ea 100644 --- a/homeassistant/components/axis/translations/pl.json +++ b/homeassistant/components/axis/translations/pl.json @@ -11,7 +11,7 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie" }, - "flow_title": "Urz\u0105dzenie Axis: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/translations/ru.json b/homeassistant/components/axis/translations/ru.json index ee1dc8494f3..6d979dc9de0 100644 --- a/homeassistant/components/axis/translations/ru.json +++ b/homeassistant/components/axis/translations/ru.json @@ -11,7 +11,7 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." }, - "flow_title": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Axis {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/translations/tr.json b/homeassistant/components/axis/translations/tr.json new file mode 100644 index 00000000000..b2d609747d1 --- /dev/null +++ b/homeassistant/components/axis/translations/tr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/translations/uk.json b/homeassistant/components/axis/translations/uk.json new file mode 100644 index 00000000000..35b849ce968 --- /dev/null +++ b/homeassistant/components/axis/translations/uk.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "link_local_address": "\u041f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0456 \u0430\u0434\u0440\u0435\u0441\u0438 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f.", + "not_axis_device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0454 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c Axis." + }, + "error": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "flow_title": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Axis {name} ({host})", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "Axis" + } + } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u043e\u0444\u0456\u043b\u044c \u043f\u043e\u0442\u043e\u043a\u0443 \u0434\u043b\u044f \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0432\u0456\u0434\u0435\u043e\u043f\u043e\u0442\u043e\u043a\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Axis" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/translations/zh-Hant.json b/homeassistant/components/axis/translations/zh-Hant.json index 1d7aaa7c74e..293f08c5f05 100644 --- a/homeassistant/components/axis/translations/zh-Hant.json +++ b/homeassistant/components/axis/translations/zh-Hant.json @@ -11,7 +11,7 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, - "flow_title": "Axis \u88dd\u7f6e\uff1a{name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/azure_devops/translations/de.json b/homeassistant/components/azure_devops/translations/de.json index 1c940ea7a35..e7d9e073ec6 100644 --- a/homeassistant/components/azure_devops/translations/de.json +++ b/homeassistant/components/azure_devops/translations/de.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { "reauth": { diff --git a/homeassistant/components/azure_devops/translations/fr.json b/homeassistant/components/azure_devops/translations/fr.json index edcf3dda517..5e62d54ec1d 100644 --- a/homeassistant/components/azure_devops/translations/fr.json +++ b/homeassistant/components/azure_devops/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9", + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "reauth_successful": "Jeton d'acc\u00e8s mis \u00e0 jour avec succ\u00e8s" }, "error": { diff --git a/homeassistant/components/azure_devops/translations/tr.json b/homeassistant/components/azure_devops/translations/tr.json new file mode 100644 index 00000000000..11a15956f63 --- /dev/null +++ b/homeassistant/components/azure_devops/translations/tr.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "project_error": "Proje bilgileri al\u0131namad\u0131." + }, + "flow_title": "Azure DevOps: {project_url}", + "step": { + "reauth": { + "title": "Yeniden kimlik do\u011frulama" + }, + "user": { + "data": { + "organization": "Organizasyon", + "personal_access_token": "Ki\u015fisel Eri\u015fim Belirteci (PAT)", + "project": "Proje" + }, + "description": "Projenize eri\u015fmek i\u00e7in bir Azure DevOps \u00f6rne\u011fi ayarlay\u0131n. Ki\u015fisel Eri\u015fim Jetonu yaln\u0131zca \u00f6zel bir proje i\u00e7in gereklidir.", + "title": "Azure DevOps Projesi Ekle" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_devops/translations/uk.json b/homeassistant/components/azure_devops/translations/uk.json index 4a42fd17fc3..848528f444e 100644 --- a/homeassistant/components/azure_devops/translations/uk.json +++ b/homeassistant/components/azure_devops/translations/uk.json @@ -1,16 +1,30 @@ { "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "project_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u043f\u0440\u043e\u0435\u043a\u0442." + }, "flow_title": "Azure DevOps: {project_url}", "step": { "reauth": { + "data": { + "personal_access_token": "\u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 (PAT)" + }, + "description": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457 {project_url} . \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456.", "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" }, "user": { "data": { "organization": "\u041e\u0440\u0433\u0430\u043d\u0456\u0437\u0430\u0446\u0456\u044f", "personal_access_token": "\u0422\u043e\u043a\u0435\u043d \u043e\u0441\u043e\u0431\u0438\u0441\u0442\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0443 (PAT)", - "project": "\u041f\u0440\u043e\u0454\u043a\u0442" + "project": "\u041f\u0440\u043e\u0435\u043a\u0442" }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u043c Azure DevOps. \u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0432\u0432\u043e\u0434\u0438\u0442\u0438 \u043b\u0438\u0448\u0435 \u0434\u043b\u044f \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u0438\u0445 \u043f\u0440\u043e\u0435\u043a\u0442\u0456\u0432.", "title": "\u0414\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u043e\u0435\u043a\u0442 Azure DevOps" } } diff --git a/homeassistant/components/binary_sensor/translations/de.json b/homeassistant/components/binary_sensor/translations/de.json index 3687536eb5b..a78befb7965 100644 --- a/homeassistant/components/binary_sensor/translations/de.json +++ b/homeassistant/components/binary_sensor/translations/de.json @@ -98,6 +98,10 @@ "off": "Normal", "on": "Schwach" }, + "battery_charging": { + "off": "L\u00e4dt nicht", + "on": "L\u00e4dt" + }, "cold": { "off": "Normal", "on": "Kalt" @@ -122,6 +126,10 @@ "off": "Normal", "on": "Hei\u00df" }, + "light": { + "off": "Kein Licht", + "on": "Licht erkannt" + }, "lock": { "off": "Verriegelt", "on": "Entriegelt" @@ -134,6 +142,10 @@ "off": "Ruhig", "on": "Bewegung erkannt" }, + "moving": { + "off": "Bewegt sich nicht", + "on": "Bewegt sich" + }, "occupancy": { "off": "Frei", "on": "Belegt" @@ -142,6 +154,10 @@ "off": "Geschlossen", "on": "Offen" }, + "plug": { + "off": "Ausgesteckt", + "on": "Eingesteckt" + }, "presence": { "off": "Abwesend", "on": "Zu Hause" diff --git a/homeassistant/components/binary_sensor/translations/en.json b/homeassistant/components/binary_sensor/translations/en.json index a9a20e3fa50..98c8a3a220a 100644 --- a/homeassistant/components/binary_sensor/translations/en.json +++ b/homeassistant/components/binary_sensor/translations/en.json @@ -159,8 +159,8 @@ "on": "Plugged in" }, "presence": { - "off": "[%key:common::state::not_home%]", - "on": "[%key:common::state::home%]" + "off": "Away", + "on": "Home" }, "problem": { "off": "OK", diff --git a/homeassistant/components/binary_sensor/translations/pl.json b/homeassistant/components/binary_sensor/translations/pl.json index 7d6e8eab4ba..726765aea02 100644 --- a/homeassistant/components/binary_sensor/translations/pl.json +++ b/homeassistant/components/binary_sensor/translations/pl.json @@ -99,7 +99,7 @@ "on": "roz\u0142adowana" }, "battery_charging": { - "off": "nie \u0142aduje", + "off": "roz\u0142adowywanie", "on": "\u0142adowanie" }, "cold": { diff --git a/homeassistant/components/binary_sensor/translations/tr.json b/homeassistant/components/binary_sensor/translations/tr.json index 3c5cfaeeacf..94e1496cc30 100644 --- a/homeassistant/components/binary_sensor/translations/tr.json +++ b/homeassistant/components/binary_sensor/translations/tr.json @@ -1,4 +1,10 @@ { + "device_automation": { + "trigger_type": { + "moist": "{entity_name} nemli oldu", + "not_opened": "{entity_name} kapat\u0131ld\u0131" + } + }, "state": { "_": { "off": "Kapal\u0131", @@ -8,6 +14,10 @@ "off": "Normal", "on": "D\u00fc\u015f\u00fck" }, + "battery_charging": { + "off": "\u015earj olmuyor", + "on": "\u015earj Oluyor" + }, "cold": { "off": "Normal", "on": "So\u011fuk" @@ -32,6 +42,10 @@ "off": "Normal", "on": "S\u0131cak" }, + "light": { + "off": "I\u015f\u0131k yok", + "on": "I\u015f\u0131k alg\u0131land\u0131" + }, "lock": { "off": "Kilit kapal\u0131", "on": "Kilit a\u00e7\u0131k" @@ -44,6 +58,10 @@ "off": "Temiz", "on": "Alg\u0131land\u0131" }, + "moving": { + "off": "Hareket etmiyor", + "on": "Hareketli" + }, "occupancy": { "off": "Temiz", "on": "Alg\u0131land\u0131" @@ -52,6 +70,10 @@ "off": "Kapal\u0131", "on": "A\u00e7\u0131k" }, + "plug": { + "off": "Fi\u015fi \u00e7ekildi", + "on": "Tak\u0131l\u0131" + }, "presence": { "off": "[%key:common::state::evde_degil%]", "on": "[%key:common::state::evde%]" diff --git a/homeassistant/components/binary_sensor/translations/uk.json b/homeassistant/components/binary_sensor/translations/uk.json index 29767f6d6d6..0f8d92749c4 100644 --- a/homeassistant/components/binary_sensor/translations/uk.json +++ b/homeassistant/components/binary_sensor/translations/uk.json @@ -2,15 +2,91 @@ "device_automation": { "condition_type": { "is_bat_low": "{entity_name} \u043d\u0438\u0437\u044c\u043a\u0438\u0439 \u0440\u0456\u0432\u0435\u043d\u044c \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440\u0430", - "is_not_bat_low": "{entity_name} \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u0438\u043c \u0437\u0430\u0440\u044f\u0434 \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440\u0430" + "is_cold": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f", + "is_connected": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "is_gas": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0433\u0430\u0437", + "is_hot": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043d\u0430\u0433\u0440\u0456\u0432", + "is_light": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0441\u0432\u0456\u0442\u043b\u043e", + "is_locked": "{entity_name} \u0432 \u0437\u0430\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_moist": "{entity_name} \u0432 \u0441\u0442\u0430\u043d\u0456 \"\u0412\u043e\u043b\u043e\u0433\u043e\"", + "is_motion": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0440\u0443\u0445", + "is_moving": "{entity_name} \u043f\u0435\u0440\u0435\u043c\u0456\u0449\u0443\u0454\u0442\u044c\u0441\u044f", + "is_no_gas": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0433\u0430\u0437", + "is_no_light": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0441\u0432\u0456\u0442\u043b\u043e", + "is_no_motion": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0440\u0443\u0445", + "is_no_problem": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u043e\u0431\u043b\u0435\u043c", + "is_no_smoke": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0434\u0438\u043c", + "is_no_sound": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0437\u0432\u0443\u043a", + "is_no_vibration": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0432\u0456\u0431\u0440\u0430\u0446\u0456\u044e", + "is_not_bat_low": "{entity_name} \u0432 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_not_cold": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f", + "is_not_connected": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "is_not_hot": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043d\u0430\u0433\u0440\u0456\u0432", + "is_not_locked": "{entity_name} \u0432 \u0440\u043e\u0437\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_not_moist": "{entity_name} \u0432 \u0441\u0442\u0430\u043d\u0456 \"\u0421\u0443\u0445\u043e\"", + "is_not_moving": "{entity_name} \u043d\u0435 \u0440\u0443\u0445\u0430\u0454\u0442\u044c\u0441\u044f", + "is_not_occupied": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", + "is_not_open": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u0438\u0442\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_not_plugged_in": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "is_not_powered": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0436\u0438\u0432\u043b\u0435\u043d\u043d\u044f", + "is_not_present": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", + "is_not_unsafe": "{entity_name} \u0432 \u0431\u0435\u0437\u043f\u0435\u0447\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_occupied": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", + "is_off": "{entity_name} \u0443 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_open": "{entity_name} \u0443 \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_plugged_in": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "is_powered": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0436\u0438\u0432\u043b\u0435\u043d\u043d\u044f", + "is_present": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", + "is_problem": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "is_smoke": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0434\u0438\u043c", + "is_sound": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0437\u0432\u0443\u043a", + "is_unsafe": "{entity_name} \u0432 \u043d\u0435\u0431\u0435\u0437\u043f\u0435\u0447\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_vibration": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0432\u0456\u0431\u0440\u0430\u0446\u0456\u044e" }, "trigger_type": { - "bat_low": "{entity_name} \u043d\u0438\u0437\u044c\u043a\u0438\u0439 \u0437\u0430\u0440\u044f\u0434 \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440\u0430", - "not_bat_low": "{entity_name} \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u0438\u0439 \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440", + "bat_low": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043d\u0438\u0437\u044c\u043a\u0438\u0439 \u0437\u0430\u0440\u044f\u0434", + "cold": "{entity_name} \u043e\u0445\u043e\u043b\u043e\u0434\u0436\u0443\u0454\u0442\u044c\u0441\u044f", + "connected": "{entity_name} \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0430\u0454\u0442\u044c\u0441\u044f", + "gas": "{entity_name} \u043f\u043e\u0447\u0438\u043d\u0430\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0433\u0430\u0437", + "hot": "{entity_name} \u043d\u0430\u0433\u0440\u0456\u0432\u0430\u0454\u0442\u044c\u0441\u044f", + "light": "{entity_name} \u043f\u043e\u0447\u0438\u043d\u0430\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0441\u0432\u0456\u0442\u043b\u043e", + "locked": "{entity_name} \u0431\u043b\u043e\u043a\u0443\u0454\u0442\u044c\u0441\u044f", + "moist": "{entity_name} \u0441\u0442\u0430\u0454 \u0432\u043e\u043b\u043e\u0433\u0438\u043c", + "motion": "{entity_name} \u043f\u043e\u0447\u0438\u043d\u0430\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0440\u0443\u0445", + "moving": "{entity_name} \u043f\u043e\u0447\u0438\u043d\u0430\u0454 \u0440\u0443\u0445\u0430\u0442\u0438\u0441\u044f", + "no_gas": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0433\u0430\u0437", + "no_light": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0441\u0432\u0456\u0442\u043b\u043e", + "no_motion": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0440\u0443\u0445", + "no_problem": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "no_smoke": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0434\u0438\u043c", + "no_sound": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0437\u0432\u0443\u043a", + "no_vibration": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0432\u0456\u0431\u0440\u0430\u0446\u0456\u044e", + "not_bat_low": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u0438\u0439 \u0437\u0430\u0440\u044f\u0434", + "not_cold": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u043e\u0445\u043e\u043b\u043e\u0434\u0436\u0443\u0432\u0430\u0442\u0438\u0441\u044f", + "not_connected": "{entity_name} \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0430\u0454\u0442\u044c\u0441\u044f", + "not_hot": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u043d\u0430\u0433\u0440\u0456\u0432\u0430\u0442\u0438\u0441\u044f", + "not_locked": "{entity_name} \u0440\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0454\u0442\u044c\u0441\u044f", + "not_moist": "{entity_name} \u0441\u0442\u0430\u0454 \u0441\u0443\u0445\u0438\u043c", + "not_moving": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u043f\u0435\u0440\u0435\u043c\u0456\u0449\u0435\u043d\u043d\u044f", + "not_occupied": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", "not_opened": "{entity_name} \u0437\u0430\u043a\u0440\u0438\u0442\u043e", + "not_plugged_in": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "not_powered": "{entity_name} \u043d\u0435 \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043d\u0430\u044f\u0432\u043d\u0456\u0441\u0442\u044c \u0436\u0438\u0432\u043b\u0435\u043d\u043d\u044f", + "not_present": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u0432\u0456\u0434\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", + "not_unsafe": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u0431\u0435\u0437\u043f\u0435\u043a\u0443", + "occupied": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", "opened": "{entity_name} \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u043e", + "plugged_in": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "powered": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043d\u0430\u044f\u0432\u043d\u0456\u0441\u0442\u044c \u0436\u0438\u0432\u043b\u0435\u043d\u043d\u044f", + "present": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", + "problem": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "smoke": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0434\u0438\u043c", + "sound": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0437\u0432\u0443\u043a", "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e", - "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" + "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e", + "unsafe": "{entity_name} \u043d\u0435 \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u0431\u0435\u0437\u043f\u0435\u043a\u0443", + "vibration": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0432\u0456\u0431\u0440\u0430\u0446\u0456\u044e" } }, "state": { @@ -22,6 +98,10 @@ "off": "\u041d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u0438\u0439", "on": "\u041d\u0438\u0437\u044c\u043a\u0438\u0439" }, + "battery_charging": { + "off": "\u041d\u0435 \u0437\u0430\u0440\u044f\u0434\u0436\u0430\u0454\u0442\u044c\u0441\u044f", + "on": "\u0417\u0430\u0440\u044f\u0434\u0436\u0430\u043d\u043d\u044f" + }, "cold": { "off": "\u041d\u043e\u0440\u043c\u0430", "on": "\u041e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f" @@ -46,6 +126,10 @@ "off": "\u041d\u043e\u0440\u043c\u0430", "on": "\u041d\u0430\u0433\u0440\u0456\u0432\u0430\u043d\u043d\u044f" }, + "light": { + "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e", + "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" + }, "lock": { "off": "\u0417\u0430\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e", "on": "\u0420\u043e\u0437\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e" @@ -58,13 +142,21 @@ "off": "\u041d\u0435\u043c\u0430\u0454 \u0440\u0443\u0445\u0443", "on": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u0440\u0443\u0445" }, + "moving": { + "off": "\u0420\u0443\u0445\u0443 \u043d\u0435\u043c\u0430\u0454", + "on": "\u0420\u0443\u0445\u0430\u0454\u0442\u044c\u0441\u044f" + }, "occupancy": { "off": "\u0427\u0438\u0441\u0442\u043e", "on": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c" }, "opening": { - "off": "\u0417\u0430\u043a\u0440\u0438\u0442\u043e", - "on": "\u0412\u0456\u0434\u043a\u0440\u0438\u0442\u0438\u0439" + "off": "\u0417\u0430\u0447\u0438\u043d\u0435\u043d\u043e", + "on": "\u0412\u0456\u0434\u0447\u0438\u043d\u0435\u043d\u043e" + }, + "plug": { + "off": "\u0412\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, "presence": { "off": "\u041d\u0435 \u0432\u0434\u043e\u043c\u0430", @@ -91,8 +183,8 @@ "on": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u0430 \u0432\u0456\u0431\u0440\u0430\u0446\u0456\u044f" }, "window": { - "off": "\u0417\u0430\u0447\u0438\u043d\u0435\u043d\u0435", - "on": "\u0412\u0456\u0434\u0447\u0438\u043d\u0435\u043d\u0435" + "off": "\u0417\u0430\u0447\u0438\u043d\u0435\u043d\u043e", + "on": "\u0412\u0456\u0434\u0447\u0438\u043d\u0435\u043d\u043e" } }, "title": "\u0411\u0456\u043d\u0430\u0440\u043d\u0438\u0439 \u0434\u0430\u0442\u0447\u0438\u043a" diff --git a/homeassistant/components/blebox/translations/de.json b/homeassistant/components/blebox/translations/de.json index baf14ba4897..37c8dde54e5 100644 --- a/homeassistant/components/blebox/translations/de.json +++ b/homeassistant/components/blebox/translations/de.json @@ -2,11 +2,11 @@ "config": { "abort": { "address_already_configured": "Ein BleBox-Ger\u00e4t ist bereits unter {address} konfiguriert.", - "already_configured": "Dieses BleBox-Ger\u00e4t ist bereits konfiguriert." + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung mit dem BleBox-Ger\u00e4t nicht m\u00f6glich. (\u00dcberpr\u00fcfen Sie die Protokolle auf Fehler).", - "unknown": "Unbekannter Fehler beim Anschlie\u00dfen an das BleBox-Ger\u00e4t. (Pr\u00fcfen Sie die Protokolle auf Fehler).", + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler", "unsupported_version": "Das BleBox-Ger\u00e4t hat eine veraltete Firmware. Bitte aktualisieren Sie es zuerst." }, "flow_title": "BleBox-Ger\u00e4t: {name} ( {host} )", diff --git a/homeassistant/components/blebox/translations/tr.json b/homeassistant/components/blebox/translations/tr.json new file mode 100644 index 00000000000..31df3fb5e30 --- /dev/null +++ b/homeassistant/components/blebox/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "address_already_configured": "Bir BleBox cihaz\u0131 zaten {address} yap\u0131land\u0131r\u0131lm\u0131\u015ft\u0131r.", + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "\u0130p Adresi", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/uk.json b/homeassistant/components/blebox/translations/uk.json new file mode 100644 index 00000000000..fb10807acff --- /dev/null +++ b/homeassistant/components/blebox/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "address_already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0437 \u0430\u0434\u0440\u0435\u0441\u043e\u044e {address} \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435.", + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430", + "unsupported_version": "\u041c\u0456\u043a\u0440\u043e\u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0430 \u0446\u044c\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0437\u0430\u0441\u0442\u0430\u0440\u0456\u043b\u0430. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u043d\u043e\u0432\u0456\u0442\u044c \u0457\u0457." + }, + "flow_title": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 BleBox: {name} ({host})", + "step": { + "user": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 BleBox.", + "title": "BleBox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/de.json b/homeassistant/components/blink/translations/de.json index f5116110a09..d4f65329f9b 100644 --- a/homeassistant/components/blink/translations/de.json +++ b/homeassistant/components/blink/translations/de.json @@ -4,7 +4,8 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, @@ -13,7 +14,7 @@ "data": { "2fa": "Zwei-Faktor Authentifizierungscode" }, - "description": "Geben Sie die an Ihre E-Mail gesendete Pin ein. Wenn die E-Mail keine PIN enth\u00e4lt, lassen Sie das Feld leer.", + "description": "Gib die an deine E-Mail gesendete Pin ein. Wenn die E-Mail keine PIN enth\u00e4lt, lass das Feld leer.", "title": "Zwei-Faktor-Authentifizierung" }, "user": { diff --git a/homeassistant/components/blink/translations/tr.json b/homeassistant/components/blink/translations/tr.json new file mode 100644 index 00000000000..8193ff9d8be --- /dev/null +++ b/homeassistant/components/blink/translations/tr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_access_token": "Ge\u00e7ersiz eri\u015fim belirteci", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "2fa": { + "description": "E-postan\u0131za g\u00f6nderilen PIN kodunu girin" + }, + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/uk.json b/homeassistant/components/blink/translations/uk.json new file mode 100644 index 00000000000..c45bf7b6651 --- /dev/null +++ b/homeassistant/components/blink/translations/uk.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_access_token": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443.", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "2fa": { + "data": { + "2fa": "\u041a\u043e\u0434 \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c PIN-\u043a\u043e\u0434, \u043d\u0430\u0434\u0456\u0441\u043b\u0430\u043d\u0438\u0439 \u043d\u0430 \u0412\u0430\u0448\u0443 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0443 \u043f\u043e\u0448\u0442\u0443", + "title": "\u0414\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "Blink" + } + } + }, + "options": { + "step": { + "simple_options": { + "data": { + "scan_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 Blink", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Blink" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/ca.json b/homeassistant/components/bmw_connected_drive/translations/ca.json new file mode 100644 index 00000000000..d6bd70064c3 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/ca.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja ha estat configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "region": "Regi\u00f3 de ConnectedDrive", + "username": "Nom d'usuari" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Nom\u00e9s de lectura (nom\u00e9s sensors i notificacions, sense execuci\u00f3 de serveis, sense bloqueig)", + "use_location": "Utilitza la ubicaci\u00f3 de Home Assistant per a les crides de localitzaci\u00f3 del cotxe (obligatori per a vehicles que no siguin i3/i8 produ\u00efts abans del 7/2014)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/cs.json b/homeassistant/components/bmw_connected_drive/translations/cs.json new file mode 100644 index 00000000000..665dccd443d --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/de.json b/homeassistant/components/bmw_connected_drive/translations/de.json new file mode 100644 index 00000000000..12a870b4cc9 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/en.json b/homeassistant/components/bmw_connected_drive/translations/en.json index f194c8a3444..dedd84d070b 100644 --- a/homeassistant/components/bmw_connected_drive/translations/en.json +++ b/homeassistant/components/bmw_connected_drive/translations/en.json @@ -11,7 +11,6 @@ "user": { "data": { "password": "Password", - "read_only": "Read-only", "region": "ConnectedDrive Region", "username": "Username" } diff --git a/homeassistant/components/bmw_connected_drive/translations/es.json b/homeassistant/components/bmw_connected_drive/translations/es.json new file mode 100644 index 00000000000..65ed9643f89 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/es.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "region": "Regi\u00f3n de ConnectedDrive", + "username": "Nombre de usuario" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "S\u00f3lo lectura (s\u00f3lo sensores y notificaci\u00f3n, sin ejecuci\u00f3n de servicios, sin bloqueo)", + "use_location": "Usar la ubicaci\u00f3n de Home Assistant para las encuestas de localizaci\u00f3n de autom\u00f3viles (necesario para los veh\u00edculos no i3/i8 producidos antes del 7/2014)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/et.json b/homeassistant/components/bmw_connected_drive/translations/et.json new file mode 100644 index 00000000000..f28209a1e7a --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/et.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine" + }, + "step": { + "user": { + "data": { + "password": "Salas\u00f5na", + "region": "ConnectedDrive'i piirkond", + "username": "Kasutajanimi" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Kirjutuskaitstud (ainult andurid ja teavitused, ei k\u00e4ivita teenuseid, kood puudub)", + "use_location": "Kasuta HA asukohta auto asukoha k\u00fcsitluste jaoks (n\u00f5utav enne 7/2014 toodetud muude kui i3 / i8 s\u00f5idukite jaoks)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/fr.json b/homeassistant/components/bmw_connected_drive/translations/fr.json new file mode 100644 index 00000000000..1b8f562669f --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/fr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec \u00e0 la connexion", + "invalid_auth": "Authentification invalide" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "region": "R\u00e9gion ConnectedDrive", + "username": "Nom d'utilisateur" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Lecture seule (uniquement capteurs et notification, pas d'ex\u00e9cution de services, pas de verrouillage)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/it.json b/homeassistant/components/bmw_connected_drive/translations/it.json new file mode 100644 index 00000000000..277ed189c43 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/it.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" + }, + "step": { + "user": { + "data": { + "password": "Password", + "region": "Regione ConnectedDrive", + "username": "Nome utente" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Sola lettura (solo sensori e notifica, nessuna esecuzione di servizi, nessun blocco)", + "use_location": "Usa la posizione di Home Assistant per richieste sulla posizione dell'auto (richiesto per veicoli non i3/i8 prodotti prima del 7/2014)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/lb.json b/homeassistant/components/bmw_connected_drive/translations/lb.json new file mode 100644 index 00000000000..9ebbe919f8b --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/lb.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Kont ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "region": "ConnectedDrive Regioun" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/no.json b/homeassistant/components/bmw_connected_drive/translations/no.json new file mode 100644 index 00000000000..f1715c550db --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/no.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "region": "ConnectedDrive-region", + "username": "Brukernavn" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Skrivebeskyttet (bare sensorer og varsler, ingen utf\u00f8relse av tjenester, ingen l\u00e5s)", + "use_location": "Bruk Home Assistant plassering for avstemningssteder for biler (p\u00e5krevd for ikke i3 / i8-kj\u00f8ret\u00f8y produsert f\u00f8r 7/2014)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/pl.json b/homeassistant/components/bmw_connected_drive/translations/pl.json new file mode 100644 index 00000000000..70467c6f9b9 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/pl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "region": "Region ConnectedDrive", + "username": "Nazwa u\u017cytkownika" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Tylko odczyt (tylko czujniki i powiadomienia, brak wykonywania us\u0142ug, brak blokady)", + "use_location": "U\u017cyj lokalizacji Home Assistant do sondowania lokalizacji samochodu (wymagane w przypadku pojazd\u00f3w innych ni\u017c i3/i8 wyprodukowanych przed 7/2014)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/pt.json b/homeassistant/components/bmw_connected_drive/translations/pt.json new file mode 100644 index 00000000000..3814c892bd1 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/pt.json @@ -0,0 +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" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/ru.json b/homeassistant/components/bmw_connected_drive/translations/ru.json new file mode 100644 index 00000000000..0840affcef4 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/ru.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "region": "\u0420\u0435\u0433\u0438\u043e\u043d ConnectedDrive", + "username": "\u041b\u043e\u0433\u0438\u043d" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "\u0422\u043e\u043b\u044c\u043a\u043e \u0447\u0442\u0435\u043d\u0438\u0435 (\u0442\u043e\u043b\u044c\u043a\u043e \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f, \u0431\u0435\u0437 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0441\u043b\u0443\u0436\u0431, \u0431\u0435\u0437 \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0438)", + "use_location": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Home Assistant \u0434\u043b\u044f \u043e\u043f\u0440\u043e\u0441\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0438\u043b\u0435\u0439 (\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0438\u043b\u0435\u0439 \u043d\u0435 i3/i8, \u0432\u044b\u043f\u0443\u0449\u0435\u043d\u043d\u044b\u0445 \u0434\u043e 7/2014)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/tr.json b/homeassistant/components/bmw_connected_drive/translations/tr.json new file mode 100644 index 00000000000..153aa4126b0 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/uk.json b/homeassistant/components/bmw_connected_drive/translations/uk.json new file mode 100644 index 00000000000..68cdee2a66f --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/uk.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "region": "ConnectedDrive Region", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "\u041b\u0438\u0448\u0435 \u0434\u043b\u044f \u0447\u0438\u0442\u0430\u043d\u043d\u044f (\u043b\u0438\u0448\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0442\u0430 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u043d\u044f, \u0431\u0435\u0437 \u0437\u0430\u043f\u0443\u0441\u043a\u0443 \u0441\u0435\u0440\u0432\u0456\u0441\u0456\u0432, \u0431\u0435\u0437 \u0431\u043b\u043e\u043a\u0443\u0432\u0430\u043d\u043d\u044f)", + "use_location": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f Home Assistant \u0434\u043b\u044f \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u044c \u043c\u0456\u0441\u0446\u044f \u0440\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0456\u043b\u0456\u0432 (\u043e\u0431\u043e\u0432\u2019\u044f\u0437\u043a\u043e\u0432\u043e \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0456\u043b\u0456\u0432, \u0449\u043e \u043d\u0435 \u043d\u0430\u043b\u0435\u0436\u0430\u0442\u044c \u0434\u043e i3/i8, \u0432\u0438\u0433\u043e\u0442\u043e\u0432\u043b\u0435\u043d\u0438\u0445 \u0434\u043e 7/2014)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/zh-Hant.json b/homeassistant/components/bmw_connected_drive/translations/zh-Hant.json new file mode 100644 index 00000000000..fde5e1e3c94 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/zh-Hant.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "region": "ConnectedDrive \u5340\u57df", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "\u552f\u8b80\uff08\u50c5\u652f\u63f4\u50b3\u611f\u5668\u8207\u901a\u77e5\uff0c\u4e0d\n\u5305\u542b\u670d\u52d9\u8207\u9396\u5b9a\uff09", + "use_location": "\u4f7f\u7528 Home Assistant \u4f4d\u7f6e\u53d6\u5f97\u6c7d\u8eca\u4f4d\u7f6e\uff08\u9700\u8981\u70ba2014/7 \u524d\u751f\u7522\u7684\u975ei3/i8 \u8eca\u6b3e\uff09" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/de.json b/homeassistant/components/bond/translations/de.json index 393232025dd..14f86a30bb2 100644 --- a/homeassistant/components/bond/translations/de.json +++ b/homeassistant/components/bond/translations/de.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindung nicht m\u00f6glich", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/bond/translations/tr.json b/homeassistant/components/bond/translations/tr.json new file mode 100644 index 00000000000..3488480a218 --- /dev/null +++ b/homeassistant/components/bond/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "confirm": { + "data": { + "access_token": "Eri\u015fim Belirteci" + } + }, + "user": { + "data": { + "access_token": "Eri\u015fim Belirteci", + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/uk.json b/homeassistant/components/bond/translations/uk.json index d7da60ea178..95ede3d4329 100644 --- a/homeassistant/components/bond/translations/uk.json +++ b/homeassistant/components/bond/translations/uk.json @@ -1,7 +1,28 @@ { "config": { "abort": { - "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "old_firmware": "\u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u043e\u043d\u043e\u0432\u0438\u0442\u0438 \u043c\u0456\u043a\u0440\u043e\u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e. \u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u043d\u0430 \u0432\u0435\u0440\u0441\u0456\u044f \u0437\u0430\u0441\u0442\u0430\u0440\u0456\u043b\u0430 \u0456 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0454\u044e.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "Bond {bond_id} ({host})", + "step": { + "confirm": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443" + }, + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {bond_id}?" + }, + "user": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443", + "host": "\u0425\u043e\u0441\u0442" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/de.json b/homeassistant/components/braviatv/translations/de.json index b17d42ffaed..8ac8c09e4fe 100644 --- a/homeassistant/components/braviatv/translations/de.json +++ b/homeassistant/components/braviatv/translations/de.json @@ -1,15 +1,18 @@ { "config": { "abort": { - "already_configured": "Dieser Fernseher ist bereits konfiguriert." + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, ung\u00fcltiger Host- oder PIN-Code.", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse", "unsupported_model": "Ihr TV-Modell wird nicht unterst\u00fctzt." }, "step": { "authorize": { + "data": { + "pin": "PIN-Code" + }, "description": "Geben Sie den auf dem Sony Bravia-Fernseher angezeigten PIN-Code ein. \n\nWenn der PIN-Code nicht angezeigt wird, m\u00fcssen Sie die Registrierung von Home Assistant auf Ihrem Fernseher aufheben, gehen Sie daf\u00fcr zu: Einstellungen -> Netzwerk -> Remote - Ger\u00e4teeinstellungen -> Registrierung des entfernten Ger\u00e4ts aufheben.", "title": "Autorisieren Sie Sony Bravia TV" }, diff --git a/homeassistant/components/braviatv/translations/tr.json b/homeassistant/components/braviatv/translations/tr.json new file mode 100644 index 00000000000..0853c8028fc --- /dev/null +++ b/homeassistant/components/braviatv/translations/tr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unsupported_model": "TV modeliniz desteklenmiyor." + }, + "step": { + "authorize": { + "title": "Sony Bravia TV'yi yetkilendirin" + }, + "user": { + "data": { + "host": "Ana Bilgisayar" + }, + "title": "Sony Bravia TV" + } + } + }, + "options": { + "step": { + "user": { + "title": "Sony Bravia TV i\u00e7in se\u00e7enekler" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/uk.json b/homeassistant/components/braviatv/translations/uk.json new file mode 100644 index 00000000000..7f66329c57e --- /dev/null +++ b/homeassistant/components/braviatv/translations/uk.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "no_ip_control": "\u041d\u0430 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0456 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a\u0435\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e IP, \u0430\u0431\u043e \u0446\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_host": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430.", + "unsupported_model": "\u0426\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f." + }, + "step": { + "authorize": { + "data": { + "pin": "PIN-\u043a\u043e\u0434" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c PIN-\u043a\u043e\u0434, \u044f\u043a\u0438\u0439 \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0454\u0442\u044c\u0441\u044f \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 Sony Bravia. \n\n\u042f\u043a\u0449\u043e \u0412\u0438 \u043d\u0435 \u0431\u0430\u0447\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0441\u043a\u0430\u0441\u0443\u0432\u0430\u0442\u0438 \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u044e Home Assistant \u043d\u0430 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0456. \u041f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0432 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 - > \u041c\u0435\u0440\u0435\u0436\u0430 - > \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0456\u0434\u0434\u0430\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e - > \u0421\u043a\u0430\u0441\u0443\u0432\u0430\u0442\u0438 \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u044e \u0432\u0456\u0434\u0434\u0430\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e.", + "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 Sony Bravia" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0454\u044e \u043f\u043e \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457:\nhttps://www.home-assistant.io/integrations/braviatv", + "title": "\u0422\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 Sony Bravia" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "ignored_sources": "\u0421\u043f\u0438\u0441\u043e\u043a \u0456\u0433\u043d\u043e\u0440\u043e\u0432\u0430\u043d\u0438\u0445 \u0434\u0436\u0435\u0440\u0435\u043b" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 Sony Bravia" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/de.json b/homeassistant/components/broadlink/translations/de.json index f915040635f..5704efe37c6 100644 --- a/homeassistant/components/broadlink/translations/de.json +++ b/homeassistant/components/broadlink/translations/de.json @@ -1,13 +1,15 @@ { "config": { "abort": { - "cannot_connect": "Verbindungsfehler", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse", "not_supported": "Ger\u00e4t nicht unterst\u00fctzt", "unknown": "Unerwarteter Fehler" }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/broadlink/translations/tr.json b/homeassistant/components/broadlink/translations/tr.json new file mode 100644 index 00000000000..d37a3203476 --- /dev/null +++ b/homeassistant/components/broadlink/translations/tr.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "not_supported": "Cihaz desteklenmiyor", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "auth": { + "title": "Cihaza kimlik do\u011frulama" + }, + "finish": { + "title": "Cihaz i\u00e7in bir isim se\u00e7in" + }, + "reset": { + "title": "Cihaz\u0131n kilidini a\u00e7\u0131n" + }, + "unlock": { + "data": { + "unlock": "Evet, yap." + }, + "title": "Cihaz\u0131n kilidini a\u00e7\u0131n (iste\u011fe ba\u011fl\u0131)" + }, + "user": { + "data": { + "host": "Ana Bilgisayar", + "timeout": "Zaman a\u015f\u0131m\u0131" + }, + "title": "Cihaza ba\u011flan\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/uk.json b/homeassistant/components/broadlink/translations/uk.json new file mode 100644 index 00000000000..ea3e3e75cd6 --- /dev/null +++ b/homeassistant/components/broadlink/translations/uk.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_host": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430.", + "not_supported": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_host": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "{name} ({model}, {host})", + "step": { + "auth": { + "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f \u043d\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457" + }, + "finish": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "title": "\u0412\u043a\u0430\u0436\u0456\u0442\u044c \u043d\u0430\u0437\u0432\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "reset": { + "description": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 {name} ({model}, {host}) \u0437\u0430\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e. \u0414\u043e\u0442\u0440\u0438\u043c\u0443\u0439\u0442\u0435\u0441\u044f \u0446\u0438\u0445 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0439, \u0449\u043e\u0431 \u0440\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0432\u0430\u0442\u0438 \u0439\u043e\u0433\u043e:\n 1. \u0412\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0443 Broadlink.\n 2. \u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439.\n 3. \u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c `...` \u0432 \u043f\u0440\u0430\u0432\u043e\u043c\u0443 \u0432\u0435\u0440\u0445\u043d\u044c\u043e\u043c\u0443 \u043a\u0443\u0442\u0456.\n 4. \u041f\u0440\u043e\u043a\u0440\u0443\u0442\u0456\u0442\u044c \u0441\u0442\u043e\u0440\u0456\u043d\u043a\u0443 \u0432\u043d\u0438\u0437.\n 5. \u0412\u0438\u043c\u043a\u043d\u0456\u0442\u044c \u0431\u043b\u043e\u043a\u0443\u0432\u0430\u043d\u043d\u044f.", + "title": "\u0420\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "unlock": { + "data": { + "unlock": "\u0422\u0430\u043a, \u0437\u0440\u043e\u0431\u0438\u0442\u0438 \u0446\u0435." + }, + "description": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 {name} ({model}, {host}) \u0437\u0430\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e. \u0426\u0435 \u043c\u043e\u0436\u0435 \u043f\u0440\u0438\u0432\u0435\u0441\u0442\u0438 \u0434\u043e \u043f\u0440\u043e\u0431\u043b\u0435\u043c \u0437 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0454\u044e \u0432 Home Assistant. \u0425\u043e\u0447\u0435\u0442\u0435 \u0439\u043e\u0433\u043e \u0440\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0432\u0430\u0442\u0438?", + "title": "\u0420\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442" + }, + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/translations/de.json b/homeassistant/components/brother/translations/de.json index 72bd052cc1d..c2a7ae8ec76 100644 --- a/homeassistant/components/brother/translations/de.json +++ b/homeassistant/components/brother/translations/de.json @@ -5,7 +5,7 @@ "unsupported_model": "Dieses Druckermodell wird nicht unterst\u00fctzt." }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", "snmp_error": "SNMP-Server deaktiviert oder Drucker nicht unterst\u00fctzt.", "wrong_host": " Ung\u00fcltiger Hostname oder IP-Adresse" }, diff --git a/homeassistant/components/brother/translations/tr.json b/homeassistant/components/brother/translations/tr.json index 160a5ecc7b7..cd91a485252 100644 --- a/homeassistant/components/brother/translations/tr.json +++ b/homeassistant/components/brother/translations/tr.json @@ -1,6 +1,20 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "unsupported_model": "Bu yaz\u0131c\u0131 modeli desteklenmiyor." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "flow_title": "Brother Yaz\u0131c\u0131: {model} {serial_number}", "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "type": "Yaz\u0131c\u0131n\u0131n t\u00fcr\u00fc" + } + }, "zeroconf_confirm": { "title": "Ke\u015ffedilen Brother Yaz\u0131c\u0131" } diff --git a/homeassistant/components/brother/translations/uk.json b/homeassistant/components/brother/translations/uk.json new file mode 100644 index 00000000000..ac5943aa85c --- /dev/null +++ b/homeassistant/components/brother/translations/uk.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "unsupported_model": "\u0426\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "snmp_error": "\u0421\u0435\u0440\u0432\u0435\u0440 SNMP \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0430\u0431\u043e \u043f\u0440\u0438\u043d\u0442\u0435\u0440 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f.", + "wrong_host": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430." + }, + "flow_title": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440 Brother: {model} {serial_number}", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "type": "\u0422\u0438\u043f \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430" + }, + "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0454\u044e \u043f\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044e \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457: https://www.home-assistant.io/integrations/brother." + }, + "zeroconf_confirm": { + "data": { + "type": "\u0422\u0438\u043f \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430" + }, + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u0438\u043d\u0442\u0435\u0440 Brother {model} \u0437 \u0441\u0435\u0440\u0456\u0439\u043d\u0438\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c `{serial_number}`?", + "title": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u043d\u0442\u0435\u0440 Brother" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/de.json b/homeassistant/components/bsblan/translations/de.json index 5fd61c0bfed..971e3c1ea8a 100644 --- a/homeassistant/components/bsblan/translations/de.json +++ b/homeassistant/components/bsblan/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/bsblan/translations/tr.json b/homeassistant/components/bsblan/translations/tr.json index 94acde2d0a3..803b5102a07 100644 --- a/homeassistant/components/bsblan/translations/tr.json +++ b/homeassistant/components/bsblan/translations/tr.json @@ -1,9 +1,17 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, "step": { "user": { "data": { + "host": "Ana Bilgisayar", "password": "\u015eifre", + "port": "Port", "username": "Kullan\u0131c\u0131 ad\u0131" } } diff --git a/homeassistant/components/bsblan/translations/uk.json b/homeassistant/components/bsblan/translations/uk.json new file mode 100644 index 00000000000..619f7c8e8a5 --- /dev/null +++ b/homeassistant/components/bsblan/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "flow_title": "BSB-Lan: {name}", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "passkey": "\u041f\u0430\u0440\u043e\u043b\u044c", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 BSB-Lan.", + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/de.json b/homeassistant/components/canary/translations/de.json index eebc9bd5fc3..bdd746c3149 100644 --- a/homeassistant/components/canary/translations/de.json +++ b/homeassistant/components/canary/translations/de.json @@ -1,10 +1,11 @@ { "config": { "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "unknown": "Unerwarteter Fehler" }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "flow_title": "Canary: {name}", "step": { diff --git a/homeassistant/components/canary/translations/tr.json b/homeassistant/components/canary/translations/tr.json new file mode 100644 index 00000000000..6d18629b067 --- /dev/null +++ b/homeassistant/components/canary/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/uk.json b/homeassistant/components/canary/translations/uk.json new file mode 100644 index 00000000000..74327f3ebd6 --- /dev/null +++ b/homeassistant/components/canary/translations/uk.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "flow_title": "Canary: {name}", + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e Canary" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "\u0410\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u0438, \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u0456 \u0432 ffmpeg \u0434\u043b\u044f \u043a\u0430\u043c\u0435\u0440", + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 \u0437\u0430\u043f\u0438\u0442\u0443 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/translations/de.json b/homeassistant/components/cast/translations/de.json index 87f8e7cb2bc..7ff1efb8ee0 100644 --- a/homeassistant/components/cast/translations/de.json +++ b/homeassistant/components/cast/translations/de.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Keine Google Cast Ger\u00e4te im Netzwerk gefunden.", - "single_instance_allowed": "Nur eine einzige Konfiguration von Google Cast ist notwendig." + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden.", + "single_instance_allowed": "Bereits konfiguriert. Es ist nur eine Konfiguration m\u00f6glich." }, "step": { "confirm": { diff --git a/homeassistant/components/cast/translations/tr.json b/homeassistant/components/cast/translations/tr.json new file mode 100644 index 00000000000..8de4663957e --- /dev/null +++ b/homeassistant/components/cast/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/translations/uk.json b/homeassistant/components/cast/translations/uk.json index 783defdca25..292861e9129 100644 --- a/homeassistant/components/cast/translations/uk.json +++ b/homeassistant/components/cast/translations/uk.json @@ -1,8 +1,12 @@ { "config": { + "abort": { + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, "step": { "confirm": { - "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Google Cast?" + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?" } } } diff --git a/homeassistant/components/cert_expiry/translations/tr.json b/homeassistant/components/cert_expiry/translations/tr.json new file mode 100644 index 00000000000..6c05bef3a65 --- /dev/null +++ b/homeassistant/components/cert_expiry/translations/tr.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/translations/uk.json b/homeassistant/components/cert_expiry/translations/uk.json new file mode 100644 index 00000000000..997e12a8cb2 --- /dev/null +++ b/homeassistant/components/cert_expiry/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant.", + "import_failed": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0456\u043c\u043f\u043e\u0440\u0442\u0443 \u0437 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457." + }, + "error": { + "connection_refused": "\u041f\u0440\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u0456 \u0434\u043e \u0445\u043e\u0441\u0442\u0443 \u0431\u0443\u043b\u043e \u0432\u0456\u0434\u043c\u043e\u0432\u043b\u0435\u043d\u043e \u0432 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u0456.", + "connection_timeout": "\u0427\u0430\u0441 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0445\u043e\u0441\u0442\u0430 \u043c\u0438\u043d\u0443\u0432.", + "resolve_failed": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044c \u0434\u043e \u0445\u043e\u0441\u0442\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u0422\u0435\u0440\u043c\u0456\u043d \u0434\u0456\u0457 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430" + } + } + }, + "title": "\u0422\u0435\u0440\u043c\u0456\u043d \u0434\u0456\u0457 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430" +} \ No newline at end of file diff --git a/homeassistant/components/climate/translations/tr.json b/homeassistant/components/climate/translations/tr.json index 0b027dbd87f..201fec4c4b6 100644 --- a/homeassistant/components/climate/translations/tr.json +++ b/homeassistant/components/climate/translations/tr.json @@ -1,4 +1,10 @@ { + "device_automation": { + "action_type": { + "set_hvac_mode": "{entity_name} \u00fczerinde HVAC modunu de\u011fi\u015ftir", + "set_preset_mode": "{entity_name} \u00fczerindeki \u00f6n ayar\u0131 de\u011fi\u015ftir" + } + }, "state": { "_": { "auto": "Otomatik", diff --git a/homeassistant/components/climate/translations/uk.json b/homeassistant/components/climate/translations/uk.json index 8d636c386e5..de6baff021c 100644 --- a/homeassistant/components/climate/translations/uk.json +++ b/homeassistant/components/climate/translations/uk.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "set_hvac_mode": "\u0417\u043c\u0456\u043d\u0438\u0442\u0438 \u0440\u0435\u0436\u0438\u043c HVAC \u043d\u0430 {entity_name}", - "set_preset_mode": "\u0417\u043c\u0456\u043d\u0438\u0442\u0438 \u043f\u043e\u043f\u0435\u0440\u0435\u0434\u043d\u044c\u043e \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043d\u0430 {entity_name}" + "set_hvac_mode": "{entity_name}: \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u0440\u0435\u0436\u0438\u043c \u0440\u043e\u0431\u043e\u0442\u0438", + "set_preset_mode": "{entity_name}: \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u043f\u0440\u0435\u0441\u0435\u0442" }, "condition_type": { - "is_hvac_mode": "{entity_name} \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u0432 \u043f\u0435\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c HVAC", + "is_hvac_mode": "{entity_name} \u0437\u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0432 \u0437\u0430\u0434\u0430\u043d\u043e\u043c\u0443 \u0440\u0435\u0436\u0438\u043c\u0456 \u0440\u043e\u0431\u043e\u0442\u0438", "is_preset_mode": "{entity_name} \u043d\u0430\u0441\u0442\u0440\u043e\u0454\u043d\u043e \u043d\u0430 \u043f\u0435\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c" }, "trigger_type": { - "current_humidity_changed": "{entity_name} \u0432\u0438\u043c\u0456\u0440\u044f\u043d\u0430 \u0432\u043e\u043b\u043e\u0433\u0456\u0441\u0442\u044c \u0437\u043c\u0456\u043d\u0435\u043d\u0430", - "current_temperature_changed": "{entity_name} \u0432\u0438\u043c\u0456\u0440\u044f\u043d\u0443 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0443 \u0437\u043c\u0456\u043d\u0435\u043d\u043e", - "hvac_mode_changed": "{entity_name} \u0420\u0435\u0436\u0438\u043c HVAC \u0437\u043c\u0456\u043d\u0435\u043d\u043e" + "current_humidity_changed": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0432\u0438\u043c\u0456\u0440\u044f\u043d\u043e\u0457 \u0432\u043e\u043b\u043e\u0433\u043e\u0441\u0442\u0456", + "current_temperature_changed": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0432\u0438\u043c\u0456\u0440\u044f\u043d\u043e\u0457 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0438", + "hvac_mode_changed": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0440\u0435\u0436\u0438\u043c \u0440\u043e\u0431\u043e\u0442\u0438" } }, "state": { @@ -21,7 +21,7 @@ "dry": "\u041e\u0441\u0443\u0448\u0435\u043d\u043d\u044f", "fan_only": "\u041b\u0438\u0448\u0435 \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0442\u043e\u0440", "heat": "\u041d\u0430\u0433\u0440\u0456\u0432\u0430\u043d\u043d\u044f", - "heat_cool": "\u041d\u0430\u0433\u0440\u0456\u0432\u0430\u043d\u043d\u044f/\u041e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f", + "heat_cool": "\u041d\u0430\u0433\u0440\u0456\u0432\u0430\u043d\u043d\u044f / \u041e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f", "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e" } }, diff --git a/homeassistant/components/cloud/translations/ca.json b/homeassistant/components/cloud/translations/ca.json index fede749c7dd..4e6a14cd2f0 100644 --- a/homeassistant/components/cloud/translations/ca.json +++ b/homeassistant/components/cloud/translations/ca.json @@ -2,9 +2,9 @@ "system_health": { "info": { "alexa_enabled": "Alexa activada", - "can_reach_cert_server": "Servidor de certificaci\u00f3 accessible", - "can_reach_cloud": "Home Assistant Cloud accessible", - "can_reach_cloud_auth": "Servidor d'autenticaci\u00f3 accessible", + "can_reach_cert_server": "Acc\u00e9s al servidor de certificaci\u00f3", + "can_reach_cloud": "Acc\u00e9s a Home Assistant Cloud", + "can_reach_cloud_auth": "Acc\u00e9s al servidor d'autenticaci\u00f3", "google_enabled": "Google activat", "logged_in": "Sessi\u00f3 iniciada", "relayer_connected": "Encaminador connectat", diff --git a/homeassistant/components/cloud/translations/de.json b/homeassistant/components/cloud/translations/de.json new file mode 100644 index 00000000000..443a5e3aa72 --- /dev/null +++ b/homeassistant/components/cloud/translations/de.json @@ -0,0 +1,15 @@ +{ + "system_health": { + "info": { + "alexa_enabled": "Alexa aktiviert", + "can_reach_cert_server": "Zertifikatsserver erreichbar", + "can_reach_cloud": "Home Assistant Cloud erreichbar", + "can_reach_cloud_auth": "Authentifizierungsserver erreichbar", + "google_enabled": "Google aktiviert", + "logged_in": "Angemeldet", + "remote_connected": "Remote verbunden", + "remote_enabled": "Remote aktiviert", + "subscription_expiration": "Ablauf des Abonnements" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloud/translations/fr.json b/homeassistant/components/cloud/translations/fr.json new file mode 100644 index 00000000000..9bb4029fce0 --- /dev/null +++ b/homeassistant/components/cloud/translations/fr.json @@ -0,0 +1,16 @@ +{ + "system_health": { + "info": { + "alexa_enabled": "Alexa activ\u00e9", + "can_reach_cert_server": "Acc\u00e9der au serveur de certificats", + "can_reach_cloud": "Acc\u00e9der \u00e0 Home Assistant Cloud", + "can_reach_cloud_auth": "Acc\u00e9der au serveur d'authentification", + "google_enabled": "Google activ\u00e9", + "logged_in": "Connect\u00e9", + "relayer_connected": "Relais connect\u00e9", + "remote_connected": "Contr\u00f4le \u00e0 distance connect\u00e9", + "remote_enabled": "Contr\u00f4le \u00e0 distance activ\u00e9", + "subscription_expiration": "Expiration de l'abonnement" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloud/translations/pl.json b/homeassistant/components/cloud/translations/pl.json index 30aaeeb77d1..1df32a14d8e 100644 --- a/homeassistant/components/cloud/translations/pl.json +++ b/homeassistant/components/cloud/translations/pl.json @@ -4,7 +4,7 @@ "alexa_enabled": "Alexa w\u0142\u0105czona", "can_reach_cert_server": "Dost\u0119p do serwera certyfikat\u00f3w", "can_reach_cloud": "Dost\u0119p do chmury Home Assistant", - "can_reach_cloud_auth": "Dost\u0119p do serwera uwierzytelniania", + "can_reach_cloud_auth": "Dost\u0119p do serwera certyfikat\u00f3w", "google_enabled": "Asystent Google w\u0142\u0105czony", "logged_in": "Zalogowany", "relayer_connected": "Relayer pod\u0142\u0105czony", diff --git a/homeassistant/components/cloud/translations/tr.json b/homeassistant/components/cloud/translations/tr.json index 0acb1e6a9a6..75d1c768beb 100644 --- a/homeassistant/components/cloud/translations/tr.json +++ b/homeassistant/components/cloud/translations/tr.json @@ -1,6 +1,9 @@ { "system_health": { "info": { + "alexa_enabled": "Alexa Etkin", + "can_reach_cloud": "Home Assistant Cloud'a ula\u015f\u0131n", + "google_enabled": "Google Etkin", "logged_in": "Giri\u015f Yapt\u0131", "relayer_connected": "Yeniden Katman ba\u011fl\u0131", "remote_connected": "Uzaktan Ba\u011fl\u0131", diff --git a/homeassistant/components/cloud/translations/uk.json b/homeassistant/components/cloud/translations/uk.json new file mode 100644 index 00000000000..a2e68b911e5 --- /dev/null +++ b/homeassistant/components/cloud/translations/uk.json @@ -0,0 +1,16 @@ +{ + "system_health": { + "info": { + "alexa_enabled": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f \u0437 Alexa", + "can_reach_cert_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0456\u0432", + "can_reach_cloud": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e Home Assistant Cloud", + "can_reach_cloud_auth": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457", + "google_enabled": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f \u0437 Google", + "logged_in": "\u0412\u0445\u0456\u0434 \u0443 \u0441\u0438\u0441\u0442\u0435\u043c\u0443", + "relayer_connected": "Relayer \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439", + "remote_connected": "\u0412\u0456\u0434\u0434\u0430\u043b\u0435\u043d\u0438\u0439 \u0434\u043e\u0441\u0442\u0443\u043f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439", + "remote_enabled": "\u0412\u0456\u0434\u0434\u0430\u043b\u0435\u043d\u0438\u0439 \u0434\u043e\u0441\u0442\u0443\u043f \u0430\u043a\u0442\u0438\u0432\u043e\u0432\u0430\u043d\u0438\u0439", + "subscription_expiration": "\u0422\u0435\u0440\u043c\u0456\u043d \u0434\u0456\u0457 \u043f\u0435\u0440\u0435\u0434\u043f\u043b\u0430\u0442\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/de.json b/homeassistant/components/cloudflare/translations/de.json index 809dad5da46..d9858b36f55 100644 --- a/homeassistant/components/cloudflare/translations/de.json +++ b/homeassistant/components/cloudflare/translations/de.json @@ -1,12 +1,15 @@ { "config": { "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "unknown": "Unerwarteter Fehler" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "invalid_zone": "Ung\u00fcltige Zone" }, + "flow_title": "Cloudflare: {name}", "step": { "records": { "data": { @@ -18,6 +21,11 @@ "api_token": "API Token" }, "title": "Mit Cloudflare verbinden" + }, + "zone": { + "data": { + "zone": "Zone" + } } } } diff --git a/homeassistant/components/cloudflare/translations/tr.json b/homeassistant/components/cloudflare/translations/tr.json index b7c7b438804..5d1180961f6 100644 --- a/homeassistant/components/cloudflare/translations/tr.json +++ b/homeassistant/components/cloudflare/translations/tr.json @@ -1,6 +1,12 @@ { "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "unknown": "Beklenmeyen hata" + }, "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "invalid_zone": "Ge\u00e7ersiz b\u00f6lge" }, "flow_title": "Cloudflare: {name}", @@ -12,6 +18,9 @@ "title": "G\u00fcncellenecek Kay\u0131tlar\u0131 Se\u00e7in" }, "user": { + "data": { + "api_token": "API Belirteci" + }, "title": "Cloudflare'ye ba\u011flan\u0131n" }, "zone": { diff --git a/homeassistant/components/cloudflare/translations/uk.json b/homeassistant/components/cloudflare/translations/uk.json new file mode 100644 index 00000000000..425ec2733b8 --- /dev/null +++ b/homeassistant/components/cloudflare/translations/uk.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "invalid_zone": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0437\u043e\u043d\u0430" + }, + "flow_title": "Cloudflare: {name}", + "step": { + "records": { + "data": { + "records": "\u0417\u0430\u043f\u0438\u0441\u0438" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0437\u0430\u043f\u0438\u0441\u0438 \u0434\u043b\u044f \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f" + }, + "user": { + "data": { + "api_token": "\u0422\u043e\u043a\u0435\u043d API" + }, + "description": "\u0414\u043b\u044f \u0446\u0456\u0454\u0457 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0442\u043e\u043a\u0435\u043d API, \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u0438\u0439 \u0437 \u0434\u043e\u0437\u0432\u043e\u043b\u0430\u043c\u0438 Zone: Zone: Read \u0456 Zone: DNS: Edit \u0434\u043b\u044f \u0432\u0441\u0456\u0445 \u0437\u043e\u043d \u0443 \u0432\u0430\u0448\u043e\u043c\u0443 \u043f\u0440\u043e\u0444\u0456\u043b\u0456.", + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e Cloudflare" + }, + "zone": { + "data": { + "zone": "\u0417\u043e\u043d\u0430" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0437\u043e\u043d\u0443 \u0434\u043b\u044f \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/control4/translations/de.json b/homeassistant/components/control4/translations/de.json index f9a5783cd91..399b8d42491 100644 --- a/homeassistant/components/control4/translations/de.json +++ b/homeassistant/components/control4/translations/de.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/control4/translations/tr.json b/homeassistant/components/control4/translations/tr.json new file mode 100644 index 00000000000..aed7e564a76 --- /dev/null +++ b/homeassistant/components/control4/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "\u0130p Adresi", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/control4/translations/uk.json b/homeassistant/components/control4/translations/uk.json index 6c0426eba8f..682d86c5deb 100644 --- a/homeassistant/components/control4/translations/uk.json +++ b/homeassistant/components/control4/translations/uk.json @@ -1,10 +1,21 @@ { "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, "step": { "user": { "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" - } + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0434\u0430\u043d\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 Control4 \u0456 IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u0412\u0430\u0448\u043e\u0433\u043e \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430." } } }, @@ -12,7 +23,7 @@ "step": { "init": { "data": { - "scan_interval": "\u0421\u0435\u043a\u0443\u043d\u0434 \u043c\u0456\u0436 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f\u043c\u0438" + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" } } } diff --git a/homeassistant/components/coolmaster/translations/de.json b/homeassistant/components/coolmaster/translations/de.json index 908dfaa448c..4e58b1ed964 100644 --- a/homeassistant/components/coolmaster/translations/de.json +++ b/homeassistant/components/coolmaster/translations/de.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", "no_units": "Es wurden keine HVAC-Ger\u00e4te im CoolMasterNet-Host gefunden." }, "step": { diff --git a/homeassistant/components/coolmaster/translations/tr.json b/homeassistant/components/coolmaster/translations/tr.json new file mode 100644 index 00000000000..4848a34362c --- /dev/null +++ b/homeassistant/components/coolmaster/translations/tr.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "off": "Kapat\u0131labilir" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/translations/uk.json b/homeassistant/components/coolmaster/translations/uk.json new file mode 100644 index 00000000000..038a7bc48f0 --- /dev/null +++ b/homeassistant/components/coolmaster/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "no_units": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u043d\u0430\u0439\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043e\u043f\u0430\u043b\u0435\u043d\u043d\u044f, \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0446\u0456\u0457 \u0442\u0430 \u043a\u043e\u043d\u0434\u0438\u0446\u0456\u043e\u043d\u0443\u0432\u0430\u043d\u043d\u044f." + }, + "step": { + "user": { + "data": { + "cool": "\u0420\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f", + "dry": "\u0420\u0435\u0436\u0438\u043c \u043e\u0441\u0443\u0448\u0435\u043d\u043d\u044f", + "fan_only": "\u0420\u0435\u0436\u0438\u043c \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0446\u0456\u0457", + "heat": "\u0420\u0435\u0436\u0438\u043c \u043e\u0431\u0456\u0433\u0440\u0456\u0432\u0443", + "heat_cool": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c", + "host": "\u0425\u043e\u0441\u0442", + "off": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + }, + "title": "CoolMasterNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coronavirus/translations/tr.json b/homeassistant/components/coronavirus/translations/tr.json new file mode 100644 index 00000000000..b608d60f824 --- /dev/null +++ b/homeassistant/components/coronavirus/translations/tr.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "country": "\u00dclke" + }, + "title": "\u0130zlemek i\u00e7in bir \u00fclke se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coronavirus/translations/uk.json b/homeassistant/components/coronavirus/translations/uk.json new file mode 100644 index 00000000000..151e7b14d3f --- /dev/null +++ b/homeassistant/components/coronavirus/translations/uk.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "step": { + "user": { + "data": { + "country": "\u041a\u0440\u0430\u0457\u043d\u0430" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043a\u0440\u0430\u0457\u043d\u0443 \u0434\u043b\u044f \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/translations/tr.json b/homeassistant/components/cover/translations/tr.json index 98bc8cdb18d..f042233a6d1 100644 --- a/homeassistant/components/cover/translations/tr.json +++ b/homeassistant/components/cover/translations/tr.json @@ -1,4 +1,10 @@ { + "device_automation": { + "action_type": { + "close": "{entity_name} kapat", + "open": "{entity_name} a\u00e7\u0131n" + } + }, "state": { "_": { "closed": "Kapal\u0131", diff --git a/homeassistant/components/cover/translations/uk.json b/homeassistant/components/cover/translations/uk.json index 66cd0c77c73..ceb49fff3e9 100644 --- a/homeassistant/components/cover/translations/uk.json +++ b/homeassistant/components/cover/translations/uk.json @@ -1,10 +1,29 @@ { "device_automation": { "action_type": { + "close": "{entity_name}: \u0437\u0430\u043a\u0440\u0438\u0442\u0438", + "close_tilt": "{entity_name}: \u0437\u0430\u043a\u0440\u0438\u0442\u0438 \u043b\u0430\u043c\u0435\u043b\u0456", + "open": "{entity_name}: \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u0438", + "open_tilt": "{entity_name}: \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u0438 \u043b\u0430\u043c\u0435\u043b\u0456", + "set_position": "{entity_name}: \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0438 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u043d\u044f", + "set_tilt_position": "{entity_name}: \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0438 \u043d\u0430\u0445\u0438\u043b \u043b\u0430\u043c\u0435\u043b\u0435\u0439", "stop": "\u0417\u0443\u043f\u0438\u043d\u0438\u0442\u0438 {entity_name}" }, + "condition_type": { + "is_closed": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u0438\u0442\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_closing": "{entity_name} \u0437\u0430\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f", + "is_open": "{entity_name} \u0443 \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_opening": "{entity_name} \u0432\u0456\u0434\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f", + "is_position": "{entity_name} \u0437\u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0432 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u043d\u0456", + "is_tilt_position": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \"{entity_name}\" \u043c\u0430\u0454 \u043d\u0430\u0445\u0438\u043b \u043b\u0430\u043c\u0435\u043b\u0435\u0439" + }, "trigger_type": { - "opened": "{entity_name} \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u043e" + "closed": "{entity_name} \u0437\u0430\u043a\u0440\u0438\u0442\u043e", + "closing": "{entity_name} \u0437\u0430\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f", + "opened": "{entity_name} \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u043e", + "opening": "{entity_name} \u0432\u0456\u0434\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f", + "position": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u043d\u044f", + "tilt_position": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u043d\u0430\u0445\u0438\u043b \u043b\u0430\u043c\u0435\u043b\u0435\u0439" } }, "state": { diff --git a/homeassistant/components/daikin/translations/de.json b/homeassistant/components/daikin/translations/de.json index bbac113eb44..dcec53c1569 100644 --- a/homeassistant/components/daikin/translations/de.json +++ b/homeassistant/components/daikin/translations/de.json @@ -2,15 +2,17 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { "user": { "data": { + "api_key": "API-Schl\u00fcssel", "host": "Host", "password": "Passwort" }, diff --git a/homeassistant/components/daikin/translations/pt.json b/homeassistant/components/daikin/translations/pt.json index dd9b538ae8b..d4188fb10f9 100644 --- a/homeassistant/components/daikin/translations/pt.json +++ b/homeassistant/components/daikin/translations/pt.json @@ -16,7 +16,7 @@ "host": "Servidor", "password": "Palavra-passe" }, - "description": "Introduza o endere\u00e7o IP do seu Daikin AC.", + "description": "Introduza Endere\u00e7o IP do seu Daikin AC.\n\nAten\u00e7\u00e3o que [%chave:common::config_flow::data::api_key%] e Palavra-passe s\u00f3 s\u00e3o utilizador pelos dispositivos BRP072Cxx e SKYFi, respectivamente.", "title": "Configurar o Daikin AC" } } diff --git a/homeassistant/components/daikin/translations/tr.json b/homeassistant/components/daikin/translations/tr.json new file mode 100644 index 00000000000..4148bf2b9f1 --- /dev/null +++ b/homeassistant/components/daikin/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "host": "Ana Bilgisayar", + "password": "Parola" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/uk.json b/homeassistant/components/daikin/translations/uk.json new file mode 100644 index 00000000000..648d68d7a81 --- /dev/null +++ b/homeassistant/components/daikin/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441\u0430 \u0412\u0430\u0448\u043e\u0433\u043e Daikin AC. \n\n\u0417\u0432\u0435\u0440\u043d\u0456\u0442\u044c \u0443\u0432\u0430\u0433\u0443, \u0449\u043e \u041a\u043b\u044e\u0447 API \u0456 \u041f\u0430\u0440\u043e\u043b\u044c \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u044e\u0442\u044c\u0441\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044f\u043c\u0438 BRP072Cxx \u0456 SKYFi \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u043d\u043e.", + "title": "Daikin AC" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/cs.json b/homeassistant/components/deconz/translations/cs.json index 3ad72dc9ac9..52cbd607b7f 100644 --- a/homeassistant/components/deconz/translations/cs.json +++ b/homeassistant/components/deconz/translations/cs.json @@ -60,14 +60,15 @@ }, "trigger_type": { "remote_awakened": "Za\u0159\u00edzen\u00ed probuzeno", - "remote_button_double_press": "Dvakr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\"", + "remote_button_double_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto dvakr\u00e1t", + "remote_button_long_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto dlouze", "remote_button_long_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\" po dlouh\u00e9m stisku", - "remote_button_quadruple_press": "\u010cty\u0159ikr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\"", - "remote_button_quintuple_press": "P\u011btkr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\"", + "remote_button_quadruple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto \u010dty\u0159ikr\u00e1t", + "remote_button_quintuple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto p\u011btkr\u00e1t", "remote_button_rotation_stopped": "Oto\u010den\u00ed tla\u010d\u00edtka \"{subtype}\" bylo zastaveno", - "remote_button_short_press": "Stiknuto tla\u010d\u00edtko \"{subtype}\"", + "remote_button_short_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto", "remote_button_short_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\"", - "remote_button_triple_press": "T\u0159ikr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\"", + "remote_button_triple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto t\u0159ikr\u00e1t", "remote_double_tap": "Dvakr\u00e1t poklep\u00e1no na za\u0159\u00edzen\u00ed \"{subtype}\"", "remote_double_tap_any_side": "Za\u0159\u00edzen\u00ed bylo poklep\u00e1no 2x na libovolnou stranu", "remote_flip_180_degrees": "Za\u0159\u00edzen\u00ed p\u0159evr\u00e1ceno o 180 stup\u0148\u016f", diff --git a/homeassistant/components/deconz/translations/de.json b/homeassistant/components/deconz/translations/de.json index f9448705c5d..d7553652412 100644 --- a/homeassistant/components/deconz/translations/de.json +++ b/homeassistant/components/deconz/translations/de.json @@ -2,8 +2,9 @@ "config": { "abort": { "already_configured": "Bridge ist bereits konfiguriert", - "already_in_progress": "Der Konfigurationsablauf f\u00fcr die Bridge wird bereits ausgef\u00fchrt.", - "no_bridges": "Keine deCON-Bridges entdeckt", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_bridges": "Keine deCONZ-Bridges entdeckt", + "no_hardware_available": "Keine Funkhardware an deCONZ angeschlossen", "not_deconz_bridge": "Keine deCONZ Bridge entdeckt", "updated_instance": "deCONZ-Instanz mit neuer Host-Adresse aktualisiert" }, @@ -13,7 +14,7 @@ "flow_title": "deCONZ Zigbee Gateway", "step": { "hassio_confirm": { - "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem deCONZ Gateway herstellt, der vom Add-on hass.io {addon} bereitgestellt wird?", + "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem deCONZ Gateway herstellt, der vom Hass.io Add-on {addon} bereitgestellt wird?", "title": "deCONZ Zigbee Gateway \u00fcber das Hass.io Add-on" }, "link": { @@ -28,7 +29,7 @@ }, "user": { "data": { - "host": "W\u00e4hlen Sie das erkannte deCONZ-Gateway aus" + "host": "W\u00e4hle das erkannte deCONZ-Gateway aus" } } } @@ -92,7 +93,8 @@ "deconz_devices": { "data": { "allow_clip_sensor": "deCONZ CLIP-Sensoren zulassen", - "allow_deconz_groups": "deCONZ-Lichtgruppen zulassen" + "allow_deconz_groups": "deCONZ-Lichtgruppen zulassen", + "allow_new_devices": "Automatisches Hinzuf\u00fcgen von neuen Ger\u00e4ten zulassen" }, "description": "Sichtbarkeit der deCONZ-Ger\u00e4tetypen konfigurieren", "title": "deCONZ-Optionen" diff --git a/homeassistant/components/deconz/translations/no.json b/homeassistant/components/deconz/translations/no.json index 48716379483..c1435dbb186 100644 --- a/homeassistant/components/deconz/translations/no.json +++ b/homeassistant/components/deconz/translations/no.json @@ -14,8 +14,8 @@ "flow_title": "", "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til \u00e5 koble seg til deCONZ-gateway levert av Hass.io-tillegget {addon} ?", - "title": "deCONZ Zigbee gateway via Hass.io tillegg" + "description": "Vil du konfigurere Home Assistant til \u00e5 koble seg til deCONZ-gateway levert av Hass.io-tillegg {addon} ?", + "title": "deCONZ Zigbee gateway via Hass.io-tillegg" }, "link": { "description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere deg med Home Assistant. \n\n 1. G\u00e5 til deCONZ-systeminnstillinger -> Gateway -> Avansert \n 2. Trykk p\u00e5 \"Autentiser app\" knappen", diff --git a/homeassistant/components/deconz/translations/pl.json b/homeassistant/components/deconz/translations/pl.json index 24a3ba61706..1b4eba97096 100644 --- a/homeassistant/components/deconz/translations/pl.json +++ b/homeassistant/components/deconz/translations/pl.json @@ -38,9 +38,9 @@ "trigger_subtype": { "both_buttons": "oba przyciski", "bottom_buttons": "dolne przyciski", - "button_1": "pierwszy przycisk", - "button_2": "drugi przycisk", - "button_3": "trzeci przycisk", + "button_1": "pierwszy", + "button_2": "drugi", + "button_3": "trzeci", "button_4": "czwarty", "close": "zamknij", "dim_down": "zmniejszenie jasno\u015bci", diff --git a/homeassistant/components/deconz/translations/ru.json b/homeassistant/components/deconz/translations/ru.json index a6bc0daaa3e..f22975530d8 100644 --- a/homeassistant/components/deconz/translations/ru.json +++ b/homeassistant/components/deconz/translations/ru.json @@ -14,8 +14,8 @@ "flow_title": "\u0428\u043b\u044e\u0437 Zigbee deCONZ ({host})", "step": { "hassio_confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a deCONZ (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io \"{addon}\")?", - "title": "Zigbee \u0448\u043b\u044e\u0437 deCONZ (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io)" + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a deCONZ (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant \"{addon}\")?", + "title": "Zigbee \u0448\u043b\u044e\u0437 deCONZ (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)" }, "link": { "description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ -> Gateway -> Advanced.\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00abAuthenticate app\u00bb.", diff --git a/homeassistant/components/deconz/translations/tr.json b/homeassistant/components/deconz/translations/tr.json index e73703043f3..22eea1278d7 100644 --- a/homeassistant/components/deconz/translations/tr.json +++ b/homeassistant/components/deconz/translations/tr.json @@ -1,4 +1,47 @@ { + "config": { + "abort": { + "already_configured": "K\u00f6pr\u00fc zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor" + }, + "step": { + "manual_input": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + }, + "user": { + "data": { + "host": "Ke\u015ffedilen deCONZ a\u011f ge\u00e7idini se\u00e7in" + } + } + } + }, + "device_automation": { + "trigger_subtype": { + "side_4": "Yan 4", + "side_5": "Yan 5", + "side_6": "Yan 6" + }, + "trigger_type": { + "remote_awakened": "Cihaz uyand\u0131", + "remote_double_tap": "\" {subtype} \" cihaz\u0131na iki kez hafif\u00e7e vuruldu", + "remote_double_tap_any_side": "Cihaz herhangi bir tarafta \u00e7ift dokundu", + "remote_falling": "Serbest d\u00fc\u015f\u00fc\u015fte cihaz", + "remote_flip_180_degrees": "Cihaz 180 derece d\u00f6nd\u00fcr\u00fcld\u00fc", + "remote_flip_90_degrees": "Cihaz 90 derece d\u00f6nd\u00fcr\u00fcld\u00fc", + "remote_moved": "Cihaz \" {subtype} \" yukar\u0131 ta\u015f\u0131nd\u0131", + "remote_moved_any_side": "Cihaz herhangi bir taraf\u0131 yukar\u0131 gelecek \u015fekilde ta\u015f\u0131nd\u0131", + "remote_rotate_from_side_1": "Cihaz, \"1. taraftan\" \" {subtype} \" e d\u00f6nd\u00fcr\u00fcld\u00fc", + "remote_rotate_from_side_2": "Cihaz, \"2. taraftan\" \" {subtype} \" e d\u00f6nd\u00fcr\u00fcld\u00fc", + "remote_rotate_from_side_3": "Cihaz \"3. taraftan\" \" {subtype} \" e d\u00f6nd\u00fcr\u00fcld\u00fc", + "remote_rotate_from_side_4": "Cihaz, \"4. taraf\" dan \" {subtype} \" e d\u00f6nd\u00fcr\u00fcld\u00fc", + "remote_rotate_from_side_5": "Cihaz, \"5. taraf\" dan \" {subtype} \" e d\u00f6nd\u00fcr\u00fcld\u00fc", + "remote_turned_clockwise": "Cihaz saat y\u00f6n\u00fcnde d\u00f6nd\u00fc", + "remote_turned_counter_clockwise": "Cihaz saat y\u00f6n\u00fcn\u00fcn tersine d\u00f6nd\u00fc" + } + }, "options": { "step": { "deconz_devices": { diff --git a/homeassistant/components/deconz/translations/uk.json b/homeassistant/components/deconz/translations/uk.json new file mode 100644 index 00000000000..b5de362a731 --- /dev/null +++ b/homeassistant/components/deconz/translations/uk.json @@ -0,0 +1,105 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0446\u044c\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u043e.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "no_bridges": "\u0428\u043b\u044e\u0437\u0438 deCONZ \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456.", + "no_hardware_available": "\u0420\u0430\u0434\u0456\u043e\u043e\u0431\u043b\u0430\u0434\u043d\u0430\u043d\u043d\u044f \u043d\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e deCONZ.", + "not_deconz_bridge": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0454 \u0448\u043b\u044e\u0437\u043e\u043c deCONZ.", + "updated_instance": "\u0410\u0434\u0440\u0435\u0441\u0443 \u0445\u043e\u0441\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043e." + }, + "error": { + "no_key": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u043a\u043b\u044e\u0447 API." + }, + "flow_title": "\u0428\u043b\u044e\u0437 Zigbee deCONZ ({host})", + "step": { + "hassio_confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e deCONZ (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io \"{addon}\")?", + "title": "Zigbee \u0448\u043b\u044e\u0437 deCONZ (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io)" + }, + "link": { + "description": "\u0420\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u0457 \u0432 Home Assistant: \n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0434\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u044c \u0441\u0438\u0441\u0442\u0435\u043c\u0438 deCONZ - > Gateway - > Advanced.\n2. \u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043a\u043d\u043e\u043f\u043a\u0443 \u00abAuthenticate app\u00bb.", + "title": "\u0417\u0432'\u044f\u0437\u043e\u043a \u0437 deCONZ" + }, + "manual_input": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + } + }, + "user": { + "data": { + "host": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u0438\u0439 \u0448\u043b\u044e\u0437 deCONZ" + } + } + } + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "\u041e\u0431\u0438\u0434\u0432\u0456 \u043a\u043d\u043e\u043f\u043a\u0438", + "bottom_buttons": "\u041d\u0438\u0436\u043d\u0456 \u043a\u043d\u043e\u043f\u043a\u0438", + "button_1": "\u041f\u0435\u0440\u0448\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_2": "\u0414\u0440\u0443\u0433\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_3": "\u0422\u0440\u0435\u0442\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "close": "\u0417\u0430\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f", + "dim_down": "\u0417\u043c\u0435\u043d\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c", + "dim_up": "\u0417\u0431\u0456\u043b\u044c\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c", + "left": "\u041b\u0456\u0432\u043e\u0440\u0443\u0447", + "open": "\u0412\u0456\u0434\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f", + "right": "\u041f\u0440\u0430\u0432\u043e\u0440\u0443\u0447", + "side_1": "\u0413\u0440\u0430\u043d\u044c 1", + "side_2": "\u0413\u0440\u0430\u043d\u044c 2", + "side_3": "\u0413\u0440\u0430\u043d\u044c 3", + "side_4": "\u0413\u0440\u0430\u043d\u044c 4", + "side_5": "\u0413\u0440\u0430\u043d\u044c 5", + "side_6": "\u0413\u0440\u0430\u043d\u044c 6", + "top_buttons": "\u0412\u0435\u0440\u0445\u043d\u0456 \u043a\u043d\u043e\u043f\u043a\u0438", + "turn_off": "\u0412\u0438\u043c\u043a\u043d\u0443\u0442\u0438", + "turn_on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438" + }, + "trigger_type": { + "remote_awakened": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0440\u043e\u0437\u0431\u0443\u0434\u0438\u043b\u0438", + "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0438", + "remote_button_long_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u0434\u043e\u0432\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430", + "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u0434\u043e\u0432\u0433\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f", + "remote_button_quadruple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0447\u043e\u0442\u0438\u0440\u0438 \u0440\u0430\u0437\u0438", + "remote_button_quintuple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u043f'\u044f\u0442\u044c \u0440\u0430\u0437\u0456\u0432", + "remote_button_rotated": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u0442\u0430", + "remote_button_rotated_fast": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u0442\u0430 \u0448\u0432\u0438\u0434\u043a\u043e", + "remote_button_rotation_stopped": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u0440\u0438\u043f\u0438\u043d\u0438\u043b\u0430 \u043e\u0431\u0435\u0440\u0442\u0430\u043d\u043d\u044f", + "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430", + "remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f", + "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0438", + "remote_double_tap": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c {subtype} \u043f\u043e\u0441\u0442\u0443\u043a\u0430\u043b\u0438 \u0434\u0432\u0456\u0447\u0456", + "remote_double_tap_any_side": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c \u043f\u043e\u0441\u0442\u0443\u043a\u0430\u043b\u0438 \u0434\u0432\u0456\u0447\u0456", + "remote_falling": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0443 \u0432\u0456\u043b\u044c\u043d\u043e\u043c\u0443 \u043f\u0430\u0434\u0456\u043d\u043d\u0456", + "remote_flip_180_degrees": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u043d\u0430 180 \u0433\u0440\u0430\u0434\u0443\u0441\u0456\u0432", + "remote_flip_90_degrees": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u043d\u0430 90 \u0433\u0440\u0430\u0434\u0443\u0441\u0456\u0432", + "remote_gyro_activated": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u043e\u0442\u0440\u044f\u0441\u043b\u0438", + "remote_moved": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0437\u0440\u0443\u0448\u0438\u043b\u0438, \u043a\u043e\u043b\u0438 {subtype} \u0437\u0432\u0435\u0440\u0445\u0443", + "remote_moved_any_side": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u043c\u0456\u0441\u0442\u0438\u043b\u0438", + "remote_rotate_from_side_1": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0437 \u0413\u0440\u0430\u043d\u0456 1 \u043d\u0430 {subtype}", + "remote_rotate_from_side_2": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0437 \u0413\u0440\u0430\u043d\u0456 2 \u043d\u0430 {subtype}", + "remote_rotate_from_side_3": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0437 \u0413\u0440\u0430\u043d\u0456 3 \u043d\u0430 {subtype}", + "remote_rotate_from_side_4": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0437 \u0413\u0440\u0430\u043d\u0456 4 \u043d\u0430 {subtype}", + "remote_rotate_from_side_5": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0437 \u0413\u0440\u0430\u043d\u0456 5 \u043d\u0430 {subtype}", + "remote_rotate_from_side_6": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0437 \u0413\u0440\u0430\u043d\u0456 6 \u043d\u0430 {subtype}", + "remote_turned_clockwise": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0437\u0430 \u0433\u043e\u0434\u0438\u043d\u043d\u0438\u043a\u043e\u0432\u043e\u044e \u0441\u0442\u0440\u0456\u043b\u043a\u043e\u044e", + "remote_turned_counter_clockwise": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u043f\u0440\u043e\u0442\u0438 \u0433\u043e\u0434\u0438\u043d\u043d\u0438\u043a\u043e\u0432\u043e\u0457 \u0441\u0442\u0440\u0456\u043b\u043a\u0438" + } + }, + "options": { + "step": { + "deconz_devices": { + "data": { + "allow_clip_sensor": "\u0412\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0438 deCONZ CLIP", + "allow_deconz_groups": "\u0412\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u0438 \u0433\u0440\u0443\u043f\u0438 \u043e\u0441\u0432\u0456\u0442\u043b\u0435\u043d\u043d\u044f deCONZ", + "allow_new_devices": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0435 \u0434\u043e\u0434\u0430\u0432\u0430\u043d\u043d\u044f \u043d\u043e\u0432\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0456 \u0442\u0438\u043f\u0456\u0432 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 deCONZ", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f deCONZ" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/tr.json b/homeassistant/components/demo/translations/tr.json new file mode 100644 index 00000000000..1ca389b0b97 --- /dev/null +++ b/homeassistant/components/demo/translations/tr.json @@ -0,0 +1,12 @@ +{ + "options": { + "step": { + "options_1": { + "data": { + "constant": "Sabit" + } + } + } + }, + "title": "Demo" +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/uk.json b/homeassistant/components/demo/translations/uk.json new file mode 100644 index 00000000000..5ac1ac74708 --- /dev/null +++ b/homeassistant/components/demo/translations/uk.json @@ -0,0 +1,21 @@ +{ + "options": { + "step": { + "options_1": { + "data": { + "bool": "\u041b\u043e\u0433\u0456\u0447\u043d\u0438\u0439", + "constant": "\u041f\u043e\u0441\u0442\u0456\u0439\u043d\u0430", + "int": "\u0427\u0438\u0441\u043b\u043e\u0432\u0438\u0439" + } + }, + "options_2": { + "data": { + "multi": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0434\u0435\u043a\u0456\u043b\u044c\u043a\u0430", + "select": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u043f\u0446\u0456\u044e", + "string": "\u0421\u0442\u0440\u043e\u043a\u043e\u0432\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f" + } + } + } + }, + "title": "\u0414\u0435\u043c\u043e" +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/de.json b/homeassistant/components/denonavr/translations/de.json index 5af7d3393e2..f52e6303091 100644 --- a/homeassistant/components/denonavr/translations/de.json +++ b/homeassistant/components/denonavr/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt" + }, "step": { "select": { "data": { diff --git a/homeassistant/components/denonavr/translations/tr.json b/homeassistant/components/denonavr/translations/tr.json new file mode 100644 index 00000000000..f618d3a3038 --- /dev/null +++ b/homeassistant/components/denonavr/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flan\u0131lamad\u0131, l\u00fctfen tekrar deneyin, ana g\u00fc\u00e7 ve ethernet kablolar\u0131n\u0131n ba\u011flant\u0131s\u0131n\u0131 kesip yeniden ba\u011flamak yard\u0131mc\u0131 olabilir" + }, + "step": { + "user": { + "data": { + "host": "\u0130p Adresi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/uk.json b/homeassistant/components/denonavr/translations/uk.json new file mode 100644 index 00000000000..efb4cb41777 --- /dev/null +++ b/homeassistant/components/denonavr/translations/uk.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "cannot_connect": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437. \u042f\u043a\u0449\u043e \u0446\u0435 \u043d\u0435 \u0441\u043f\u0440\u0430\u0446\u044e\u0432\u0430\u043b\u043e, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043a\u0430\u0431\u0435\u043b\u044c Ethernet \u0456 \u043a\u0430\u0431\u0435\u043b\u044c \u0436\u0438\u0432\u043b\u0435\u043d\u043d\u044f.", + "not_denonavr_manufacturer": "\u0426\u0435 \u043d\u0435 \u0440\u0435\u0441\u0438\u0432\u0435\u0440 Denon. \u0412\u0438\u0440\u043e\u0431\u043d\u0438\u043a \u043d\u0435 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u0430\u0454.", + "not_denonavr_missing": "\u041d\u0435\u043f\u043e\u0432\u043d\u0430 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044f \u0434\u043b\u044f \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e." + }, + "error": { + "discovery_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u043d\u0430\u0439\u0442\u0438 \u0440\u0435\u0441\u0438\u0432\u0435\u0440 Denon." + }, + "flow_title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon: {name}", + "step": { + "confirm": { + "description": "\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0456\u0442\u044c \u0434\u043e\u0434\u0430\u0432\u0430\u043d\u043d\u044f \u0440\u0435\u0441\u0438\u0432\u0435\u0440\u0430", + "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon" + }, + "select": { + "data": { + "select_host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430" + }, + "description": "\u041f\u043e\u0447\u043d\u0456\u0442\u044c \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u043d\u043e\u0432\u0443, \u044f\u043a\u0449\u043e \u0432\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 \u0456\u043d\u0448\u0438\u0439 \u0440\u0435\u0441\u0438\u0432\u0435\u0440", + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0440\u0435\u0441\u0438\u0432\u0435\u0440, \u044f\u043a\u0438\u0439 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438" + }, + "user": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430" + }, + "description": "\u042f\u043a\u0449\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u0432\u043a\u0430\u0437\u0430\u043d\u0430, \u0431\u0443\u0434\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f", + "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_all_sources": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u0432\u0441\u0456 \u0434\u0436\u0435\u0440\u0435\u043b\u0430", + "zone2": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u043e\u043d\u0438 2", + "zone3": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u043e\u043d\u0438 3" + }, + "description": "\u0414\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f", + "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/translations/de.json b/homeassistant/components/device_tracker/translations/de.json index 651805dcb14..fe59183e67a 100644 --- a/homeassistant/components/device_tracker/translations/de.json +++ b/homeassistant/components/device_tracker/translations/de.json @@ -1,8 +1,12 @@ { "device_automation": { "condition_type": { - "is_home": "{entity_name} ist Zuhause", - "is_not_home": "{entity_name} ist nicht zu Hause" + "is_home": "{entity_name} ist zuhause", + "is_not_home": "{entity_name} ist nicht zuhause" + }, + "trigger_type": { + "enters": "{entity_name} betritt einen Bereich", + "leaves": "{entity_name} verl\u00e4sst einen Bereich" } }, "state": { diff --git a/homeassistant/components/device_tracker/translations/uk.json b/homeassistant/components/device_tracker/translations/uk.json index f49c7acc0e3..87945d2a19a 100644 --- a/homeassistant/components/device_tracker/translations/uk.json +++ b/homeassistant/components/device_tracker/translations/uk.json @@ -1,8 +1,18 @@ { + "device_automation": { + "condition_type": { + "is_home": "{entity_name} \u0432\u0434\u043e\u043c\u0430", + "is_not_home": "{entity_name} \u043d\u0435 \u0432\u0434\u043e\u043c\u0430" + }, + "trigger_type": { + "enters": "{entity_name} \u0432\u0445\u043e\u0434\u0438\u0442\u044c \u0432 \u0437\u043e\u043d\u0443", + "leaves": "{entity_name} \u043f\u043e\u043a\u0438\u0434\u0430\u0454 \u0437\u043e\u043d\u0443" + } + }, "state": { "_": { "home": "\u0412\u0434\u043e\u043c\u0430", - "not_home": "\u0412\u0456\u0434\u0441\u0443\u0442\u043d\u0456\u0439" + "not_home": "\u041d\u0435 \u0432\u0434\u043e\u043c\u0430" } }, "title": "\u0422\u0440\u0435\u043a\u0435\u0440 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" diff --git a/homeassistant/components/devolo_home_control/translations/de.json b/homeassistant/components/devolo_home_control/translations/de.json index 112daf582b3..6cf7ed3c821 100644 --- a/homeassistant/components/devolo_home_control/translations/de.json +++ b/homeassistant/components/devolo_home_control/translations/de.json @@ -1,7 +1,10 @@ { "config": { "abort": { - "already_configured": "Diese Home Control Zentral wird bereits verwendet." + "already_configured": "Konto wurde bereits konfiguriert" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { "user": { @@ -9,7 +12,7 @@ "home_control_url": "Home Control URL", "mydevolo_url": "mydevolo URL", "password": "Passwort", - "username": "E-Mail-Adresse / devolo ID" + "username": "E-Mail / devolo ID" } } } diff --git a/homeassistant/components/devolo_home_control/translations/tr.json b/homeassistant/components/devolo_home_control/translations/tr.json new file mode 100644 index 00000000000..4c6b158f694 --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "E-posta / devolo ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/uk.json b/homeassistant/components/devolo_home_control/translations/uk.json new file mode 100644 index 00000000000..d230d1918f5 --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "user": { + "data": { + "home_control_url": "Home Control URL-\u0430\u0434\u0440\u0435\u0441\u0430", + "mydevolo_url": "mydevolo URL-\u0430\u0434\u0440\u0435\u0441\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438 / devolo ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/de.json b/homeassistant/components/dexcom/translations/de.json index fadb459a3d3..d567dd6b611 100644 --- a/homeassistant/components/dexcom/translations/de.json +++ b/homeassistant/components/dexcom/translations/de.json @@ -4,7 +4,8 @@ "already_configured": "Konto ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/dexcom/translations/fr.json b/homeassistant/components/dexcom/translations/fr.json index d10643a3c1e..095c769a1be 100644 --- a/homeassistant/components/dexcom/translations/fr.json +++ b/homeassistant/components/dexcom/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9" + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" }, "error": { "cannot_connect": "\u00c9chec de connexion", diff --git a/homeassistant/components/dexcom/translations/tr.json b/homeassistant/components/dexcom/translations/tr.json index 80638d181b2..ec93dc078af 100644 --- a/homeassistant/components/dexcom/translations/tr.json +++ b/homeassistant/components/dexcom/translations/tr.json @@ -2,6 +2,28 @@ "config": { "abort": { "already_configured": "Hesap zaten konfig\u00fcre edilmi\u015fi durumda" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "\u00d6l\u00e7\u00fc birimi" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/uk.json b/homeassistant/components/dexcom/translations/uk.json new file mode 100644 index 00000000000..66727af90d1 --- /dev/null +++ b/homeassistant/components/dexcom/translations/uk.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "server": "\u0421\u0435\u0440\u0432\u0435\u0440", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0412\u0430\u0448\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456.", + "title": "Dexcom" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "\u041e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u0443" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/translations/de.json b/homeassistant/components/dialogflow/translations/de.json index f1853107cc2..2035b818b44 100644 --- a/homeassistant/components/dialogflow/translations/de.json +++ b/homeassistant/components/dialogflow/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", + "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." + }, "create_entry": { "default": "Um Ereignisse an den Home Assistant zu senden, musst du [Webhook-Integration von Dialogflow]({dialogflow_url}) einrichten. \n\nF\u00fclle die folgenden Informationen aus: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhaltstyp: application / json \n\nWeitere Informationen findest du in der [Dokumentation]({docs_url})." }, diff --git a/homeassistant/components/dialogflow/translations/tr.json b/homeassistant/components/dialogflow/translations/tr.json new file mode 100644 index 00000000000..84adcdf8225 --- /dev/null +++ b/homeassistant/components/dialogflow/translations/tr.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/translations/uk.json b/homeassistant/components/dialogflow/translations/uk.json new file mode 100644 index 00000000000..625d2db78dc --- /dev/null +++ b/homeassistant/components/dialogflow/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f [Dialogflow]({dialogflow_url}). \n\n\u0414\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457." + }, + "step": { + "user": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Dialogflow?", + "title": "Dialogflow" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/translations/tr.json b/homeassistant/components/directv/translations/tr.json new file mode 100644 index 00000000000..daca8f1ef62 --- /dev/null +++ b/homeassistant/components/directv/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "ssdp_confirm": { + "description": "{name} kurmak istiyor musunuz?" + }, + "user": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/translations/uk.json b/homeassistant/components/directv/translations/uk.json new file mode 100644 index 00000000000..5371f638e3d --- /dev/null +++ b/homeassistant/components/directv/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "flow_title": "DirecTV: {name}", + "step": { + "ssdp_confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name}?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/de.json b/homeassistant/components/doorbird/translations/de.json index 62bb11d6a8c..0d6bef7a63f 100644 --- a/homeassistant/components/doorbird/translations/de.json +++ b/homeassistant/components/doorbird/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dieser DoorBird ist bereits konfiguriert", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "link_local_address": "Lokale Linkadressen werden nicht unterst\u00fctzt", "not_doorbird_device": "Dieses Ger\u00e4t ist kein DoorBird" }, diff --git a/homeassistant/components/doorbird/translations/tr.json b/homeassistant/components/doorbird/translations/tr.json new file mode 100644 index 00000000000..d7a1ca8a93a --- /dev/null +++ b/homeassistant/components/doorbird/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "name": "Cihaz ad\u0131", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/uk.json b/homeassistant/components/doorbird/translations/uk.json new file mode 100644 index 00000000000..07bbdfacafe --- /dev/null +++ b/homeassistant/components/doorbird/translations/uk.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "link_local_address": "\u041f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0456 \u0430\u0434\u0440\u0435\u0441\u0438 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f.", + "not_doorbird_device": "\u0426\u0435 \u043d\u0435 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 DoorBird." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "DoorBird {name} ({host})", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e DoorBird" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "events": "\u0421\u043f\u0438\u0441\u043e\u043a \u043f\u043e\u0434\u0456\u0439 \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u043c\u0443." + }, + "description": "\u0414\u043e\u0434\u0430\u0439\u0442\u0435 \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u043c\u0443 \u043d\u0430\u0437\u0432\u0438 \u043f\u043e\u0434\u0456\u0439, \u044f\u043a\u0435 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0432\u0456\u0434\u0441\u043b\u0456\u0434\u043a\u043e\u0432\u0443\u0432\u0430\u0442\u0438. \u041f\u0456\u0441\u043b\u044f \u0446\u044c\u043e\u0433\u043e, \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a DoorBird, \u0449\u043e\u0431 \u043f\u0440\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438 \u0457\u0445 \u0434\u043e \u043f\u0435\u0432\u043d\u043e\u0457 \u043f\u043e\u0434\u0456\u0457. \u041f\u0440\u0438\u043a\u043b\u0430\u0434: somebody_pressed_the_button, motion. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457: https://www.home-assistant.io/integrations/doorbird/#events." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/de.json b/homeassistant/components/dsmr/translations/de.json new file mode 100644 index 00000000000..da1d200c2a2 --- /dev/null +++ b/homeassistant/components/dsmr/translations/de.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/fr.json b/homeassistant/components/dsmr/translations/fr.json index ea382532a71..cb08a7865b3 100644 --- a/homeassistant/components/dsmr/translations/fr.json +++ b/homeassistant/components/dsmr/translations/fr.json @@ -7,5 +7,15 @@ "one": "", "other": "Autre" } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "Temps minimum entre les mises \u00e0 jour des entit\u00e9s" + }, + "title": "Options DSMR" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/tr.json b/homeassistant/components/dsmr/translations/tr.json index 94c31d0e156..0857160dc51 100644 --- a/homeassistant/components/dsmr/translations/tr.json +++ b/homeassistant/components/dsmr/translations/tr.json @@ -1,4 +1,9 @@ { + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/dsmr/translations/uk.json b/homeassistant/components/dsmr/translations/uk.json new file mode 100644 index 00000000000..9bca6b00c74 --- /dev/null +++ b/homeassistant/components/dsmr/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 DSMR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dunehd/translations/tr.json b/homeassistant/components/dunehd/translations/tr.json new file mode 100644 index 00000000000..0f8c17228fd --- /dev/null +++ b/homeassistant/components/dunehd/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar" + }, + "title": "Dune HD" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dunehd/translations/uk.json b/homeassistant/components/dunehd/translations/uk.json new file mode 100644 index 00000000000..d2f4eadbdcb --- /dev/null +++ b/homeassistant/components/dunehd/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_host": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Dune HD. \u042f\u043a\u0449\u043e \u0443 \u0412\u0430\u0441 \u0432\u0438\u043d\u0438\u043a\u043b\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0437 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u043c, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438 \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e: https://www.home-assistant.io/integrations/dunehd \n\n \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0412\u0430\u0448 \u043f\u043b\u0435\u0454\u0440 \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439.", + "title": "Dune HD" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eafm/translations/de.json b/homeassistant/components/eafm/translations/de.json new file mode 100644 index 00000000000..da1d200c2a2 --- /dev/null +++ b/homeassistant/components/eafm/translations/de.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eafm/translations/tr.json b/homeassistant/components/eafm/translations/tr.json new file mode 100644 index 00000000000..4ed0f406e57 --- /dev/null +++ b/homeassistant/components/eafm/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "no_stations": "Ak\u0131\u015f izleme istasyonu bulunamad\u0131." + }, + "step": { + "user": { + "data": { + "station": "\u0130stasyon" + }, + "title": "Ak\u0131\u015f izleme istasyonunu takip edin" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eafm/translations/uk.json b/homeassistant/components/eafm/translations/uk.json new file mode 100644 index 00000000000..4f84eb92722 --- /dev/null +++ b/homeassistant/components/eafm/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "no_stations": "\u0421\u0442\u0430\u043d\u0446\u0456\u0457 \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443 \u043f\u043e\u0432\u0435\u043d\u0435\u0439 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456." + }, + "step": { + "user": { + "data": { + "station": "\u0421\u0442\u0430\u043d\u0446\u0456\u044f" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0442\u0430\u043d\u0446\u0456\u044e \u0434\u043b\u044f \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443", + "title": "\u0421\u0442\u0430\u043d\u0446\u0456\u0457 \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443 \u043f\u043e\u0432\u0435\u043d\u0435\u0439" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/translations/de.json b/homeassistant/components/ecobee/translations/de.json index bc65fddebdd..0c89a696b2c 100644 --- a/homeassistant/components/ecobee/translations/de.json +++ b/homeassistant/components/ecobee/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits eingerichtet. Es ist nur eine Konfiguration m\u00f6glich." + }, "error": { "pin_request_failed": "Fehler beim Anfordern der PIN von ecobee; Bitte \u00fcberpr\u00fcfe, ob der API-Schl\u00fcssel korrekt ist.", "token_request_failed": "Fehler beim Anfordern eines Token von ecobee; Bitte versuche es erneut." diff --git a/homeassistant/components/ecobee/translations/tr.json b/homeassistant/components/ecobee/translations/tr.json new file mode 100644 index 00000000000..23ece38682d --- /dev/null +++ b/homeassistant/components/ecobee/translations/tr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/translations/uk.json b/homeassistant/components/ecobee/translations/uk.json new file mode 100644 index 00000000000..7cf7df53429 --- /dev/null +++ b/homeassistant/components/ecobee/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "error": { + "pin_request_failed": "\u0421\u0442\u0430\u043b\u0430\u0441\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0456\u0434 \u0447\u0430\u0441 \u0437\u0430\u043f\u0438\u0442\u0443 PIN-\u043a\u043e\u0434\u0443 \u0443 ecobee; \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0456\u0441\u0442\u044c \u043a\u043b\u044e\u0447\u0430 API.", + "token_request_failed": "\u0421\u0442\u0430\u043b\u0430\u0441\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0456\u0434 \u0447\u0430\u0441 \u0437\u0430\u043f\u0438\u0442\u0443 \u0442\u043e\u043a\u0435\u043d\u0456\u0432 \u0443 ecobee; \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437." + }, + "step": { + "authorize": { + "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0440\u043e\u0439\u0434\u0456\u0442\u044c \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e https://www.ecobee.com/consumerportal/index.html \u0456 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e PIN-\u043a\u043e\u0434\u0443: \n\n{pin}\n\n\u041f\u043e\u0442\u0456\u043c \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u041d\u0430\u0434\u0456\u0441\u043b\u0430\u0442\u0438.", + "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f \u0434\u043e\u0434\u0430\u0442\u043a\u0430 \u043d\u0430 ecobee.com" + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u0438\u0439 \u0432\u0456\u0434 ecobee.com.", + "title": "ecobee" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/ca.json b/homeassistant/components/econet/translations/ca.json new file mode 100644 index 00000000000..c53914f8cb9 --- /dev/null +++ b/homeassistant/components/econet/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "step": { + "user": { + "data": { + "email": "Correu electr\u00f2nic", + "password": "Contrasenya" + }, + "title": "Configuraci\u00f3 del compte Rheem EcoNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/en.json b/homeassistant/components/econet/translations/en.json index 4061c094c1f..ad499b0e37c 100644 --- a/homeassistant/components/econet/translations/en.json +++ b/homeassistant/components/econet/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Device is already configured", "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication" }, diff --git a/homeassistant/components/econet/translations/es.json b/homeassistant/components/econet/translations/es.json new file mode 100644 index 00000000000..ac69f8f7be1 --- /dev/null +++ b/homeassistant/components/econet/translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, + "step": { + "user": { + "data": { + "email": "Correo electr\u00f3nico", + "password": "Contrase\u00f1a" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/et.json b/homeassistant/components/econet/translations/et.json new file mode 100644 index 00000000000..349a4d21111 --- /dev/null +++ b/homeassistant/components/econet/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine" + }, + "step": { + "user": { + "data": { + "email": "E-posti aadress", + "password": "Salas\u00f5na" + }, + "title": "Seadista Rheem EcoNeti konto" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/it.json b/homeassistant/components/econet/translations/it.json new file mode 100644 index 00000000000..3074c72b083 --- /dev/null +++ b/homeassistant/components/econet/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Password" + }, + "title": "Imposta account Rheem EcoNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/no.json b/homeassistant/components/econet/translations/no.json new file mode 100644 index 00000000000..f54cedffda8 --- /dev/null +++ b/homeassistant/components/econet/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" + }, + "step": { + "user": { + "data": { + "email": "E-post", + "password": "Passord" + }, + "title": "Konfigurer Rheem EcoNet-konto" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/pl.json b/homeassistant/components/econet/translations/pl.json new file mode 100644 index 00000000000..e5d74de590d --- /dev/null +++ b/homeassistant/components/econet/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "step": { + "user": { + "data": { + "email": "Adres e-mail", + "password": "Has\u0142o" + }, + "title": "Konfiguracja konta Rheem EcoNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/ru.json b/homeassistant/components/econet/translations/ru.json new file mode 100644 index 00000000000..109ded8db99 --- /dev/null +++ b/homeassistant/components/econet/translations/ru.json @@ -0,0 +1,22 @@ +{ + "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.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + }, + "step": { + "user": { + "data": { + "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "title": "Rheem EcoNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/tr.json b/homeassistant/components/econet/translations/tr.json new file mode 100644 index 00000000000..237a87d0268 --- /dev/null +++ b/homeassistant/components/econet/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "\u015eifre" + }, + "title": "Rheem EcoNet Hesab\u0131n\u0131 Kur" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/zh-Hant.json b/homeassistant/components/econet/translations/zh-Hant.json new file mode 100644 index 00000000000..50824c19814 --- /dev/null +++ b/homeassistant/components/econet/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "step": { + "user": { + "data": { + "email": "\u96fb\u5b50\u90f5\u4ef6", + "password": "\u5bc6\u78bc" + }, + "title": "\u8a2d\u5b9a Rheem EcoNet \u5e33\u865f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/translations/de.json b/homeassistant/components/elgato/translations/de.json index 74974604453..1df8f91ecd6 100644 --- a/homeassistant/components/elgato/translations/de.json +++ b/homeassistant/components/elgato/translations/de.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "Dieses Elgato Key Light-Ger\u00e4t ist bereits konfiguriert.", - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "flow_title": "Elgato Key Light: {serial_number}", "step": { diff --git a/homeassistant/components/elgato/translations/tr.json b/homeassistant/components/elgato/translations/tr.json new file mode 100644 index 00000000000..b2d1753fd68 --- /dev/null +++ b/homeassistant/components/elgato/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/translations/uk.json b/homeassistant/components/elgato/translations/uk.json new file mode 100644 index 00000000000..978ff1a3100 --- /dev/null +++ b/homeassistant/components/elgato/translations/uk.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Elgato Key Light \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Home Assistant." + }, + "zeroconf_confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 Elgato Key Light \u0437 \u0441\u0435\u0440\u0456\u0439\u043d\u0438\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c `{serial_number}`?", + "title": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Elgato Key Light" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/de.json b/homeassistant/components/elkm1/translations/de.json index 8c562a75026..8157a061d82 100644 --- a/homeassistant/components/elkm1/translations/de.json +++ b/homeassistant/components/elkm1/translations/de.json @@ -5,7 +5,7 @@ "already_configured": "Ein ElkM1 mit diesem Pr\u00e4fix ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/elkm1/translations/tr.json b/homeassistant/components/elkm1/translations/tr.json new file mode 100644 index 00000000000..9259220985b --- /dev/null +++ b/homeassistant/components/elkm1/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "address_already_configured": "Bu adrese sahip bir ElkM1 zaten yap\u0131land\u0131r\u0131lm\u0131\u015ft\u0131r", + "already_configured": "Bu \u00f6nek ile bir ElkM1 zaten yap\u0131land\u0131r\u0131lm\u0131\u015ft\u0131r" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/uk.json b/homeassistant/components/elkm1/translations/uk.json new file mode 100644 index 00000000000..a8e711a4590 --- /dev/null +++ b/homeassistant/components/elkm1/translations/uk.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "address_already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0437 \u0446\u0456\u0454\u044e \u0430\u0434\u0440\u0435\u0441\u043e\u044e \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435.", + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0437 \u0446\u0438\u043c \u043f\u0440\u0435\u0444\u0456\u043a\u0441\u043e\u043c \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430, \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e \u043f\u043e\u0441\u043b\u0456\u0434\u043e\u0432\u043d\u0438\u0439 \u043f\u043e\u0440\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "prefix": "\u0423\u043d\u0456\u043a\u0430\u043b\u044c\u043d\u0438\u0439 \u043f\u0440\u0435\u0444\u0456\u043a\u0441 (\u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c, \u044f\u043a\u0449\u043e \u0443 \u0412\u0430\u0441 \u0442\u0456\u043b\u044c\u043a\u0438 \u043e\u0434\u0438\u043d ElkM1)", + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "temperature_unit": "\u041e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u0443 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0438", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0420\u044f\u0434\u043e\u043a \u0430\u0434\u0440\u0435\u0441\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0430 \u0431\u0443\u0442\u0438 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'addres[:port]' \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0456\u0432 'secure' \u0456 'non-secure' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: '192.168.1.1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'port' \u0432\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e, \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0456\u043d \u0434\u043e\u0440\u0456\u0432\u043d\u044e\u0454 2101 \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 'non-secure' \u0456 2601 \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 'secure'. \u0414\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 'serial' \u0430\u0434\u0440\u0435\u0441\u0430 \u043f\u043e\u0432\u0438\u043d\u043d\u0430 \u0431\u0443\u0442\u0438 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'tty[:baud]' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: '/dev/ttyS1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'baud' \u0432\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e, \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0456\u043d \u0434\u043e\u0440\u0456\u0432\u043d\u044e\u0454 115200.", + "title": "Elk-M1 Control" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/translations/de.json b/homeassistant/components/emulated_roku/translations/de.json index a0bfd9f83aa..39c8da5197f 100644 --- a/homeassistant/components/emulated_roku/translations/de.json +++ b/homeassistant/components/emulated_roku/translations/de.json @@ -8,7 +8,7 @@ "data": { "advertise_ip": "IP Adresse annoncieren", "advertise_port": "Port annoncieren", - "host_ip": "Host-IP", + "host_ip": "Host-IP-Adresse", "listen_port": "Listen-Port", "name": "Name", "upnp_bind_multicast": "Multicast binden (True/False)" diff --git a/homeassistant/components/emulated_roku/translations/tr.json b/homeassistant/components/emulated_roku/translations/tr.json new file mode 100644 index 00000000000..5307276a71d --- /dev/null +++ b/homeassistant/components/emulated_roku/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/emulated_roku/translations/uk.json b/homeassistant/components/emulated_roku/translations/uk.json new file mode 100644 index 00000000000..a299f3a5ebc --- /dev/null +++ b/homeassistant/components/emulated_roku/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "step": { + "user": { + "data": { + "advertise_ip": "\u041e\u0433\u043e\u043b\u043e\u0448\u0443\u0432\u0430\u0442\u0438 IP", + "advertise_port": "\u041e\u0433\u043e\u043b\u043e\u0448\u0443\u0432\u0430\u0442\u0438 \u043f\u043e\u0440\u0442", + "host_ip": "\u0425\u043e\u0441\u0442", + "listen_port": "\u041f\u043e\u0440\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430", + "upnp_bind_multicast": "\u041f\u0440\u0438\u0432'\u044f\u0437\u0430\u0442\u0438 multicast (True / False)" + }, + "title": "EmulatedRoku" + } + } + }, + "title": "Emulated Roku" +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/tr.json b/homeassistant/components/enocean/translations/tr.json new file mode 100644 index 00000000000..b4e6be555ff --- /dev/null +++ b/homeassistant/components/enocean/translations/tr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "invalid_dongle_path": "Ge\u00e7ersiz dongle yolu", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "invalid_dongle_path": "Bu yol i\u00e7in ge\u00e7erli bir dongle bulunamad\u0131" + }, + "step": { + "detect": { + "data": { + "path": "USB dongle yolu" + } + }, + "manual": { + "data": { + "path": "USB dongle yolu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/uk.json b/homeassistant/components/enocean/translations/uk.json new file mode 100644 index 00000000000..5c3e2d6eb6e --- /dev/null +++ b/homeassistant/components/enocean/translations/uk.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "invalid_dongle_path": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u0448\u043b\u044f\u0445 \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e.", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "error": { + "invalid_dongle_path": "\u041d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 \u0437\u0430 \u0446\u0438\u043c \u0448\u043b\u044f\u0445\u043e\u043c." + }, + "step": { + "detect": { + "data": { + "path": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "title": "ENOcean" + }, + "manual": { + "data": { + "path": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "title": "ENOcean" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/epson/translations/de.json b/homeassistant/components/epson/translations/de.json index c03615a39ff..a91e3831cdb 100644 --- a/homeassistant/components/epson/translations/de.json +++ b/homeassistant/components/epson/translations/de.json @@ -1,12 +1,14 @@ { "config": { "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { "data": { - "name": "Name" + "host": "Host", + "name": "Name", + "port": "Port" } } } diff --git a/homeassistant/components/epson/translations/tr.json b/homeassistant/components/epson/translations/tr.json index aafc2e2b303..9ffd77fc50f 100644 --- a/homeassistant/components/epson/translations/tr.json +++ b/homeassistant/components/epson/translations/tr.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/epson/translations/uk.json b/homeassistant/components/epson/translations/uk.json new file mode 100644 index 00000000000..65566a8f4aa --- /dev/null +++ b/homeassistant/components/epson/translations/uk.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/de.json b/homeassistant/components/esphome/translations/de.json index 826574cb7e0..fdaea452c45 100644 --- a/homeassistant/components/esphome/translations/de.json +++ b/homeassistant/components/esphome/translations/de.json @@ -2,10 +2,11 @@ "config": { "abort": { "already_configured": "ESP ist bereits konfiguriert", - "already_in_progress": "Die ESP-Konfiguration wird bereits ausgef\u00fchrt" + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt" }, "error": { "connection_error": "Keine Verbindung zum ESP m\u00f6glich. Achte darauf, dass deine YAML-Datei eine Zeile 'api:' enth\u00e4lt.", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "resolve_error": "Adresse des ESP kann nicht aufgel\u00f6st werden. Wenn dieser Fehler weiterhin besteht, lege eine statische IP-Adresse fest: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "flow_title": "ESPHome: {name}", diff --git a/homeassistant/components/esphome/translations/pt.json b/homeassistant/components/esphome/translations/pt.json index 6ff4d786447..60eeaa3f4b2 100644 --- a/homeassistant/components/esphome/translations/pt.json +++ b/homeassistant/components/esphome/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "O ESP j\u00e1 est\u00e1 configurado", + "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": { diff --git a/homeassistant/components/esphome/translations/tr.json b/homeassistant/components/esphome/translations/tr.json index 15028c4fe65..81f85d4980b 100644 --- a/homeassistant/components/esphome/translations/tr.json +++ b/homeassistant/components/esphome/translations/tr.json @@ -1,8 +1,27 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, "step": { + "authenticate": { + "data": { + "password": "Parola" + }, + "description": "L\u00fctfen yap\u0131land\u0131rman\u0131zda {name} i\u00e7in belirledi\u011finiz parolay\u0131 girin." + }, "discovery_confirm": { "title": "Ke\u015ffedilen ESPHome d\u00fc\u011f\u00fcm\u00fc" + }, + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } } } } diff --git a/homeassistant/components/esphome/translations/uk.json b/homeassistant/components/esphome/translations/uk.json index d17ec64e548..4643c19cf5d 100644 --- a/homeassistant/components/esphome/translations/uk.json +++ b/homeassistant/components/esphome/translations/uk.json @@ -1,22 +1,25 @@ { "config": { "abort": { - "already_configured": "ESP \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454." }, "error": { "connection_error": "\u041d\u0435 \u0432\u0434\u0430\u0454\u0442\u044c\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e ESP. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0444\u0430\u0439\u043b YAML \u043c\u0456\u0441\u0442\u0438\u0442\u044c \u0440\u044f\u0434\u043e\u043a \"api:\".", - "resolve_error": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0430\u0434\u0440\u0435\u0441\u0443 ESP. \u042f\u043a\u0449\u043e \u0446\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043d\u0435 \u0437\u043d\u0438\u043a\u0430\u0454, \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0456\u0442\u044c \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u0443 IP-\u0430\u0434\u0440\u0435\u0441\u0443: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "resolve_error": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u0432\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438 \u0430\u0434\u0440\u0435\u0441\u0443 ESP. \u042f\u043a\u0449\u043e \u0446\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044e\u0454\u0442\u044c\u0441\u044f, \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u0443 IP-\u0430\u0434\u0440\u0435\u0441\u0443: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips." }, + "flow_title": "ESPHome: {name}", "step": { "authenticate": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, - "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c, \u044f\u043a\u0438\u0439 \u0432\u0438 \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043b\u0438 \u0443 \u0441\u0432\u043e\u0457\u0439 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457." + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c, \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0439 \u0432 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457 {name}." }, "discovery_confirm": { "description": "\u0414\u043e\u0434\u0430\u0442\u0438 ESPHome \u0432\u0443\u0437\u043e\u043b {name} \u0443 Home Assistant?", - "title": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u0432\u0443\u0437\u043e\u043b ESPHome" + "title": "ESPHome" }, "user": { "data": { diff --git a/homeassistant/components/fan/translations/tr.json b/homeassistant/components/fan/translations/tr.json index 4ffc57601bd..52a07c35d83 100644 --- a/homeassistant/components/fan/translations/tr.json +++ b/homeassistant/components/fan/translations/tr.json @@ -1,4 +1,14 @@ { + "device_automation": { + "action_type": { + "turn_off": "{entity_name} kapat", + "turn_on": "{entity_name} a\u00e7\u0131n" + }, + "trigger_type": { + "turned_off": "{entity_name} kapat\u0131ld\u0131", + "turned_on": "{entity_name} a\u00e7\u0131ld\u0131" + } + }, "state": { "_": { "off": "Kapal\u0131", diff --git a/homeassistant/components/fan/translations/uk.json b/homeassistant/components/fan/translations/uk.json index 3fd103cd244..0e0bafcbfc4 100644 --- a/homeassistant/components/fan/translations/uk.json +++ b/homeassistant/components/fan/translations/uk.json @@ -1,8 +1,16 @@ { "device_automation": { + "action_type": { + "turn_off": "{entity_name}: \u0432\u0438\u043c\u043a\u043d\u0443\u0442\u0438", + "turn_on": "{entity_name}: \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438" + }, + "condition_type": { + "is_off": "{entity_name} \u0443 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456" + }, "trigger_type": { - "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e", - "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" + "turned_off": "{entity_name} \u0432\u0438\u043c\u0438\u043a\u0430\u0454\u0442\u044c\u0441\u044f", + "turned_on": "{entity_name} \u0432\u043c\u0438\u043a\u0430\u0454\u0442\u044c\u0441\u044f" } }, "state": { diff --git a/homeassistant/components/fireservicerota/translations/fr.json b/homeassistant/components/fireservicerota/translations/fr.json new file mode 100644 index 00000000000..a8803f63fca --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/fr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte \u00e0 d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9" + }, + "create_entry": { + "default": "Autentification r\u00e9ussie" + }, + "error": { + "invalid_auth": "Autentification invalide" + }, + "step": { + "reauth": { + "data": { + "password": "Mot de passe" + } + }, + "user": { + "data": { + "password": "Mot de passe", + "url": "Site web", + "username": "Utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/tr.json b/homeassistant/components/fireservicerota/translations/tr.json index a2d2cab3b74..f54d10f6cbf 100644 --- a/homeassistant/components/fireservicerota/translations/tr.json +++ b/homeassistant/components/fireservicerota/translations/tr.json @@ -1,5 +1,15 @@ { "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, "step": { "reauth": { "data": { diff --git a/homeassistant/components/fireservicerota/translations/uk.json b/homeassistant/components/fireservicerota/translations/uk.json new file mode 100644 index 00000000000..2d3bf8c596e --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/uk.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "reauth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0422\u043e\u043a\u0435\u043d\u0438 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457 \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0456, \u0443\u0432\u0456\u0439\u0434\u0456\u0442\u044c, \u0449\u043e\u0431 \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u0457\u0445 \u0437\u0430\u043d\u043e\u0432\u043e." + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "url": "\u0412\u0435\u0431-\u0441\u0430\u0439\u0442", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/firmata/translations/tr.json b/homeassistant/components/firmata/translations/tr.json new file mode 100644 index 00000000000..b7d038a229b --- /dev/null +++ b/homeassistant/components/firmata/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/firmata/translations/uk.json b/homeassistant/components/firmata/translations/uk.json new file mode 100644 index 00000000000..41b670fbb18 --- /dev/null +++ b/homeassistant/components/firmata/translations/uk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + } + } +} \ 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 ed0ef205ff0..3e3568c45f8 100644 --- a/homeassistant/components/flick_electric/translations/de.json +++ b/homeassistant/components/flick_electric/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Dieses Konto ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/flick_electric/translations/tr.json b/homeassistant/components/flick_electric/translations/tr.json new file mode 100644 index 00000000000..a83e1936fb4 --- /dev/null +++ b/homeassistant/components/flick_electric/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/uk.json b/homeassistant/components/flick_electric/translations/uk.json new file mode 100644 index 00000000000..4d72844bc74 --- /dev/null +++ b/homeassistant/components/flick_electric/translations/uk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "client_id": "ID \u043a\u043b\u0456\u0454\u043d\u0442\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "client_secret": "\u0421\u0435\u043a\u0440\u0435\u0442 \u043a\u043b\u0456\u0454\u043d\u0442\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "Flick Electric" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flo/translations/de.json b/homeassistant/components/flo/translations/de.json index 38215675701..625c7372347 100644 --- a/homeassistant/components/flo/translations/de.json +++ b/homeassistant/components/flo/translations/de.json @@ -1,12 +1,17 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { "user": { "data": { + "host": "Host", "password": "Passwort", "username": "Benutzername" } diff --git a/homeassistant/components/flo/translations/tr.json b/homeassistant/components/flo/translations/tr.json new file mode 100644 index 00000000000..40c9c39b967 --- /dev/null +++ b/homeassistant/components/flo/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flo/translations/uk.json b/homeassistant/components/flo/translations/uk.json new file mode 100644 index 00000000000..2df11f74455 --- /dev/null +++ b/homeassistant/components/flo/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/de.json b/homeassistant/components/flume/translations/de.json index 692c38350a8..c38a5593ac7 100644 --- a/homeassistant/components/flume/translations/de.json +++ b/homeassistant/components/flume/translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Dieses Konto ist bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/flume/translations/tr.json b/homeassistant/components/flume/translations/tr.json new file mode 100644 index 00000000000..a83e1936fb4 --- /dev/null +++ b/homeassistant/components/flume/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/uk.json b/homeassistant/components/flume/translations/uk.json new file mode 100644 index 00000000000..53fb4f3d6d7 --- /dev/null +++ b/homeassistant/components/flume/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "client_id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0456\u0454\u043d\u0442\u0430", + "client_secret": "\u0421\u0435\u043a\u0440\u0435\u0442 \u043a\u043b\u0456\u0454\u043d\u0442\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0429\u043e\u0431 \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e API Flume, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 'ID \u043a\u043b\u0456\u0454\u043d\u0442\u0430' \u0456 '\u0421\u0435\u043a\u0440\u0435\u0442 \u043a\u043b\u0456\u0454\u043d\u0442\u0430' \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e https://portal.flumetech.com/settings#token.", + "title": "Flume" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/de.json b/homeassistant/components/flunearyou/translations/de.json index cd2934170c9..1c94931f405 100644 --- a/homeassistant/components/flunearyou/translations/de.json +++ b/homeassistant/components/flunearyou/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Diese Koordinaten sind bereits registriert." + "already_configured": "Standort ist bereits konfiguriert" }, "error": { "unknown": "Unerwarteter Fehler" diff --git a/homeassistant/components/flunearyou/translations/tr.json b/homeassistant/components/flunearyou/translations/tr.json new file mode 100644 index 00000000000..6e749e3c827 --- /dev/null +++ b/homeassistant/components/flunearyou/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "latitude": "Enlem", + "longitude": "Boylam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/uk.json b/homeassistant/components/flunearyou/translations/uk.json new file mode 100644 index 00000000000..354a04d8e7a --- /dev/null +++ b/homeassistant/components/flunearyou/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + }, + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430" + }, + "description": "\u041c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0446\u044c\u043a\u0438\u0445 \u0456 CDC \u0437\u0432\u0456\u0442\u0456\u0432 \u0437\u0430 \u0432\u043a\u0430\u0437\u0430\u043d\u0438\u043c\u0438 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0430\u043c\u0438.", + "title": "Flu Near You" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/de.json b/homeassistant/components/forked_daapd/translations/de.json index a3cdc53c52a..e90ffc71f90 100644 --- a/homeassistant/components/forked_daapd/translations/de.json +++ b/homeassistant/components/forked_daapd/translations/de.json @@ -5,7 +5,7 @@ }, "error": { "unknown_error": "Unbekannter Fehler", - "wrong_host_or_port": "Verbindung konnte nicht hergestellt werden. Bitte \u00fcberpr\u00fcfen Sie Host und Port.", + "wrong_host_or_port": "Verbindung konnte nicht hergestellt werden. Bitte Host und Port pr\u00fcfen.", "wrong_password": "Ung\u00fcltiges Passwort", "wrong_server_type": "F\u00fcr die forked-daapd Integration ist ein forked-daapd Server mit der Version > = 27.0 erforderlich." }, diff --git a/homeassistant/components/forked_daapd/translations/tr.json b/homeassistant/components/forked_daapd/translations/tr.json new file mode 100644 index 00000000000..cf354c5c87f --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "unknown_error": "Beklenmeyen hata", + "wrong_password": "Yanl\u0131\u015f parola." + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "name": "Kolay ad", + "password": "API parolas\u0131 (parola yoksa bo\u015f b\u0131rak\u0131n)", + "port": "API ba\u011flant\u0131 noktas\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/uk.json b/homeassistant/components/forked_daapd/translations/uk.json new file mode 100644 index 00000000000..19caf9b5bd0 --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/uk.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "not_forked_daapd": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 forked-daapd." + }, + "error": { + "forbidden": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u0456 \u0434\u043e\u0437\u0432\u043e\u043b\u0438 forked-daapd.", + "unknown_error": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430", + "websocket_not_enabled": "\u0412\u0435\u0431-\u0441\u043e\u043a\u0435\u0442 forked-daapd \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439.", + "wrong_host_or_port": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u0445\u043e\u0441\u0442\u0430.", + "wrong_password": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u043f\u0430\u0440\u043e\u043b\u044c.", + "wrong_server_type": "\u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0438\u0439 \u0441\u0435\u0440\u0432\u0435\u0440 forked-daapd \u0432\u0435\u0440\u0441\u0456\u0457 27.0 \u0430\u0431\u043e \u0432\u0438\u0449\u0435." + }, + "flow_title": "\u0421\u0435\u0440\u0432\u0435\u0440 forked-daapd: {name} ({host})", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c API (\u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c, \u044f\u043a\u0449\u043e \u0443 \u0432\u0430\u0441 \u043d\u0435\u043c\u0430\u0454 \u043f\u0430\u0440\u043e\u043b\u044f)", + "port": "\u041f\u043e\u0440\u0442 API" + }, + "title": "forked-daapd" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "librespot_java_port": "\u041f\u043e\u0440\u0442 \u0434\u043b\u044f \u0443\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f \u043a\u0430\u043d\u0430\u043b\u043e\u043c librespot-java (\u044f\u043a\u0449\u043e \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f)", + "max_playlists": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430 \u043a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u043f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u0456\u0432, \u0449\u043e \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u044e\u0442\u044c\u0441\u044f \u044f\u043a \u0434\u0436\u0435\u0440\u0435\u043b\u0430", + "tts_pause_time": "\u0427\u0430\u0441 \u043f\u0430\u0443\u0437\u0438 \u0434\u043e \u0456 \u043f\u0456\u0441\u043b\u044f TTS (\u0441\u0435\u043a.)", + "tts_volume": "\u0413\u0443\u0447\u043d\u0456\u0441\u0442\u044c TTS (\u0447\u0438\u0441\u043b\u043e \u0432 \u0434\u0456\u0430\u043f\u0430\u0437\u043e\u043d\u0456 \u0432\u0456\u0434 0 \u0434\u043e 1)" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 forked-daapd.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f forked-daapd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/af.json b/homeassistant/components/foscam/translations/af.json new file mode 100644 index 00000000000..4a9930dd95d --- /dev/null +++ b/homeassistant/components/foscam/translations/af.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Senha" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/ca.json b/homeassistant/components/foscam/translations/ca.json new file mode 100644 index 00000000000..5a6c84f400e --- /dev/null +++ b/homeassistant/components/foscam/translations/ca.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya", + "port": "Port", + "stream": "Flux de v\u00eddeo", + "username": "Nom d'usuari" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/cs.json b/homeassistant/components/foscam/translations/cs.json new file mode 100644 index 00000000000..b6f3c40abf6 --- /dev/null +++ b/homeassistant/components/foscam/translations/cs.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/de.json b/homeassistant/components/foscam/translations/de.json new file mode 100644 index 00000000000..603be1847cc --- /dev/null +++ b/homeassistant/components/foscam/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/en.json b/homeassistant/components/foscam/translations/en.json index 521a22076dd..3d1454a4ebd 100644 --- a/homeassistant/components/foscam/translations/en.json +++ b/homeassistant/components/foscam/translations/en.json @@ -1,24 +1,24 @@ { - "config": { - "abort": { - "already_configured": "Device is already configured" - }, - "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, - "step": { - "user": { - "data": { - "host": "Host", - "password": "Password", - "port": "Port", - "stream": "Stream", - "username": "Username" + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Port", + "stream": "Stream", + "username": "Username" + } + } } - } - } - }, - "title": "Foscam" -} + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/es.json b/homeassistant/components/foscam/translations/es.json new file mode 100644 index 00000000000..27f7ac36489 --- /dev/null +++ b/homeassistant/components/foscam/translations/es.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "port": "Puerto", + "stream": "Stream", + "username": "Usuario" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/et.json b/homeassistant/components/foscam/translations/et.json new file mode 100644 index 00000000000..b20a33aec1d --- /dev/null +++ b/homeassistant/components/foscam/translations/et.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5na", + "port": "Port", + "stream": "Voog", + "username": "Kasutajanimi" + } + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/fr.json b/homeassistant/components/foscam/translations/fr.json new file mode 100644 index 00000000000..9af8115c305 --- /dev/null +++ b/homeassistant/components/foscam/translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Le p\u00e9riph\u00e9rique est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Echec de connection", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "password": "Mot de passe", + "port": "Port", + "stream": "Flux", + "username": "Nom d'utilisateur" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/it.json b/homeassistant/components/foscam/translations/it.json new file mode 100644 index 00000000000..0562012b1fa --- /dev/null +++ b/homeassistant/components/foscam/translations/it.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Porta", + "stream": "Flusso", + "username": "Nome utente" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/lb.json b/homeassistant/components/foscam/translations/lb.json new file mode 100644 index 00000000000..123b3f4be76 --- /dev/null +++ b/homeassistant/components/foscam/translations/lb.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwuert", + "port": "Port", + "stream": "Stream", + "username": "Benotzernumm" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/no.json b/homeassistant/components/foscam/translations/no.json new file mode 100644 index 00000000000..5e1b494c88a --- /dev/null +++ b/homeassistant/components/foscam/translations/no.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "password": "Passord", + "port": "Port", + "stream": "Str\u00f8m", + "username": "Brukernavn" + } + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/pl.json b/homeassistant/components/foscam/translations/pl.json new file mode 100644 index 00000000000..ef0bcda2b3a --- /dev/null +++ b/homeassistant/components/foscam/translations/pl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "port": "Port", + "stream": "Strumie\u0144", + "username": "Nazwa u\u017cytkownika" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/pt.json b/homeassistant/components/foscam/translations/pt.json new file mode 100644 index 00000000000..b8a454fbaba --- /dev/null +++ b/homeassistant/components/foscam/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/ru.json b/homeassistant/components/foscam/translations/ru.json new file mode 100644 index 00000000000..ad8b7961ca3 --- /dev/null +++ b/homeassistant/components/foscam/translations/ru.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "stream": "\u041f\u043e\u0442\u043e\u043a", + "username": "\u041b\u043e\u0433\u0438\u043d" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/tr.json b/homeassistant/components/foscam/translations/tr.json new file mode 100644 index 00000000000..b3e964ae08e --- /dev/null +++ b/homeassistant/components/foscam/translations/tr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen Hata" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "\u015eifre", + "port": "Port", + "stream": "Ak\u0131\u015f", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/zh-Hant.json b/homeassistant/components/foscam/translations/zh-Hant.json new file mode 100644 index 00000000000..2cc6303c17a --- /dev/null +++ b/homeassistant/components/foscam/translations/zh-Hant.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "stream": "\u4e32\u6d41", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/de.json b/homeassistant/components/freebox/translations/de.json index c21e3c6b67f..738b9d48f3c 100644 --- a/homeassistant/components/freebox/translations/de.json +++ b/homeassistant/components/freebox/translations/de.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Host bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "register_failed": "Registrieren fehlgeschlagen, bitte versuche es erneut", - "unknown": "Unbekannter Fehler: Bitte versuchen Sie es sp\u00e4ter erneut" + "unknown": "Unerwarteter Fehler" }, "step": { "link": { diff --git a/homeassistant/components/freebox/translations/tr.json b/homeassistant/components/freebox/translations/tr.json new file mode 100644 index 00000000000..b675d38057d --- /dev/null +++ b/homeassistant/components/freebox/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + }, + "title": "Freebox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/uk.json b/homeassistant/components/freebox/translations/uk.json new file mode 100644 index 00000000000..8676c9164a1 --- /dev/null +++ b/homeassistant/components/freebox/translations/uk.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "register_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0432\u0430\u0442\u0438\u0441\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "link": { + "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c '\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438', \u043f\u043e\u0442\u0456\u043c \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043a\u043d\u043e\u043f\u043a\u0443 \u0437\u0456 \u0441\u0442\u0440\u0456\u043b\u043a\u043e\u044e \u043d\u0430 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0456, \u0449\u043e\u0431 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0432\u0430\u0442\u0438 Freebox \u0432 Home Assistant. \n\n![\u0420\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043d\u043e\u043f\u043a\u0438 \u043d\u0430 \u0440\u043e\u0443\u0442\u0435\u0440\u0456] (/ static / images / config_freebox.png)", + "title": "Freebox" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "Freebox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/ca.json b/homeassistant/components/fritzbox/translations/ca.json index 8b0122dbe18..f8550b5bc32 100644 --- a/homeassistant/components/fritzbox/translations/ca.json +++ b/homeassistant/components/fritzbox/translations/ca.json @@ -4,7 +4,8 @@ "already_configured": "El dispositiu ja est\u00e0 configurat", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "no_devices_found": "No s'han trobat dispositius a la xarxa", - "not_supported": "Connectat a AVM FRITZ!Box per\u00f2 no es poden controlar dispositius Smart Home." + "not_supported": "Connectat a AVM FRITZ!Box per\u00f2 no es poden controlar dispositius Smart Home.", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" @@ -18,6 +19,13 @@ }, "description": "Vols configurar {name}?" }, + "reauth_confirm": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Actualitza la informaci\u00f3 d'inici de sessi\u00f3 de {name}." + }, "user": { "data": { "host": "Amfitri\u00f3", diff --git a/homeassistant/components/fritzbox/translations/cs.json b/homeassistant/components/fritzbox/translations/cs.json index 67ff5db7f99..b3b41afe383 100644 --- a/homeassistant/components/fritzbox/translations/cs.json +++ b/homeassistant/components/fritzbox/translations/cs.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "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" + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" @@ -17,6 +18,12 @@ }, "description": "Chcete nastavit {name}?" }, + "reauth_confirm": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, "user": { "data": { "host": "Hostitel", diff --git a/homeassistant/components/fritzbox/translations/de.json b/homeassistant/components/fritzbox/translations/de.json index 19ca2e80903..9b76ad19ff4 100644 --- a/homeassistant/components/fritzbox/translations/de.json +++ b/homeassistant/components/fritzbox/translations/de.json @@ -1,10 +1,14 @@ { "config": { "abort": { - "already_configured": "Diese AVM FRITZ! Box ist bereits konfiguriert.", - "already_in_progress": "Die Konfiguration der AVM FRITZ! Box ist bereits in Bearbeitung.", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", "not_supported": "Verbunden mit AVM FRITZ! Box, kann jedoch keine Smart Home-Ger\u00e4te steuern." }, + "error": { + "invalid_auth": "Ung\u00fcltige Zugangsdaten" + }, "flow_title": "AVM FRITZ! Box: {name}", "step": { "confirm": { @@ -12,7 +16,7 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "M\u00f6chten Sie {name} einrichten?" + "description": "M\u00f6chtest du {name} einrichten?" }, "user": { "data": { @@ -20,7 +24,7 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "Geben Sie Ihre AVM FRITZ! Box-Informationen ein." + "description": "Gib deine AVM FRITZ! Box-Informationen ein." } } } diff --git a/homeassistant/components/fritzbox/translations/en.json b/homeassistant/components/fritzbox/translations/en.json index 1f22bc30252..61ca1e957bb 100644 --- a/homeassistant/components/fritzbox/translations/en.json +++ b/homeassistant/components/fritzbox/translations/en.json @@ -4,7 +4,8 @@ "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", "no_devices_found": "No devices found on the network", - "not_supported": "Connected to AVM FRITZ!Box but it's unable to control Smart Home devices." + "not_supported": "Connected to AVM FRITZ!Box but it's unable to control Smart Home devices.", + "reauth_successful": "Re-authentication was successful" }, "error": { "invalid_auth": "Invalid authentication" @@ -18,6 +19,13 @@ }, "description": "Do you want to set up {name}?" }, + "reauth_confirm": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "Update your login information for {name}." + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/fritzbox/translations/et.json b/homeassistant/components/fritzbox/translations/et.json index 702488bce0c..5ee2dc801f4 100644 --- a/homeassistant/components/fritzbox/translations/et.json +++ b/homeassistant/components/fritzbox/translations/et.json @@ -4,7 +4,8 @@ "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "already_in_progress": "Seadistamine on juba k\u00e4imas", "no_devices_found": "V\u00f5rgust ei leitud seadmeid", - "not_supported": "\u00dchendatud AVM FRITZ!Boxiga! kuid see ei saa juhtida Smart Home seadmeid." + "not_supported": "\u00dchendatud AVM FRITZ!Boxiga! kuid see ei saa juhtida Smart Home seadmeid.", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "invalid_auth": "Tuvastamise viga" @@ -18,6 +19,13 @@ }, "description": "Kas soovid seadistada {name}?" }, + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "V\u00e4rskenda konto {name} sisselogimisteavet." + }, "user": { "data": { "host": "", diff --git a/homeassistant/components/fritzbox/translations/it.json b/homeassistant/components/fritzbox/translations/it.json index ab44ae13863..a420b3f6de7 100644 --- a/homeassistant/components/fritzbox/translations/it.json +++ b/homeassistant/components/fritzbox/translations/it.json @@ -4,7 +4,8 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "no_devices_found": "Nessun dispositivo trovato sulla rete", - "not_supported": "Collegato a AVM FRITZ!Box ma non \u00e8 in grado di controllare i dispositivi Smart Home." + "not_supported": "Collegato a AVM FRITZ!Box ma non \u00e8 in grado di controllare i dispositivi Smart Home.", + "reauth_successful": "La riautenticazione ha avuto successo" }, "error": { "invalid_auth": "Autenticazione non valida" @@ -18,6 +19,13 @@ }, "description": "Vuoi impostare {name}?" }, + "reauth_confirm": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Aggiorna le tue informazioni di accesso per {name}." + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/fritzbox/translations/no.json b/homeassistant/components/fritzbox/translations/no.json index 024e25741a7..bd64b428bdf 100644 --- a/homeassistant/components/fritzbox/translations/no.json +++ b/homeassistant/components/fritzbox/translations/no.json @@ -4,7 +4,8 @@ "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", - "not_supported": "Tilkoblet AVM FRITZ! Box, men den klarer ikke \u00e5 kontrollere Smart Home-enheter." + "not_supported": "Tilkoblet AVM FRITZ! Box, men den klarer ikke \u00e5 kontrollere Smart Home-enheter.", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning" @@ -18,6 +19,13 @@ }, "description": "Vil du sette opp {name} ?" }, + "reauth_confirm": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Oppdater p\u00e5loggingsinformasjonen for {name} ." + }, "user": { "data": { "host": "Vert", diff --git a/homeassistant/components/fritzbox/translations/pl.json b/homeassistant/components/fritzbox/translations/pl.json index fc162310189..dc05e431832 100644 --- a/homeassistant/components/fritzbox/translations/pl.json +++ b/homeassistant/components/fritzbox/translations/pl.json @@ -4,7 +4,8 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja jest ju\u017c w toku", "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", - "not_supported": "Po\u0142\u0105czony z AVM FRITZ!Box, ale nie jest w stanie kontrolowa\u0107 urz\u0105dze\u0144 Smart Home" + "not_supported": "Po\u0142\u0105czony z AVM FRITZ!Box, ale nie jest w stanie kontrolowa\u0107 urz\u0105dze\u0144 Smart Home", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "invalid_auth": "Niepoprawne uwierzytelnienie" @@ -18,6 +19,13 @@ }, "description": "Czy chcesz skonfigurowa\u0107 {name}?" }, + "reauth_confirm": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Zaktualizuj dane logowania dla {name}" + }, "user": { "data": { "host": "Nazwa hosta lub adres IP", diff --git a/homeassistant/components/fritzbox/translations/ru.json b/homeassistant/components/fritzbox/translations/ru.json index 322f677c2af..50146b490ba 100644 --- a/homeassistant/components/fritzbox/translations/ru.json +++ b/homeassistant/components/fritzbox/translations/ru.json @@ -4,7 +4,8 @@ "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.", "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.", - "not_supported": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a AVM FRITZ! Box \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043e, \u043d\u043e \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c\u0438 Smart Home \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e." + "not_supported": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a AVM FRITZ! Box \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043e, \u043d\u043e \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c\u0438 Smart Home \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e.", + "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": { "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." @@ -18,6 +19,13 @@ }, "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "description": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0434\u043b\u044f {name}." + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/fritzbox/translations/tr.json b/homeassistant/components/fritzbox/translations/tr.json new file mode 100644 index 00000000000..746fe594e19 --- /dev/null +++ b/homeassistant/components/fritzbox/translations/tr.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "confirm": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "{name} kurmak istiyor musunuz?" + }, + "reauth_confirm": { + "data": { + "password": "\u015eifre", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Giri\u015f bilgilerinizi {name} i\u00e7in g\u00fcncelleyin." + }, + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/uk.json b/homeassistant/components/fritzbox/translations/uk.json new file mode 100644 index 00000000000..5a2d8a1c35e --- /dev/null +++ b/homeassistant/components/fritzbox/translations/uk.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "not_supported": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e AVM FRITZ! Box \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u043e, \u0430\u043b\u0435 \u0443\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044f\u043c\u0438 Smart Home \u043d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "flow_title": "AVM FRITZ!Box: {name}", + "step": { + "confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name}?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 AVM FRITZ! Box." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/zh-Hant.json b/homeassistant/components/fritzbox/translations/zh-Hant.json index 7b85df577ef..71a74785267 100644 --- a/homeassistant/components/fritzbox/translations/zh-Hant.json +++ b/homeassistant/components/fritzbox/translations/zh-Hant.json @@ -4,7 +4,8 @@ "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\u88dd\u7f6e", - "not_supported": "\u5df2\u9023\u7dda\u81f3 AVM FRITZ!Box \u4f46\u7121\u6cd5\u63a7\u5236\u667a\u80fd\u5bb6\u5ead\u88dd\u7f6e\u3002" + "not_supported": "\u5df2\u9023\u7dda\u81f3 AVM FRITZ!Box \u4f46\u7121\u6cd5\u63a7\u5236\u667a\u80fd\u5bb6\u5ead\u88dd\u7f6e\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" @@ -18,6 +19,13 @@ }, "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" }, + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u66f4\u65b0 {name} \u767b\u5165\u8cc7\u8a0a\u3002" + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", diff --git a/homeassistant/components/fritzbox_callmonitor/translations/ca.json b/homeassistant/components/fritzbox_callmonitor/translations/ca.json new file mode 100644 index 00000000000..808b642f4ff --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/ca.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "insufficient_permissions": "L'usuari no t\u00e9 permisos suficients per accedir a la configuraci\u00f3 d'AVM FRITZ!Box i les seves agendes.", + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "flow_title": "Sensor de trucades d'AVM FRITZ!Box: {name}", + "step": { + "phonebook": { + "data": { + "phonebook": "Agenda" + } + }, + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya", + "port": "Port", + "username": "Nom d'usuari" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "El format dels prefixos no \u00e9s correcte, comprova'l." + }, + "step": { + "init": { + "data": { + "prefixes": "Prefixos (llista separada per comes)" + }, + "title": "Configuraci\u00f3 dels prefixos" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/cs.json b/homeassistant/components/fritzbox_callmonitor/translations/cs.json new file mode 100644 index 00000000000..c40da2900bc --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/et.json b/homeassistant/components/fritzbox_callmonitor/translations/et.json new file mode 100644 index 00000000000..7770f31ae0e --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/et.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "insufficient_permissions": "Kasutajal ei ole piisavalt \u00f5igusi juurdep\u00e4\u00e4suks AVM FRITZ! Box'i seadetele jatelefoniraamatutele.", + "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet" + }, + "error": { + "invalid_auth": "Vigane autentimine" + }, + "flow_title": "AVM FRITZ! K\u00f5nekontroll: {name}", + "step": { + "phonebook": { + "data": { + "phonebook": "Telefoniraamat" + } + }, + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5na", + "port": "Port", + "username": "Kasutajanimi" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "Eesliited on valesti vormindatud, kontrolli nende vormingut." + }, + "step": { + "init": { + "data": { + "prefixes": "Eesliited (komadega eraldatud loend)" + }, + "title": "Eesliidete seadistamine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/it.json b/homeassistant/components/fritzbox_callmonitor/translations/it.json new file mode 100644 index 00000000000..5696bf86fd1 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/it.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "insufficient_permissions": "L'utente non dispone di autorizzazioni sufficienti per accedere alle impostazioni di AVM FRITZ! Box e alle sue rubriche.", + "no_devices_found": "Nessun dispositivo trovato sulla rete" + }, + "error": { + "invalid_auth": "Autenticazione non valida" + }, + "flow_title": "Monitoraggio chiamate FRITZ! Box AVM: {name}", + "step": { + "phonebook": { + "data": { + "phonebook": "Rubrica telefonica" + } + }, + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Porta", + "username": "Nome utente" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "I prefissi non sono corretti, controlla il loro formato." + }, + "step": { + "init": { + "data": { + "prefixes": "Prefissi (elenco separato da virgole)" + }, + "title": "Configura prefissi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/lb.json b/homeassistant/components/fritzbox_callmonitor/translations/lb.json new file mode 100644 index 00000000000..67b5879a557 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/lb.json @@ -0,0 +1,33 @@ +{ + "config": { + "flow_title": "AVM FRITZ!Box Call Monitor: {name}", + "step": { + "phonebook": { + "data": { + "phonebook": "Adressbuch" + } + }, + "user": { + "data": { + "host": "Host", + "password": "Passwuert", + "port": "Port", + "username": "Benotzernumm" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "Pr\u00e9fixe sinn am falsche Format, iwwerpr\u00e9if dat w.e.g" + }, + "step": { + "init": { + "data": { + "prefixes": "Pr\u00e9fixe (komma getrennte L\u00ebscht)" + }, + "title": "Pr\u00e9fixe konfigur\u00e9ieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/no.json b/homeassistant/components/fritzbox_callmonitor/translations/no.json new file mode 100644 index 00000000000..12883b0140d --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/no.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "insufficient_permissions": "Brukeren har utilstrekkelig tillatelse til \u00e5 f\u00e5 tilgang til AVM FRITZ! Box-innstillingene og telefonb\u00f8kene.", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" + }, + "error": { + "invalid_auth": "Ugyldig godkjenning" + }, + "flow_title": "AVM FRITZ! Box monitor: {name}", + "step": { + "phonebook": { + "data": { + "phonebook": "Telefonbok" + } + }, + "user": { + "data": { + "host": "Vert", + "password": "Passord", + "port": "Port", + "username": "Brukernavn" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "Prefikser er misformet, vennligst sjekk deres format." + }, + "step": { + "init": { + "data": { + "prefixes": "Prefikser (kommaseparert liste)" + }, + "title": "Konfigurer prefiks" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/pl.json b/homeassistant/components/fritzbox_callmonitor/translations/pl.json new file mode 100644 index 00000000000..fa0317f5c9d --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/pl.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "insufficient_permissions": "U\u017cytkownik ma niewystarczaj\u0105ce uprawnienia, aby uzyska\u0107 dost\u0119p do ustawie\u0144 AVM FRITZ! Box i jego ksi\u0105\u017cek telefonicznych.", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "flow_title": "Monitor po\u0142\u0105cze\u0144 AVM FRITZ!Box: {name}", + "step": { + "phonebook": { + "data": { + "phonebook": "Ksi\u0105\u017cka telefoniczna" + } + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "Prefiksy s\u0105 nieprawid\u0142owe, prosz\u0119 sprawdzi\u0107 ich format." + }, + "step": { + "init": { + "data": { + "prefixes": "Prefiksy (lista oddzielona przecinkami)" + }, + "title": "Skonfiguruj prefiksy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/ru.json b/homeassistant/components/fritzbox_callmonitor/translations/ru.json new file mode 100644 index 00000000000..3eb432532c4 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/ru.json @@ -0,0 +1,41 @@ +{ + "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.", + "insufficient_permissions": "\u0423 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043d\u0435\u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u043f\u0440\u0430\u0432 \u0434\u043b\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c AVM FRITZ!Box \u0438 \u0435\u0433\u043e \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u043d\u044b\u043c \u043a\u043d\u0438\u0433\u0430\u043c.", + "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." + }, + "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." + }, + "flow_title": "AVM FRITZ!Box call monitor: {name}", + "step": { + "phonebook": { + "data": { + "phonebook": "\u0422\u0435\u043b\u0435\u0444\u043e\u043d\u043d\u0430\u044f \u043a\u043d\u0438\u0433\u0430" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041b\u043e\u0433\u0438\u043d" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441\u044b \u0438\u043c\u0435\u044e\u0442 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u0444\u043e\u0440\u043c\u0430\u0442, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0438\u0445." + }, + "step": { + "init": { + "data": { + "prefixes": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441\u044b (\u0441\u043f\u0438\u0441\u043e\u043a, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0439 \u0437\u0430\u043f\u044f\u0442\u044b\u043c\u0438)" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u0432" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/tr.json b/homeassistant/components/fritzbox_callmonitor/translations/tr.json new file mode 100644 index 00000000000..76799f24af8 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/tr.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "insufficient_permissions": "Kullan\u0131c\u0131, AVM FRITZ! Box ayarlar\u0131na ve telefon defterlerine eri\u015fmek i\u00e7in yeterli izne sahip de\u011fil.", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "flow_title": "AVM FRITZ! Box \u00e7a\u011fr\u0131 monit\u00f6r\u00fc: {name}", + "step": { + "phonebook": { + "data": { + "phonebook": "Telefon rehberi" + } + }, + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "\u015eifre", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "\u00d6nekler yanl\u0131\u015f bi\u00e7imlendirilmi\u015ftir, l\u00fctfen bi\u00e7imlerini kontrol edin." + }, + "step": { + "init": { + "data": { + "prefixes": "\u00d6nekler (virg\u00fclle ayr\u0131lm\u0131\u015f liste)" + }, + "title": "\u00d6nekleri Yap\u0131land\u0131r" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/zh-Hant.json b/homeassistant/components/fritzbox_callmonitor/translations/zh-Hant.json new file mode 100644 index 00000000000..d159f5df0f9 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/zh-Hant.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "insufficient_permissions": "\u4f7f\u7528\u8005\u6c92\u6709\u8db3\u5920\u6b0a\u9650\u4ee5\u5b58\u53d6 AVM FRITZ!Box \u8a2d\u5b9a\u53ca\u96fb\u8a71\u7c3f\u3002", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "flow_title": "AVM FRITZ!Box \u901a\u8a71\u76e3\u63a7\u5668\uff1a{name}", + "step": { + "phonebook": { + "data": { + "phonebook": "\u96fb\u8a71\u7c3f" + } + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "\u524d\u7db4\u5b57\u9996\u683c\u5f0f\u932f\u8aa4\uff0c\u8acb\u518d\u78ba\u8a8d\u5176\u683c\u5f0f\u3002" + }, + "step": { + "init": { + "data": { + "prefixes": "\u524d\u7db4\u5b57\u9996\uff08\u4ee5\u9017\u865f\u5206\u9694\uff09" + }, + "title": "\u8a2d\u5b9a\u524d\u7db4\u5b57\u9996" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/de.json b/homeassistant/components/garmin_connect/translations/de.json index 54d27e9956e..9186f753a77 100644 --- a/homeassistant/components/garmin_connect/translations/de.json +++ b/homeassistant/components/garmin_connect/translations/de.json @@ -4,10 +4,10 @@ "already_configured": "Dieses Konto ist bereits konfiguriert." }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen. Bitte versuchen Sie es erneut.", - "invalid_auth": "Ung\u00fcltige Authentifizierung.", - "too_many_requests": "Zu viele Anfragen, wiederholen Sie es sp\u00e4ter.", - "unknown": "Unerwarteter Fehler." + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "too_many_requests": "Zu viele Anfragen, versuche es sp\u00e4ter erneut.", + "unknown": "Unerwarteter Fehler" }, "step": { "user": { diff --git a/homeassistant/components/garmin_connect/translations/tr.json b/homeassistant/components/garmin_connect/translations/tr.json new file mode 100644 index 00000000000..a83e1936fb4 --- /dev/null +++ b/homeassistant/components/garmin_connect/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/uk.json b/homeassistant/components/garmin_connect/translations/uk.json new file mode 100644 index 00000000000..aef0632b0f1 --- /dev/null +++ b/homeassistant/components/garmin_connect/translations/uk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "too_many_requests": "\u0417\u0430\u043d\u0430\u0434\u0442\u043e \u0431\u0430\u0433\u0430\u0442\u043e \u0437\u0430\u043f\u0438\u0442\u0456\u0432, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437 \u043f\u0456\u0437\u043d\u0456\u0448\u0435.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0412\u0430\u0448\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456.", + "title": "Garmin Connect" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/translations/de.json b/homeassistant/components/gdacs/translations/de.json index 07d1a4bdb79..a69295f0640 100644 --- a/homeassistant/components/gdacs/translations/de.json +++ b/homeassistant/components/gdacs/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Der Standort ist bereits konfiguriert." + "already_configured": "Der Dienst ist bereits konfiguriert" }, "step": { "user": { diff --git a/homeassistant/components/gdacs/translations/tr.json b/homeassistant/components/gdacs/translations/tr.json new file mode 100644 index 00000000000..aeb6a5a345e --- /dev/null +++ b/homeassistant/components/gdacs/translations/tr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "radius": "Yar\u0131\u00e7ap" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/translations/uk.json b/homeassistant/components/gdacs/translations/uk.json new file mode 100644 index 00000000000..0ab20bc55a3 --- /dev/null +++ b/homeassistant/components/gdacs/translations/uk.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "step": { + "user": { + "data": { + "radius": "\u0420\u0430\u0434\u0456\u0443\u0441" + }, + "title": "\u041c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/translations/de.json b/homeassistant/components/geofency/translations/de.json index 31b8a5eb321..9c3fd3ea1b0 100644 --- a/homeassistant/components/geofency/translations/de.json +++ b/homeassistant/components/geofency/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", + "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." + }, "create_entry": { "default": "Um Ereignisse an den Home Assistant zu senden, musst das Webhook Feature in Geofency konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})." }, diff --git a/homeassistant/components/geofency/translations/tr.json b/homeassistant/components/geofency/translations/tr.json new file mode 100644 index 00000000000..84adcdf8225 --- /dev/null +++ b/homeassistant/components/geofency/translations/tr.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/translations/uk.json b/homeassistant/components/geofency/translations/uk.json new file mode 100644 index 00000000000..54a14afb764 --- /dev/null +++ b/homeassistant/components/geofency/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f Geofency. \n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457." + }, + "step": { + "user": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Geofency?", + "title": "Geofency" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/translations/tr.json b/homeassistant/components/geonetnz_quakes/translations/tr.json new file mode 100644 index 00000000000..717f6d72b94 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/translations/uk.json b/homeassistant/components/geonetnz_quakes/translations/uk.json new file mode 100644 index 00000000000..35653baa945 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/translations/uk.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "\u0420\u0430\u0434\u0456\u0443\u0441" + }, + "title": "GeoNet NZ Quakes" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/translations/de.json b/homeassistant/components/geonetnz_volcano/translations/de.json index b573d93cd5a..a29555e53ab 100644 --- a/homeassistant/components/geonetnz_volcano/translations/de.json +++ b/homeassistant/components/geonetnz_volcano/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Standort ist bereits konfiguriert" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/geonetnz_volcano/translations/tr.json b/homeassistant/components/geonetnz_volcano/translations/tr.json new file mode 100644 index 00000000000..980be333568 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/translations/tr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "radius": "Yar\u0131\u00e7ap" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/translations/uk.json b/homeassistant/components/geonetnz_volcano/translations/uk.json new file mode 100644 index 00000000000..77a4f1eee68 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/translations/uk.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + }, + "step": { + "user": { + "data": { + "radius": "\u0420\u0430\u0434\u0456\u0443\u0441" + }, + "title": "GeoNet NZ Volcano" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/de.json b/homeassistant/components/gios/translations/de.json index 0a5cea1819d..7bbb01cf18d 100644 --- a/homeassistant/components/gios/translations/de.json +++ b/homeassistant/components/gios/translations/de.json @@ -4,14 +4,14 @@ "already_configured": "GIO\u015a integration f\u00fcr diese Messstation ist bereits konfiguriert. " }, "error": { - "cannot_connect": "Es kann keine Verbindung zum GIO\u015a-Server hergestellt werden.", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_sensors_data": "Ung\u00fcltige Sensordaten f\u00fcr diese Messstation.", "wrong_station_id": "ID der Messstation ist nicht korrekt." }, "step": { "user": { "data": { - "name": "Name der Integration", + "name": "Name", "station_id": "ID der Messstation" }, "description": "Einrichtung von GIO\u015a (Polnische Hauptinspektion f\u00fcr Umweltschutz) Integration der Luftqualit\u00e4t. Wenn du Hilfe bei der Konfiguration ben\u00f6tigst, schaue hier: https://www.home-assistant.io/integrations/gios", diff --git a/homeassistant/components/gios/translations/fr.json b/homeassistant/components/gios/translations/fr.json index b06c41208bc..2b02b5cfea0 100644 --- a/homeassistant/components/gios/translations/fr.json +++ b/homeassistant/components/gios/translations/fr.json @@ -18,5 +18,10 @@ "title": "GIO\u015a (Inspection g\u00e9n\u00e9rale polonaise de la protection de l'environnement)" } } + }, + "system_health": { + "info": { + "can_reach_server": "Acc\u00e9der au serveur GIO\u015a" + } } } \ 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/gios/translations/lb.json b/homeassistant/components/gios/translations/lb.json index 8e8ab861b43..cafea72fb78 100644 --- a/homeassistant/components/gios/translations/lb.json +++ b/homeassistant/components/gios/translations/lb.json @@ -18,5 +18,10 @@ "title": "GIO\u015a (Polnesch Chefinspektorat vum \u00cbmweltschutz)" } } + }, + "system_health": { + "info": { + "can_reach_server": "GIO\u015a Server ereechbar" + } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/tr.json b/homeassistant/components/gios/translations/tr.json new file mode 100644 index 00000000000..590aec1894c --- /dev/null +++ b/homeassistant/components/gios/translations/tr.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/uk.json b/homeassistant/components/gios/translations/uk.json new file mode 100644 index 00000000000..f62408c5e8e --- /dev/null +++ b/homeassistant/components/gios/translations/uk.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_sensors_data": "\u041d\u0435\u0432\u0456\u0440\u043d\u0456 \u0434\u0430\u043d\u0456 \u0441\u0435\u043d\u0441\u043e\u0440\u0456\u0432 \u0434\u043b\u044f \u0446\u0456\u0454\u0457 \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043b\u044c\u043d\u043e\u0457 \u0441\u0442\u0430\u043d\u0446\u0456\u0457.", + "wrong_station_id": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 ID \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043b\u044c\u043d\u043e\u0457 \u0441\u0442\u0430\u043d\u0446\u0456\u0457." + }, + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430", + "station_id": "ID \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043b\u044c\u043d\u043e\u0457 \u0441\u0442\u0430\u043d\u0446\u0456\u0457" + }, + "description": "\u0406\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044f \u043f\u0440\u043e \u044f\u043a\u0456\u0441\u0442\u044c \u043f\u043e\u0432\u0456\u0442\u0440\u044f \u0432\u0456\u0434 \u041f\u043e\u043b\u044c\u0441\u044c\u043a\u043e\u0457 \u0456\u043d\u0441\u043f\u0435\u043a\u0446\u0456\u0457 \u0437 \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \u043d\u0430\u0432\u043a\u043e\u043b\u0438\u0448\u043d\u044c\u043e\u0433\u043e \u0441\u0435\u0440\u0435\u0434\u043e\u0432\u0438\u0449\u0430 (GIO\u015a). \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0454\u044e \u043f\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044e \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457: https://www.home-assistant.io/integrations/gios.", + "title": "GIO\u015a (\u041f\u043e\u043b\u044c\u0441\u044c\u043a\u0430 \u0456\u043d\u0441\u043f\u0435\u043a\u0446\u0456\u044f \u0437 \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \u043d\u0430\u0432\u043a\u043e\u043b\u0438\u0448\u043d\u044c\u043e\u0433\u043e \u0441\u0435\u0440\u0435\u0434\u043e\u0432\u0438\u0449\u0430)" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 GIO\u015a" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/translations/de.json b/homeassistant/components/glances/translations/de.json index 69c34907f19..e464bfdee34 100644 --- a/homeassistant/components/glances/translations/de.json +++ b/homeassistant/components/glances/translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Host ist bereits konfiguriert." + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung zum Host nicht m\u00f6glich", + "cannot_connect": "Verbindung fehlgeschlagen", "wrong_version": "Version nicht unterst\u00fctzt (nur 2 oder 3)" }, "step": { diff --git a/homeassistant/components/glances/translations/tr.json b/homeassistant/components/glances/translations/tr.json new file mode 100644 index 00000000000..69f0cd7ceb1 --- /dev/null +++ b/homeassistant/components/glances/translations/tr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "G\u00fcncelleme s\u0131kl\u0131\u011f\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/translations/uk.json b/homeassistant/components/glances/translations/uk.json new file mode 100644 index 00000000000..1fab197fe42 --- /dev/null +++ b/homeassistant/components/glances/translations/uk.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "wrong_version": "\u041f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u044e\u0442\u044c\u0441\u044f \u0442\u0456\u043b\u044c\u043a\u0438 \u0432\u0435\u0440\u0441\u0456\u0457 2 \u0442\u0430 3." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL", + "version": "\u0412\u0435\u0440\u0441\u0456\u044f API Glances (2 \u0430\u0431\u043e 3)" + }, + "title": "Glances" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f" + }, + "description": "\u0420\u043e\u0437\u0448\u0438\u0440\u0435\u043d\u0456 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/de.json b/homeassistant/components/goalzero/translations/de.json index d79c03f0179..7d8962cdb11 100644 --- a/homeassistant/components/goalzero/translations/de.json +++ b/homeassistant/components/goalzero/translations/de.json @@ -1,9 +1,20 @@ { "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse", "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/fr.json b/homeassistant/components/goalzero/translations/fr.json index 5c4b7a01580..7bd4929ad92 100644 --- a/homeassistant/components/goalzero/translations/fr.json +++ b/homeassistant/components/goalzero/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9" + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" }, "error": { "cannot_connect": "\u00c9chec de connexion", diff --git a/homeassistant/components/goalzero/translations/tr.json b/homeassistant/components/goalzero/translations/tr.json new file mode 100644 index 00000000000..ae77262b2b3 --- /dev/null +++ b/homeassistant/components/goalzero/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/uk.json b/homeassistant/components/goalzero/translations/uk.json new file mode 100644 index 00000000000..6d67d949c28 --- /dev/null +++ b/homeassistant/components/goalzero/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_host": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u0421\u043f\u043e\u0447\u0430\u0442\u043a\u0443 \u0412\u0430\u043c \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0437\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0438\u0442\u0438 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Goal Zero: https://www.goalzero.com/product-features/yeti-app/. \n\n \u0414\u043e\u0442\u0440\u0438\u043c\u0443\u0439\u0442\u0435\u0441\u044c \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0439 \u043f\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044e Yeti \u0434\u043e \u043c\u0435\u0440\u0435\u0436\u0456 WiFi. \u041f\u043e\u0442\u0456\u043c \u0434\u0456\u0437\u043d\u0430\u0439\u0442\u0435\u0441\u044f IP \u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e, \u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434, \u0437 \u0412\u0430\u0448\u043e\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430. \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0431\u0443\u0442\u0438 \u0442\u0430\u043a\u0438\u043c\u0438, \u0449\u043e\u0431 IP \u0430\u0434\u0440\u0435\u0441\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u043d\u0435 \u0437\u043c\u0456\u043d\u044e\u0432\u0430\u043b\u0430\u0441\u044c \u0437 \u0447\u0430\u0441\u043e\u043c. \u041f\u0440\u043e \u0442\u0435, \u044f\u043a \u0446\u0435 \u0437\u0440\u043e\u0431\u0438\u0442\u0438, \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0456\u0437\u043d\u0430\u0442\u0438\u0441\u044f \u0432 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0457 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0412\u0430\u0448\u043e\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430.", + "title": "Goal Zero Yeti" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/tr.json b/homeassistant/components/gogogate2/translations/tr.json new file mode 100644 index 00000000000..e912e7f8012 --- /dev/null +++ b/homeassistant/components/gogogate2/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "ip_address": "\u0130p Adresi", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/uk.json b/homeassistant/components/gogogate2/translations/uk.json new file mode 100644 index 00000000000..c88b9b60384 --- /dev/null +++ b/homeassistant/components/gogogate2/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 GogoGate2.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f GogoGate2 \u0430\u0431\u043e iSmartGate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/translations/de.json b/homeassistant/components/gpslogger/translations/de.json index d976a5fd663..7215f0c458f 100644 --- a/homeassistant/components/gpslogger/translations/de.json +++ b/homeassistant/components/gpslogger/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", + "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." + }, "create_entry": { "default": "Um Ereignisse an Home Assistant zu senden, muss das Webhook Feature in der GPSLogger konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})." }, diff --git a/homeassistant/components/gpslogger/translations/tr.json b/homeassistant/components/gpslogger/translations/tr.json new file mode 100644 index 00000000000..84adcdf8225 --- /dev/null +++ b/homeassistant/components/gpslogger/translations/tr.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/translations/uk.json b/homeassistant/components/gpslogger/translations/uk.json new file mode 100644 index 00000000000..5b0b6305cdb --- /dev/null +++ b/homeassistant/components/gpslogger/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f GPSLogger. \n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457." + }, + "step": { + "user": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 GPSLogger?", + "title": "GPSLogger" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gree/translations/de.json b/homeassistant/components/gree/translations/de.json new file mode 100644 index 00000000000..96ed09a974f --- /dev/null +++ b/homeassistant/components/gree/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": "M\u00f6chtest du mit der Einrichtung beginnen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gree/translations/tr.json b/homeassistant/components/gree/translations/tr.json new file mode 100644 index 00000000000..8de4663957e --- /dev/null +++ b/homeassistant/components/gree/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gree/translations/uk.json b/homeassistant/components/gree/translations/uk.json new file mode 100644 index 00000000000..292861e9129 --- /dev/null +++ b/homeassistant/components/gree/translations/uk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "step": { + "confirm": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/de.json b/homeassistant/components/griddy/translations/de.json index ad6a6e10ab0..4a6c477059c 100644 --- a/homeassistant/components/griddy/translations/de.json +++ b/homeassistant/components/griddy/translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Diese Ladezone ist bereits konfiguriert" + "already_configured": "Standort ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/griddy/translations/tr.json b/homeassistant/components/griddy/translations/tr.json index d887b148658..26e0fa73065 100644 --- a/homeassistant/components/griddy/translations/tr.json +++ b/homeassistant/components/griddy/translations/tr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, "error": { "cannot_connect": "Ba\u011flant\u0131 kurulamad\u0131, l\u00fctfen tekrar deneyin", "unknown": "Beklenmeyen hata" diff --git a/homeassistant/components/griddy/translations/uk.json b/homeassistant/components/griddy/translations/uk.json new file mode 100644 index 00000000000..e366f0e8b24 --- /dev/null +++ b/homeassistant/components/griddy/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "loadzone": "\u0417\u043e\u043d\u0430 \u043d\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0435\u043d\u043d\u044f (\u0440\u043e\u0437\u0440\u0430\u0445\u0443\u043d\u043a\u043e\u0432\u0430 \u0442\u043e\u0447\u043a\u0430)" + }, + "description": "\u0417\u043e\u043d\u0430 \u043d\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0435\u043d\u043d\u044f \u0437\u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0443 \u0432\u0430\u0448\u043e\u043c\u0443 \u043f\u0440\u043e\u0444\u0456\u043b\u0456 Griddy \u0432 \u0440\u043e\u0437\u0434\u0456\u043b\u0456 Account > Meter > Load Zone.", + "title": "Griddy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/translations/uk.json b/homeassistant/components/group/translations/uk.json index 2d57686134a..08cee558f27 100644 --- a/homeassistant/components/group/translations/uk.json +++ b/homeassistant/components/group/translations/uk.json @@ -9,7 +9,7 @@ "ok": "\u041e\u041a", "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e", "open": "\u0412\u0456\u0434\u0447\u0438\u043d\u0435\u043d\u043e", - "problem": "\u0425\u0430\u043b\u0435\u043f\u0430", + "problem": "\u041f\u0440\u043e\u0431\u043b\u0435\u043c\u0430", "unlocked": "\u0420\u043e\u0437\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e" } }, diff --git a/homeassistant/components/guardian/translations/de.json b/homeassistant/components/guardian/translations/de.json index 27770d690f0..d1218cb2372 100644 --- a/homeassistant/components/guardian/translations/de.json +++ b/homeassistant/components/guardian/translations/de.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "cannot_connect": "Verbindungsfehler" + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/guardian/translations/tr.json b/homeassistant/components/guardian/translations/tr.json new file mode 100644 index 00000000000..1e520a16995 --- /dev/null +++ b/homeassistant/components/guardian/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "ip_address": "\u0130p Adresi", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/uk.json b/homeassistant/components/guardian/translations/uk.json new file mode 100644 index 00000000000..439a225895e --- /dev/null +++ b/homeassistant/components/guardian/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Elexa Guardian." + }, + "zeroconf_confirm": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0446\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Elexa Guardian?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/de.json b/homeassistant/components/hangouts/translations/de.json index 5c8ab51cf4e..7b888cf531e 100644 --- a/homeassistant/components/hangouts/translations/de.json +++ b/homeassistant/components/hangouts/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Google Hangouts ist bereits konfiguriert", - "unknown": "Ein unbekannter Fehler ist aufgetreten." + "unknown": "Unerwarteter Fehler" }, "error": { "invalid_2fa": "Ung\u00fcltige 2-Faktor Authentifizierung, bitte versuche es erneut.", diff --git a/homeassistant/components/hangouts/translations/tr.json b/homeassistant/components/hangouts/translations/tr.json new file mode 100644 index 00000000000..a204200a2d8 --- /dev/null +++ b/homeassistant/components/hangouts/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "email": "E-posta", + "password": "Parola" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/uk.json b/homeassistant/components/hangouts/translations/uk.json new file mode 100644 index 00000000000..93eb699d37c --- /dev/null +++ b/homeassistant/components/hangouts/translations/uk.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "invalid_2fa": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f, \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443.", + "invalid_2fa_method": "\u041d\u0435\u043f\u0440\u0438\u043f\u0443\u0441\u0442\u0438\u043c\u0438\u0439 \u0441\u043f\u043e\u0441\u0456\u0431 \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457 (\u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0438\u0442\u0438 \u043d\u0430 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0456).", + "invalid_login": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043b\u043e\u0433\u0456\u043d, \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443." + }, + "step": { + "2fa": { + "data": { + "2fa": "\u041f\u0456\u043d-\u043a\u043e\u0434 \u0434\u043b\u044f \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "description": "\u043f\u043e\u0440\u043e\u0436\u043d\u044c\u043e", + "title": "\u0414\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + }, + "user": { + "data": { + "authorization_code": "\u041a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457 (\u0432\u0438\u043c\u0430\u0433\u0430\u0454\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0440\u0443\u0447\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457)", + "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u043f\u043e\u0440\u043e\u0436\u043d\u044c\u043e", + "title": "Google Hangouts" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/de.json b/homeassistant/components/harmony/translations/de.json index f10dfe1432c..9cd07f09529 100644 --- a/homeassistant/components/harmony/translations/de.json +++ b/homeassistant/components/harmony/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, "flow_title": "Logitech Harmony Hub {name}", diff --git a/homeassistant/components/harmony/translations/tr.json b/homeassistant/components/harmony/translations/tr.json new file mode 100644 index 00000000000..c77f0f8e07e --- /dev/null +++ b/homeassistant/components/harmony/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/uk.json b/homeassistant/components/harmony/translations/uk.json new file mode 100644 index 00000000000..5bb2da811f3 --- /dev/null +++ b/homeassistant/components/harmony/translations/uk.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "Logitech Harmony Hub {name}", + "step": { + "link": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name} ({host})?", + "title": "Logitech Harmony Hub" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "title": "Logitech Harmony Hub" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "activity": "\u0410\u043a\u0442\u0438\u0432\u043d\u0456\u0441\u0442\u044c \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c, \u043a\u043e\u043b\u0438 \u0436\u043e\u0434\u043d\u0430 \u0437 \u043d\u0438\u0445 \u043d\u0435 \u0432\u043a\u0430\u0437\u0430\u043d\u0430.", + "delay_secs": "\u0417\u0430\u0442\u0440\u0438\u043c\u043a\u0430 \u043c\u0456\u0436 \u043d\u0430\u0434\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c \u043a\u043e\u043c\u0430\u043d\u0434." + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 Harmony Hub" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/ca.json b/homeassistant/components/hassio/translations/ca.json index 0cdb9318428..19b4316c9ce 100644 --- a/homeassistant/components/hassio/translations/ca.json +++ b/homeassistant/components/hassio/translations/ca.json @@ -12,7 +12,7 @@ "supervisor_version": "Versi\u00f3 del Supervisor", "supported": "Compatible", "update_channel": "Canal d'actualitzaci\u00f3", - "version_api": "API de versions" + "version_api": "Versi\u00f3 d'APIs" } }, "title": "Hass.io" diff --git a/homeassistant/components/hassio/translations/de.json b/homeassistant/components/hassio/translations/de.json index 981cb51c83a..939821edb54 100644 --- a/homeassistant/components/hassio/translations/de.json +++ b/homeassistant/components/hassio/translations/de.json @@ -1,3 +1,18 @@ { + "system_health": { + "info": { + "board": "Board", + "disk_total": "Speicherplatz gesamt", + "disk_used": "Speicherplatz genutzt", + "docker_version": "Docker-Version", + "host_os": "Host-Betriebssystem", + "installed_addons": "Installierte Add-ons", + "supervisor_api": "Supervisor-API", + "supervisor_version": "Supervisor-Version", + "supported": "Unterst\u00fctzt", + "update_channel": "Update-Channel", + "version_api": "Versions-API" + } + }, "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/en.json b/homeassistant/components/hassio/translations/en.json index aadcdabfb94..230e0c11fea 100644 --- a/homeassistant/components/hassio/translations/en.json +++ b/homeassistant/components/hassio/translations/en.json @@ -9,11 +9,11 @@ "host_os": "Host Operating System", "installed_addons": "Installed Add-ons", "supervisor_api": "Supervisor API", - "supervisor_version": "Version", + "supervisor_version": "Supervisor Version", "supported": "Supported", "update_channel": "Update Channel", "version_api": "Version API" } }, - "title": "Home Assistant Supervisor" + "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/fr.json b/homeassistant/components/hassio/translations/fr.json index 981cb51c83a..2bb52c3c54c 100644 --- a/homeassistant/components/hassio/translations/fr.json +++ b/homeassistant/components/hassio/translations/fr.json @@ -1,3 +1,18 @@ { + "system_health": { + "info": { + "board": "Tableau de bord", + "disk_total": "Taille total du disque", + "disk_used": "Taille du disque utilis\u00e9", + "docker_version": "Version de Docker", + "healthy": "Sain", + "installed_addons": "Add-ons install\u00e9s", + "supervisor_api": "API du superviseur", + "supervisor_version": "Version du supervisor", + "supported": "Prise en charge", + "update_channel": "Mise \u00e0 jour", + "version_api": "Version API" + } + }, "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/tr.json b/homeassistant/components/hassio/translations/tr.json index d368ac0fb3c..f2c2d52f60d 100644 --- a/homeassistant/components/hassio/translations/tr.json +++ b/homeassistant/components/hassio/translations/tr.json @@ -6,7 +6,13 @@ "disk_used": "Kullan\u0131lan Disk", "docker_version": "Docker S\u00fcr\u00fcm\u00fc", "healthy": "Sa\u011fl\u0131kl\u0131", - "host_os": "Ana Bilgisayar \u0130\u015fletim Sistemi" + "host_os": "Ana Bilgisayar \u0130\u015fletim Sistemi", + "installed_addons": "Y\u00fckl\u00fc Eklentiler", + "supervisor_api": "Supervisor API", + "supervisor_version": "S\u00fcperviz\u00f6r S\u00fcr\u00fcm\u00fc", + "supported": "Destekleniyor", + "update_channel": "Kanal\u0131 G\u00fcncelle", + "version_api": "S\u00fcr\u00fcm API" } }, "title": "Hass.io" diff --git a/homeassistant/components/hassio/translations/uk.json b/homeassistant/components/hassio/translations/uk.json index 981cb51c83a..19a40730897 100644 --- a/homeassistant/components/hassio/translations/uk.json +++ b/homeassistant/components/hassio/translations/uk.json @@ -1,3 +1,19 @@ { + "system_health": { + "info": { + "board": "\u041f\u043b\u0430\u0442\u0430", + "disk_total": "\u0414\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u043f\u0430\u043c'\u044f\u0442\u044c", + "disk_used": "\u041f\u0430\u043c'\u044f\u0442\u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043e", + "docker_version": "\u0412\u0435\u0440\u0441\u0456\u044f Docker", + "healthy": "\u0412 \u043d\u043e\u0440\u043c\u0456", + "host_os": "\u041e\u043f\u0435\u0440\u0430\u0446\u0456\u0439\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u0445\u043e\u0441\u0442\u0430", + "installed_addons": "\u0412\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0456 \u0434\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f", + "supervisor_api": "Supervisor API", + "supervisor_version": "\u0412\u0435\u0440\u0441\u0456\u044f Supervisor", + "supported": "\u041f\u0456\u0434\u0442\u0440\u0438\u043c\u043a\u0430", + "update_channel": "\u041a\u0430\u043d\u0430\u043b \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u044c", + "version_api": "\u0412\u0435\u0440\u0441\u0456\u044f API" + } + }, "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/heos/translations/de.json b/homeassistant/components/heos/translations/de.json index 92ab6c1c8ff..ba8a5318951 100644 --- a/homeassistant/components/heos/translations/de.json +++ b/homeassistant/components/heos/translations/de.json @@ -1,7 +1,10 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/heos/translations/tr.json b/homeassistant/components/heos/translations/tr.json new file mode 100644 index 00000000000..4f1ad775905 --- /dev/null +++ b/homeassistant/components/heos/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/heos/translations/uk.json b/homeassistant/components/heos/translations/uk.json new file mode 100644 index 00000000000..c0a5fdf04bf --- /dev/null +++ b/homeassistant/components/heos/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043c'\u044f \u0445\u043e\u0441\u0442\u0430 \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e HEOS (\u0431\u0430\u0436\u0430\u043d\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0434\u043e \u043c\u0435\u0440\u0435\u0436\u0456 \u0447\u0435\u0440\u0435\u0437 \u043a\u0430\u0431\u0435\u043b\u044c).", + "title": "HEOS" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/translations/de.json b/homeassistant/components/hisense_aehw4a1/translations/de.json index d5f4f429740..7c0bd96a9c9 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/de.json +++ b/homeassistant/components/hisense_aehw4a1/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Es wurden keine Hisense AEH-W4A1-Ger\u00e4te im Netzwerk gefunden.", - "single_instance_allowed": "Es ist nur eine einzige Konfiguration von Hisense AEH-W4A1 m\u00f6glich." + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { "confirm": { diff --git a/homeassistant/components/hisense_aehw4a1/translations/tr.json b/homeassistant/components/hisense_aehw4a1/translations/tr.json new file mode 100644 index 00000000000..a893a653a78 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "Hisense AEH-W4A1'i kurmak istiyor musunuz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/translations/uk.json b/homeassistant/components/hisense_aehw4a1/translations/uk.json new file mode 100644 index 00000000000..900882513d5 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/translations/uk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "step": { + "confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Hisense AEH-W4A1?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/translations/de.json b/homeassistant/components/hlk_sw16/translations/de.json index 94b8d6526d1..625c7372347 100644 --- a/homeassistant/components/hlk_sw16/translations/de.json +++ b/homeassistant/components/hlk_sw16/translations/de.json @@ -1,11 +1,17 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { "user": { "data": { + "host": "Host", "password": "Passwort", "username": "Benutzername" } diff --git a/homeassistant/components/hlk_sw16/translations/tr.json b/homeassistant/components/hlk_sw16/translations/tr.json new file mode 100644 index 00000000000..40c9c39b967 --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/translations/uk.json b/homeassistant/components/hlk_sw16/translations/uk.json new file mode 100644 index 00000000000..2df11f74455 --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_connect/translations/de.json b/homeassistant/components/home_connect/translations/de.json index 05204c35c41..2454c039361 100644 --- a/homeassistant/components/home_connect/translations/de.json +++ b/homeassistant/components/home_connect/translations/de.json @@ -1,14 +1,15 @@ { "config": { "abort": { - "missing_configuration": "Die Komponente Home Connect ist nicht konfiguriert. Bitte folgen Sie der Dokumentation." + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url})." }, "create_entry": { - "default": "Erfolgreich mit Home Connect authentifiziert." + "default": "Erfolgreich authentifiziert" }, "step": { "pick_implementation": { - "title": "Authentifizierungsmethode ausw\u00e4hlen" + "title": "W\u00e4hle die Authentifizierungsmethode" } } } diff --git a/homeassistant/components/home_connect/translations/uk.json b/homeassistant/components/home_connect/translations/uk.json new file mode 100644 index 00000000000..247ffd16713 --- /dev/null +++ b/homeassistant/components/home_connect/translations/uk.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443." + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "step": { + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/de.json b/homeassistant/components/homeassistant/translations/de.json index 45768a9f127..e24568ff212 100644 --- a/homeassistant/components/homeassistant/translations/de.json +++ b/homeassistant/components/homeassistant/translations/de.json @@ -1,10 +1,21 @@ { "system_health": { "info": { + "arch": "CPU-Architektur", + "chassis": "Chassis", + "dev": "Entwicklung", "docker": "Docker", "docker_version": "Docker", "hassio": "Supervisor", - "host_os": "Home Assistant OS" + "host_os": "Home Assistant OS", + "installation_type": "Installationstyp", + "os_name": "Betriebssystemfamilie", + "os_version": "Betriebssystem-Version", + "python_version": "Python-Version", + "supervisor": "Supervisor", + "timezone": "Zeitzone", + "version": "Version", + "virtualenv": "Virtuelle Umgebung" } } } \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/fr.json b/homeassistant/components/homeassistant/translations/fr.json new file mode 100644 index 00000000000..194254a0384 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/fr.json @@ -0,0 +1,21 @@ +{ + "system_health": { + "info": { + "arch": "Architecture du processeur", + "chassis": "Ch\u00e2ssis", + "dev": "D\u00e9veloppement", + "docker": "Docker", + "docker_version": "Docker", + "hassio": "Superviseur", + "host_os": "Home Assistant OS", + "installation_type": "Type d'installation", + "os_name": "Famille du syst\u00e8me d'exploitation", + "os_version": "Version du syst\u00e8me d'exploitation", + "python_version": "Version de Python", + "supervisor": "Supervisor", + "timezone": "Fuseau horaire", + "version": "Version", + "virtualenv": "Environnement virtuel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/tr.json b/homeassistant/components/homeassistant/translations/tr.json index 1ff8ea1b3a9..c2b7ca1b10c 100644 --- a/homeassistant/components/homeassistant/translations/tr.json +++ b/homeassistant/components/homeassistant/translations/tr.json @@ -2,9 +2,11 @@ "system_health": { "info": { "arch": "CPU Mimarisi", + "chassis": "Ana G\u00f6vde", "dev": "Geli\u015ftirme", "docker": "Konteyner", "docker_version": "Konteyner", + "hassio": "S\u00fcperviz\u00f6r", "host_os": "Home Assistant OS", "installation_type": "Kurulum T\u00fcr\u00fc", "os_name": "\u0130\u015fletim Sistemi Ailesi", diff --git a/homeassistant/components/homeassistant/translations/uk.json b/homeassistant/components/homeassistant/translations/uk.json new file mode 100644 index 00000000000..19e07c8f822 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/uk.json @@ -0,0 +1,21 @@ +{ + "system_health": { + "info": { + "arch": "\u0410\u0440\u0445\u0456\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u0426\u041f", + "chassis": "\u0428\u0430\u0441\u0456", + "dev": "\u0421\u0435\u0440\u0435\u0434\u043e\u0432\u0438\u0449\u0435 \u0440\u043e\u0437\u0440\u043e\u0431\u043a\u0438", + "docker": "Docker", + "docker_version": "Docker", + "hassio": "Supervisor", + "host_os": "Home Assistant OS", + "installation_type": "\u0422\u0438\u043f \u0456\u043d\u0441\u0442\u0430\u043b\u044f\u0446\u0456\u0457", + "os_name": "\u0421\u0456\u043c\u0435\u0439\u0441\u0442\u0432\u043e \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0439\u043d\u0438\u0445 \u0441\u0438\u0441\u0442\u0435\u043c", + "os_version": "\u0412\u0435\u0440\u0441\u0456\u044f \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0439\u043d\u043e\u0457 \u0441\u0438\u0441\u0442\u0435\u043c\u0438", + "python_version": "\u0412\u0435\u0440\u0441\u0456\u044f Python", + "supervisor": "Supervisor", + "timezone": "\u0427\u0430\u0441\u043e\u0432\u0438\u0439 \u043f\u043e\u044f\u0441", + "version": "\u0412\u0435\u0440\u0441\u0456\u044f", + "virtualenv": "\u0412\u0456\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u0435 \u043e\u0442\u043e\u0447\u0435\u043d\u043d\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/ca.json b/homeassistant/components/homekit/translations/ca.json index 63f461ea344..0870b05a6d1 100644 --- a/homeassistant/components/homekit/translations/ca.json +++ b/homeassistant/components/homekit/translations/ca.json @@ -4,6 +4,20 @@ "port_name_in_use": "Ja hi ha un enlla\u00e7 o accessori configurat amb aquest nom o port." }, "step": { + "accessory_mode": { + "data": { + "entity_id": "Entitat" + }, + "description": "Escull l'entitat que vulguis incloure. En mode accessori, nom\u00e9s s'inclou una sola entitat.", + "title": "Selecciona l'entitat a incloure" + }, + "bridge_mode": { + "data": { + "include_domains": "Dominis a incloure" + }, + "description": "Escull els dominis que vulguis incloure. S'inclouran totes les entitats del domini que siguin compatibles.", + "title": "Selecciona els dominis a incloure" + }, "pairing": { "description": "Tan aviat com {name} estigui llest, la vinculaci\u00f3 estar\u00e0 disponible a \"Notificacions\" com a \"Configuraci\u00f3 de l'enlla\u00e7 HomeKit\".", "title": "Vinculaci\u00f3 HomeKit" @@ -11,9 +25,10 @@ "user": { "data": { "auto_start": "Autoarrencada (desactiva-ho si fas servir Z-Wave o algun altre sistema d'inici lent)", - "include_domains": "Dominis a incloure" + "include_domains": "Dominis a incloure", + "mode": "Mode" }, - "description": "La integraci\u00f3 HomeKit et permet l'acc\u00e9s a les teves entitats de Home Assistant a HomeKit. En mode enlla\u00e7, els enlla\u00e7os HomeKit estan limitats a un m\u00e0xim de 150 accessoris per inst\u00e0ncia (incl\u00f2s el propi enlla\u00e7). Si volguessis enlla\u00e7ar m\u00e9s accessoris que el m\u00e0xim perm\u00e8s, \u00e9s recomanable que utilitzis diferents enlla\u00e7os HomeKit per a dominis diferents. La configuraci\u00f3 avan\u00e7ada d'entitat nom\u00e9s est\u00e0 disponible en YAML per l'enlla\u00e7 prinipal.", + "description": "La integraci\u00f3 HomeKit et permetr\u00e0 l'acc\u00e9s a les teves entitats de Home Assistant a HomeKit. En mode enlla\u00e7, els enlla\u00e7os HomeKit estan limitats a un m\u00e0xim de 150 accessoris per inst\u00e0ncia (incl\u00f2s el propi enlla\u00e7). Si volguessis enlla\u00e7ar m\u00e9s accessoris que el m\u00e0xim perm\u00e8s, \u00e9s recomanable que utilitzis diferents enlla\u00e7os HomeKit per a dominis diferents. La configuraci\u00f3 avan\u00e7ada d'entitat nom\u00e9s est\u00e0 disponible en YAML. Per obtenir el millor rendiment i evitar errors de disponibilitat inesperats , crea i vincula una inst\u00e0ncia HomeKit en mode accessori per a cada repoductor multim\u00e8dia/TV i c\u00e0mera.", "title": "Activaci\u00f3 de HomeKit" } } @@ -22,7 +37,7 @@ "step": { "advanced": { "data": { - "auto_start": "[%key::component::homekit::config::step::user::data::auto_start%]", + "auto_start": "Inici autom\u00e0tic (desactiva-ho si crides el servei homekit.start manualment)", "safe_mode": "Mode segur (habilita-ho nom\u00e9s si falla la vinculaci\u00f3)" }, "description": "Aquests par\u00e0metres nom\u00e9s s'han d'ajustar si HomeKit no \u00e9s funcional.", @@ -40,16 +55,16 @@ "entities": "Entitats", "mode": "Mode" }, - "description": "Tria les entitats que vulguis exposar. En mode accessori, nom\u00e9s s'exposa una sola entitat. En mode enlla\u00e7 inclusiu, s'exposaran totes les entitats del domini tret que se seleccionin algunes en concret. En mode enlla\u00e7 excusiu, s'exposaran totes les entitats del domini excepte les entitats excloses.", - "title": "Selecci\u00f3 de les entitats a exposar" + "description": "Tria les entitats que vulguis incloure. En mode accessori, nom\u00e9s s'inclou una sola entitat. En mode enlla\u00e7 inclusiu, s'exposaran totes les entitats del domini tret de que se'n seleccionin algunes en concret. En mode enlla\u00e7 excusiu, s'inclouran totes les entitats del domini excepte les entitats excloses. Per obtenir el millor rendiment i evitar errors de disponibilitat inesperats , crea i vincula una inst\u00e0ncia HomeKit en mode accessori per a cada repoductor multim\u00e8dia/TV i c\u00e0mera.", + "title": "Selecciona les entitats a incloure" }, "init": { "data": { - "include_domains": "[%key::component::homekit::config::step::user::data::include_domains%]", + "include_domains": "Dominis a incloure", "mode": "Mode" }, - "description": "HomeKit es pot configurar per exposar un enlla\u00e7 o un sol accessori. En mode accessori, nom\u00e9s es pot utilitzar una entitat. El mode accessori \u00e9s necessari en reproductors multim\u00e8dia amb classe de dispositiu TV perqu\u00e8 funcionin correctament. Les entitats a \"Dominis a incloure\" s'exposaran a HomeKit. A la seg\u00fcent pantalla podr\u00e0s seleccionar quines entitats vols incloure o excloure d'aquesta llista.", - "title": "Selecci\u00f3 dels dominis a exposar." + "description": "HomeKit es pot configurar per exposar un enlla\u00e7 o un sol accessori. En mode accessori, nom\u00e9s es pot utilitzar una entitat. El mode accessori \u00e9s necessari perqu\u00e8 els reproductors multim\u00e8dia amb classe de dispositiu TV funcionin correctament. Les entitats a \"Dominis a incloure\" s'inclouran a HomeKit. A la seg\u00fcent pantalla podr\u00e0s seleccionar quines entitats vols incloure o excloure d'aquesta llista.", + "title": "Selecciona els dominis a incloure." }, "yaml": { "description": "Aquesta entrada es controla en YAML", diff --git a/homeassistant/components/homekit/translations/cs.json b/homeassistant/components/homekit/translations/cs.json index c070c852f09..faf1b1d74fc 100644 --- a/homeassistant/components/homekit/translations/cs.json +++ b/homeassistant/components/homekit/translations/cs.json @@ -4,12 +4,18 @@ "port_name_in_use": "P\u0159\u00edslu\u0161enstv\u00ed nebo p\u0159emost\u011bn\u00ed se stejn\u00fdm n\u00e1zvem nebo portem je ji\u017e nastaveno." }, "step": { + "accessory_mode": { + "data": { + "entity_id": "Entita" + } + }, "pairing": { "title": "P\u00e1rov\u00e1n\u00ed s HomeKit" }, "user": { "data": { - "include_domains": "Dom\u00e9ny, kter\u00e9 maj\u00ed b\u00fdt zahrnuty" + "include_domains": "Dom\u00e9ny, kter\u00e9 maj\u00ed b\u00fdt zahrnuty", + "mode": "Re\u017eim" }, "title": "Aktivace HomeKit" } diff --git a/homeassistant/components/homekit/translations/de.json b/homeassistant/components/homekit/translations/de.json index 9b1ec14a1dd..6d69c498bac 100644 --- a/homeassistant/components/homekit/translations/de.json +++ b/homeassistant/components/homekit/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "port_name_in_use": "A bridge with the same name or port is already configured.\nEine HomeKit Bridge mit dem selben Namen oder Port ist bereits vorhanden" + "port_name_in_use": "Eine HomeKit Bridge mit demselben Namen oder Port ist bereits vorhanden." }, "step": { "pairing": { @@ -37,14 +37,15 @@ }, "init": { "data": { + "include_domains": "Einzubeziehende Domains", "mode": "Modus" }, "description": "HomeKit kann so konfiguriert werden, dass eine Br\u00fccke oder ein einzelnes Zubeh\u00f6r verf\u00fcgbar gemacht wird. Im Zubeh\u00f6rmodus kann nur eine einzelne Entit\u00e4t verwendet werden. F\u00fcr Media Player mit der TV-Ger\u00e4teklasse ist ein Zubeh\u00f6rmodus erforderlich, damit sie ordnungsgem\u00e4\u00df funktionieren. Entit\u00e4ten in den \"einzuschlie\u00dfenden Dom\u00e4nen\" werden f\u00fcr HomeKit verf\u00fcgbar gemacht. Auf dem n\u00e4chsten Bildschirm k\u00f6nnen Sie ausw\u00e4hlen, welche Entit\u00e4ten in diese Liste aufgenommen oder aus dieser ausgeschlossen werden sollen.", - "title": "W\u00e4hlen Sie die zu \u00fcberbr\u00fcckenden Dom\u00e4nen aus." + "title": "W\u00e4hle die zu \u00fcberbr\u00fcckenden Dom\u00e4nen aus." }, "yaml": { "description": "Dieser Eintrag wird \u00fcber YAML gesteuert", - "title": "Passen Sie die HomeKit Bridge-Optionen an" + "title": "Passe die HomeKit Bridge-Optionen an" } } } diff --git a/homeassistant/components/homekit/translations/en.json b/homeassistant/components/homekit/translations/en.json index cc6c8f8dc31..db0656c0450 100644 --- a/homeassistant/components/homekit/translations/en.json +++ b/homeassistant/components/homekit/translations/en.json @@ -24,9 +24,11 @@ }, "user": { "data": { + "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", + "include_domains": "Domains to include", "mode": "Mode" }, - "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 best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each TV, media player and camera.", + "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 best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.", "title": "Activate HomeKit" } } @@ -35,7 +37,8 @@ "step": { "advanced": { "data": { - "auto_start": "Autostart (disable if you are calling the homekit.start service manually)" + "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.", "title": "Advanced Configuration" @@ -69,4 +72,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/et.json b/homeassistant/components/homekit/translations/et.json index 76a78602bd3..37bff5f9b70 100644 --- a/homeassistant/components/homekit/translations/et.json +++ b/homeassistant/components/homekit/translations/et.json @@ -4,6 +4,20 @@ "port_name_in_use": "Sama nime v\u00f5i pordiga tarvik v\u00f5i sild on juba konfigureeritud." }, "step": { + "accessory_mode": { + "data": { + "entity_id": "Olem" + }, + "description": "Vali kaasatav olem. Lisare\u017eiimis on kaasatav ainult \u00fcks olem.", + "title": "Vali kaasatav olem" + }, + "bridge_mode": { + "data": { + "include_domains": "Kaasatavad domeenid" + }, + "description": "Vali kaasatavad domeenid. Kaasatakse k\u00f5ik domeenis toetatud olemid.", + "title": "Vali kaasatavad domeenid" + }, "pairing": { "description": "Niipea kui {name} on valmis, on sidumine saadaval jaotises \"Notifications\" kui \"HomeKit Bridge Setup\".", "title": "HomeKiti sidumine" @@ -11,9 +25,10 @@ "user": { "data": { "auto_start": "Autostart (keela, kui kasutad Z-Wave'i v\u00f5i muud viivitatud k\u00e4ivituss\u00fcsteemi)", - "include_domains": "Kaasatavad domeenid" + "include_domains": "Kaasatavad domeenid", + "mode": "Re\u017eiim" }, - "description": "HomeKiti integreerimine v\u00f5imaldab teil p\u00e4\u00e4seda juurde HomeKiti \u00fcksustele Home Assistant. Sildire\u017eiimis on HomeKit Bridges piiratud 150 lisaseadmega, sealhulgas sild ise. Kui soovid \u00fchendada rohkem lisatarvikuid, on soovitatav kasutada erinevate domeenide jaoks mitut HomeKiti silda. \u00dcksuse \u00fcksikasjalik konfiguratsioon on esmase silla jaoks saadaval ainult YAML-i kaudu.", + "description": "HomeKiti integreerimine v\u00f5imaldab teil p\u00e4\u00e4seda juurde HomeKiti \u00fcksustele Home Assistant. Sildire\u017eiimis on HomeKit Bridges piiratud 150 lisaseadmega, sealhulgas sild ise. Kui soovid \u00fchendada rohkem lisatarvikuid, on soovitatav kasutada erinevate domeenide jaoks mitut HomeKiti silda. \u00dcksuse \u00fcksikasjalik konfiguratsioon on esmase silla jaoks saadaval ainult YAML-i kaudu. Parema tulemuse saavutamiseks ja ootamatute seadmete kadumise v\u00e4ltimiseks loo ja seo eraldi HomeKiti seade tarviku re\u017eiimis kga meediaesitaja ja kaamera jaoks.", "title": "Aktiveeri HomeKit" } } @@ -22,7 +37,7 @@ "step": { "advanced": { "data": { - "auto_start": "Autostart (keela, kui kasutad Z-Wave'i v\u00f5i muud viivitatud k\u00e4ivituss\u00fcsteemi)", + "auto_start": "Autostart (keela kui kasutad homekit.start teenust k\u00e4sitsi)", "safe_mode": "Turvare\u017eiim (luba ainult siis, kui sidumine nurjub)" }, "description": "Neid s\u00e4tteid tuleb muuta ainult siis kui HomeKit ei t\u00f6\u00f6ta.", @@ -40,8 +55,8 @@ "entities": "Olemid", "mode": "Re\u017eiim" }, - "description": "Vali avaldatavad olemid. Tarvikute re\u017eiimis on avaldatav ainult \u00fcks olem. Silla re\u017eiimis, kuvatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud juhul, kui valitud on kindlad olemid. Silla v\u00e4listamisre\u017eiimis avaldatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud v\u00e4listatud olemid.", - "title": "Vali avaldatavad olemid" + "description": "Vali kaasatavad olemid. Tarvikute re\u017eiimis on kaasatav ainult \u00fcks olem. Silla re\u017eiimis, kuvatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud juhul, kui valitud on kindlad olemid. Silla v\u00e4listamisre\u017eiimis kaasatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud v\u00e4listatud olemid.", + "title": "Vali kaasatavd olemid" }, "init": { "data": { @@ -49,7 +64,7 @@ "mode": "Re\u017eiim" }, "description": "HomeKiti saab seadistada silla v\u00f5i \u00fche lisaseadme avaldamiseks. Lisare\u017eiimis saab kasutada ainult \u00fchte \u00fcksust. Teleriseadmete klassiga meediumipleierite n\u00f5uetekohaseks toimimiseks on vaja lisare\u017eiimi. \u201eKaasatavate domeenide\u201d \u00fcksused puutuvad kokku HomeKitiga. J\u00e4rgmisel ekraanil saad valida, millised \u00fcksused sellesse loendisse lisada v\u00f5i sellest v\u00e4lja j\u00e4tta.", - "title": "Valige avaldatavad domeenid." + "title": "Vali kaasatavad domeenid" }, "yaml": { "description": "Seda sisestust juhitakse YAML-i kaudu", diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json index 7e9c8a05b9d..9a85d1e6e9f 100644 --- a/homeassistant/components/homekit/translations/it.json +++ b/homeassistant/components/homekit/translations/it.json @@ -4,6 +4,20 @@ "port_name_in_use": "Un accessorio o un bridge con lo stesso nome o porta \u00e8 gi\u00e0 configurato." }, "step": { + "accessory_mode": { + "data": { + "entity_id": "Entit\u00e0" + }, + "description": "Scegli l'entit\u00e0 da includere. In modalit\u00e0 accessorio, \u00e8 inclusa solo una singola entit\u00e0.", + "title": "Seleziona l'entit\u00e0 da includere" + }, + "bridge_mode": { + "data": { + "include_domains": "Domini da includere" + }, + "description": "Scegli i domini da includere. Verranno incluse tutte le entit\u00e0 supportate nel dominio.", + "title": "Seleziona i domini da includere" + }, "pairing": { "description": "Non appena il {name} \u00e8 pronto, l'associazione sar\u00e0 disponibile in \"Notifiche\" come \"Configurazione HomeKit Bridge\".", "title": "Associa HomeKit" @@ -11,7 +25,8 @@ "user": { "data": { "auto_start": "Avvio automatico (disabilitare se si utilizza Z-Wave o un altro sistema di avvio ritardato)", - "include_domains": "Domini da includere" + "include_domains": "Domini da includere", + "mode": "Modalit\u00e0" }, "description": "L'integrazione di HomeKit ti consentir\u00e0 di accedere alle entit\u00e0 di Home Assistant in HomeKit. In modalit\u00e0 bridge, i bridge HomeKit sono limitati a 150 accessori per istanza, incluso il bridge stesso. Se desideri eseguire il bridge di un numero di accessori superiore a quello massimo, si consiglia di utilizzare pi\u00f9 bridge HomeKit per domini diversi. La configurazione dettagliata dell'entit\u00e0 \u00e8 disponibile solo tramite YAML per il bridge principale.", "title": "Attiva HomeKit" @@ -22,7 +37,7 @@ "step": { "advanced": { "data": { - "auto_start": "Avvio automatico (disabilitare se si utilizza Z-Wave o un altro sistema di avvio ritardato)", + "auto_start": "Avvio automatico (disabilitare se stai chiamando manualmente il servizio homekit.start)", "safe_mode": "Modalit\u00e0 provvisoria (attivare solo in caso di errore di associazione)" }, "description": "Queste impostazioni devono essere regolate solo se HomeKit non funziona.", @@ -40,8 +55,8 @@ "entities": "Entit\u00e0", "mode": "Modalit\u00e0" }, - "description": "Scegliere le entit\u00e0 da esporre. In modalit\u00e0 accessorio, \u00e8 esposta una sola entit\u00e0. In modalit\u00e0 di inclusione bridge, tutte le entit\u00e0 nel dominio saranno esposte, a meno che non siano selezionate entit\u00e0 specifiche. In modalit\u00e0 di esclusione bridge, tutte le entit\u00e0 nel dominio saranno esposte, ad eccezione delle entit\u00e0 escluse.", - "title": "Selezionare le entit\u00e0 da esporre" + "description": "Scegliere le entit\u00e0 da includere. In modalit\u00e0 accessorio, \u00e8 inclusa una sola entit\u00e0. In modalit\u00e0 di inclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, a meno che non siano selezionate entit\u00e0 specifiche. In modalit\u00e0 di esclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, ad eccezione delle entit\u00e0 escluse. Per prestazioni ottimali e per evitare una indisponibilit\u00e0 imprevista, creare e associare un'istanza HomeKit separata in modalit\u00e0 accessorio per ogni lettore multimediale, TV e videocamera.", + "title": "Seleziona le entit\u00e0 da includere" }, "init": { "data": { @@ -49,7 +64,7 @@ "mode": "Modalit\u00e0" }, "description": "HomeKit pu\u00f2 essere configurato esponendo un bridge o un singolo accessorio. In modalit\u00e0 accessorio, pu\u00f2 essere utilizzata solo una singola entit\u00e0. La modalit\u00e0 accessorio \u00e8 necessaria per il corretto funzionamento dei lettori multimediali con la classe di apparecchi TV. Le entit\u00e0 nei \"Domini da includere\" saranno esposte ad HomeKit. Sar\u00e0 possibile selezionare quali entit\u00e0 includere o escludere da questo elenco nella schermata successiva.", - "title": "Selezionare i domini da esporre." + "title": "Seleziona i domini da includere." }, "yaml": { "description": "Questa voce \u00e8 controllata tramite YAML", diff --git a/homeassistant/components/homekit/translations/no.json b/homeassistant/components/homekit/translations/no.json index 7eff4d37668..9a64def4156 100644 --- a/homeassistant/components/homekit/translations/no.json +++ b/homeassistant/components/homekit/translations/no.json @@ -4,6 +4,20 @@ "port_name_in_use": "Et tilbeh\u00f8r eller bro med samme navn eller port er allerede konfigurert." }, "step": { + "accessory_mode": { + "data": { + "entity_id": "Enhet" + }, + "description": "Velg enheten som skal inkluderes. I tilbeh\u00f8rsmodus er bare en enkelt enhet inkludert.", + "title": "Velg enhet som skal inkluderes" + }, + "bridge_mode": { + "data": { + "include_domains": "Domener \u00e5 inkludere" + }, + "description": "Velg domenene som skal inkluderes. Alle st\u00f8ttede enheter i domenet vil bli inkludert.", + "title": "Velg domener som skal inkluderes" + }, "pairing": { "description": "S\u00e5 snart {name} er klart, vil sammenkobling v\u00e6re tilgjengelig i \"Notifications\" som \"HomeKit Bridge Setup\".", "title": "Koble sammen HomeKit" @@ -11,9 +25,10 @@ "user": { "data": { "auto_start": "Autostart (deaktiver hvis du bruker Z-Wave eller annet forsinket startsystem)", - "include_domains": "Domener \u00e5 inkludere" + "include_domains": "Domener \u00e5 inkludere", + "mode": "Modus" }, - "description": "HomeKit-integrasjonen gir deg tilgang til Home Assistant-entitetene dine i HomeKit. I bromodus er HomeKit Broer begrenset til 150 tilbeh\u00f8rsenhet per forekomst inkludert selve broen. Hvis du \u00f8nsker \u00e5 \u00f8ke maksimalt antall tilbeh\u00f8rsenheter, anbefales det at du bruker flere HomeKit-broer for forskjellige domener. Detaljert entitetskonfigurasjon er bare tilgjengelig via YAML for den prim\u00e6re broen.", + "description": "HomeKit-integrasjonen gir deg tilgang til Home Assistant-enhetene dine i HomeKit. I bromodus er HomeKit Bridges begrenset til 150 tilbeh\u00f8r per forekomst inkludert selve broen. Hvis du \u00f8nsker \u00e5 bygge bro over maksimalt antall tilbeh\u00f8r, anbefales det at du bruker flere HomeKit-broer for forskjellige domener. Detaljert enhetskonfigurasjon er bare tilgjengelig via YAML. For best ytelse og for \u00e5 forhindre uventet utilgjengelighet, opprett og par sammen en egen HomeKit-forekomst i tilbeh\u00f8rsmodus for hver tv-mediaspiller og kamera.", "title": "Aktiver HomeKit" } } @@ -22,7 +37,7 @@ "step": { "advanced": { "data": { - "auto_start": "Autostart (deaktiver hvis du bruker Z-Wave eller annet forsinket startsystem)", + "auto_start": "Autostart (deaktiver hvis du ringer til homekit.start-tjenesten manuelt)", "safe_mode": "Sikker modus (aktiver bare hvis sammenkoblingen mislykkes)" }, "description": "Disse innstillingene m\u00e5 bare justeres hvis HomeKit ikke fungerer.", @@ -40,16 +55,16 @@ "entities": "Entiteter", "mode": "Modus" }, - "description": "Velg entitene som skal eksponeres. I tilbeh\u00f8rsmodus er bare en enkelt entitet eksponert. I bro-inkluderingsmodus vil alle entiteter i domenet bli eksponert med mindre spesifikke entiteter er valgt. I bro-ekskluderingsmodus vil alle entiteter i domenet bli eksponert bortsett fra de ekskluderte entitetene.", - "title": "Velg entiteter som skal eksponeres" + "description": "Velg enhetene som skal inkluderes. I tilbeh\u00f8rsmodus er bare \u00e9n enkelt enhet inkludert. I bridge include-modus inkluderes alle enheter i domenet med mindre bestemte enheter er valgt. I brounnlatingsmodus inkluderes alle enheter i domenet, med unntak av de utelatte enhetene. For best mulig ytelse, og for \u00e5 forhindre uventet utilgjengelighet, opprett og par en separat HomeKit-forekomst i tilbeh\u00f8rsmodus for hver tv-mediespiller og kamera.", + "title": "Velg enheter som skal inkluderes" }, "init": { "data": { "include_domains": "Domener \u00e5 inkludere", "mode": "Modus" }, - "description": "HomeKit kan konfigureres for \u00e5 eksponere en bro eller en enkelt tilbeh\u00f8rsenhet. I tilbeh\u00f8rsmodus kan bare en enkelt entitet brukes. Tilbeh\u00f8rsmodus er n\u00f8dvendig for at mediaspillere med TV-enhetsklasse skal fungere skikkelig. Entiteter i \u201cDomains to include\u201d vil bli eksponert for HomeKit. Du vil kunne velge hvilke entiteter du vil inkludere eller ekskludere fra denne listen p\u00e5 neste skjermbilde.", - "title": "Velg domener du vil eksponere." + "description": "HomeKit kan konfigureres vise en bro eller ett enkelt tilbeh\u00f8r. I tilbeh\u00f8rsmodus kan bare \u00e9n enkelt enhet brukes. Tilbeh\u00f8rsmodus kreves for at mediespillere med TV-enhetsklassen skal fungere som de skal. Enheter i \"Domener som skal inkluderes\" inkluderes i HomeKit. Du kan velge hvilke enheter som skal inkluderes eller ekskluderes fra denne listen p\u00e5 neste skjermbilde.", + "title": "Velg domener som skal inkluderes." }, "yaml": { "description": "Denne oppf\u00f8ringen kontrolleres via YAML", diff --git a/homeassistant/components/homekit/translations/pl.json b/homeassistant/components/homekit/translations/pl.json index 3210f0f4430..2679a4de20a 100644 --- a/homeassistant/components/homekit/translations/pl.json +++ b/homeassistant/components/homekit/translations/pl.json @@ -4,6 +4,20 @@ "port_name_in_use": "Akcesorium lub mostek o tej samej nazwie lub adresie IP jest ju\u017c skonfigurowany" }, "step": { + "accessory_mode": { + "data": { + "entity_id": "Encja" + }, + "description": "Wybierz uwzgl\u0119dniane encje. W trybie akcesori\u00f3w uwzgl\u0119dniana jest tylko jedna encja.", + "title": "Wybierz uwzgl\u0119dniane encje" + }, + "bridge_mode": { + "data": { + "include_domains": "Domeny do uwzgl\u0119dnienia" + }, + "description": "Wybierz uwzgl\u0119dniane domeny. Wszystkie obs\u0142ugiwane encje w domenie zostan\u0105 uwzgl\u0119dnione.", + "title": "Wybierz uwzgl\u0119dniane domeny" + }, "pairing": { "description": "Gdy tylko {name} b\u0119dzie gotowy, opcja parowania b\u0119dzie dost\u0119pna w \u201ePowiadomieniach\u201d jako \u201eKonfiguracja mostka HomeKit\u201d.", "title": "Parowanie z HomeKit" @@ -11,9 +25,10 @@ "user": { "data": { "auto_start": "Automatyczne uruchomienie (wy\u0142\u0105cz, je\u015bli u\u017cywasz Z-Wave lub innej integracji op\u00f3\u017aniaj\u0105cej start systemu)", - "include_domains": "Domeny do uwzgl\u0119dnienia" + "include_domains": "Domeny do uwzgl\u0119dnienia", + "mode": "Tryb" }, - "description": "Integracja HomeKit pozwala na dost\u0119p do Twoich encji Home Assistant w HomeKit. W trybie \"Mostka\", mostki HomeKit s\u0105 ograniczone do 150 urz\u0105dze\u0144, w\u0142\u0105czaj\u0105c w to sam mostek. Je\u015bli chcesz wi\u0119cej ni\u017c dozwolona maksymalna liczba urz\u0105dze\u0144, zaleca si\u0119 u\u017cywanie wielu most\u00f3w HomeKit dla r\u00f3\u017cnych domen. Szczeg\u00f3\u0142owa konfiguracja encji jest dost\u0119pna tylko w trybie YAML dla g\u0142\u00f3wnego mostka.", + "description": "Integracja HomeKit pozwala na dost\u0119p do Twoich encji Home Assistant w HomeKit. W trybie \"Mostka\", mostki HomeKit s\u0105 ograniczone do 150 urz\u0105dze\u0144, w\u0142\u0105czaj\u0105c w to sam mostek. Je\u015bli chcesz wi\u0119cej ni\u017c dozwolona maksymalna liczba urz\u0105dze\u0144, zaleca si\u0119 u\u017cywanie wielu most\u00f3w HomeKit dla r\u00f3\u017cnych domen. Szczeg\u00f3\u0142owa konfiguracja encji jest dost\u0119pna tylko w trybie YAML dla g\u0142\u00f3wnego mostka. Dla najlepszej wydajno\u015bci oraz by zapobiec nieprzewidzianej niedost\u0119pno\u015bci urz\u0105dzenia, utw\u00f3rz i sparuj oddzieln\u0105 instancj\u0119 HomeKit w trybie akcesorium dla ka\u017cdego media playera oraz kamery.", "title": "Aktywacja HomeKit" } } @@ -22,7 +37,7 @@ "step": { "advanced": { "data": { - "auto_start": "Automatyczne uruchomienie (wy\u0142\u0105cz, je\u015bli u\u017cywasz Z-Wave lub innej integracji op\u00f3\u017aniaj\u0105cej start systemu)", + "auto_start": "Automatyczne uruchomienie (wy\u0142\u0105cz, je\u015bli r\u0119cznie uruchamiasz us\u0142ug\u0119 homekit.start)", "safe_mode": "Tryb awaryjny (w\u0142\u0105cz tylko wtedy, gdy parowanie nie powiedzie si\u0119)" }, "description": "Te ustawienia nale\u017cy dostosowa\u0107 tylko wtedy, gdy HomeKit nie dzia\u0142a.", @@ -40,8 +55,8 @@ "entities": "Encje", "mode": "Tryb" }, - "description": "Wybierz encje, kt\u00f3re maj\u0105 by\u0107 widoczne. W trybie \"Akcesorium\", tylko jedna encja jest widoczna. W trybie \"Uwzgl\u0119dnij mostek\", wszystkie encje w danej domenie b\u0119d\u0105 widoczne, chyba \u017ce wybrane s\u0105 tylko konkretne encje. W trybie \"Wyklucz mostek\", wszystkie encje b\u0119d\u0105 widoczne, z wyj\u0105tkiem tych wybranych.", - "title": "Wybierz encje, kt\u00f3re maj\u0105 by\u0107 widoczne" + "description": "Wybierz encje, kt\u00f3re maj\u0105 by\u0107 uwzgl\u0119dnione. W trybie \"Akcesorium\" tylko jedna encja jest uwzgl\u0119dniona. W trybie \"Uwzgl\u0119dnij mostek\", wszystkie encje w danej domenie b\u0119d\u0105 uwzgl\u0119dnione, chyba \u017ce wybrane s\u0105 tylko konkretne encje. W trybie \"Wyklucz mostek\", wszystkie encje b\u0119d\u0105 uwzgl\u0119dnione, z wyj\u0105tkiem tych wybranych. Dla najlepszej wydajno\u015bci oraz by zapobiec nieprzewidzianej niedost\u0119pno\u015bci urz\u0105dzenia, utw\u00f3rz i sparuj oddzieln\u0105 instancj\u0119 HomeKit w trybie akcesorium dla ka\u017cdego media playera oraz kamery.", + "title": "Wybierz encje, kt\u00f3re maj\u0105 by\u0107 uwzgl\u0119dnione" }, "init": { "data": { diff --git a/homeassistant/components/homekit/translations/ro.json b/homeassistant/components/homekit/translations/ro.json new file mode 100644 index 00000000000..82e8344417b --- /dev/null +++ b/homeassistant/components/homekit/translations/ro.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Integrarea HomeKit v\u0103 va permite s\u0103 accesa\u021bi entit\u0103\u021bile Home Assistant din HomeKit. \u00cen modul bridge, HomeKit Bridges sunt limitate la 150 de accesorii pe instan\u021b\u0103, inclusiv bridge-ul \u00een sine. Dac\u0103 dori\u021bi s\u0103 face\u021bi mai mult dec\u00e2t num\u0103rul maxim de accesorii, este recomandat s\u0103 utiliza\u021bi mai multe poduri HomeKit pentru diferite domenii. Configurarea detaliat\u0103 a entit\u0103\u021bii este disponibil\u0103 numai prin YAML. Pentru cele mai bune performan\u021be \u0219i pentru a preveni indisponibilitatea nea\u0219teptat\u0103, crea\u021bi \u0219i \u00eemperechea\u021bi o instan\u021b\u0103 HomeKit separat\u0103 \u00een modul accesoriu pentru fiecare player media TV \u0219i camer\u0103." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/ru.json b/homeassistant/components/homekit/translations/ru.json index 3cb5e84936a..6cf96c2dd78 100644 --- a/homeassistant/components/homekit/translations/ru.json +++ b/homeassistant/components/homekit/translations/ru.json @@ -4,6 +4,11 @@ "port_name_in_use": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c \u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c \u0438\u043b\u0438 \u043f\u043e\u0440\u0442\u043e\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "step": { + "accessory_mode": { + "data": { + "entity_id": "\u041e\u0431\u044a\u0435\u043a\u0442" + } + }, "pairing": { "description": "\u041a\u0430\u043a \u0442\u043e\u043b\u044c\u043a\u043e {name} \u0431\u0443\u0434\u0435\u0442 \u0433\u043e\u0442\u043e\u0432\u043e, \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u0432 \"\u0423\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f\u0445\" \u043a\u0430\u043a \"HomeKit Bridge Setup\".", "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 HomeKit" @@ -13,7 +18,7 @@ "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 Z-Wave \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043e\u0442\u043b\u043e\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0430)", "include_domains": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u044b" }, - "description": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u043e\u0431\u044a\u0435\u043a\u0442\u0430\u043c Home Assistant \u0447\u0435\u0440\u0435\u0437 HomeKit. HomeKit Bridge \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d 150 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430\u043c\u0438 \u043d\u0430 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440, \u0432\u043a\u043b\u044e\u0447\u0430\u044f \u0441\u0430\u043c \u0431\u0440\u0438\u0434\u0436. \u0415\u0441\u043b\u0438 \u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0431\u043e\u043b\u044c\u0448\u0435, \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e HomeKit Bridge \u0434\u043b\u044f \u0440\u0430\u0437\u043d\u044b\u0445 \u0434\u043e\u043c\u0435\u043d\u043e\u0432. \u0414\u0435\u0442\u0430\u043b\u044c\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 YAML \u0434\u043b\u044f \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0433\u043e \u0431\u0440\u0438\u0434\u0436\u0430.", + "description": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u043e\u0431\u044a\u0435\u043a\u0442\u0430\u043c Home Assistant \u0447\u0435\u0440\u0435\u0437 HomeKit. HomeKit Bridge \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d 150 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430\u043c\u0438 \u043d\u0430 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440, \u0432\u043a\u043b\u044e\u0447\u0430\u044f \u0441\u0430\u043c \u0431\u0440\u0438\u0434\u0436. \u0415\u0441\u043b\u0438 \u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0431\u043e\u043b\u044c\u0448\u0435, \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e HomeKit Bridge \u0434\u043b\u044f \u0440\u0430\u0437\u043d\u044b\u0445 \u0434\u043e\u043c\u0435\u043d\u043e\u0432. \u0414\u0435\u0442\u0430\u043b\u044c\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 YAML. \u0414\u043b\u044f \u043b\u0443\u0447\u0448\u0435\u0439 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0438 \u043f\u0440\u0435\u0434\u043e\u0442\u0432\u0440\u0430\u0449\u0435\u043d\u0438\u044f \u043d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u043e\u0441\u0442\u0435\u0439 \u0441\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0443\u044e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440\u0430 \u0438\u043b\u0438 \u043a\u0430\u043c\u0435\u0440\u044b.", "title": "HomeKit" } } @@ -22,7 +27,7 @@ "step": { "advanced": { "data": { - "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 Z-Wave \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043e\u0442\u043b\u043e\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0430)", + "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u0435, \u0435\u0441\u043b\u0438 \u0412\u044b \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442\u0435 \u0441\u043b\u0443\u0436\u0431\u0443 homekit.start)", "safe_mode": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c (\u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u0441\u0431\u043e\u044f \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f)" }, "description": "\u042d\u0442\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b, \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 HomeKit \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442.", @@ -40,7 +45,7 @@ "entities": "\u041e\u0431\u044a\u0435\u043a\u0442\u044b", "mode": "\u0420\u0435\u0436\u0438\u043c" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u043e\u0431\u044a\u0435\u043a\u0442. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u043c\u043e\u0441\u0442\u0430 \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u0435\u0441\u043b\u0438 \u043d\u0435 \u0432\u044b\u0431\u0440\u0430\u043d\u044b \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u043a\u0440\u043e\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044b\u0445.", + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u043e\u0431\u044a\u0435\u043a\u0442. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u043c\u043e\u0441\u0442\u0430 \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u0435\u0441\u043b\u0438 \u043d\u0435 \u0432\u044b\u0431\u0440\u0430\u043d\u044b \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u043a\u0440\u043e\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044b\u0445. \u0414\u043b\u044f \u043b\u0443\u0447\u0448\u0435\u0439 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0438 \u043f\u0440\u0435\u0434\u043e\u0442\u0432\u0440\u0430\u0449\u0435\u043d\u0438\u044f \u043d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u043e\u0441\u0442\u0435\u0439 \u0441\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0443\u044e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440\u0430 \u0438\u043b\u0438 \u043a\u0430\u043c\u0435\u0440\u044b.", "title": "\u0412\u044b\u0431\u043e\u0440 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit" }, "init": { diff --git a/homeassistant/components/homekit/translations/sv.json b/homeassistant/components/homekit/translations/sv.json index 5aa9507a85d..1e2fcae04b5 100644 --- a/homeassistant/components/homekit/translations/sv.json +++ b/homeassistant/components/homekit/translations/sv.json @@ -1,4 +1,19 @@ { + "config": { + "step": { + "bridge_mode": { + "data": { + "include_domains": "Dom\u00e4ner att inkludera" + } + }, + "pairing": { + "title": "Para HomeKit" + }, + "user": { + "title": "Aktivera HomeKit" + } + } + }, "options": { "step": { "cameras": { @@ -7,6 +22,12 @@ }, "description": "Kontrollera alla kameror som st\u00f6der inbyggda H.264-str\u00f6mmar. Om kameran inte skickar ut en H.264-str\u00f6m kodar systemet videon till H.264 f\u00f6r HomeKit. Transkodning kr\u00e4ver h\u00f6g prestanda och kommer troligtvis inte att fungera p\u00e5 enkortsdatorer.", "title": "V\u00e4lj kamerans videoavkodare." + }, + "init": { + "data": { + "include_domains": "Dom\u00e4ner att inkludera" + }, + "title": "V\u00e4lj dom\u00e4ner som ska inkluderas." } } } diff --git a/homeassistant/components/homekit/translations/tr.json b/homeassistant/components/homekit/translations/tr.json new file mode 100644 index 00000000000..f9391fd0686 --- /dev/null +++ b/homeassistant/components/homekit/translations/tr.json @@ -0,0 +1,60 @@ +{ + "config": { + "abort": { + "port_name_in_use": "Ayn\u0131 ada veya ba\u011flant\u0131 noktas\u0131na sahip bir aksesuar veya k\u00f6pr\u00fc zaten yap\u0131land\u0131r\u0131lm\u0131\u015f." + }, + "step": { + "accessory_mode": { + "data": { + "entity_id": "Varl\u0131k" + }, + "description": "Dahil edilecek varl\u0131\u011f\u0131 se\u00e7in. Aksesuar modunda, yaln\u0131zca tek bir varl\u0131k dahildir.", + "title": "Dahil edilecek varl\u0131\u011f\u0131 se\u00e7in" + }, + "bridge_mode": { + "data": { + "include_domains": "\u0130\u00e7erecek etki alanlar\u0131" + }, + "description": "Dahil edilecek alanlar\u0131 se\u00e7in. Etki alan\u0131ndaki t\u00fcm desteklenen varl\u0131klar dahil edilecektir.", + "title": "Dahil edilecek etki alanlar\u0131n\u0131 se\u00e7in" + }, + "pairing": { + "description": "{name} haz\u0131r olur olmaz e\u015fle\u015ftirme, \"Bildirimler\" i\u00e7inde \"HomeKit K\u00f6pr\u00fc Kurulumu\" olarak mevcut olacakt\u0131r.", + "title": "HomeKit'i E\u015fle\u015ftir" + }, + "user": { + "data": { + "mode": "Mod" + } + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "safe_mode": "G\u00fcvenli Mod (yaln\u0131zca e\u015fle\u015ftirme ba\u015far\u0131s\u0131z olursa etkinle\u015ftirin)" + } + }, + "cameras": { + "data": { + "camera_copy": "Yerel H.264 ak\u0131\u015flar\u0131n\u0131 destekleyen kameralar" + }, + "description": "Yerel H.264 ak\u0131\u015flar\u0131n\u0131 destekleyen t\u00fcm kameralar\u0131 kontrol edin. Kamera bir H.264 ak\u0131\u015f\u0131 vermezse, sistem videoyu HomeKit i\u00e7in H.264'e d\u00f6n\u00fc\u015ft\u00fcr\u00fcr. Kod d\u00f6n\u00fc\u015ft\u00fcrme, y\u00fcksek performansl\u0131 bir CPU gerektirir ve tek kartl\u0131 bilgisayarlarda \u00e7al\u0131\u015fma olas\u0131l\u0131\u011f\u0131 d\u00fc\u015f\u00fckt\u00fcr.", + "title": "Kamera video codec bile\u015fenini se\u00e7in." + }, + "include_exclude": { + "data": { + "entities": "Varl\u0131klar", + "mode": "Mod" + }, + "title": "Dahil edilecek varl\u0131klar\u0131 se\u00e7in" + }, + "init": { + "data": { + "mode": "Mod" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/uk.json b/homeassistant/components/homekit/translations/uk.json index 10cd42ccecb..876b200bdf8 100644 --- a/homeassistant/components/homekit/translations/uk.json +++ b/homeassistant/components/homekit/translations/uk.json @@ -1,10 +1,59 @@ { + "config": { + "abort": { + "port_name_in_use": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437 \u0442\u0430\u043a\u043e\u044e \u0436 \u043d\u0430\u0437\u0432\u043e\u044e \u0430\u0431\u043e \u043f\u043e\u0440\u0442\u043e\u043c \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + }, + "step": { + "pairing": { + "description": "\u042f\u043a \u0442\u0456\u043b\u044c\u043a\u0438 {name} \u0431\u0443\u0434\u0435 \u0433\u043e\u0442\u043e\u0432\u0438\u0439, \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438 \u0431\u0443\u0434\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u0432 \"\u0421\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u043d\u044f\u0445\" \u044f\u043a \"HomeKit Bridge Setup\".", + "title": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u0437 HomeKit" + }, + "user": { + "data": { + "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043f\u0440\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u0456 Z-Wave \u0430\u0431\u043e \u0456\u043d\u0448\u043e\u0457 \u0441\u0438\u0441\u0442\u0435\u043c\u0438 \u0432\u0456\u0434\u043a\u043b\u0430\u0434\u0435\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0443)", + "include_domains": "\u0412\u0438\u0431\u0440\u0430\u0442\u0438 \u0434\u043e\u043c\u0435\u043d\u0438" + }, + "description": "\u0426\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f \u0434\u043e\u0437\u0432\u043e\u043b\u044f\u0454 \u043e\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u043e\u0431'\u0454\u043a\u0442\u0456\u0432 Home Assistant \u0447\u0435\u0440\u0435\u0437 HomeKit. HomeKit Bridge \u043e\u0431\u043c\u0435\u0436\u0435\u043d\u0438\u0439 150 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0430\u043c\u0438 \u043d\u0430 \u0435\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440, \u0432\u043a\u043b\u044e\u0447\u0430\u044e\u0447\u0438 \u0441\u0430\u043c \u0431\u0440\u0438\u0434\u0436. \u042f\u043a\u0449\u043e \u0412\u0430\u043c \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0431\u0456\u043b\u044c\u0448\u0435, \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0454\u0442\u044c\u0441\u044f \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u043a\u0456\u043b\u044c\u043a\u0430 HomeKit Bridge \u0434\u043b\u044f \u0440\u0456\u0437\u043d\u0438\u0445 \u0434\u043e\u043c\u0435\u043d\u0456\u0432. \u0414\u0435\u0442\u0430\u043b\u044c\u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u043a\u043e\u0436\u043d\u043e\u0433\u043e \u043e\u0431'\u0454\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0435 \u0442\u0456\u043b\u044c\u043a\u0438 \u0447\u0435\u0440\u0435\u0437 YAML \u0434\u043b\u044f \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0433\u043e \u043c\u043e\u0441\u0442\u0430.", + "title": "HomeKit" + } + } + }, "options": { "step": { + "advanced": { + "data": { + "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043f\u0440\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u0456 Z-Wave \u0430\u0431\u043e \u0456\u043d\u0448\u043e\u0457 \u0441\u0438\u0441\u0442\u0435\u043c\u0438 \u0432\u0456\u0434\u043a\u043b\u0430\u0434\u0435\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0443)", + "safe_mode": "\u0411\u0435\u0437\u043f\u0435\u0447\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c (\u0443\u0432\u0456\u043c\u043a\u043d\u0456\u0442\u044c \u0442\u0456\u043b\u044c\u043a\u0438 \u0432 \u0440\u0430\u0437\u0456 \u0437\u0431\u043e\u044e \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f)" + }, + "description": "\u0426\u0456 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u0456, \u043b\u0438\u0448\u0435 \u044f\u043a\u0449\u043e HomeKit \u043d\u0435 \u043f\u0440\u0430\u0446\u044e\u0454.", + "title": "\u0420\u043e\u0437\u0448\u0438\u0440\u0435\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f" + }, + "cameras": { + "data": { + "camera_copy": "\u041a\u0430\u043c\u0435\u0440\u0438, \u044f\u043a\u0456 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u044e\u0442\u044c \u043f\u043e\u0442\u043e\u043a\u0438 H.264" + }, + "description": "\u042f\u043a\u0449\u043e \u043a\u0430\u043c\u0435\u0440\u0430 \u043d\u0435 \u0432\u0438\u0432\u043e\u0434\u0438\u0442\u044c \u043f\u043e\u0442\u0456\u043a H.264, \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u043f\u0435\u0440\u0435\u043a\u043e\u0434\u043e\u0432\u0443\u0454 \u0432\u0456\u0434\u0435\u043e \u0432 H.264 \u0434\u043b\u044f HomeKit. \u0422\u0440\u0430\u043d\u0441\u043a\u043e\u0434\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u043c\u0430\u0433\u0430\u0454 \u0432\u0438\u0441\u043e\u043a\u043e\u043f\u0440\u043e\u0434\u0443\u043a\u0442\u0438\u0432\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0446\u0435\u0441\u043e\u0440\u0430 \u0456 \u043d\u0430\u0432\u0440\u044f\u0434 \u0447\u0438 \u0431\u0443\u0434\u0435 \u043f\u0440\u0430\u0446\u044e\u0432\u0430\u0442\u0438 \u043d\u0430 \u043e\u0434\u043d\u043e\u043f\u043b\u0430\u0442\u043d\u0438\u0445 \u043a\u043e\u043c\u043f'\u044e\u0442\u0435\u0440\u0430\u0445.", + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0432\u0456\u0434\u0435\u043e\u043a\u043e\u0434\u0435\u043a \u043a\u0430\u043c\u0435\u0440\u0438." + }, + "include_exclude": { + "data": { + "entities": "\u0421\u0443\u0442\u043d\u043e\u0441\u0442\u0456", + "mode": "\u0420\u0435\u0436\u0438\u043c" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0456 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0456 \u0432 HomeKit. \u0423 \u0440\u0435\u0436\u0438\u043c\u0456 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u043d\u0430 \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c. \u0423 \u0440\u0435\u0436\u0438\u043c\u0456 \u043c\u043e\u0441\u0442\u0430 \u0431\u0443\u0434\u0443\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u0456 \u0432\u0441\u0456 \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0456, \u0449\u043e \u043d\u0430\u043b\u0435\u0436\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u0443, \u044f\u043a\u0449\u043e \u043d\u0435 \u0432\u0438\u0431\u0440\u0430\u043d\u0456 \u043f\u0435\u0432\u043d\u0456 \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0456. \u0423 \u0440\u0435\u0436\u0438\u043c\u0456 \u0432\u0438\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0431\u0443\u0434\u0443\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u0456 \u0432\u0441\u0456 \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0456, \u0449\u043e \u043d\u0430\u043b\u0435\u0436\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u0443, \u043a\u0440\u0456\u043c \u0432\u0438\u0431\u0440\u0430\u043d\u0438\u0445.", + "title": "\u0412\u0438\u0431\u0456\u0440 \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0435\u0439 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0456 \u0432 HomeKit" + }, "init": { "data": { + "include_domains": "\u0412\u0438\u0431\u0440\u0430\u0442\u0438 \u0434\u043e\u043c\u0435\u043d\u0438", "mode": "\u0420\u0435\u0436\u0438\u043c" - } + }, + "description": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f \u0437 HomeKit \u043c\u043e\u0436\u0435 \u0431\u0443\u0442\u0438 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0430 \u0432 \u0440\u0435\u0436\u0438\u043c\u0456 \u043c\u043e\u0441\u0442\u0430 \u0430\u0431\u043e \u044f\u043a \u043e\u043a\u0440\u0435\u043c\u0438\u0439 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440. \u0423 \u0440\u0435\u0436\u0438\u043c\u0456 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0442\u0456\u043b\u044c\u043a\u0438 \u043e\u0434\u0438\u043d \u043e\u0431'\u0454\u043a\u0442. \u041c\u0435\u0434\u0456\u0430\u043f\u043b\u0435\u0454\u0440\u0438, \u044f\u043a\u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u044e\u0442\u044c\u0441\u044f \u0432 Home Assistant \u0437 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u043c 'device_class: tv', \u0434\u043b\u044f \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0457 \u0440\u043e\u0431\u043e\u0442\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0431\u0443\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u043e\u0432\u0430\u043d\u0456 \u0432 Homekit \u0432 \u0440\u0435\u0436\u0438\u043c\u0456 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0430. \u041e\u0431'\u0454\u043a\u0442\u0438, \u0449\u043e \u043d\u0430\u043b\u0435\u0436\u0430\u0442\u044c \u043e\u0431\u0440\u0430\u043d\u0438\u043c \u0434\u043e\u043c\u0435\u043d\u0430\u043c, \u0431\u0443\u0434\u0443\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u0456 \u0432 HomeKit. \u041d\u0430 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u043e\u043c\u0443 \u0435\u0442\u0430\u043f\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0412\u0438 \u0437\u043c\u043e\u0436\u0435\u0442\u0435 \u0432\u0438\u0431\u0440\u0430\u0442\u0438, \u044f\u043a\u0456 \u043e\u0431'\u0454\u043a\u0442\u0438 \u0432\u0438\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0437 \u0446\u0438\u0445 \u0434\u043e\u043c\u0435\u043d\u0456\u0432.", + "title": "\u0412\u0438\u0431\u0456\u0440 \u0434\u043e\u043c\u0435\u043d\u0456\u0432 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0456 \u0432 HomeKit" + }, + "yaml": { + "description": "\u0426\u0435\u0439 \u0437\u0430\u043f\u0438\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e \u0447\u0435\u0440\u0435\u0437 YAML", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f HomeKit" } } } diff --git a/homeassistant/components/homekit/translations/zh-Hant.json b/homeassistant/components/homekit/translations/zh-Hant.json index 0f1093f5b5b..605263c4489 100644 --- a/homeassistant/components/homekit/translations/zh-Hant.json +++ b/homeassistant/components/homekit/translations/zh-Hant.json @@ -4,6 +4,20 @@ "port_name_in_use": "\u4f7f\u7528\u76f8\u540c\u540d\u7a31\u6216\u901a\u8a0a\u57e0\u7684\u914d\u4ef6\u6216 Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" }, "step": { + "accessory_mode": { + "data": { + "entity_id": "\u5be6\u9ad4" + }, + "description": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4\u3002\u65bc\u914d\u4ef6\u6a21\u5f0f\u4e0b\uff0c\u50c5\u80fd\u5305\u542b\u55ae\u4e00\u5be6\u9ad4\u3002", + "title": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4" + }, + "bridge_mode": { + "data": { + "include_domains": "\u5305\u542b\u7db2\u57df" + }, + "description": "\u9078\u64c7\u8981\u5305\u542b\u7684\u7db2\u57df\u3002\u6240\u6709\u7db2\u57df\u5167\u652f\u63f4\u7684\u5be6\u9ad4\u90fd\u6703\u5305\u542b\u3002", + "title": "\u9078\u64c7\u8981\u5305\u542b\u7684\u7db2\u57df" + }, "pairing": { "description": "\u65bc {name} \u5c31\u7dd2\u5f8c\u3001\u5c07\u6703\u65bc\u300c\u901a\u77e5\u300d\u4e2d\u986f\u793a\u300cHomeKit Bridge \u8a2d\u5b9a\u300d\u7684\u914d\u5c0d\u8cc7\u8a0a\u3002", "title": "\u914d\u5c0d HomeKit" @@ -11,9 +25,10 @@ "user": { "data": { "auto_start": "\u81ea\u52d5\u555f\u52d5\uff08\u5047\u5982\u4f7f\u7528 Z-Wave \u6216\u5176\u4ed6\u5ef6\u9072\u555f\u52d5\u7cfb\u7d71\u6642\u3001\u8acb\u95dc\u9589\uff09", - "include_domains": "\u5305\u542b Domain" + "include_domains": "\u5305\u542b\u7db2\u57df", + "mode": "\u6a21\u5f0f" }, - "description": "HomeKit \u6574\u5408\u5c07\u53ef\u5141\u8a31\u65bc Homekit \u4e2d\u4f7f\u7528 Home Assistant \u5be6\u9ad4\u3002\u65bc\u6a4b\u63a5\u6a21\u5f0f\u4e0b\u3001HomeKit Bridges \u6700\u9ad8\u9650\u5236\u70ba 150 \u500b\u914d\u4ef6\u3001\u5305\u542b Bridge \u672c\u8eab\u3002\u5047\u5982\u60f3\u8981\u4f7f\u7528\u8d85\u904e\u9650\u5236\u4ee5\u4e0a\u7684\u914d\u4ef6\uff0c\u5efa\u8b70\u53ef\u4ee5\u4e0d\u540c Domain \u4f7f\u7528\u591a\u500b HomeKit bridges \u9054\u5230\u6b64\u9700\u6c42\u3002\u50c5\u80fd\u65bc\u4e3b Bridge \u4ee5 YAML \u8a2d\u5b9a\u8a73\u7d30\u5be6\u9ad4\u3002", + "description": "HomeKit \u6574\u5408\u5c07\u53ef\u5141\u8a31\u65bc Homekit \u4e2d\u4f7f\u7528 Home Assistant \u5be6\u9ad4\u3002\u65bc\u6a4b\u63a5\u6a21\u5f0f\u4e0b\u3001HomeKit Bridges \u6700\u9ad8\u9650\u5236\u70ba 150 \u500b\u914d\u4ef6\u3001\u5305\u542b Bridge \u672c\u8eab\u3002\u5047\u5982\u60f3\u8981\u4f7f\u7528\u8d85\u904e\u9650\u5236\u4ee5\u4e0a\u7684\u914d\u4ef6\uff0c\u5efa\u8b70\u53ef\u4ee5\u4e0d\u540c\u7db2\u57df\u4f7f\u7528\u591a\u500b HomeKit bridges \u9054\u5230\u6b64\u9700\u6c42\u3002\u50c5\u80fd\u65bc\u4e3b Bridge \u4ee5 YAML \u8a2d\u5b9a\u8a73\u7d30\u5be6\u9ad4\u3002\u70ba\u53d6\u5f97\u6700\u4f73\u6548\u80fd\u3001\u4e26\u907f\u514d\u672a\u9810\u671f\u7121\u6cd5\u4f7f\u7528\u72c0\u614b\uff0c\u96fb\u8996\u5a92\u9ad4\u64ad\u653e\u5668\u8207\u651d\u5f71\u6a5f\uff0c\u8acb\u65bc Homekit \u914d\u4ef6\u6a21\u5f0f\u4e2d\u5206\u5225\u9032\u884c\u914d\u5c0d\u3002", "title": "\u555f\u7528 HomeKit" } } @@ -22,7 +37,7 @@ "step": { "advanced": { "data": { - "auto_start": "\u81ea\u52d5\u555f\u52d5\uff08\u5047\u5982\u4f7f\u7528 Z-Wave \u6216\u5176\u4ed6\u5ef6\u9072\u555f\u52d5\u7cfb\u7d71\u6642\u3001\u8acb\u95dc\u9589\uff09", + "auto_start": "\u81ea\u52d5\u555f\u52d5\uff08\u5047\u5982\u624b\u52d5\u4f7f\u7528 homekit.start \u670d\u52d9\u6642\u3001\u8acb\u95dc\u9589\uff09", "safe_mode": "\u5b89\u5168\u6a21\u5f0f\uff08\u50c5\u65bc\u914d\u5c0d\u5931\u6557\u6642\u4f7f\u7528\uff09" }, "description": "\u50c5\u65bc Homekit \u7121\u6cd5\u6b63\u5e38\u4f7f\u7528\u6642\uff0c\u8abf\u6574\u6b64\u4e9b\u8a2d\u5b9a\u3002", @@ -40,16 +55,16 @@ "entities": "\u5be6\u9ad4", "mode": "\u6a21\u5f0f" }, - "description": "\u9078\u64c7\u9032\u884c\u63a5\u901a\u7684\u5be6\u9ad4\u3002\u65bc\u5305\u542b\u6a21\u5f0f\u4e2d\u3001\u6240\u6709\u7db2\u57df\u7684\u5be6\u9ad4\u90fd\u5c07\u9032\u884c\u63a5\u901a\uff0c\u9664\u975e\u9078\u64c7\u7279\u5b9a\u7684\u5be6\u9ad4\u3002\u65bc\u6392\u9664\u6a21\u5f0f\u4e2d\u3001\u6240\u6709\u7db2\u57df\u4e2d\u7684\u9032\u884c\u63a5\u901a\uff0c\u9664\u4e86\u6392\u9664\u7684\u5be6\u9ad4\u3002", - "title": "\u9078\u64c7\u8981\u63a5\u901a\u7684\u5be6\u9ad4" + "description": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4\u3002\u65bc\u914d\u4ef6\u6a21\u5f0f\u4e0b\u3001\u50c5\u6709\u55ae\u4e00\u5be6\u9ad4\u5c07\u6703\u5305\u542b\u3002\u65bc\u6a4b\u63a5\u5305\u542b\u6a21\u5f0f\u4e0b\u3001\u6240\u6709\u7db2\u57df\u7684\u5be6\u9ad4\u90fd\u5c07\u5305\u542b\uff0c\u9664\u975e\u9078\u64c7\u7279\u5b9a\u7684\u5be6\u9ad4\u3002\u65bc\u6a4b\u63a5\u6392\u9664\u6a21\u5f0f\u4e2d\u3001\u6240\u6709\u7db2\u57df\u4e2d\u7684\u5be6\u9ad4\u90fd\u5c07\u5305\u542b\uff0c\u9664\u4e86\u6392\u9664\u7684\u5be6\u9ad4\u3002\u70ba\u53d6\u5f97\u6700\u4f73\u6548\u80fd\u3001\u4e26\u907f\u514d\u672a\u9810\u671f\u7121\u6cd5\u4f7f\u7528\u72c0\u614b\uff0c\u96fb\u8996\u5a92\u9ad4\u64ad\u653e\u5668\u8207\u651d\u5f71\u6a5f\uff0c\u8acb\u65bc Homekit \u914d\u4ef6\u6a21\u5f0f\u4e2d\u5206\u5225\u9032\u884c\u914d\u5c0d\u3002", + "title": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4" }, "init": { "data": { - "include_domains": "\u5305\u542b Domain", + "include_domains": "\u5305\u542b\u7db2\u57df", "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\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" + "description": "HomeKit \u80fd\u5920\u8a2d\u5b9a\u63a5\u901a\u6a4b\u63a5\u6216\u55ae\u4e00\u914d\u4ef6\u6a21\u5f0f\u3002 \u65bc\u914d\u4ef6\u6a21\u5f0f\u4e0b\u3001\u50c5\u6709\u55ae\u4e00\u5be6\u9ad4\u5c07\u6703\u5305\u542b\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\u7db2\u57df\" \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\u5305\u542b\u7684\u7db2\u57df\u3002" }, "yaml": { "description": "\u6b64\u5be6\u9ad4\u70ba\u900f\u904e YAML \u63a7\u5236", diff --git a/homeassistant/components/homekit_controller/translations/cs.json b/homeassistant/components/homekit_controller/translations/cs.json index 9a2159eda05..d5f7a502921 100644 --- a/homeassistant/components/homekit_controller/translations/cs.json +++ b/homeassistant/components/homekit_controller/translations/cs.json @@ -62,9 +62,9 @@ "doorbell": "Zvonek" }, "trigger_type": { - "double_press": "Dvakr\u00e1t stisknuto \"{subtype}\"", - "long_press": "Stisknuto a podr\u017eeno \"{subtype}\"", - "single_press": "Stisknuto \"{subtype}\"" + "double_press": "\"{subtype}\" stisknuto dvakr\u00e1t", + "long_press": "\"{subtype}\" stisknuto a podr\u017eeno", + "single_press": "\"{subtype}\" stisknuto" } }, "title": "HomeKit ovlada\u010d" diff --git a/homeassistant/components/homekit_controller/translations/de.json b/homeassistant/components/homekit_controller/translations/de.json index 3d5c538b62b..7bab8f30574 100644 --- a/homeassistant/components/homekit_controller/translations/de.json +++ b/homeassistant/components/homekit_controller/translations/de.json @@ -3,7 +3,7 @@ "abort": { "accessory_not_found_error": "Die Kopplung kann nicht durchgef\u00fchrt werden, da das Ger\u00e4t nicht mehr gefunden werden kann.", "already_configured": "Das Zubeh\u00f6r ist mit diesem Controller bereits konfiguriert.", - "already_in_progress": "Der Konfigurationsablauf f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt.", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "already_paired": "Dieses Zubeh\u00f6r ist bereits mit einem anderen Ger\u00e4t gekoppelt. Setze das Zubeh\u00f6r zur\u00fcck und versuche es erneut.", "ignored_model": "Die Unterst\u00fctzung von HomeKit f\u00fcr dieses Modell ist blockiert, da eine vollst\u00e4ndige native Integration verf\u00fcgbar ist.", "invalid_config_entry": "Dieses Ger\u00e4t wird als bereit zum Koppeln angezeigt, es gibt jedoch bereits einen widerspr\u00fcchlichen Konfigurationseintrag in Home Assistant, der zuerst entfernt werden muss.", @@ -30,7 +30,7 @@ "device": "Ger\u00e4t" }, "description": "W\u00e4hle das Ger\u00e4t aus, mit dem du die Kopplung herstellen m\u00f6chtest", - "title": "Mit HomeKit Zubeh\u00f6r koppeln" + "title": "Ger\u00e4teauswahl" } } }, diff --git a/homeassistant/components/homekit_controller/translations/pl.json b/homeassistant/components/homekit_controller/translations/pl.json index 50fdf6a17e4..3ccdfe452e5 100644 --- a/homeassistant/components/homekit_controller/translations/pl.json +++ b/homeassistant/components/homekit_controller/translations/pl.json @@ -62,9 +62,9 @@ "doorbell": "dzwonek do drzwi" }, "trigger_type": { - "double_press": "\"{subtype}\" naci\u015bni\u0119ty dwukrotnie", - "long_press": "\"{subtype}\" naci\u015bni\u0119ty i przytrzymany", - "single_press": "\"{subtype}\" naci\u015bni\u0119ty" + "double_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty dwukrotnie", + "long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty i przytrzymany", + "single_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty" } }, "title": "Kontroler HomeKit" diff --git a/homeassistant/components/homekit_controller/translations/tr.json b/homeassistant/components/homekit_controller/translations/tr.json new file mode 100644 index 00000000000..9d72049ba21 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/tr.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Aksesuar zaten bu denetleyici ile yap\u0131land\u0131r\u0131lm\u0131\u015ft\u0131r.", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor" + }, + "error": { + "authentication_error": "Yanl\u0131\u015f HomeKit kodu. L\u00fctfen kontrol edip tekrar deneyin.", + "unknown_error": "Cihaz bilinmeyen bir hata bildirdi. E\u015fle\u015ftirme ba\u015far\u0131s\u0131z oldu." + }, + "step": { + "busy_error": { + "title": "Cihaz zaten ba\u015fka bir oyun kumandas\u0131yla e\u015fle\u015fiyor" + }, + "max_tries_error": { + "title": "Maksimum kimlik do\u011frulama giri\u015fimi a\u015f\u0131ld\u0131" + } + } + }, + "device_automation": { + "trigger_subtype": { + "button1": "D\u00fc\u011fme 1", + "button10": "D\u00fc\u011fme 10", + "button2": "D\u00fc\u011fme 2", + "button3": "D\u00fc\u011fme 3", + "button4": "D\u00fc\u011fme 4", + "button5": "D\u00fc\u011fme 5", + "button6": "D\u00fc\u011fme 6", + "button7": "D\u00fc\u011fme 7", + "button8": "D\u00fc\u011fme 8", + "button9": "D\u00fc\u011fme 9", + "doorbell": "Kap\u0131 zili" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/uk.json b/homeassistant/components/homekit_controller/translations/uk.json new file mode 100644 index 00000000000..66eb1741208 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/uk.json @@ -0,0 +1,71 @@ +{ + "config": { + "abort": { + "accessory_not_found_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0438\u043a\u043e\u043d\u0430\u0442\u0438 \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f, \u043e\u0441\u043a\u0456\u043b\u044c\u043a\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e.", + "already_configured": "\u0410\u043a\u0441\u0435\u0441\u0443\u0430\u0440 \u0432\u0436\u0435 \u043f\u043e\u0432'\u044f\u0437\u0430\u043d\u0438\u0439 \u0437 \u0446\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u043e\u043c.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "already_paired": "\u0426\u0435\u0439 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440 \u0432\u0436\u0435 \u043f\u043e\u0432'\u044f\u0437\u0430\u043d\u0438\u0439 \u0437 \u0456\u043d\u0448\u0438\u043c \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043a\u0438\u043d\u044c\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0430 \u0456 \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443.", + "ignored_model": "\u041f\u0456\u0434\u0442\u0440\u0438\u043c\u043a\u0430 HomeKit \u0434\u043b\u044f \u0446\u0456\u0454\u0457 \u043c\u043e\u0434\u0435\u043b\u0456 \u0437\u0430\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u0430, \u043e\u0441\u043a\u0456\u043b\u044c\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0431\u0456\u043b\u044c\u0448 \u043f\u043e\u0432\u043d\u0430 \u043d\u0430\u0442\u0438\u0432\u043d\u0430 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f.", + "invalid_config_entry": "\u0426\u0435 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0454\u0442\u044c\u0441\u044f \u044f\u043a \u0433\u043e\u0442\u043e\u0432\u0438\u0439 \u0434\u043e \u043e\u0431'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0432 \u043f\u0430\u0440\u0443, \u0430\u043b\u0435 \u0432 Home Assistant \u0432\u0436\u0435 \u0454 \u043a\u043e\u043d\u0444\u043b\u0456\u043a\u0442\u043d\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457 \u0434\u043b\u044f \u043d\u044c\u043e\u0433\u043e, \u044f\u043a\u0438\u0439 \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438.", + "invalid_properties": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0456 \u0432\u043b\u0430\u0441\u0442\u0438\u0432\u043e\u0441\u0442\u0456, \u043e\u0433\u043e\u043b\u043e\u0448\u0435\u043d\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c.", + "no_devices": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457, \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0456 \u0434\u043b\u044f \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f, \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456." + }, + "error": { + "authentication_error": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043e\u0434 HomeKit. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043a\u043e\u0434 \u0456 \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437.", + "max_peers_error": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0456\u0434\u0445\u0438\u043b\u0438\u0432 \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u0447\u0435\u0440\u0435\u0437 \u0432\u0456\u0434\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c \u0432\u0456\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u044f.", + "pairing_failed": "\u041f\u0456\u0434 \u0447\u0430\u0441 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438 \u0441\u0442\u0430\u043b\u0430\u0441\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430. \u0426\u0435 \u043c\u043e\u0436\u0435 \u0431\u0443\u0442\u0438 \u0442\u0438\u043c\u0447\u0430\u0441\u043e\u0432\u0438\u0439 \u0437\u0431\u0456\u0439 \u0430\u0431\u043e \u0412\u0430\u0448 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0430 \u0434\u0430\u043d\u0438\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u0449\u0435 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f.", + "unable_to_pair": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043f\u0430\u0440\u0443. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437.", + "unknown_error": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u0438\u0432 \u043f\u0440\u043e \u043d\u0435\u0432\u0456\u0434\u043e\u043c\u0443 \u043f\u043e\u043c\u0438\u043b\u043a\u0443. \u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043f\u0430\u0440\u0443." + }, + "flow_title": "{name} \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0456\u0432 HomeKit", + "step": { + "busy_error": { + "description": "\u0421\u043a\u0430\u0441\u0443\u0439\u0442\u0435 \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u043d\u0430 \u0432\u0441\u0456\u0445 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430\u0445 \u0430\u0431\u043e \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u043f\u043e\u0442\u0456\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u0456\u0442\u044c \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f.", + "title": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0443\u0436\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e \u0456\u043d\u0448\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430." + }, + "max_tries_error": { + "description": "\u041f\u043e\u043d\u0430\u0434 100 \u0441\u043f\u0440\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457 \u043f\u0440\u043e\u0439\u0448\u043b\u0438 \u043d\u0435\u0432\u0434\u0430\u043b\u043e. \u0421\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u0430 \u043f\u043e\u0442\u0456\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u0456\u0442\u044c \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f.", + "title": "\u041f\u0435\u0440\u0435\u0432\u0438\u0449\u0435\u043d\u043e \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0443 \u043a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u0441\u043f\u0440\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457." + }, + "pair": { + "data": { + "pairing_code": "\u041a\u043e\u0434 \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "description": "HomeKit Controller \u043e\u0431\u043c\u0456\u043d\u044e\u0454\u0442\u044c\u0441\u044f \u0434\u0430\u043d\u0438\u043c\u0438 \u0437 {name} \u043f\u043e \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0456\u0439 \u043c\u0435\u0440\u0435\u0436\u0456, \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u044e\u0447\u0438 \u0431\u0435\u0437\u043f\u0435\u0447\u043d\u0435 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0435 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0431\u0435\u0437 \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e\u0441\u0442\u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f \u043e\u043a\u0440\u0435\u043c\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430 HomeKit \u0430\u0431\u043e iCloud. \u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043e\u0434 \u0441\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044f HomeKit (\u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 XXX-XX-XXX), \u0449\u043e\u0431 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0446\u0435\u0439 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440. \u0426\u0435\u0439 \u043a\u043e\u0434 \u0437\u0430\u0437\u0432\u0438\u0447\u0430\u0439 \u0437\u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u043d\u0430 \u0441\u0430\u043c\u043e\u043c\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u0430\u0431\u043e \u043d\u0430 \u0443\u043f\u0430\u043a\u043e\u0432\u0446\u0456.", + "title": "\u0421\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438 \u0437 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0456\u0432 HomeKit" + }, + "protocol_error": { + "description": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u043c\u043e\u0436\u043b\u0438\u0432\u043e, \u043d\u0435 \u0437\u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0432 \u0440\u0435\u0436\u0438\u043c\u0456 \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u0456 \u043c\u043e\u0436\u0435 \u0437\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0438\u0441\u044f \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f \u0444\u0456\u0437\u0438\u0447\u043d\u043e\u0457 \u0430\u0431\u043e \u0432\u0456\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0457 \u043a\u043d\u043e\u043f\u043a\u0438. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0437\u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0432 \u0440\u0435\u0436\u0438\u043c\u0456 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438, \u0430\u0431\u043e \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u0456 \u043f\u043e\u0442\u0456\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u0456\u0442\u044c \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f.", + "title": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437\u0432'\u044f\u0437\u043a\u0443 \u0437 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u043e\u043c." + }, + "user": { + "data": { + "device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + }, + "description": "HomeKit Controller \u043e\u0431\u043c\u0456\u043d\u044e\u0454\u0442\u044c\u0441\u044f \u0434\u0430\u043d\u0438\u043c\u0438 \u0432 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0456\u0439 \u043c\u0435\u0440\u0435\u0436\u0456 \u0437 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f\u043c \u0431\u0435\u0437\u043f\u0435\u0447\u043d\u043e\u0433\u043e \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043e\u0433\u043e \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0431\u0435\u0437 \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e\u0441\u0442\u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f \u043e\u043a\u0440\u0435\u043c\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430 HomeKit \u0430\u0431\u043e iCloud. \u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u0437 \u044f\u043a\u0438\u043c \u0445\u043e\u0447\u0435\u0442\u0435 \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043f\u0430\u0440\u0443:", + "title": "\u0412\u0438\u0431\u0456\u0440 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + }, + "device_automation": { + "trigger_subtype": { + "button1": "\u041a\u043d\u043e\u043f\u043a\u0430 1", + "button10": "\u041a\u043d\u043e\u043f\u043a\u0430 10", + "button2": "\u041a\u043d\u043e\u043f\u043a\u0430 2", + "button3": "\u041a\u043d\u043e\u043f\u043a\u0430 3", + "button4": "\u041a\u043d\u043e\u043f\u043a\u0430 4", + "button5": "\u041a\u043d\u043e\u043f\u043a\u0430 5", + "button6": "\u041a\u043d\u043e\u043f\u043a\u0430 6", + "button7": "\u041a\u043d\u043e\u043f\u043a\u0430 7", + "button8": "\u041a\u043d\u043e\u043f\u043a\u0430 8", + "button9": "\u041a\u043d\u043e\u043f\u043a\u0430 9", + "doorbell": "\u0414\u0432\u0435\u0440\u043d\u0438\u0439 \u0434\u0437\u0432\u0456\u043d\u043e\u043a" + }, + "trigger_type": { + "double_press": "\"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0434\u0432\u0456\u0447\u0456", + "long_press": "\"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0456 \u0443\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f", + "single_press": "\"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430" + } + }, + "title": "HomeKit Controller" +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/de.json b/homeassistant/components/homematicip_cloud/translations/de.json index c421620fd98..1da1e06c0fb 100644 --- a/homeassistant/components/homematicip_cloud/translations/de.json +++ b/homeassistant/components/homematicip_cloud/translations/de.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "Der Accesspoint ist bereits konfiguriert", - "connection_aborted": "Konnte nicht mit HMIP Server verbinden", - "unknown": "Ein unbekannter Fehler ist aufgetreten." + "connection_aborted": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" }, "error": { - "invalid_sgtin_or_pin": "Ung\u00fcltige PIN, bitte versuche es erneut.", + "invalid_sgtin_or_pin": "Ung\u00fcltige SGTIN oder PIN-Code, bitte versuche es erneut.", "press_the_button": "Bitte dr\u00fccke die blaue Taste.", "register_failed": "Registrierung fehlgeschlagen, bitte versuche es erneut.", "timeout_button": "Zeit\u00fcberschreitung beim Dr\u00fccken der blauen Taste. Bitte versuche es erneut." @@ -16,7 +16,7 @@ "data": { "hapid": "Accesspoint ID (SGTIN)", "name": "Name (optional, wird als Pr\u00e4fix f\u00fcr alle Ger\u00e4te verwendet)", - "pin": "PIN Code (optional)" + "pin": "PIN-Code" }, "title": "HomematicIP Accesspoint ausw\u00e4hlen" }, diff --git a/homeassistant/components/homematicip_cloud/translations/tr.json b/homeassistant/components/homematicip_cloud/translations/tr.json new file mode 100644 index 00000000000..72f139217ca --- /dev/null +++ b/homeassistant/components/homematicip_cloud/translations/tr.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "connection_aborted": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/uk.json b/homeassistant/components/homematicip_cloud/translations/uk.json new file mode 100644 index 00000000000..1ed2e317f8b --- /dev/null +++ b/homeassistant/components/homematicip_cloud/translations/uk.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "connection_aborted": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "invalid_sgtin_or_pin": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 SGTIN \u0430\u0431\u043e PIN-\u043a\u043e\u0434, \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443.", + "press_the_button": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u0441\u0438\u043d\u044e \u043a\u043d\u043e\u043f\u043a\u0443.", + "register_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0432\u0430\u0442\u0438\u0441\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443.", + "timeout_button": "\u0412\u0438 \u043d\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u043b\u0438 \u043d\u0430 \u0441\u0438\u043d\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u0432 \u043c\u0435\u0436\u0430\u0445 \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e\u0433\u043e \u0447\u0430\u0441\u0443, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443." + }, + "step": { + "init": { + "data": { + "hapid": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0442\u043e\u0447\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0443 (SGTIN)", + "name": "\u041d\u0430\u0437\u0432\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e, \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u044f\u043a \u043f\u0440\u0435\u0444\u0456\u043a\u0441 \u0434\u043b\u044f \u043d\u0430\u0437\u0432\u0438 \u0432\u0441\u0456\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432)", + "pin": "PIN-\u043a\u043e\u0434" + }, + "title": "HomematicIP Cloud" + }, + "link": { + "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u0441\u0438\u043d\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0442\u043e\u0447\u0446\u0456 \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u0456 \u043a\u043d\u043e\u043f\u043a\u0443 ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **, \u0449\u043e\u0431 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0432\u0430\u0442\u0438 HomematicIP \u0432 Home Assistant. \n\n![\u0420\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043d\u043e\u043f\u043a\u0438] (/static/images/config_flows/config_homematicip_cloud.png)", + "title": "\u041f\u0440\u0438\u0432'\u044f\u0437\u0430\u0442\u0438 \u0442\u043e\u0447\u043a\u0443 \u0434\u043e\u0441\u0442\u0443\u043f\u0443" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/de.json b/homeassistant/components/huawei_lte/translations/de.json index 7da997f12d6..43361e46929 100644 --- a/homeassistant/components/huawei_lte/translations/de.json +++ b/homeassistant/components/huawei_lte/translations/de.json @@ -1,14 +1,15 @@ { "config": { "abort": { - "already_configured": "Dieses Ger\u00e4t wurde bereits konfiguriert", - "already_in_progress": "Dieses Ger\u00e4t wurde bereits konfiguriert", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "not_huawei_lte": "Kein Huawei LTE-Ger\u00e4t" }, "error": { "connection_timeout": "Verbindungszeit\u00fcberschreitung", "incorrect_password": "Ung\u00fcltiges Passwort", "incorrect_username": "Ung\u00fcltiger Benutzername", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "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", diff --git a/homeassistant/components/huawei_lte/translations/tr.json b/homeassistant/components/huawei_lte/translations/tr.json index a76e31fa483..ba934acc39b 100644 --- a/homeassistant/components/huawei_lte/translations/tr.json +++ b/homeassistant/components/huawei_lte/translations/tr.json @@ -1,8 +1,35 @@ { + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor" + }, + "error": { + "connection_timeout": "Ba\u011flant\u0131 zamana\u015f\u0131m\u0131", + "incorrect_password": "Yanl\u0131\u015f parola", + "incorrect_username": "Yanl\u0131\u015f kullan\u0131c\u0131 ad\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_url": "Ge\u00e7ersiz URL", + "login_attempts_exceeded": "Maksimum oturum a\u00e7ma denemesi a\u015f\u0131ld\u0131, l\u00fctfen daha sonra tekrar deneyin", + "response_error": "Cihazdan bilinmeyen hata", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "url": "URL", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Cihaz eri\u015fim ayr\u0131nt\u0131lar\u0131n\u0131 girin. Kullan\u0131c\u0131 ad\u0131 ve parolan\u0131n belirtilmesi iste\u011fe ba\u011fl\u0131d\u0131r, ancak daha fazla entegrasyon \u00f6zelli\u011fi i\u00e7in destek sa\u011flar. \u00d6te yandan, yetkili bir ba\u011flant\u0131n\u0131n kullan\u0131lmas\u0131, entegrasyon aktifken Ev Asistan\u0131 d\u0131\u015f\u0131ndan cihaz web aray\u00fcz\u00fcne eri\u015fimde sorunlara neden olabilir ve tam tersi." + } + } + }, "options": { "step": { "init": { "data": { + "recipient": "SMS bildirimi al\u0131c\u0131lar\u0131", "track_new_devices": "Yeni cihazlar\u0131 izle" } } diff --git a/homeassistant/components/huawei_lte/translations/uk.json b/homeassistant/components/huawei_lte/translations/uk.json new file mode 100644 index 00000000000..17f3d3b71c3 --- /dev/null +++ b/homeassistant/components/huawei_lte/translations/uk.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "not_huawei_lte": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0454 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c Huawei LTE" + }, + "error": { + "connection_timeout": "\u0427\u0430\u0441 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043c\u0438\u043d\u0443\u0432.", + "incorrect_password": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u043f\u0430\u0440\u043e\u043b\u044c.", + "incorrect_username": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0456\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "invalid_url": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0430 URL-\u0430\u0434\u0440\u0435\u0441\u0430.", + "login_attempts_exceeded": "\u041f\u0435\u0440\u0435\u0432\u0438\u0449\u0435\u043d\u043e \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0443 \u043a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u0441\u043f\u0440\u043e\u0431 \u0432\u0445\u043e\u0434\u0443, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437 \u043f\u0456\u0437\u043d\u0456\u0448\u0435.", + "response_error": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "Huawei LTE: {name}", + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0434\u0430\u043d\u0456 \u0434\u043b\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e. \u0412\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u043b\u043e\u0433\u0456\u043d \u0456 \u043f\u0430\u0440\u043e\u043b\u044c \u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e, \u0430\u043b\u0435 \u0446\u0435 \u0434\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u044c \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u0444\u0443\u043d\u043a\u0446\u0456\u0457. \u0417 \u0456\u043d\u0448\u043e\u0433\u043e \u0431\u043e\u043a\u0443, \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043e\u0433\u043e \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u043c\u043e\u0436\u0435 \u0432\u0438\u043a\u043b\u0438\u043a\u0430\u0442\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0437 \u0434\u043e\u0441\u0442\u0443\u043f\u043e\u043c \u0434\u043e \u0432\u0435\u0431-\u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u043d\u0435 \u0437 Home Assistant, \u043a\u043e\u043b\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f \u0430\u043a\u0442\u0438\u0432\u043d\u0430, \u0456 \u043d\u0430\u0432\u043f\u0430\u043a\u0438.", + "title": "Huawei LTE" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430 \u0441\u043b\u0443\u0436\u0431\u0438 \u0441\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u044c (\u043f\u043e\u0442\u0440\u0456\u0431\u0435\u043d \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a)", + "recipient": "\u041e\u0434\u0435\u0440\u0436\u0443\u0432\u0430\u0447\u0456 SMS-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c", + "track_new_devices": "\u0412\u0456\u0434\u0441\u0442\u0435\u0436\u0443\u0432\u0430\u0442\u0438 \u043d\u043e\u0432\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/cs.json b/homeassistant/components/hue/translations/cs.json index 76606338320..1708abfe750 100644 --- a/homeassistant/components/hue/translations/cs.json +++ b/homeassistant/components/hue/translations/cs.json @@ -48,7 +48,7 @@ }, "trigger_type": { "remote_button_long_release": "Tla\u010d\u00edtko \"{subtype}\" uvoln\u011bno po dlouh\u00e9m stisku", - "remote_button_short_press": "Stisknuto tla\u010d\u00edtko \"{subtype}\"", + "remote_button_short_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto", "remote_button_short_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\"", "remote_double_button_long_press": "Oba \"{subtype}\" uvoln\u011bny po dlouh\u00e9m stisku", "remote_double_button_short_press": "Oba \"{subtype}\" uvoln\u011bny" diff --git a/homeassistant/components/hue/translations/de.json b/homeassistant/components/hue/translations/de.json index 0defb33ae5e..122e1ba6f5c 100644 --- a/homeassistant/components/hue/translations/de.json +++ b/homeassistant/components/hue/translations/de.json @@ -1,17 +1,17 @@ { "config": { "abort": { - "all_configured": "Alle Philips Hue Bridges sind bereits konfiguriert", - "already_configured": "Bridge ist bereits konfiguriert", - "already_in_progress": "Der Konfigurationsablauf f\u00fcr die Bridge wird bereits ausgef\u00fchrt.", - "cannot_connect": "Verbindung zur Bridge nicht m\u00f6glich", - "discover_timeout": "Nicht in der Lage Hue Bridges zu entdecken", - "no_bridges": "Keine Philips Hue Bridges entdeckt", + "all_configured": "Es sind bereits alle Philips Hue Bridges konfiguriert", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen", + "discover_timeout": "Es k\u00f6nnen keine Hue Bridges erkannt werden", + "no_bridges": "Keine Philips Hue Bridges erkannt", "not_hue_bridge": "Keine Philips Hue Bridge entdeckt", "unknown": "Unbekannter Fehler ist aufgetreten" }, "error": { - "linking": "Unbekannter Link-Fehler aufgetreten.", + "linking": "Unerwarteter Fehler", "register_failed": "Registrieren fehlgeschlagen, bitte versuche es erneut" }, "step": { @@ -22,7 +22,7 @@ "title": "W\u00e4hle eine Hue Bridge" }, "link": { - "description": "Dr\u00fccke den Knopf auf der Bridge, um Philips Hue mit Home Assistant zu registrieren.\n\n![Position des Buttons auf der Bridge](/static/images/config_philips_hue.jpg)", + "description": "Dr\u00fccke den Knopf auf der Bridge, um Philips Hue mit Home Assistant zu verkn\u00fcpfen.\n\n![Position des Buttons auf der Bridge](/static/images/config_philips_hue.jpg)", "title": "Hub verbinden" }, "manual": { @@ -58,8 +58,8 @@ "step": { "init": { "data": { - "allow_hue_groups": "Erlaube Hue Gruppen", - "allow_unreachable": "Erlauben Sie unerreichbaren Gl\u00fchbirnen, ihren Zustand korrekt zu melden" + "allow_hue_groups": "Hue-Gruppen erlauben", + "allow_unreachable": "Erlaube nicht erreichbaren Gl\u00fchlampen, ihren Zustand korrekt zu melden" } } } diff --git a/homeassistant/components/hue/translations/pl.json b/homeassistant/components/hue/translations/pl.json index 873f60946d5..b144393c3d1 100644 --- a/homeassistant/components/hue/translations/pl.json +++ b/homeassistant/components/hue/translations/pl.json @@ -47,11 +47,11 @@ "turn_on": "w\u0142\u0105cznik" }, "trigger_type": { - "remote_button_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", - "remote_button_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty", - "remote_button_short_release": "\"{subtype}\" zostanie zwolniony", - "remote_double_button_long_press": "oba \"{subtype}\" zostan\u0105 zwolnione po d\u0142ugim naci\u015bni\u0119ciu", - "remote_double_button_short_press": "oba \"{subtype}\" zostan\u0105 zwolnione" + "remote_button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", + "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", + "remote_double_button_long_press": "oba przyciski \"{subtype}\" zostan\u0105 zwolnione po d\u0142ugim naci\u015bni\u0119ciu", + "remote_double_button_short_press": "oba przyciski \"{subtype}\" zostan\u0105 zwolnione" } }, "options": { diff --git a/homeassistant/components/hue/translations/pt.json b/homeassistant/components/hue/translations/pt.json index 8eabbbb08cc..09d839cbd5c 100644 --- a/homeassistant/components/hue/translations/pt.json +++ b/homeassistant/components/hue/translations/pt.json @@ -2,16 +2,16 @@ "config": { "abort": { "all_configured": "Todos os hubs Philips Hue j\u00e1 est\u00e3o configurados", - "already_configured": "Hue j\u00e1 est\u00e1 configurado", + "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": "N\u00e3o foi poss\u00edvel conectar-se ao hub", + "cannot_connect": "Falha na liga\u00e7\u00e3o", "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" + "unknown": "Erro inesperado" }, "error": { - "linking": "Ocorreu um erro de liga\u00e7\u00e3o desconhecido.", + "linking": "Erro inesperado", "register_failed": "Falha ao registar, por favor, tente novamente" }, "step": { diff --git a/homeassistant/components/hue/translations/tr.json b/homeassistant/components/hue/translations/tr.json new file mode 100644 index 00000000000..984c91e8f36 --- /dev/null +++ b/homeassistant/components/hue/translations/tr.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "error": { + "linking": "Beklenmeyen hata" + }, + "step": { + "init": { + "data": { + "host": "Ana Bilgisayar" + } + }, + "manual": { + "data": { + "host": "Ana Bilgisayar" + }, + "title": "Bir Hue k\u00f6pr\u00fcs\u00fcn\u00fc manuel olarak yap\u0131land\u0131rma" + } + } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "\u0130lk d\u00fc\u011fme", + "button_2": "\u0130kinci d\u00fc\u011fme", + "button_3": "\u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fme", + "button_4": "D\u00f6rd\u00fcnc\u00fc d\u00fc\u011fme", + "double_buttons_1_3": "Birinci ve \u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fmeler", + "double_buttons_2_4": "\u0130kinci ve D\u00f6rd\u00fcnc\u00fc d\u00fc\u011fmeler", + "turn_off": "Kapat", + "turn_on": "A\u00e7" + } + }, + "options": { + "step": { + "init": { + "data": { + "allow_hue_groups": "Hue gruplar\u0131na izin ver", + "allow_unreachable": "Ula\u015f\u0131lamayan ampullerin durumlar\u0131n\u0131 do\u011fru \u015fekilde bildirmesine izin verin" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/uk.json b/homeassistant/components/hue/translations/uk.json new file mode 100644 index 00000000000..8e9c5ca82cb --- /dev/null +++ b/homeassistant/components/hue/translations/uk.json @@ -0,0 +1,67 @@ +{ + "config": { + "abort": { + "all_configured": "\u0412\u0441\u0456 \u0448\u043b\u044e\u0437\u0438 Philips Hue \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0456.", + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "discover_timeout": "\u0428\u043b\u044e\u0437 Philips Hue \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e.", + "no_bridges": "\u0428\u043b\u044e\u0437\u0438 Philips Hue \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456.", + "not_hue_bridge": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0454 \u0448\u043b\u044e\u0437\u043e\u043c Hue.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "linking": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430", + "register_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0432\u0430\u0442\u0438\u0441\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443." + }, + "step": { + "init": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0448\u043b\u044e\u0437 Hue" + }, + "link": { + "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0448\u043b\u044e\u0437\u0456 \u0434\u043b\u044f \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u0457 Philips Hue \u0432 Home Assistant. \n\n![\u0420\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043d\u043e\u043f\u043a\u0438 \u043d\u0430 \u0448\u043b\u044e\u0437\u0456] (/static/images/config_philips_hue.jpg)", + "title": "Philips Hue" + }, + "manual": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "title": "\u0420\u0443\u0447\u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0448\u043b\u044e\u0437\u0443" + } + } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "\u041f\u0435\u0440\u0448\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_2": "\u0414\u0440\u0443\u0433\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_3": "\u0422\u0440\u0435\u0442\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "dim_down": "\u0417\u043c\u0435\u043d\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c", + "dim_up": "\u0417\u0431\u0456\u043b\u044c\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c", + "double_buttons_1_3": "\u041f\u0435\u0440\u0448\u0430 \u0456 \u0442\u0440\u0435\u0442\u044f \u043a\u043d\u043e\u043f\u043a\u0438", + "double_buttons_2_4": "\u0414\u0440\u0443\u0433\u0430 \u0456 \u0447\u0435\u0442\u0432\u0435\u0440\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0438", + "turn_off": "\u0412\u0438\u043c\u043a\u043d\u0443\u0442\u0438", + "turn_on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438" + }, + "trigger_type": { + "remote_button_long_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u0434\u043e\u0432\u0433\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f", + "remote_button_short_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430", + "remote_button_short_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f", + "remote_double_button_long_press": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u043e \u043f\u0456\u0441\u043b\u044f \u0434\u043e\u0432\u0433\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f", + "remote_double_button_short_press": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u043e \u043f\u0456\u0441\u043b\u044f \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f" + } + }, + "options": { + "step": { + "init": { + "data": { + "allow_hue_groups": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0433\u0440\u0443\u043f\u0438 Hue", + "allow_unreachable": "\u041f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u044f\u0442\u0438 \u0441\u0442\u0430\u043d \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/ca.json b/homeassistant/components/huisbaasje/translations/ca.json new file mode 100644 index 00000000000..99d99d4340f --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "connection_exception": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unauthenticated_exception": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/cs.json b/homeassistant/components/huisbaasje/translations/cs.json new file mode 100644 index 00000000000..07a1d29330b --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "connection_exception": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unauthenticated_exception": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/en.json b/homeassistant/components/huisbaasje/translations/en.json new file mode 100644 index 00000000000..16832be30e7 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "connection_exception": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unauthenticated_exception": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/et.json b/homeassistant/components/huisbaasje/translations/et.json new file mode 100644 index 00000000000..d079bf2a0c7 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "connection_exception": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine", + "unauthenticated_exception": "Vigane autentimine", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/it.json b/homeassistant/components/huisbaasje/translations/it.json new file mode 100644 index 00000000000..0171bdcd9f2 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "connection_exception": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unauthenticated_exception": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/no.json b/homeassistant/components/huisbaasje/translations/no.json new file mode 100644 index 00000000000..81351599c16 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "connection_exception": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unauthenticated_exception": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/pl.json b/homeassistant/components/huisbaasje/translations/pl.json new file mode 100644 index 00000000000..ab38d61de0b --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "connection_exception": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unauthenticated_exception": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/ru.json b/homeassistant/components/huisbaasje/translations/ru.json new file mode 100644 index 00000000000..ada9aed539a --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "connection_exception": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unauthenticated_exception": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/tr.json b/homeassistant/components/huisbaasje/translations/tr.json new file mode 100644 index 00000000000..fa5bd311286 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "connection_exception": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unauthenticated_exception": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen Hata" + }, + "step": { + "user": { + "data": { + "password": "\u015eifre", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/zh-Hant.json b/homeassistant/components/huisbaasje/translations/zh-Hant.json new file mode 100644 index 00000000000..bb120ab60dd --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "connection_exception": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unauthenticated_exception": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/tr.json b/homeassistant/components/humidifier/translations/tr.json new file mode 100644 index 00000000000..7bcdbc46a0b --- /dev/null +++ b/homeassistant/components/humidifier/translations/tr.json @@ -0,0 +1,25 @@ +{ + "device_automation": { + "action_type": { + "set_mode": "{entity_name} \u00fczerindeki mod de\u011fi\u015ftirme", + "turn_on": "{entity_name} a\u00e7\u0131n" + }, + "condition_type": { + "is_mode": "{entity_name} belirli bir moda ayarland\u0131", + "is_off": "{entity_name} kapal\u0131", + "is_on": "{entity_name} a\u00e7\u0131k" + }, + "trigger_type": { + "target_humidity_changed": "{entity_name} hedef nem de\u011fi\u015fti", + "turned_off": "{entity_name} kapat\u0131ld\u0131", + "turned_on": "{entity_name} a\u00e7\u0131ld\u0131" + } + }, + "state": { + "_": { + "off": "Kapal\u0131", + "on": "A\u00e7\u0131k" + } + }, + "title": "Nemlendirici" +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/uk.json b/homeassistant/components/humidifier/translations/uk.json index 4081c4e13fc..484f014bd92 100644 --- a/homeassistant/components/humidifier/translations/uk.json +++ b/homeassistant/components/humidifier/translations/uk.json @@ -1,8 +1,28 @@ { "device_automation": { + "action_type": { + "set_humidity": "{entity_name}: \u0437\u0430\u0434\u0430\u0442\u0438 \u0432\u043e\u043b\u043e\u0433\u0456\u0441\u0442\u044c", + "set_mode": "{entity_name}: \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u0440\u0435\u0436\u0438\u043c", + "toggle": "{entity_name}: \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u0438", + "turn_off": "{entity_name}: \u0432\u0438\u043c\u043a\u043d\u0443\u0442\u0438", + "turn_on": "{entity_name}: \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438" + }, + "condition_type": { + "is_mode": "{entity_name} \u0437\u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0432 \u0437\u0430\u0434\u0430\u043d\u043e\u043c\u0443 \u0440\u0435\u0436\u0438\u043c\u0456 \u0440\u043e\u0431\u043e\u0442\u0438", + "is_off": "{entity_name} \u0443 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456" + }, "trigger_type": { + "target_humidity_changed": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0437\u0430\u0434\u0430\u043d\u043e\u0457 \u0432\u043e\u043b\u043e\u0433\u043e\u0441\u0442\u0456", "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e", - "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u043e" + "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" } - } + }, + "state": { + "_": { + "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e", + "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" + } + }, + "title": "\u0417\u0432\u043e\u043b\u043e\u0436\u0443\u0432\u0430\u0447" } \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/tr.json b/homeassistant/components/hunterdouglas_powerview/translations/tr.json new file mode 100644 index 00000000000..01b0359789e --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "\u0130p Adresi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/uk.json b/homeassistant/components/hunterdouglas_powerview/translations/uk.json new file mode 100644 index 00000000000..959fcff12b0 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/uk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "link": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name} ({host})?", + "title": "Hunter Douglas PowerView" + }, + "user": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430" + }, + "title": "Hunter Douglas PowerView" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hvv_departures/translations/tr.json b/homeassistant/components/hvv_departures/translations/tr.json new file mode 100644 index 00000000000..74fc593062b --- /dev/null +++ b/homeassistant/components/hvv_departures/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hvv_departures/translations/uk.json b/homeassistant/components/hvv_departures/translations/uk.json new file mode 100644 index 00000000000..364d351a99c --- /dev/null +++ b/homeassistant/components/hvv_departures/translations/uk.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "no_results": "\u041d\u0435\u043c\u0430\u0454 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0456\u0432. \u0421\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437 \u0456\u043d\u0448\u043e\u044e \u0441\u0442\u0430\u043d\u0446\u0456\u0454\u044e / \u0430\u0434\u0440\u0435\u0441\u043e\u044e." + }, + "step": { + "station": { + "data": { + "station": "\u0421\u0442\u0430\u043d\u0446\u0456\u044f / \u0410\u0434\u0440\u0435\u0441\u0430" + }, + "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0441\u0442\u0430\u043d\u0446\u0456\u044e / \u0430\u0434\u0440\u0435\u0441\u0443" + }, + "station_select": { + "data": { + "station": "\u0421\u0442\u0430\u043d\u0446\u0456\u044f / \u0410\u0434\u0440\u0435\u0441\u0430" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0442\u0430\u043d\u0446\u0456\u044e / \u0430\u0434\u0440\u0435\u0441\u0443" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e API HVV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "filter": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043b\u0456\u043d\u0456\u0457", + "offset": "\u0417\u043c\u0456\u0449\u0435\u043d\u043d\u044f (\u0432 \u0445\u0432\u0438\u043b\u0438\u043d\u0430\u0445)", + "real_time": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0434\u0430\u043d\u0456 \u0432 \u0440\u0435\u0436\u0438\u043c\u0456 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0447\u0430\u0441\u0443" + }, + "description": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u044f", + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/fr.json b/homeassistant/components/hyperion/translations/fr.json index 8c1cb919d11..90733a8968b 100644 --- a/homeassistant/components/hyperion/translations/fr.json +++ b/homeassistant/components/hyperion/translations/fr.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "auth_new_token_not_work_error": "\u00c9chec de l'authentification \u00e0 l'aide du jeton nouvellement cr\u00e9\u00e9", + "cannot_connect": "Echec de connection" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/hyperion/translations/it.json b/homeassistant/components/hyperion/translations/it.json index ff3170ffb98..6fee49ebe14 100644 --- a/homeassistant/components/hyperion/translations/it.json +++ b/homeassistant/components/hyperion/translations/it.json @@ -8,7 +8,7 @@ "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", - "reauth_successful": "Ri-autenticazione completata con successo" + "reauth_successful": "La riautenticazione ha avuto successo" }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/hyperion/translations/tr.json b/homeassistant/components/hyperion/translations/tr.json index 6f46000e3e2..7b3f9f845a1 100644 --- a/homeassistant/components/hyperion/translations/tr.json +++ b/homeassistant/components/hyperion/translations/tr.json @@ -2,11 +2,17 @@ "config": { "abort": { "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "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" + "no_id": "Hyperion Ambilight \u00f6rne\u011fi kimli\u011fini bildirmedi", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_access_token": "Ge\u00e7ersiz eri\u015fim belirteci" }, "step": { "auth": { @@ -15,6 +21,9 @@ "token": "Veya \u00f6nceden varolan belirte\u00e7 leri sa\u011flay\u0131n" } }, + "confirm": { + "title": "Hyperion Ambilight hizmetinin eklenmesini onaylay\u0131n" + }, "create_token": { "title": "Otomatik olarak yeni kimlik do\u011frulama belirteci olu\u015fturun" }, @@ -23,6 +32,7 @@ }, "user": { "data": { + "host": "Ana Bilgisayar", "port": "Port" } } diff --git a/homeassistant/components/hyperion/translations/uk.json b/homeassistant/components/hyperion/translations/uk.json new file mode 100644 index 00000000000..ae44b0610da --- /dev/null +++ b/homeassistant/components/hyperion/translations/uk.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "auth_new_token_not_granted_error": "\u0421\u0442\u0432\u043e\u0440\u0435\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u043d\u0435 \u0431\u0443\u0432 \u0441\u0445\u0432\u0430\u043b\u0435\u043d\u0438\u0439 \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0456 Hyperion.", + "auth_new_token_not_work_error": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u043f\u0440\u043e\u0439\u0442\u0438 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0437 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f\u043c \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043e\u0433\u043e \u0442\u043e\u043a\u0435\u043d\u0430.", + "auth_required_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438, \u0447\u0438 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "no_id": "Hyperion Ambilight \u043d\u0435 \u043d\u0430\u0434\u0430\u0432 \u0441\u0432\u0456\u0439 \u0456\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_access_token": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443." + }, + "step": { + "auth": { + "data": { + "create_token": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043d\u043e\u0432\u0438\u0439 \u0442\u043e\u043a\u0435\u043d", + "token": "\u0410\u0431\u043e \u043d\u0430\u0434\u0430\u0442\u0438 \u043d\u0430\u044f\u0432\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d" + }, + "description": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0456 Hyperion Ambilight." + }, + "confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 Hyperion Ambilight? \n\n ** \u0425\u043e\u0441\u0442: ** {host}\n ** \u041f\u043e\u0440\u0442: ** {port}\n ** ID **: {id}", + "title": "\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0456\u0442\u044c \u0434\u043e\u0434\u0430\u0432\u0430\u043d\u043d\u044f \u0441\u043b\u0443\u0436\u0431\u0438 Hyperion Ambilight" + }, + "create_token": { + "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438 ** \u043d\u0438\u0436\u0447\u0435, \u0449\u043e\u0431 \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u043d\u043e\u0432\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457. \u0412\u0438 \u0431\u0443\u0434\u0435\u0442\u0435 \u043f\u0435\u0440\u0435\u043d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0456 \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 Hyperion \u0434\u043b\u044f \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f \u0437\u0430\u043f\u0438\u0442\u0443. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u0438\u0439 \u0456\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 - \" {auth_id} \"", + "title": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043d\u043e\u0432\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "create_token_external": { + "title": "\u041f\u0440\u0438\u0439\u043d\u044f\u0442\u0438 \u043d\u043e\u0432\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0456 Hyperion" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "priority": "\u041f\u0440\u0456\u043e\u0440\u0438\u0442\u0435\u0442 Hyperion \u0434\u043b\u044f \u043a\u043e\u043b\u044c\u043e\u0440\u0456\u0432 \u0456 \u0435\u0444\u0435\u043a\u0442\u0456\u0432" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/translations/de.json b/homeassistant/components/iaqualink/translations/de.json index e7e1002015c..0a678baf7ca 100644 --- a/homeassistant/components/iaqualink/translations/de.json +++ b/homeassistant/components/iaqualink/translations/de.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/iaqualink/translations/tr.json b/homeassistant/components/iaqualink/translations/tr.json new file mode 100644 index 00000000000..c2c70f3e45b --- /dev/null +++ b/homeassistant/components/iaqualink/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "L\u00fctfen iAqualink hesab\u0131n\u0131z i\u00e7in kullan\u0131c\u0131 ad\u0131 ve parolay\u0131 girin." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/translations/uk.json b/homeassistant/components/iaqualink/translations/uk.json new file mode 100644 index 00000000000..b855d755726 --- /dev/null +++ b/homeassistant/components/iaqualink/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043b\u043e\u0433\u0456\u043d \u0456 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0412\u0430\u0448\u043e\u0433\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 iAqualink.", + "title": "Jandy iAqualink" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/de.json b/homeassistant/components/icloud/translations/de.json index e7441792d91..64a6bcd885c 100644 --- a/homeassistant/components/icloud/translations/de.json +++ b/homeassistant/components/icloud/translations/de.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Konto bereits konfiguriert", - "no_device": "Auf keinem Ihrer Ger\u00e4te ist \"Find my iPhone\" aktiviert" + "already_configured": "Konto wurde bereits konfiguriert", + "no_device": "Auf keinem Ihrer Ger\u00e4te ist \"Find my iPhone\" aktiviert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", "send_verification_code": "Fehler beim Senden des Best\u00e4tigungscodes", "validate_verification_code": "Verifizierung des Verifizierungscodes fehlgeschlagen. W\u00e4hle ein vertrauensw\u00fcrdiges Ger\u00e4t aus und starte die Verifizierung erneut" }, @@ -12,7 +14,8 @@ "reauth": { "data": { "password": "Passwort" - } + }, + "title": "Integration erneut authentifizieren" }, "trusted_device": { "data": { diff --git a/homeassistant/components/icloud/translations/tr.json b/homeassistant/components/icloud/translations/tr.json index 3d74852ce50..86581625d96 100644 --- a/homeassistant/components/icloud/translations/tr.json +++ b/homeassistant/components/icloud/translations/tr.json @@ -1,7 +1,28 @@ { "config": { "abort": { - "no_device": "Hi\u00e7bir cihaz\u0131n\u0131zda \"iPhone'umu bul\" etkin de\u011fil" + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "no_device": "Hi\u00e7bir cihaz\u0131n\u0131zda \"iPhone'umu bul\" etkin de\u011fil", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "validate_verification_code": "Do\u011frulama kodunuzu do\u011frulamay\u0131 ba\u015faramad\u0131n\u0131z, bir g\u00fcven ayg\u0131t\u0131 se\u00e7in ve do\u011frulamay\u0131 yeniden ba\u015flat\u0131n" + }, + "step": { + "reauth": { + "data": { + "password": "Parola" + }, + "description": "{username} i\u00e7in \u00f6nceden girdi\u011finiz \u015fifreniz art\u0131k \u00e7al\u0131\u015fm\u0131yor. Bu entegrasyonu kullanmaya devam etmek i\u00e7in \u015fifrenizi g\u00fcncelleyin." + }, + "user": { + "data": { + "password": "Parola", + "username": "E-posta", + "with_family": "Aileyle" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/uk.json b/homeassistant/components/icloud/translations/uk.json new file mode 100644 index 00000000000..ac65157f050 --- /dev/null +++ b/homeassistant/components/icloud/translations/uk.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "no_device": "\u041d\u0430 \u0436\u043e\u0434\u043d\u043e\u043c\u0443 \u0437 \u0412\u0430\u0448\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 \u043d\u0435 \u0430\u043a\u0442\u0438\u0432\u043e\u0432\u0430\u043d\u0430 \u0444\u0443\u043d\u043a\u0446\u0456\u044f \"\u0417\u043d\u0430\u0439\u0442\u0438 iPhone\".", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "send_verification_code": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u0438\u0442\u0438 \u043a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f.", + "validate_verification_code": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0438\u0442\u0438 \u043a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f, \u0432\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0434\u043e\u0432\u0456\u0440\u0435\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0442\u0430 \u043f\u043e\u0447\u043d\u0456\u0442\u044c \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0443 \u0437\u043d\u043e\u0432\u0443." + }, + "step": { + "reauth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0420\u0430\u043d\u0456\u0448\u0435 \u0432\u0432\u0435\u0434\u0435\u043d\u0438\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username} \u0431\u0456\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u0440\u0430\u0446\u044e\u0454. \u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c, \u0449\u043e\u0431 \u043f\u0440\u043e\u0434\u043e\u0432\u0436\u0438\u0442\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0446\u044e \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e" + }, + "trusted_device": { + "data": { + "trusted_device": "\u0414\u043e\u0432\u0456\u0440\u0435\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0434\u043e\u0432\u0456\u0440\u0435\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439", + "title": "\u0414\u043e\u0432\u0456\u0440\u0435\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 iCloud" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438", + "with_family": "\u0417 \u0441\u0456\u043c'\u0454\u044e" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0412\u0430\u0448\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456.", + "title": "\u041e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456 iCloud" + }, + "verification_code": { + "data": { + "verification_code": "\u041a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f, \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u0438\u0439 \u0432\u0456\u0434 iCloud", + "title": "\u041a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f iCloud" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/translations/de.json b/homeassistant/components/ifttt/translations/de.json index c96928afa18..5184e89f29a 100644 --- a/homeassistant/components/ifttt/translations/de.json +++ b/homeassistant/components/ifttt/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", + "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." + }, "create_entry": { "default": "Um Ereignisse an Home Assistant zu senden, musst du die Aktion \"Eine Webanforderung erstellen\" aus dem [IFTTT Webhook Applet]({applet_url}) ausw\u00e4hlen.\n\nF\u00fclle folgende Informationen aus: \n- URL: `{webhook_url}`\n- Methode: POST\n- Inhaltstyp: application/json\n\nIn der Dokumentation ({docs_url}) findest du Informationen zur Konfiguration der Automation eingehender Daten." }, diff --git a/homeassistant/components/ifttt/translations/tr.json b/homeassistant/components/ifttt/translations/tr.json new file mode 100644 index 00000000000..84adcdf8225 --- /dev/null +++ b/homeassistant/components/ifttt/translations/tr.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/translations/uk.json b/homeassistant/components/ifttt/translations/uk.json new file mode 100644 index 00000000000..8ea8f2b1970 --- /dev/null +++ b/homeassistant/components/ifttt/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0434\u0456\u044e \"Make a web request\" \u0437 [IFTTT Webhook applet]({applet_url}). \n\n\u0417\u0430\u043f\u043e\u0432\u043d\u0456\u0442\u044c \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0456\u0439 \u043f\u043e \u043e\u0431\u0440\u043e\u0431\u0446\u0456 \u0434\u0430\u043d\u0438\u0445, \u0449\u043e \u043d\u0430\u0434\u0445\u043e\u0434\u044f\u0442\u044c." + }, + "step": { + "user": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 IFTTT?", + "title": "IFTTT" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/input_boolean/translations/uk.json b/homeassistant/components/input_boolean/translations/uk.json index c677957de47..be22ae53807 100644 --- a/homeassistant/components/input_boolean/translations/uk.json +++ b/homeassistant/components/input_boolean/translations/uk.json @@ -5,5 +5,5 @@ "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" } }, - "title": "\u0412\u0432\u0435\u0434\u0435\u043d\u043d\u044f \u043b\u043e\u0433\u0456\u0447\u043d\u043e\u0433\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f" + "title": "Input Boolean" } \ No newline at end of file diff --git a/homeassistant/components/input_datetime/translations/uk.json b/homeassistant/components/input_datetime/translations/uk.json index bd087e535a5..c0aeb11882f 100644 --- a/homeassistant/components/input_datetime/translations/uk.json +++ b/homeassistant/components/input_datetime/translations/uk.json @@ -1,3 +1,3 @@ { - "title": "\u0412\u0432\u0435\u0434\u0435\u043d\u043d\u044f \u0434\u0430\u0442\u0438" + "title": "Input Datetime" } \ No newline at end of file diff --git a/homeassistant/components/input_number/translations/uk.json b/homeassistant/components/input_number/translations/uk.json index 0e4265d7ca0..e09531134cd 100644 --- a/homeassistant/components/input_number/translations/uk.json +++ b/homeassistant/components/input_number/translations/uk.json @@ -1,3 +1,3 @@ { - "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043d\u043e\u043c\u0435\u0440" + "title": "Input Number" } \ No newline at end of file diff --git a/homeassistant/components/input_select/translations/uk.json b/homeassistant/components/input_select/translations/uk.json index ace44f8d7a7..b33e64fbf48 100644 --- a/homeassistant/components/input_select/translations/uk.json +++ b/homeassistant/components/input_select/translations/uk.json @@ -1,3 +1,3 @@ { - "title": "\u0412\u0438\u0431\u0440\u0430\u0442\u0438" + "title": "Input Select" } \ No newline at end of file diff --git a/homeassistant/components/input_text/translations/uk.json b/homeassistant/components/input_text/translations/uk.json index a80f4325203..84bddfe3e07 100644 --- a/homeassistant/components/input_text/translations/uk.json +++ b/homeassistant/components/input_text/translations/uk.json @@ -1,3 +1,3 @@ { - "title": "\u0412\u0432\u0435\u0434\u0435\u043d\u043d\u044f \u0442\u0435\u043a\u0441\u0442\u0443" + "title": "Input Text" } \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/de.json b/homeassistant/components/insteon/translations/de.json index dfa4f3f7567..6bbc4d5474f 100644 --- a/homeassistant/components/insteon/translations/de.json +++ b/homeassistant/components/insteon/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { "cannot_connect": "Verbindung fehlgeschlagen" @@ -22,6 +23,9 @@ } }, "plm": { + "data": { + "device": "USB-Ger\u00e4te-Pfad" + }, "title": "Insteon PLM" }, "user": { diff --git a/homeassistant/components/insteon/translations/tr.json b/homeassistant/components/insteon/translations/tr.json new file mode 100644 index 00000000000..6c41f53b31e --- /dev/null +++ b/homeassistant/components/insteon/translations/tr.json @@ -0,0 +1,89 @@ +{ + "config": { + "abort": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "hubv1": { + "data": { + "host": "\u0130p Adresi", + "port": "Port" + }, + "title": "Insteon Hub S\u00fcr\u00fcm 1" + }, + "hubv2": { + "data": { + "host": "\u0130p Adresi", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Insteon Hub S\u00fcr\u00fcm 2'yi yap\u0131land\u0131r\u0131n.", + "title": "Insteon Hub S\u00fcr\u00fcm 2" + }, + "plm": { + "description": "Insteon PowerLink Modemini (PLM) yap\u0131land\u0131r\u0131n." + }, + "user": { + "data": { + "modem_type": "Modem t\u00fcr\u00fc." + }, + "description": "Insteon modem tipini se\u00e7in.", + "title": "Insteon" + } + } + }, + "options": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "input_error": "Ge\u00e7ersiz giri\u015fler, l\u00fctfen de\u011ferlerinizi kontrol edin." + }, + "step": { + "add_x10": { + "data": { + "unitcode": "Birim kodu (1-16)" + }, + "description": "Insteon Hub parolas\u0131n\u0131 de\u011fi\u015ftirin.", + "title": "Insteon" + }, + "change_hub_config": { + "data": { + "host": "\u0130p Adresi", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "title": "Insteon" + }, + "init": { + "data": { + "add_override": "Bir cihaz\u0131 ge\u00e7ersiz k\u0131lma ekleyin.", + "add_x10": "Bir X10 cihaz\u0131 ekleyin.", + "change_hub_config": "Hub yap\u0131land\u0131rmas\u0131n\u0131 de\u011fi\u015ftirin.", + "remove_override": "Bir cihaz\u0131 ge\u00e7ersiz k\u0131lma i\u015flemini kald\u0131r\u0131n.", + "remove_x10": "Bir X10 cihaz\u0131n\u0131 \u00e7\u0131kar\u0131n." + }, + "description": "Yap\u0131land\u0131rmak i\u00e7in bir se\u00e7enek se\u00e7in.", + "title": "Insteon" + }, + "remove_override": { + "data": { + "address": "Kald\u0131r\u0131lacak bir cihaz adresi se\u00e7in" + }, + "description": "Bir cihaz\u0131 ge\u00e7ersiz k\u0131lmay\u0131 kald\u0131rma", + "title": "Insteon" + }, + "remove_x10": { + "data": { + "address": "Kald\u0131r\u0131lacak bir cihaz adresi se\u00e7in" + }, + "description": "Bir X10 cihaz\u0131n\u0131 kald\u0131r\u0131n", + "title": "Insteon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/uk.json b/homeassistant/components/insteon/translations/uk.json new file mode 100644 index 00000000000..302d8c3676a --- /dev/null +++ b/homeassistant/components/insteon/translations/uk.json @@ -0,0 +1,109 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "select_single": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u043f\u0446\u0456\u044e." + }, + "step": { + "hubv1": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Insteon Hub \u0432\u0435\u0440\u0441\u0456\u0457 1 (\u0434\u043e 2014 \u0440\u043e\u043a\u0443)", + "title": "Insteon Hub. \u0412\u0435\u0440\u0441\u0456\u044f 1" + }, + "hubv2": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Insteon Hub \u0432\u0435\u0440\u0441\u0456\u0457 2", + "title": "Insteon Hub. \u0412\u0435\u0440\u0441\u0456\u044f 2" + }, + "plm": { + "data": { + "device": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043c\u043e\u0434\u0435\u043c\u0443 Insteon PowerLink (PLM)", + "title": "Insteon PLM" + }, + "user": { + "data": { + "modem_type": "\u0422\u0438\u043f \u043c\u043e\u0434\u0435\u043c\u0443" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0438\u043f \u043c\u043e\u0434\u0435\u043c\u0443 Insteon.", + "title": "Insteon" + } + } + }, + "options": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "input_error": "\u041d\u0435\u0432\u0456\u0440\u043d\u0456 \u0434\u0430\u043d\u0456, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0432\u043a\u0430\u0437\u0430\u043d\u0456 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f.", + "select_single": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u043f\u0446\u0456\u044e." + }, + "step": { + "add_override": { + "data": { + "address": "\u0410\u0434\u0440\u0435\u0441\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: 1a2b3c)", + "cat": "\u041a\u0430\u0442\u0435\u0433\u043e\u0440\u0456\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: 0x10)", + "subcat": "\u041f\u0456\u0434\u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0456\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434, 0x0a)" + }, + "description": "\u041f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", + "title": "Insteon" + }, + "add_x10": { + "data": { + "housecode": "\u041a\u043e\u0434 \u0431\u0443\u0434\u0438\u043d\u043a\u0443 (a - p)", + "platform": "\u041f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430", + "steps": "\u041a\u0440\u043e\u043a \u0434\u0456\u043c\u043c\u0435\u0440\u0430 (\u0442\u0456\u043b\u044c\u043a\u0438 \u0434\u043b\u044f \u043e\u0441\u0432\u0456\u0442\u043b\u044e\u0432\u0430\u043b\u044c\u043d\u0438\u0445 \u043f\u0440\u0438\u043b\u0430\u0434\u0456\u0432, \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c 22)", + "unitcode": "\u042e\u043d\u0456\u0442\u043a\u043e\u0434 (1 - 16)" + }, + "description": "\u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043e Insteon Hub", + "title": "Insteon" + }, + "change_hub_config": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0417\u043c\u0456\u043d\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f Insteon Hub. \u041f\u0456\u0441\u043b\u044f \u0432\u043d\u0435\u0441\u0435\u043d\u043d\u044f \u0446\u0438\u0445 \u0437\u043c\u0456\u043d \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 Home Assistant. \u0426\u0435 \u043d\u0435 \u0437\u043c\u0456\u043d\u044e\u0454 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0441\u0430\u043c\u043e\u0433\u043e \u0445\u0430\u0431\u0430. \u0429\u043e\u0431 \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0445\u0430\u0431\u0430, \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Hub.", + "title": "Insteon" + }, + "init": { + "data": { + "add_override": "\u041f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", + "add_x10": "\u0414\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 X10", + "change_hub_config": "\u0417\u043c\u0456\u043d\u0438\u0442\u0438 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0445\u0430\u0431\u0430", + "remove_override": "\u0412\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u043f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", + "remove_x10": "\u0412\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 X10" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u043f\u0446\u0456\u044e \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f", + "title": "Insteon" + }, + "remove_override": { + "data": { + "address": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e, \u044f\u043a\u0438\u0439 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438" + }, + "description": "\u0412\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u043f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", + "title": "Insteon" + }, + "remove_x10": { + "data": { + "address": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e, \u044f\u043a\u0438\u0439 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438" + }, + "description": "\u0412\u0438\u0434\u0430\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e X10", + "title": "Insteon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/translations/de.json b/homeassistant/components/ios/translations/de.json index e9e592d18c2..bc427bd2992 100644 --- a/homeassistant/components/ios/translations/de.json +++ b/homeassistant/components/ios/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Es wird nur eine Konfiguration von Home Assistant iOS ben\u00f6tigt" + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { "confirm": { diff --git a/homeassistant/components/ios/translations/tr.json b/homeassistant/components/ios/translations/tr.json new file mode 100644 index 00000000000..8de4663957e --- /dev/null +++ b/homeassistant/components/ios/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/translations/uk.json b/homeassistant/components/ios/translations/uk.json new file mode 100644 index 00000000000..5f8d69f5f29 --- /dev/null +++ b/homeassistant/components/ios/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "step": { + "confirm": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/lb.json b/homeassistant/components/ipma/translations/lb.json index 7b2d374b6f5..006d80d3786 100644 --- a/homeassistant/components/ipma/translations/lb.json +++ b/homeassistant/components/ipma/translations/lb.json @@ -15,5 +15,10 @@ "title": "Standuert" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "IPMA API Endpunkt ereechbar" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/tr.json b/homeassistant/components/ipma/translations/tr.json index 488ad379942..a8df63645ab 100644 --- a/homeassistant/components/ipma/translations/tr.json +++ b/homeassistant/components/ipma/translations/tr.json @@ -1,4 +1,15 @@ { + "config": { + "step": { + "user": { + "data": { + "latitude": "Enlem", + "longitude": "Boylam", + "mode": "Mod" + } + } + } + }, "system_health": { "info": { "api_endpoint_reachable": "Ula\u015f\u0131labilir IPMA API u\u00e7 noktas\u0131" diff --git a/homeassistant/components/ipma/translations/uk.json b/homeassistant/components/ipma/translations/uk.json index bb294cc5d21..ee84e7d16f2 100644 --- a/homeassistant/components/ipma/translations/uk.json +++ b/homeassistant/components/ipma/translations/uk.json @@ -1,9 +1,24 @@ { "config": { + "error": { + "name_exists": "\u0426\u044f \u043d\u0430\u0437\u0432\u0430 \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f." + }, "step": { "user": { - "title": "\u0420\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f" + "data": { + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", + "mode": "\u0420\u0435\u0436\u0438\u043c", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u041f\u043e\u0440\u0442\u0443\u0433\u0430\u043b\u044c\u0441\u044c\u043a\u0438\u0439 \u0456\u043d\u0441\u0442\u0438\u0442\u0443\u0442 \u043c\u043e\u0440\u044f \u0442\u0430 \u0430\u0442\u043c\u043e\u0441\u0444\u0435\u0440\u0438.", + "title": "\u041c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e API IPMA" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/de.json b/homeassistant/components/ipp/translations/de.json index 73dd3f69bcc..69402c8fdba 100644 --- a/homeassistant/components/ipp/translations/de.json +++ b/homeassistant/components/ipp/translations/de.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", "connection_upgrade": "Verbindung zum Drucker fehlgeschlagen, da ein Verbindungsupgrade erforderlich ist.", "ipp_error": "IPP-Fehler festgestellt.", "ipp_version_error": "IPP-Version wird vom Drucker nicht unterst\u00fctzt.", @@ -9,6 +10,7 @@ "unique_id_required": "Ger\u00e4t fehlt die f\u00fcr die Entdeckung erforderliche eindeutige Identifizierung." }, "error": { + "cannot_connect": "Verbindung fehlgeschlagen", "connection_upgrade": "Verbindung zum Drucker fehlgeschlagen. Bitte versuchen Sie es erneut mit aktivierter SSL / TLS-Option." }, "flow_title": "Drucker: {name}", @@ -18,14 +20,14 @@ "base_path": "Relativer Pfad zum Drucker", "host": "Host", "port": "Port", - "ssl": "Der Drucker unterst\u00fctzt die Kommunikation \u00fcber SSL / TLS", - "verify_ssl": "Der Drucker verwendet ein ordnungsgem\u00e4\u00dfes SSL-Zertifikat" + "ssl": "Verwendet ein SSL-Zertifikat", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, "description": "Richten Sie Ihren Drucker \u00fcber das Internet Printing Protocol (IPP) f\u00fcr die Integration in Home Assistant ein.", "title": "Verbinden Sie Ihren Drucker" }, "zeroconf_confirm": { - "description": "M\u00f6chten Sie den Drucker mit dem Namen \"{name}\" zu Home Assistant hinzuf\u00fcgen?", + "description": "M\u00f6chtest du {name} einrichten?", "title": "Entdeckter Drucker" } } diff --git a/homeassistant/components/ipp/translations/tr.json b/homeassistant/components/ipp/translations/tr.json index dbb14fe825e..78b9a868bd2 100644 --- a/homeassistant/components/ipp/translations/tr.json +++ b/homeassistant/components/ipp/translations/tr.json @@ -1,7 +1,24 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "connection_upgrade": "Yaz\u0131c\u0131ya ba\u011flan\u0131lamad\u0131. L\u00fctfen SSL / TLS se\u00e7ene\u011fi i\u015faretli olarak tekrar deneyin." + }, + "flow_title": "Yaz\u0131c\u0131: {name}", "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + }, + "title": "Yaz\u0131c\u0131n\u0131z\u0131 ba\u011flay\u0131n" + }, "zeroconf_confirm": { + "description": "{name} kurmak istiyor musunuz?", "title": "Ke\u015ffedilen yaz\u0131c\u0131" } } diff --git a/homeassistant/components/ipp/translations/uk.json b/homeassistant/components/ipp/translations/uk.json new file mode 100644 index 00000000000..bb6df07f1e4 --- /dev/null +++ b/homeassistant/components/ipp/translations/uk.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "connection_upgrade": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u043e\u043c \u0447\u0435\u0440\u0435\u0437 \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0456\u0441\u0442\u044c \u043f\u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f.", + "ipp_error": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 IPP.", + "ipp_version_error": "\u0412\u0435\u0440\u0441\u0456\u044f IPP \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u043e\u043c.", + "parse_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0440\u043e\u0437\u0456\u0431\u0440\u0430\u0442\u0438 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u044c \u0432\u0456\u0434 \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430.", + "unique_id_required": "\u041d\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u0432\u0456\u0434\u0441\u0443\u0442\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044f, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0430 \u0434\u043b\u044f \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "connection_upgrade": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u043e\u043c. \u0421\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0447\u0435\u0440\u0435\u0437 SSL / TLS." + }, + "flow_title": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440: {name}", + "step": { + "user": { + "data": { + "base_path": "\u0412\u0456\u0434\u043d\u043e\u0441\u043d\u0438\u0439 \u0448\u043b\u044f\u0445 \u0434\u043e \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430", + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430 \u043f\u043e \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 IPP.", + "title": "Internet Printing Protocol (IPP)" + }, + "zeroconf_confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u0438\u043d\u0442\u0435\u0440 `{name}`?", + "title": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u043d\u0442\u0435\u0440" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/translations/tr.json b/homeassistant/components/iqvia/translations/tr.json new file mode 100644 index 00000000000..717f6d72b94 --- /dev/null +++ b/homeassistant/components/iqvia/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/translations/uk.json b/homeassistant/components/iqvia/translations/uk.json new file mode 100644 index 00000000000..ab9813d6289 --- /dev/null +++ b/homeassistant/components/iqvia/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "invalid_zip_code": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043f\u043e\u0448\u0442\u043e\u0432\u0438\u0439 \u0456\u043d\u0434\u0435\u043a\u0441." + }, + "step": { + "user": { + "data": { + "zip_code": "\u041f\u043e\u0448\u0442\u043e\u0432\u0438\u0439 \u0456\u043d\u0434\u0435\u043a\u0441" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0441\u0432\u0456\u0439 \u043f\u043e\u0448\u0442\u043e\u0432\u0438\u0439 \u0456\u043d\u0434\u0435\u043a\u0441 (\u0434\u043b\u044f \u0421\u0428\u0410 \u0430\u0431\u043e \u041a\u0430\u043d\u0430\u0434\u0438).", + "title": "IQVIA" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/translations/de.json b/homeassistant/components/islamic_prayer_times/translations/de.json index af38303c9a2..b06137bdb0e 100644 --- a/homeassistant/components/islamic_prayer_times/translations/de.json +++ b/homeassistant/components/islamic_prayer_times/translations/de.json @@ -1,3 +1,8 @@ { + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + } + }, "title": "Islamische Gebetszeiten" } \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/translations/tr.json b/homeassistant/components/islamic_prayer_times/translations/tr.json new file mode 100644 index 00000000000..a152eb19468 --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/translations/uk.json b/homeassistant/components/islamic_prayer_times/translations/uk.json new file mode 100644 index 00000000000..9290114899a --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/translations/uk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "step": { + "user": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 \u0440\u043e\u0437\u043a\u043b\u0430\u0434 \u0447\u0430\u0441\u0443 \u043d\u0430\u043c\u0430\u0437\u0443?", + "title": "\u0427\u0430\u0441 \u043d\u0430\u043c\u0430\u0437\u0443" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "calculation_method": "\u0421\u043f\u043e\u0441\u0456\u0431 \u0440\u043e\u0437\u0440\u0430\u0445\u0443\u043d\u043a\u0443" + } + } + } + }, + "title": "\u0427\u0430\u0441 \u043d\u0430\u043c\u0430\u0437\u0443" +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/de.json b/homeassistant/components/isy994/translations/de.json index 99d11e5d6c9..18d6a1603c4 100644 --- a/homeassistant/components/isy994/translations/de.json +++ b/homeassistant/components/isy994/translations/de.json @@ -18,7 +18,7 @@ "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" + "title": "Stelle eine Verbindung zu deinem ISY994 her" } } }, diff --git a/homeassistant/components/isy994/translations/tr.json b/homeassistant/components/isy994/translations/tr.json new file mode 100644 index 00000000000..d1423202fe0 --- /dev/null +++ b/homeassistant/components/isy994/translations/tr.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "URL", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "variable_sensor_string": "De\u011fi\u015fken Sens\u00f6r Dizesi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/uk.json b/homeassistant/components/isy994/translations/uk.json new file mode 100644 index 00000000000..c874b8654f5 --- /dev/null +++ b/homeassistant/components/isy994/translations/uk.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "invalid_host": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043c\u0430\u0454 \u0431\u0443\u0442\u0438 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'address[:port]' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: 'http://192.168.10.100:80').", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "Universal Devices ISY994 {name} ({host})", + "step": { + "user": { + "data": { + "host": "URL-\u0430\u0434\u0440\u0435\u0441\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "tls": "\u0412\u0435\u0440\u0441\u0456\u044f TLS \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0412\u043a\u0430\u0436\u0456\u0442\u044c URL-\u0430\u0434\u0440\u0435\u0441\u0443 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'address[:port]' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: 'http://192.168.10.100:80').", + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ignore_string": "\u0406\u0433\u043d\u043e\u0440\u0443\u0432\u0430\u0442\u0438", + "restore_light_state": "\u0412\u0456\u0434\u043d\u043e\u0432\u043b\u044e\u0432\u0430\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c \u0441\u0432\u0456\u0442\u043b\u0430", + "sensor_string": "\u0406\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0432\u0443\u0437\u043e\u043b \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440", + "variable_sensor_string": "\u0406\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0437\u043c\u0456\u043d\u043d\u0443 \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440" + }, + "description": "\u041e\u043f\u0438\u0441 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432:\n \u2022 \u0406\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0432\u0443\u0437\u043e\u043b \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440: \u0431\u0443\u0434\u044c-\u044f\u043a\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0430\u0431\u043e \u043f\u0430\u043f\u043a\u0430, \u0432 \u0456\u043c\u0435\u043d\u0456 \u044f\u043a\u043e\u0457 \u043c\u0456\u0441\u0442\u0438\u0442\u044c\u0441\u044f \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u0440\u044f\u0434\u043e\u043a, \u0431\u0443\u0434\u0435 \u0456\u043c\u043f\u043e\u0440\u0442\u043e\u0432\u0430\u043d\u043e \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440 \u0430\u0431\u043e \u0431\u0456\u043d\u0430\u0440\u043d\u0438\u0439 \u0441\u0435\u043d\u0441\u043e\u0440.\n \u2022 \u0406\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0437\u043c\u0456\u043d\u043d\u0443 \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440: \u0431\u0443\u0434\u044c-\u044f\u043a\u0430 \u0437\u043c\u0456\u043d\u043d\u0430, \u044f\u043a\u0430 \u043c\u0456\u0441\u0442\u0438\u0442\u044c \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u0440\u044f\u0434\u043e\u043a, \u0431\u0443\u0434\u0435 \u0456\u043c\u043f\u043e\u0440\u0442\u043e\u0432\u0430\u043d\u0430 \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440.\n \u2022 \u0406\u0433\u043d\u043e\u0440\u0443\u0432\u0430\u0442\u0438: \u0431\u0443\u0434\u044c-\u044f\u043a\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u0432 \u0456\u043c\u0435\u043d\u0456 \u044f\u043a\u043e\u0433\u043e \u043c\u0456\u0441\u0442\u0438\u0442\u044c\u0441\u044f \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u0440\u044f\u0434\u043e\u043a, \u0431\u0443\u0434\u0435 \u0456\u0433\u043d\u043e\u0440\u0443\u0432\u0430\u0442\u0438\u0441\u044f.\n \u2022 \u0412\u0456\u0434\u043d\u043e\u0432\u043b\u044e\u0432\u0430\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c \u0441\u0432\u0456\u0442\u043b\u0430: \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u0456 \u043e\u0441\u0432\u0456\u0442\u043b\u0435\u043d\u043d\u044f \u0431\u0443\u0434\u0435 \u0432\u0456\u0434\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u044f\u0441\u043a\u0440\u0430\u0432\u043e\u0441\u0442\u0456, \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0435 \u0434\u043e \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f ISY994" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/translations/de.json b/homeassistant/components/izone/translations/de.json index ea59cc39b27..f6e03c3af27 100644 --- a/homeassistant/components/izone/translations/de.json +++ b/homeassistant/components/izone/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Es wurden keine iZone-Ger\u00e4te im Netzwerk gefunden.", - "single_instance_allowed": "Es ist nur eine einzige Konfiguration von iZone erforderlich." + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { "confirm": { diff --git a/homeassistant/components/izone/translations/tr.json b/homeassistant/components/izone/translations/tr.json new file mode 100644 index 00000000000..faa20ed0ece --- /dev/null +++ b/homeassistant/components/izone/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "\u0130Zone'u kurmak istiyor musunuz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/translations/uk.json b/homeassistant/components/izone/translations/uk.json new file mode 100644 index 00000000000..8ab6c1e1664 --- /dev/null +++ b/homeassistant/components/izone/translations/uk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "step": { + "confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 iZone?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/de.json b/homeassistant/components/juicenet/translations/de.json index 16f48ef3837..7a6b5cff541 100644 --- a/homeassistant/components/juicenet/translations/de.json +++ b/homeassistant/components/juicenet/translations/de.json @@ -1,20 +1,20 @@ { "config": { "abort": { - "already_configured": "Dieses JuiceNet-Konto ist bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { "user": { "data": { - "api_token": "JuiceNet API Token" + "api_token": "API-Token" }, - "description": "Sie ben\u00f6tigen das API-Token von https://home.juice.net/Manage.", - "title": "Stellen Sie eine Verbindung zu JuiceNet her" + "description": "Du ben\u00f6tigst das API-Token von https://home.juice.net/Manage.", + "title": "Stelle eine Verbindung zu JuiceNet her" } } } diff --git a/homeassistant/components/juicenet/translations/tr.json b/homeassistant/components/juicenet/translations/tr.json new file mode 100644 index 00000000000..53890eb41e2 --- /dev/null +++ b/homeassistant/components/juicenet/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "api_token": "API Belirteci" + }, + "description": "API Belirtecine https://home.juice.net/Manage adresinden ihtiyac\u0131n\u0131z olacak.", + "title": "JuiceNet'e ba\u011flan\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/uk.json b/homeassistant/components/juicenet/translations/uk.json new file mode 100644 index 00000000000..903ea5f6e74 --- /dev/null +++ b/homeassistant/components/juicenet/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_token": "\u0422\u043e\u043a\u0435\u043d API" + }, + "description": "\u0414\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0442\u0440\u0456\u0431\u0435\u043d \u0442\u043e\u043a\u0435\u043d API \u0437 \u0441\u0430\u0439\u0442\u0443 https://home.juice.net/Manage.", + "title": "JuiceNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/de.json b/homeassistant/components/kodi/translations/de.json index a0bf05cb5ec..1d229e5a428 100644 --- a/homeassistant/components/kodi/translations/de.json +++ b/homeassistant/components/kodi/translations/de.json @@ -1,10 +1,14 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "flow_title": "Kodi: {name}", diff --git a/homeassistant/components/kodi/translations/tr.json b/homeassistant/components/kodi/translations/tr.json new file mode 100644 index 00000000000..54ad8e0b6fd --- /dev/null +++ b/homeassistant/components/kodi/translations/tr.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "credentials": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "L\u00fctfen Kodi kullan\u0131c\u0131 ad\u0131n\u0131z\u0131 ve \u015fifrenizi girin. Bunlar Sistem / Ayarlar / A\u011f / Hizmetler'de bulunabilir." + }, + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + }, + "ws_port": { + "data": { + "ws_port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/uk.json b/homeassistant/components/kodi/translations/uk.json new file mode 100644 index 00000000000..d2acde5dffb --- /dev/null +++ b/homeassistant/components/kodi/translations/uk.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "no_uuid": "\u0423 \u0434\u0430\u043d\u043e\u0433\u043e \u0435\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0430 Kodi \u043d\u0435\u043c\u0430\u0454 \u0443\u043d\u0456\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0456\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440\u0430. \u0406\u043c\u043e\u0432\u0456\u0440\u043d\u043e, \u0446\u0435 \u043f\u043e\u0432'\u044f\u0437\u0430\u043d\u043e \u0437 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f\u043c \u0441\u0442\u0430\u0440\u043e\u0457 \u0432\u0435\u0440\u0441\u0456\u0457 Kodi (17.x \u0430\u0431\u043e \u043d\u0438\u0436\u0447\u0435). \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e \u0432\u0440\u0443\u0447\u043d\u0443 \u0430\u0431\u043e \u043f\u0435\u0440\u0435\u0439\u0442\u0438 \u043d\u0430 \u043d\u043e\u0432\u0456\u0448\u0443 \u0432\u0435\u0440\u0441\u0456\u044e Kodi.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "Kodi: {name}", + "step": { + "credentials": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0456 \u043f\u0430\u0440\u043e\u043b\u044c Kodi. \u0407\u0445 \u043c\u043e\u0436\u043d\u0430 \u0437\u043d\u0430\u0439\u0442\u0438, \u043f\u0435\u0440\u0435\u0439\u0448\u043e\u0432\u0448\u0438 \u0432 \"\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\" - \"\u0421\u043b\u0443\u0436\u0431\u0438\" - \"\u0423\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f\"." + }, + "discovery_confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 Kodi (`{name}`)?", + "title": "Kodi" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL" + }, + "description": "\u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e \"\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0432\u0456\u0434\u0434\u0430\u043b\u0435\u043d\u0435 \u0443\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f \u043f\u043e HTTP\" \u0432 \u0440\u043e\u0437\u0434\u0456\u043b\u0456 \"\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\" - \"\u0421\u043b\u0443\u0436\u0431\u0438\" - \"\u0423\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f\"." + }, + "ws_port": { + "data": { + "ws_port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043f\u043e WebSocket. \u0429\u043e\u0431 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0447\u0435\u0440\u0435\u0437 WebSocket, \u0412\u0430\u043c \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f \u0434\u043e\u0434\u0430\u0442\u043a\u0430\u043c\u0438 \u0432 \u0440\u043e\u0437\u0434\u0456\u043b\u0456 \"\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\" - \"\u0421\u043b\u0443\u0436\u0431\u0438\" - \"\u0423\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f\". \u042f\u043a\u0449\u043e WebSocket \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439, \u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_off": "\u0437\u0430\u043f\u0438\u0442\u0430\u043d\u043e \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043d\u044f {entity_name}", + "turn_on": "\u0437\u0430\u043f\u0438\u0442\u0430\u043d\u043e \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u044f {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/de.json b/homeassistant/components/konnected/translations/de.json index ad2ed659522..2ec1657990b 100644 --- a/homeassistant/components/konnected/translations/de.json +++ b/homeassistant/components/konnected/translations/de.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "already_in_progress": "Der Konfigurationsfluss f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt.", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "not_konn_panel": "Kein anerkanntes Konnected.io-Ger\u00e4t", - "unknown": "Unbekannter Fehler ist aufgetreten" + "unknown": "Unerwarteter Fehler" }, "error": { "cannot_connect": "Es konnte keine Verbindung zu einem Konnected-Panel unter {host}:{port} hergestellt werden." @@ -43,7 +43,7 @@ "name": "Name (optional)", "type": "Bin\u00e4rer Sensortyp" }, - "description": "Bitte w\u00e4hlen Sie die Optionen f\u00fcr den an {zone} angeschlossenen Bin\u00e4rsensor", + "description": "Bitte w\u00e4hle die Optionen f\u00fcr den an {zone} angeschlossenen Bin\u00e4rsensor", "title": "Konfigurieren Sie den Bin\u00e4rsensor" }, "options_digital": { @@ -52,7 +52,7 @@ "poll_interval": "Abfrageintervall (Minuten) (optional)", "type": "Sensortyp" }, - "description": "Bitte w\u00e4hlen Sie die Optionen f\u00fcr den an {zone} angeschlossenen digitalen Sensor aus", + "description": "Bitte w\u00e4hle die Optionen f\u00fcr den an {zone} angeschlossenen digitalen Sensor aus", "title": "Konfigurieren Sie den digitalen Sensor" }, "options_io": { @@ -98,9 +98,9 @@ "more_states": "Konfigurieren Sie zus\u00e4tzliche Zust\u00e4nde f\u00fcr diese Zone", "name": "Name (optional)", "pause": "Pause zwischen Impulsen (ms) (optional)", - "repeat": "Zeit zum Wiederholen (-1 = unendlich) (optional)" + "repeat": "Mal wiederholen (-1 = unendlich) (optional)" }, - "description": "Bitte w\u00e4hlen Sie die Ausgabeoptionen f\u00fcr {zone} : Status {state}", + "description": "Bitte w\u00e4hlen die Ausgabeoptionen f\u00fcr {zone} : Status {state}", "title": "Konfigurieren Sie den schaltbaren Ausgang" } } diff --git a/homeassistant/components/konnected/translations/pl.json b/homeassistant/components/konnected/translations/pl.json index ee6c10cbdd8..f6e9a2dbfbc 100644 --- a/homeassistant/components/konnected/translations/pl.json +++ b/homeassistant/components/konnected/translations/pl.json @@ -45,7 +45,7 @@ "name": "Nazwa (opcjonalnie)", "type": "Typ sensora binarnego" }, - "description": "Wybierz opcje dla sensora binarnego powi\u0105zanego ze {zone}", + "description": "Opcje {zone}", "title": "Konfiguracja sensora binarnego" }, "options_digital": { @@ -54,7 +54,7 @@ "poll_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (minuty) (opcjonalnie)", "type": "Typ sensora" }, - "description": "Wybierz opcje dla cyfrowego sensora powi\u0105zanego ze {zone}", + "description": "Opcje {zone}", "title": "Konfiguracja sensora cyfrowego" }, "options_io": { diff --git a/homeassistant/components/konnected/translations/tr.json b/homeassistant/components/konnected/translations/tr.json new file mode 100644 index 00000000000..a0e759903bd --- /dev/null +++ b/homeassistant/components/konnected/translations/tr.json @@ -0,0 +1,60 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "\u0130p Adresi", + "port": "Port" + } + } + } + }, + "options": { + "error": { + "bad_host": "Ge\u00e7ersiz, Ge\u00e7ersiz K\u0131lma API ana makine url'si" + }, + "step": { + "options_binary": { + "data": { + "inverse": "A\u00e7\u0131k / kapal\u0131 durumunu tersine \u00e7evirin" + } + }, + "options_io": { + "data": { + "3": "B\u00f6lge 3", + "4": "B\u00f6lge 4", + "5": "B\u00f6lge 5", + "6": "B\u00f6lge 6", + "7": "B\u00f6lge 7", + "out": "OUT" + } + }, + "options_io_ext": { + "data": { + "10": "B\u00f6lge 10", + "11": "B\u00f6lge 11", + "12": "B\u00f6lge 12", + "8": "B\u00f6lge 8", + "9": "B\u00f6lge 9", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2/ALARM2", + "out1": "OUT1" + } + }, + "options_misc": { + "data": { + "api_host": "API ana makine URL'sini ge\u00e7ersiz k\u0131l (iste\u011fe ba\u011fl\u0131)", + "override_api_host": "Varsay\u0131lan Home Assistant API ana bilgisayar paneli URL'sini ge\u00e7ersiz k\u0131l" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/uk.json b/homeassistant/components/konnected/translations/uk.json new file mode 100644 index 00000000000..92cd3744d94 --- /dev/null +++ b/homeassistant/components/konnected/translations/uk.json @@ -0,0 +1,108 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "not_konn_panel": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Konnected.io \u043d\u0435 \u0440\u043e\u0437\u043f\u0456\u0437\u043d\u0430\u043d\u043e.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "confirm": { + "description": "\u041c\u043e\u0434\u0435\u043b\u044c: {model}\nID: {id}\n\u0425\u043e\u0441\u0442: {host}\n\u041f\u043e\u0440\u0442: {port} \n\n\u0417\u043c\u0456\u043d\u0430 \u043b\u043e\u0433\u0456\u043a\u0438 \u0440\u043e\u0431\u043e\u0442\u0438 \u043f\u0430\u043d\u0435\u043b\u0456, \u0430 \u0442\u0430\u043a\u043e\u0436 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u0432\u0445\u043e\u0434\u0456\u0432 \u0456 \u0432\u0438\u0445\u043e\u0434\u0456\u0432 \u0432\u0438\u043a\u043e\u043d\u0443\u0454\u0442\u044c\u0441\u044f \u0432 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u0445 \u043f\u0430\u043d\u0435\u043b\u0456 \u0441\u0438\u0433\u043d\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u0457 Konnected.", + "title": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Konnected \u0433\u043e\u0442\u043e\u0432\u0456\u0439 \u0434\u043e \u0440\u043e\u0431\u043e\u0442\u0438." + }, + "import_confirm": { + "description": "\u041f\u0430\u043d\u0435\u043b\u044c \u0441\u0438\u0433\u043d\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u0457 Konnected ID {id} \u0440\u0430\u043d\u0456\u0448\u0435 \u0432\u0436\u0435 \u0431\u0443\u043b\u0430 \u0434\u043e\u0434\u0430\u043d\u0430 \u0447\u0435\u0440\u0435\u0437 configuration.yaml. \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0456\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0446\u0435\u0439 \u0437\u0430\u043f\u0438\u0441 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0434\u0430\u043d\u043e\u0433\u043e \u043f\u043e\u0441\u0456\u0431\u043d\u0438\u043a\u0430 \u0437 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f.", + "title": "\u0406\u043c\u043f\u043e\u0440\u0442 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Konnected" + }, + "user": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0430\u043d\u0435\u043b\u0456 Konnected." + } + } + }, + "options": { + "abort": { + "not_konn_panel": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Konnected.io \u043d\u0435 \u0440\u043e\u0437\u043f\u0456\u0437\u043d\u0430\u043d\u043e." + }, + "error": { + "bad_host": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 URL \u043f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0445\u043e\u0441\u0442\u0430 API." + }, + "step": { + "options_binary": { + "data": { + "inverse": "\u0406\u043d\u0432\u0435\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u0438\u0439/\u0437\u0430\u043a\u0440\u0438\u0442\u0438\u0439 \u0441\u0442\u0430\u043d", + "name": "\u041d\u0430\u0437\u0432\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "type": "\u0422\u0438\u043f \u0431\u0456\u043d\u0430\u0440\u043d\u043e\u0433\u043e \u0441\u0435\u043d\u0441\u043e\u0440\u0430" + }, + "description": "\u041e\u043f\u0446\u0456\u0457 {zone}", + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0431\u0456\u043d\u0430\u0440\u043d\u043e\u0433\u043e \u0441\u0435\u043d\u0441\u043e\u0440\u0430" + }, + "options_digital": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "poll_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432 \u0445\u0432\u0438\u043b\u0438\u043d\u0430\u0445 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "type": "\u0422\u0438\u043f \u0441\u0435\u043d\u0441\u043e\u0440\u0430" + }, + "description": "\u041e\u043f\u0446\u0456\u0457 {zone}", + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430" + }, + "options_io": { + "data": { + "1": "\u0417\u043e\u043d\u0430 1", + "2": "\u0417\u043e\u043d\u0430 2", + "3": "\u0417\u043e\u043d\u0430 3", + "4": "\u0417\u043e\u043d\u0430 4", + "5": "\u0417\u043e\u043d\u0430 5", + "6": "\u0417\u043e\u043d\u0430 6", + "7": "\u0417\u043e\u043d\u0430 7", + "out": "\u0412\u0418\u0425\u0406\u0414" + }, + "description": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 {model} \u0437 \u0430\u0434\u0440\u0435\u0441\u043e\u044e {host}. \u0417\u0430\u043b\u0435\u0436\u043d\u043e \u0432\u0456\u0434 \u043e\u0431\u0440\u0430\u043d\u043e\u0457 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457 \u0432\u0445\u043e\u0434\u0456\u0432 / \u0432\u0438\u0445\u043e\u0434\u0456\u0432, \u0434\u043e \u043f\u0430\u043d\u0435\u043b\u0456 \u043c\u043e\u0436\u0443\u0442\u044c \u0431\u0443\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0456 \u0431\u0456\u043d\u0430\u0440\u043d\u0456 \u0441\u0435\u043d\u0441\u043e\u0440\u0438 (\u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u0442\u044f / \u0437\u0430\u043a\u0440\u0438\u0442\u0442\u044f), \u0446\u0438\u0444\u0440\u043e\u0432\u0456 \u0441\u0435\u043d\u0441\u043e\u0440\u0438 (dht \u0456 ds18b20) \u0430\u0431\u043e \u043f\u0435\u0440\u0435\u043c\u0438\u043a\u0430\u044e\u0447\u0456 \u0432\u0438\u0445\u043e\u0434\u0438. \u0411\u0456\u043b\u044c\u0448 \u0434\u0435\u0442\u0430\u043b\u044c\u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0431\u0443\u0434\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0435 \u043d\u0430 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u043a\u0440\u043e\u043a\u0430\u0445.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0445\u043e\u0434\u0456\u0432 / \u0432\u0438\u0445\u043e\u0434\u0456\u0432" + }, + "options_io_ext": { + "data": { + "10": "\u0417\u043e\u043d\u0430 10", + "11": "\u0417\u043e\u043d\u0430 11", + "12": "\u0417\u043e\u043d\u0430 12", + "8": "\u0417\u043e\u043d\u0430 8", + "9": "\u0417\u043e\u043d\u0430 9", + "alarm1": "\u0422\u0420\u0418\u0412\u041e\u0413\u04101", + "alarm2_out2": "\u0412\u0418\u0425\u0406\u04142 / \u0422\u0420\u0418\u0412\u041e\u0413\u04102", + "out1": "\u0412\u0418\u0425\u0406\u04141" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0440\u0435\u0448\u0442\u0438 \u0432\u0445\u043e\u0434\u0456\u0432 / \u0432\u0438\u0445\u043e\u0434\u0456\u0432. \u0411\u0456\u043b\u044c\u0448 \u0434\u0435\u0442\u0430\u043b\u044c\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0431\u0443\u0434\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u043d\u0430 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u043a\u0440\u043e\u043a\u0430\u0445.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0438\u0445 \u0432\u0445\u043e\u0434\u0456\u0432 / \u0432\u0438\u0445\u043e\u0434\u0456\u0432" + }, + "options_misc": { + "data": { + "api_host": "\u041f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438 URL \u0445\u043e\u0441\u0442\u0430 API (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "blink": "LED-\u0456\u043d\u0434\u0438\u043a\u0430\u0446\u0456\u044f \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 \u043f\u0440\u0438 \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u0446\u0456 \u0441\u0442\u0430\u043d\u0443", + "discovery": "\u0412\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u0430\u0442\u0438 \u043d\u0430 \u0437\u0430\u043f\u0438\u0442\u0438 \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f \u0443 \u0412\u0430\u0448\u0456\u0439 \u043c\u0435\u0440\u0435\u0436\u0456", + "override_api_host": "\u041f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438 URL-\u0430\u0434\u0440\u0435\u0441\u0443 \u0445\u043e\u0441\u0442-\u043f\u0430\u043d\u0435\u043b\u0456 Home Assistant API" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0431\u0430\u0436\u0430\u043d\u0443 \u043f\u043e\u0432\u0435\u0434\u0456\u043d\u043a\u0443 \u0434\u043b\u044f \u0412\u0430\u0448\u043e\u0457 \u043f\u0430\u043d\u0435\u043b\u0456.", + "title": "\u0406\u043d\u0448\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f" + }, + "options_switch": { + "data": { + "activation": "\u0412\u0438\u0445\u0456\u0434 \u043f\u0440\u0438 \u0432\u043c\u0438\u043a\u0430\u043d\u043d\u0456", + "momentary": "\u0422\u0440\u0438\u0432\u0430\u043b\u0456\u0441\u0442\u044c \u0456\u043c\u043f\u0443\u043b\u044c\u0441\u0443 (\u043c\u0441) (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "more_states": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u0441\u0442\u0430\u043d\u0438 \u0434\u043b\u044f \u0446\u0456\u0454\u0457 \u0437\u043e\u043d\u0438", + "name": "\u041d\u0430\u0437\u0432\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "pause": "\u041f\u0430\u0443\u0437\u0430 \u043c\u0456\u0436 \u0456\u043c\u043f\u0443\u043b\u044c\u0441\u0430\u043c\u0438 (\u043c\u0441) (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "repeat": "\u041a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u0435\u043d\u044c (-1 = \u043d\u0435\u0441\u043a\u0456\u043d\u0447\u0435\u043d\u043d\u043e) (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)" + }, + "description": "{zone}: \u0441\u0442\u0430\u043d {state}", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0435\u0440\u0435\u043c\u0438\u043a\u0430\u044e\u0447\u043e\u0433\u043e \u0432\u0438\u0445\u043e\u0434\u0443" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/de.json b/homeassistant/components/kulersky/translations/de.json index 3fc69f85947..96ed09a974f 100644 --- a/homeassistant/components/kulersky/translations/de.json +++ b/homeassistant/components/kulersky/translations/de.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Wollen Sie mit der Einrichtung beginnen?" + "description": "M\u00f6chtest du mit der Einrichtung beginnen?" } } } diff --git a/homeassistant/components/kulersky/translations/lb.json b/homeassistant/components/kulersky/translations/lb.json new file mode 100644 index 00000000000..4ea09574c0b --- /dev/null +++ b/homeassistant/components/kulersky/translations/lb.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keng Apparater am Netzwierk fonnt", + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." + }, + "step": { + "confirm": { + "description": "Soll den Ariichtungs Prozess gestart ginn?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/tr.json b/homeassistant/components/kulersky/translations/tr.json index 49fa9545e94..3df15466f03 100644 --- a/homeassistant/components/kulersky/translations/tr.json +++ b/homeassistant/components/kulersky/translations/tr.json @@ -1,7 +1,13 @@ { "config": { "abort": { - "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/uk.json b/homeassistant/components/kulersky/translations/uk.json new file mode 100644 index 00000000000..292861e9129 --- /dev/null +++ b/homeassistant/components/kulersky/translations/uk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "step": { + "confirm": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/de.json b/homeassistant/components/life360/translations/de.json index 731ebdceef7..7e495987b45 100644 --- a/homeassistant/components/life360/translations/de.json +++ b/homeassistant/components/life360/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "create_entry": { @@ -8,6 +9,7 @@ }, "error": { "already_configured": "Konto ist bereits konfiguriert", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "invalid_username": "Ung\u00fcltiger Benutzername", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/life360/translations/fr.json b/homeassistant/components/life360/translations/fr.json index 72f56ed8784..cb86d8c6590 100644 --- a/homeassistant/components/life360/translations/fr.json +++ b/homeassistant/components/life360/translations/fr.json @@ -8,7 +8,7 @@ "default": "Pour d\u00e9finir les options avanc\u00e9es, voir [Documentation de Life360]( {docs_url} )." }, "error": { - "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9", + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "invalid_auth": "Authentification invalide", "invalid_username": "Nom d'utilisateur invalide", "unknown": "Erreur inattendue" diff --git a/homeassistant/components/life360/translations/tr.json b/homeassistant/components/life360/translations/tr.json index 3f923c096cd..e1e57b39737 100644 --- a/homeassistant/components/life360/translations/tr.json +++ b/homeassistant/components/life360/translations/tr.json @@ -1,11 +1,22 @@ { "config": { "abort": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmedik hata" }, "error": { "already_configured": "Hesap zaten konfig\u00fcre edilmi\u015fi durumda", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_username": "Ge\u00e7ersiz kullan\u0131c\u0131 ad\u0131", "unknown": "Beklenmedik hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/uk.json b/homeassistant/components/life360/translations/uk.json new file mode 100644 index 00000000000..caecf494388 --- /dev/null +++ b/homeassistant/components/life360/translations/uk.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "create_entry": { + "default": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u043e\u0437\u0448\u0438\u0440\u0435\u043d\u0438\u0445 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u044c." + }, + "error": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "invalid_username": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0456\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u043e\u0437\u0448\u0438\u0440\u0435\u043d\u0438\u0445 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u044c. \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0437\u0440\u043e\u0431\u0438\u0442\u0438 \u0446\u0435 \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", + "title": "Life360" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/de.json b/homeassistant/components/lifx/translations/de.json index f88e27ff168..83eded1ddc6 100644 --- a/homeassistant/components/lifx/translations/de.json +++ b/homeassistant/components/lifx/translations/de.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Keine LIFX Ger\u00e4te im Netzwerk gefunden.", - "single_instance_allowed": "Nur eine einzige Konfiguration von LIFX ist zul\u00e4ssig." + "no_devices_found": "Keine LIFX Ger\u00e4te im Netzwerk gefunden", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { "confirm": { diff --git a/homeassistant/components/lifx/translations/tr.json b/homeassistant/components/lifx/translations/tr.json new file mode 100644 index 00000000000..fc7532a1e34 --- /dev/null +++ b/homeassistant/components/lifx/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "LIFX'i kurmak istiyor musunuz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/uk.json b/homeassistant/components/lifx/translations/uk.json new file mode 100644 index 00000000000..8c32e79533d --- /dev/null +++ b/homeassistant/components/lifx/translations/uk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "step": { + "confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 LIFX?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/translations/uk.json b/homeassistant/components/light/translations/uk.json index 67685889c54..86eee7d6b23 100644 --- a/homeassistant/components/light/translations/uk.json +++ b/homeassistant/components/light/translations/uk.json @@ -1,5 +1,17 @@ { "device_automation": { + "action_type": { + "brightness_decrease": "{entity_name}: \u0437\u043c\u0435\u043d\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c", + "brightness_increase": "{entity_name}: \u0437\u0431\u0456\u043b\u044c\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c", + "flash": "{entity_name}: \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043c\u0438\u0433\u0430\u043d\u043d\u044f", + "toggle": "{entity_name}: \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u0438", + "turn_off": "{entity_name}: \u0432\u0438\u043c\u043a\u043d\u0443\u0442\u0438", + "turn_on": "{entity_name}: \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438" + }, + "condition_type": { + "is_off": "{entity_name} \u0443 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456" + }, "trigger_type": { "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e", "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" diff --git a/homeassistant/components/local_ip/translations/de.json b/homeassistant/components/local_ip/translations/de.json index 072f6ec964d..9e2a6eda5c6 100644 --- a/homeassistant/components/local_ip/translations/de.json +++ b/homeassistant/components/local_ip/translations/de.json @@ -1,13 +1,14 @@ { "config": { "abort": { - "single_instance_allowed": "Es ist nur eine einzige Konfiguration der lokalen IP zul\u00e4ssig." + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { "user": { "data": { "name": "Sensorname" }, + "description": "M\u00f6chtest du mit der Einrichtung beginnen?", "title": "Lokale IP-Adresse" } } diff --git a/homeassistant/components/local_ip/translations/es.json b/homeassistant/components/local_ip/translations/es.json index fe9a0ad1414..a3048d396d5 100644 --- a/homeassistant/components/local_ip/translations/es.json +++ b/homeassistant/components/local_ip/translations/es.json @@ -8,7 +8,7 @@ "data": { "name": "Nombre del sensor" }, - "description": "\u00bfQuieres empezar a configurar?", + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?", "title": "Direcci\u00f3n IP local" } } diff --git a/homeassistant/components/local_ip/translations/tr.json b/homeassistant/components/local_ip/translations/tr.json new file mode 100644 index 00000000000..e8e82814f8a --- /dev/null +++ b/homeassistant/components/local_ip/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "user": { + "data": { + "name": "Sens\u00f6r Ad\u0131" + }, + "description": "Kuruluma ba\u015flamak ister misiniz?", + "title": "Yerel IP Adresi" + } + } + }, + "title": "Yerel IP Adresi" +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/translations/uk.json b/homeassistant/components/local_ip/translations/uk.json new file mode 100644 index 00000000000..b88c1c002bf --- /dev/null +++ b/homeassistant/components/local_ip/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?", + "title": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u0430 IP-\u0430\u0434\u0440\u0435\u0441\u0430" + } + } + }, + "title": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u0430 IP-\u0430\u0434\u0440\u0435\u0441\u0430" +} \ No newline at end of file diff --git a/homeassistant/components/locative/translations/de.json b/homeassistant/components/locative/translations/de.json index 32617094146..a6dcf4150d0 100644 --- a/homeassistant/components/locative/translations/de.json +++ b/homeassistant/components/locative/translations/de.json @@ -1,11 +1,15 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", + "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." + }, "create_entry": { "default": "Um Standorte Home Assistant zu senden, muss das Webhook Feature in der Locative App konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})." }, "step": { "user": { - "description": "M\u00f6chtest du den Locative Webhook wirklich einrichten?", + "description": "M\u00f6chtest du mit der Einrichtung beginnen?", "title": "Locative Webhook einrichten" } } diff --git a/homeassistant/components/locative/translations/tr.json b/homeassistant/components/locative/translations/tr.json new file mode 100644 index 00000000000..84adcdf8225 --- /dev/null +++ b/homeassistant/components/locative/translations/tr.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/translations/uk.json b/homeassistant/components/locative/translations/uk.json new file mode 100644 index 00000000000..d9a47130871 --- /dev/null +++ b/homeassistant/components/locative/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f Locative. \n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457." + }, + "step": { + "user": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?", + "title": "Locative" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/translations/pt.json b/homeassistant/components/lock/translations/pt.json index 5ba9f10db14..44f30900572 100644 --- a/homeassistant/components/lock/translations/pt.json +++ b/homeassistant/components/lock/translations/pt.json @@ -5,6 +5,9 @@ "open": "Abrir {entity_name}", "unlock": "Desbloquear {entity_name}" }, + "condition_type": { + "is_unlocked": "{entity_name} est\u00e1 destrancado" + }, "trigger_type": { "locked": "{entity_name} fechada", "unlocked": "{entity_name} aberta" diff --git a/homeassistant/components/lock/translations/tr.json b/homeassistant/components/lock/translations/tr.json index 95b50398fda..ea6ff1a157d 100644 --- a/homeassistant/components/lock/translations/tr.json +++ b/homeassistant/components/lock/translations/tr.json @@ -1,4 +1,10 @@ { + "device_automation": { + "trigger_type": { + "locked": "{entity_name} kilitlendi", + "unlocked": "{entity_name} kilidi a\u00e7\u0131ld\u0131" + } + }, "state": { "_": { "locked": "Kilitli", diff --git a/homeassistant/components/lock/translations/uk.json b/homeassistant/components/lock/translations/uk.json index d919252eb56..96b92012e9d 100644 --- a/homeassistant/components/lock/translations/uk.json +++ b/homeassistant/components/lock/translations/uk.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "lock": "{entity_name}: \u0437\u0430\u0431\u043b\u043e\u043a\u0443\u0432\u0430\u0442\u0438", + "open": "{entity_name}: \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u0438", + "unlock": "{entity_name}: \u0440\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0432\u0430\u0442\u0438" + }, + "condition_type": { + "is_locked": "{entity_name} \u0432 \u0437\u0430\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_unlocked": "{entity_name} \u0432 \u0440\u043e\u0437\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456" + }, + "trigger_type": { + "locked": "{entity_name} \u0431\u043b\u043e\u043a\u0443\u0454\u0442\u044c\u0441\u044f", + "unlocked": "{entity_name} \u0440\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0454\u0442\u044c\u0441\u044f" + } + }, "state": { "_": { "locked": "\u0417\u0430\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e", diff --git a/homeassistant/components/logi_circle/translations/de.json b/homeassistant/components/logi_circle/translations/de.json index ab4a194fda0..1eec1d3c4a5 100644 --- a/homeassistant/components/logi_circle/translations/de.json +++ b/homeassistant/components/logi_circle/translations/de.json @@ -1,11 +1,15 @@ { "config": { "abort": { + "already_configured": "Konto wurde bereits konfiguriert", "external_error": "Es ist eine Ausnahme in einem anderen Flow aufgetreten.", - "external_setup": "Logi Circle wurde erfolgreich aus einem anderen Flow konfiguriert." + "external_setup": "Logi Circle wurde erfolgreich aus einem anderen Flow konfiguriert.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen." }, "error": { - "follow_link": "Bitte folge dem Link und authentifiziere dich, bevor du auf Senden klickst." + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "follow_link": "Bitte folge dem Link und authentifiziere dich, bevor du auf Senden klickst.", + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { "auth": { diff --git a/homeassistant/components/logi_circle/translations/fr.json b/homeassistant/components/logi_circle/translations/fr.json index 7ac388ccb3f..6bd22f473e7 100644 --- a/homeassistant/components/logi_circle/translations/fr.json +++ b/homeassistant/components/logi_circle/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9", + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "external_error": "Une exception est survenue \u00e0 partir d'un autre flux.", "external_setup": "Logi Circle a \u00e9t\u00e9 configur\u00e9 avec succ\u00e8s \u00e0 partir d'un autre flux.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation." diff --git a/homeassistant/components/logi_circle/translations/lb.json b/homeassistant/components/logi_circle/translations/lb.json index fab157b2655..82be2f6a82d 100644 --- a/homeassistant/components/logi_circle/translations/lb.json +++ b/homeassistant/components/logi_circle/translations/lb.json @@ -7,6 +7,7 @@ "missing_configuration": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun." }, "error": { + "authorize_url_timeout": "Z\u00e4itiwwerschreidung beim erstellen vun der Authorisatiouns URL.", "follow_link": "Follegt w.e.g dem Link an authentifiz\u00e9iert iech ier de op Ofsch\u00e9cken dr\u00e9ckt.", "invalid_auth": "Ong\u00eblteg Authentifikatioun" }, diff --git a/homeassistant/components/logi_circle/translations/tr.json b/homeassistant/components/logi_circle/translations/tr.json new file mode 100644 index 00000000000..0b0f58116c2 --- /dev/null +++ b/homeassistant/components/logi_circle/translations/tr.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/translations/uk.json b/homeassistant/components/logi_circle/translations/uk.json new file mode 100644 index 00000000000..2c021992413 --- /dev/null +++ b/homeassistant/components/logi_circle/translations/uk.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "external_error": "\u0412\u0438\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0432\u0456\u0434\u0431\u0443\u043b\u043e\u0441\u044f \u0437 \u0456\u043d\u0448\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0443.", + "external_setup": "Logi Circle \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439 \u0437 \u0456\u043d\u0448\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0443.", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438." + }, + "error": { + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "follow_link": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0437\u0430 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c \u0456 \u043f\u0440\u043e\u0439\u0434\u0456\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e, \u043f\u0435\u0440\u0448 \u043d\u0456\u0436 \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0438 \"\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438\".", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "auth": { + "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043f\u043e [\u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c]({authorization_url}) \u0456 ** \u0414\u043e\u0437\u0432\u043e\u043b\u044c\u0442\u0435 ** \u0434\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0432\u0430\u0448\u043e\u0433\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 Logi Circle, \u043f\u043e\u0442\u0456\u043c \u043f\u043e\u0432\u0435\u0440\u043d\u0456\u0442\u044c\u0441\u044f \u0441\u044e\u0434\u0438 \u0456 \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **.", + "title": "Logi Circle" + }, + "user": { + "data": { + "flow_impl": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457, \u0447\u0435\u0440\u0435\u0437 \u044f\u043a\u0438\u0439 \u0431\u0443\u0434\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0438\u0439 \u0432\u0445\u0456\u0434.", + "title": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/de.json b/homeassistant/components/lovelace/translations/de.json index c8680fcb7e5..b6c7562f0ec 100644 --- a/homeassistant/components/lovelace/translations/de.json +++ b/homeassistant/components/lovelace/translations/de.json @@ -1,6 +1,9 @@ { "system_health": { "info": { + "dashboards": "Dashboards", + "mode": "Modus", + "resources": "Ressourcen", "views": "Ansichten" } } diff --git a/homeassistant/components/lovelace/translations/fr.json b/homeassistant/components/lovelace/translations/fr.json new file mode 100644 index 00000000000..f2847bcc177 --- /dev/null +++ b/homeassistant/components/lovelace/translations/fr.json @@ -0,0 +1,10 @@ +{ + "system_health": { + "info": { + "dashboards": "Tableaux de bord", + "mode": "Mode", + "resources": "Ressources", + "views": "Vues" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/tr.json b/homeassistant/components/lovelace/translations/tr.json index 9f763d0d6cc..d159e058ffa 100644 --- a/homeassistant/components/lovelace/translations/tr.json +++ b/homeassistant/components/lovelace/translations/tr.json @@ -3,7 +3,8 @@ "info": { "dashboards": "Kontrol panelleri", "mode": "Mod", - "resources": "Kaynaklar" + "resources": "Kaynaklar", + "views": "G\u00f6r\u00fcn\u00fcmler" } } } \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/uk.json b/homeassistant/components/lovelace/translations/uk.json new file mode 100644 index 00000000000..21d97fd14c3 --- /dev/null +++ b/homeassistant/components/lovelace/translations/uk.json @@ -0,0 +1,10 @@ +{ + "system_health": { + "info": { + "dashboards": "\u041f\u0430\u043d\u0435\u043b\u0456", + "mode": "\u0420\u0435\u0436\u0438\u043c", + "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u0438", + "views": "\u0412\u043a\u043b\u0430\u0434\u043a\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/translations/de.json b/homeassistant/components/luftdaten/translations/de.json index 122dc611870..499a65623b0 100644 --- a/homeassistant/components/luftdaten/translations/de.json +++ b/homeassistant/components/luftdaten/translations/de.json @@ -1,6 +1,7 @@ { "config": { "error": { + "already_configured": "Der Dienst ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen", "invalid_sensor": "Sensor nicht verf\u00fcgbar oder ung\u00fcltig" }, diff --git a/homeassistant/components/luftdaten/translations/tr.json b/homeassistant/components/luftdaten/translations/tr.json new file mode 100644 index 00000000000..04565de3d28 --- /dev/null +++ b/homeassistant/components/luftdaten/translations/tr.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/translations/uk.json b/homeassistant/components/luftdaten/translations/uk.json new file mode 100644 index 00000000000..9fd33dc3da2 --- /dev/null +++ b/homeassistant/components/luftdaten/translations/uk.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_sensor": "\u0421\u0435\u043d\u0441\u043e\u0440 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0430\u0431\u043e \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439." + }, + "step": { + "user": { + "data": { + "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u043d\u0430 \u043c\u0430\u043f\u0456", + "station_id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 Luftdaten" + }, + "title": "Luftdaten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/ca.json b/homeassistant/components/lutron_caseta/translations/ca.json index c3b0e686cc4..5f2cc5d4087 100644 --- a/homeassistant/components/lutron_caseta/translations/ca.json +++ b/homeassistant/components/lutron_caseta/translations/ca.json @@ -2,16 +2,75 @@ "config": { "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", - "cannot_connect": "Ha fallat la connexi\u00f3" + "cannot_connect": "Ha fallat la connexi\u00f3", + "not_lutron_device": "El dispositiu descobert no \u00e9s un dispositiu Lutron" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, + "flow_title": "Lutron Cas\u00e9ta {name} ({host})", "step": { "import_failed": { "description": "No s'ha pogut configurar l'enlla\u00e7 (amfitri\u00f3: {host}) importat de configuration.yaml.", "title": "No s'ha pogut importar la configuraci\u00f3 de l'enlla\u00e7 de Cas\u00e9ta." + }, + "link": { + "description": "Per a vincular amb {name} ({host}), despr\u00e9s d'enviar aquest formulari, prem el bot\u00f3 negre de la part posterior de l'enlla\u00e7.", + "title": "Vinculaci\u00f3 amb enlla\u00e7" + }, + "user": { + "data": { + "host": "Amfitri\u00f3" + }, + "description": "Introdueix l'adre\u00e7a IP del dispositiu.", + "title": "Connexi\u00f3 autom\u00e0tica amb l'enlla\u00e7" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Primer bot\u00f3", + "button_2": "Segon bot\u00f3", + "button_3": "Tercer bot\u00f3", + "button_4": "Quart bot\u00f3", + "close_1": "Tanca 1", + "close_2": "Tanca 2", + "close_3": "Tanca 3", + "close_4": "Tanca 4", + "close_all": "Tanca-ho tot", + "group_1_button_1": "Primer bot\u00f3 del primer grup", + "group_1_button_2": "Segon bot\u00f3 del primer grup", + "group_2_button_1": "Primer bot\u00f3 del segon grup", + "group_2_button_2": "Segon bot\u00f3 del segon grup", + "lower": "Baixa", + "lower_1": "Baixa 1", + "lower_2": "Baixa 2", + "lower_3": "Baixa 3", + "lower_4": "Baixa 4", + "lower_all": "Baixa-ho tot", + "off": "OFF", + "on": "ON", + "open_1": "Obre 1", + "open_2": "Obre 2", + "open_3": "Obre 3", + "open_4": "Obre 4", + "open_all": "Obre-ho tot", + "raise": "Puja", + "raise_1": "Puja 1", + "raise_2": "Puja 2", + "raise_3": "Puja 3", + "raise_4": "Puja 4", + "raise_all": "Puja-ho tot", + "stop": "Atura (preferit)", + "stop_1": "Atura 1", + "stop_2": "Atura 2", + "stop_3": "Atura 3", + "stop_4": "Atura 4", + "stop_all": "Atura-ho tot" + }, + "trigger_type": { + "press": "\"{subtype}\" premut", + "release": "\"{subtype}\" alliberat" + } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/cs.json b/homeassistant/components/lutron_caseta/translations/cs.json index 60fa7fddced..4ccfa17e6d3 100644 --- a/homeassistant/components/lutron_caseta/translations/cs.json +++ b/homeassistant/components/lutron_caseta/translations/cs.json @@ -6,6 +6,13 @@ }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/es.json b/homeassistant/components/lutron_caseta/translations/es.json index cfd8551bab9..37b1a0d9072 100644 --- a/homeassistant/components/lutron_caseta/translations/es.json +++ b/homeassistant/components/lutron_caseta/translations/es.json @@ -2,16 +2,50 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "cannot_connect": "No se pudo conectar" + "cannot_connect": "No se pudo conectar", + "not_lutron_device": "El dispositivo descubierto no es un dispositivo de Lutron" }, "error": { "cannot_connect": "No se pudo conectar" }, + "flow_title": "Lutron Cas\u00e9ta {name} ({host})", "step": { "import_failed": { "description": "No se puede configurar bridge (host: {host}) importado desde configuration.yaml.", "title": "Error al importar la configuraci\u00f3n del bridge Cas\u00e9ta." + }, + "link": { + "description": "Para emparejar con {name} ({host}), despu\u00e9s de enviar este formulario, presione el bot\u00f3n negro en la parte posterior del puente.", + "title": "Emparejar con el puente" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Introduzca la direcci\u00f3n ip del dispositivo.", + "title": "Conectar autom\u00e1ticamente con el dispositivo" } } + }, + "device_automation": { + "trigger_subtype": { + "open_1": "Abrir 1", + "open_2": "Abrir 2", + "open_3": "Abrir 3", + "open_4": "Abrir 4", + "open_all": "Abrir todo", + "raise": "Levantar", + "raise_1": "Levantar 1", + "raise_2": "Levantar 2", + "raise_3": "Levantar 3", + "raise_4": "Levantar 4", + "raise_all": "Levantar todo", + "stop": "Detener (favorito)", + "stop_1": "Detener 1", + "stop_2": "Detener 2", + "stop_3": "Detener 3", + "stop_4": "Detener 4", + "stop_all": "Detener todo" + } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/et.json b/homeassistant/components/lutron_caseta/translations/et.json index ed352c7bcc4..81fee6d5b4a 100644 --- a/homeassistant/components/lutron_caseta/translations/et.json +++ b/homeassistant/components/lutron_caseta/translations/et.json @@ -2,16 +2,75 @@ "config": { "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", - "cannot_connect": "\u00dchendamine nurjus" + "cannot_connect": "\u00dchendamine nurjus", + "not_lutron_device": "Avastatud seade ei ole Lutroni seade" }, "error": { "cannot_connect": "\u00dchendamine nurjus" }, + "flow_title": "Lutron Cas\u00e9ta {name} ( {host} )", "step": { "import_failed": { "description": "Silla (host: {host} ) seadistamine configuration.yaml kirje teabest nurjus.", "title": "Cas\u00e9ta Bridge seadete importimine nurjus." + }, + "link": { + "description": "{name} ({host}) sidumiseks vajuta p\u00e4rast selle vormi esitamist silla tagak\u00fcljel olevat musta nuppu.", + "title": "Sillaga sidumine" + }, + "user": { + "data": { + "host": "" + }, + "description": "Sisesta seadme IP-aadress.", + "title": "\u00dchendu sillaga automaatselt" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Esimene nupp", + "button_2": "Teine nupp", + "button_3": "Kolmas nupp", + "button_4": "Neljas nupp", + "close_1": "Sule #1", + "close_2": "Sule #2", + "close_3": "Sule #3", + "close_4": "Sule #4", + "close_all": "Sulge k\u00f5ik", + "group_1_button_1": "Esimese r\u00fchma esimene nupp", + "group_1_button_2": "Esimene r\u00fchma teine nupp", + "group_2_button_1": "Teise r\u00fchma esimene nupp", + "group_2_button_2": "Teise r\u00fchma teine nupp", + "lower": "Langeta", + "lower_1": "Langeta #1", + "lower_2": "Langeta #2", + "lower_3": "Langeta #3", + "lower_4": "Langeta #4", + "lower_all": "Langeta k\u00f5ik", + "off": "V\u00e4ljas", + "on": "Sees", + "open_1": "Ava #1", + "open_2": "Ava #2", + "open_3": "Ava #3", + "open_4": "Ava #4", + "open_all": "Ava k\u00f5ik", + "raise": "T\u00f5sta", + "raise_1": "T\u00f5sta #1", + "raise_2": "T\u00f5sta #2", + "raise_3": "T\u00f5sta #3", + "raise_4": "T\u00f5sta #4", + "raise_all": "T\u00f5sta k\u00f5ik", + "stop": "Peata lemmikasendis", + "stop_1": "Peata #1", + "stop_2": "Peata #2", + "stop_3": "Peata #3", + "stop_4": "Peata #4", + "stop_all": "Peata k\u00f5ik" + }, + "trigger_type": { + "press": "vajutati \" {subtype} \"", + "release": "\" {subtype} \" vabastati" + } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/it.json b/homeassistant/components/lutron_caseta/translations/it.json index 5bdcf87607d..d1b3b754812 100644 --- a/homeassistant/components/lutron_caseta/translations/it.json +++ b/homeassistant/components/lutron_caseta/translations/it.json @@ -2,16 +2,75 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "cannot_connect": "Impossibile connettersi" + "cannot_connect": "Impossibile connettersi", + "not_lutron_device": "Il dispositivo rilevato non \u00e8 un dispositivo Lutron" }, "error": { "cannot_connect": "Impossibile connettersi" }, + "flow_title": "Lutron Cas\u00e9ta {name} ({host})", "step": { "import_failed": { "description": "Impossibile impostare il bridge (host: {host}) importato da configuration.yaml.", "title": "Impossibile importare la configurazione del bridge Cas\u00e9ta." + }, + "link": { + "description": "Per eseguire l'associazione con {name} ({host}), dopo aver inviato questo modulo, premere il pulsante nero sul retro del bridge.", + "title": "Associa con il bridge" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Immettere l'indirizzo IP del dispositivo.", + "title": "Connetti automaticamente al bridge" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Primo pulsante", + "button_2": "Secondo pulsante", + "button_3": "Terzo pulsante", + "button_4": "Quarto pulsante", + "close_1": "Chiudi 1", + "close_2": "Chiudi 2", + "close_3": "Chiudi 3", + "close_4": "Chiudi 4", + "close_all": "Chiudi tutti", + "group_1_button_1": "Primo Gruppo primo pulsante", + "group_1_button_2": "Primo Gruppo secondo pulsante", + "group_2_button_1": "Secondo Gruppo primo pulsante", + "group_2_button_2": "Secondo Gruppo secondo pulsante", + "lower": "Abbassa", + "lower_1": "Abbassa 1", + "lower_2": "Abbassa 2", + "lower_3": "Abbassa 3", + "lower_4": "Abbassa 4", + "lower_all": "Abbassa tutti", + "off": "Spento", + "on": "Acceso", + "open_1": "Apri 1", + "open_2": "Apri 2", + "open_3": "Apri 3", + "open_4": "Apri 4", + "open_all": "Apri tutti", + "raise": "Alza", + "raise_1": "Alza 1", + "raise_2": "Alza 2", + "raise_3": "Alza 3", + "raise_4": "Alza 4", + "raise_all": "Alza tutti", + "stop": "Ferma (preferito)", + "stop_1": "Ferma 1", + "stop_2": "Ferma 2", + "stop_3": "Ferma 3", + "stop_4": "Ferma 4", + "stop_all": "Fermare tutti" + }, + "trigger_type": { + "press": "\"{subtype}\" premuto", + "release": "\"{subtype}\" rilasciato" + } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/no.json b/homeassistant/components/lutron_caseta/translations/no.json index 7afac9c51a5..477370100af 100644 --- a/homeassistant/components/lutron_caseta/translations/no.json +++ b/homeassistant/components/lutron_caseta/translations/no.json @@ -2,16 +2,75 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "cannot_connect": "Tilkobling mislyktes" + "cannot_connect": "Tilkobling mislyktes", + "not_lutron_device": "Oppdaget enhet er ikke en Lutron-enhet" }, "error": { "cannot_connect": "Tilkobling mislyktes" }, + "flow_title": "Lutron Cas\u00e9ta {name} ({host})", "step": { "import_failed": { "description": "Kunne ikke konfigurere bridge (host: {host} ) importert fra configuration.yaml.", "title": "Kan ikke importere Cas\u00e9ta bridge-konfigurasjon." + }, + "link": { + "description": "Hvis du vil pare med {name} ({host}), trykker du den svarte knappen p\u00e5 baksiden av broen etter at du har sendt dette skjemaet.", + "title": "Par med broen" + }, + "user": { + "data": { + "host": "Vert" + }, + "description": "Skriv inn ip-adressen til enheten.", + "title": "Koble automatisk til broen" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "F\u00f8rste knapp", + "button_2": "Andre knapp", + "button_3": "Tredje knapp", + "button_4": "Fjerde knapp", + "close_1": "Lukk 1", + "close_2": "Lukk 2", + "close_3": "Lukk 3", + "close_4": "Lukk 4", + "close_all": "Lukk alle", + "group_1_button_1": "F\u00f8rste gruppe f\u00f8rste knapp", + "group_1_button_2": "F\u00f8rste gruppe andre knapp", + "group_2_button_1": "Andre gruppe f\u00f8rste knapp", + "group_2_button_2": "Andre gruppeknapp", + "lower": "Senk", + "lower_1": "Senk 1", + "lower_2": "Senk 2", + "lower_3": "Senk 3", + "lower_4": "Senk 4", + "lower_all": "Senk alle", + "off": "Av", + "on": "P\u00e5", + "open_1": "\u00c5pne 1", + "open_2": "\u00c5pne 2", + "open_3": "\u00c5pne 3", + "open_4": "\u00c5pne 4", + "open_all": "\u00c5pne alle", + "raise": "Hev", + "raise_1": "Hev 1", + "raise_2": "Hev 2", + "raise_3": "Hev 3", + "raise_4": "Hev 4", + "raise_all": "Hev alle", + "stop": "Stopp (favoritt)", + "stop_1": "Stopp 1", + "stop_2": "Stopp 2", + "stop_3": "Stopp 3", + "stop_4": "Stopp 4", + "stop_all": "Stopp alle" + }, + "trigger_type": { + "press": "\"{subtype}\" trykket", + "release": "\"{subtype}\" utgitt" + } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/pl.json b/homeassistant/components/lutron_caseta/translations/pl.json index 07417b0149e..8a8c0a759b0 100644 --- a/homeassistant/components/lutron_caseta/translations/pl.json +++ b/homeassistant/components/lutron_caseta/translations/pl.json @@ -2,16 +2,75 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "not_lutron_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem Lutron" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, + "flow_title": "Lutron Cas\u00e9ta {name} ({host})", "step": { "import_failed": { "description": "Nie mo\u017cna skonfigurowa\u0107 mostka (host: {host}) zaimportowanego z pliku configuration.yaml.", "title": "Nie uda\u0142o si\u0119 zaimportowa\u0107 konfiguracji mostka Cas\u00e9ta." + }, + "link": { + "description": "Aby sparowa\u0107 z {name} ({host}), po przes\u0142aniu tego formularza naci\u015bnij czarny przycisk z ty\u0142u mostka.", + "title": "Sparuj z mostkiem" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "description": "Wprowad\u017a adres IP urz\u0105dzenia", + "title": "Po\u0142\u0105cz si\u0119 automatycznie z mostkiem" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "pierwszy", + "button_2": "drugi", + "button_3": "trzeci", + "button_4": "czwarty", + "close_1": "zamknij 1", + "close_2": "zamknij 2", + "close_3": "zamknij 3", + "close_4": "zamknij 4", + "close_all": "zamknij wszystkie", + "group_1_button_1": "pierwsza grupa pierwszy przycisk", + "group_1_button_2": "pierwsza grupa drugi przycisk", + "group_2_button_1": "druga grupa pierwszy przycisk", + "group_2_button_2": "druga grupa drugi przycisk", + "lower": "opu\u015b\u0107", + "lower_1": "opu\u015b\u0107 1", + "lower_2": "opu\u015b\u0107 2", + "lower_3": "opu\u015b\u0107 3", + "lower_4": "opu\u015b\u0107 4", + "lower_all": "opu\u015b\u0107 wszystkie", + "off": "wy\u0142\u0105cz", + "on": "w\u0142\u0105cz", + "open_1": "otw\u00f3rz 1", + "open_2": "otw\u00f3rz 2", + "open_3": "otw\u00f3rz 3", + "open_4": "otw\u00f3rz 4", + "open_all": "otw\u00f3rz wszystkie", + "raise": "podnie\u015b", + "raise_1": "podnie\u015b 1", + "raise_2": "podnie\u015b 2", + "raise_3": "podnie\u015b 3", + "raise_4": "podnie\u015b 4", + "raise_all": "podnie\u015b wszystkie", + "stop": "zatrzymaj (ulubione)", + "stop_1": "zatrzymaj 1", + "stop_2": "zatrzymaj 2", + "stop_3": "zatrzymaj 3", + "stop_4": "zatrzymaj 4", + "stop_all": "zatrzymaj wszystkie" + }, + "trigger_type": { + "press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", + "release": "przycisk \"{subtype}\" zostanie zwolniony" + } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/ru.json b/homeassistant/components/lutron_caseta/translations/ru.json index 05bd4f51c70..edda7af8e9a 100644 --- a/homeassistant/components/lutron_caseta/translations/ru.json +++ b/homeassistant/components/lutron_caseta/translations/ru.json @@ -2,16 +2,56 @@ "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.", - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "not_lutron_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 Lutron." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, + "flow_title": "Lutron Cas\u00e9ta {name} ({host})", "step": { "import_failed": { "description": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0448\u043b\u044e\u0437 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml (\u0445\u043e\u0441\u0442: {host}).", "title": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0448\u043b\u044e\u0437\u0430." + }, + "link": { + "description": "\u0427\u0442\u043e\u0431\u044b \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 {name} ({host}), \u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u044d\u0442\u043e\u0439 \u0444\u043e\u0440\u043c\u044b \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u0447\u0435\u0440\u043d\u0443\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0437\u0430\u0434\u043d\u0435\u0439 \u0441\u0442\u043e\u0440\u043e\u043d\u0435 \u0448\u043b\u044e\u0437\u0430.", + "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441\u043e \u0448\u043b\u044e\u0437\u043e\u043c" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", + "title": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "\u041f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "close_1": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c 1", + "close_2": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c 2", + "close_3": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c 3", + "close_4": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c 4", + "close_all": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c \u0432\u0441\u0435", + "group_1_button_1": "\u041f\u0435\u0440\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "group_1_button_2": "\u041f\u0435\u0440\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u0432\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "group_2_button_1": "\u0412\u0442\u043e\u0440\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "group_2_button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u0432\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "stop": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c (\u043b\u044e\u0431\u0438\u043c\u0430\u044f)", + "stop_1": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c 1", + "stop_2": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c 2", + "stop_3": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c 3", + "stop_4": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c 4", + "stop_all": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0432\u0441\u0435" + }, + "trigger_type": { + "press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", + "release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f" + } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/tr.json b/homeassistant/components/lutron_caseta/translations/tr.json new file mode 100644 index 00000000000..fdc5e71a7ac --- /dev/null +++ b/homeassistant/components/lutron_caseta/translations/tr.json @@ -0,0 +1,72 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "not_lutron_device": "Bulunan cihaz bir Lutron cihaz\u0131 de\u011fil" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "flow_title": "Lutron Cas\u00e9ta {name} ( {host} )", + "step": { + "link": { + "description": "{name} ( {host} ) ile e\u015fle\u015ftirmek i\u00e7in, bu formu g\u00f6nderdikten sonra k\u00f6pr\u00fcn\u00fcn arkas\u0131ndaki siyah d\u00fc\u011fmeye bas\u0131n.", + "title": "K\u00f6pr\u00fc ile e\u015fle\u015ftirin" + }, + "user": { + "data": { + "host": "Ana Bilgisayar" + }, + "description": "Cihaz\u0131n ip adresini girin.", + "title": "K\u00f6pr\u00fcye otomatik olarak ba\u011flan\u0131n" + } + } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "\u0130lk d\u00fc\u011fme", + "button_2": "\u0130kinci d\u00fc\u011fme", + "button_3": "\u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fme", + "button_4": "D\u00f6rd\u00fcnc\u00fc d\u00fc\u011fme", + "close_1": "Kapat 1", + "close_2": "Kapat 2", + "close_3": "Kapat 3", + "close_4": "Kapat 4", + "close_all": "Hepsini kapat", + "group_1_button_1": "Birinci Grup ilk d\u00fc\u011fme", + "group_1_button_2": "Birinci Grup ikinci d\u00fc\u011fme", + "group_2_button_1": "\u0130kinci Grup birinci d\u00fc\u011fme", + "group_2_button_2": "\u0130kinci Grup ikinci d\u00fc\u011fme", + "lower": "Alt", + "lower_1": "Alt 1", + "lower_2": "Alt 2", + "lower_3": "Alt 3", + "lower_4": "Alt 4", + "lower_all": "Hepsini indir", + "off": "Kapal\u0131", + "on": "A\u00e7\u0131k", + "open_1": "A\u00e7 1", + "open_2": "A\u00e7 2", + "open_3": "A\u00e7 3", + "open_4": "A\u00e7\u0131k 4", + "open_all": "Hepsini a\u00e7", + "raise": "Y\u00fckseltmek", + "raise_1": "Y\u00fckselt 1", + "raise_2": "Y\u00fckselt 2", + "raise_3": "Y\u00fckselt 3", + "raise_4": "Y\u00fckselt 4", + "raise_all": "Hepsini Y\u00fckseltin", + "stop": "Durak (favori)", + "stop_1": "Durak 1", + "stop_2": "Durdur 2", + "stop_3": "Durdur 3", + "stop_4": "Durdur 4", + "stop_all": "Hepsini durdur" + }, + "trigger_type": { + "press": "\" {subtype} \" bas\u0131ld\u0131", + "release": "\" {subtype} \" yay\u0131nland\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/uk.json b/homeassistant/components/lutron_caseta/translations/uk.json new file mode 100644 index 00000000000..238e17405ce --- /dev/null +++ b/homeassistant/components/lutron_caseta/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "import_failed": { + "description": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0456\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0448\u043b\u044e\u0437 \u0437 \u0444\u0430\u0439\u043b\u0443 'configuration.yaml' (\u0445\u043e\u0441\u0442: {host}).", + "title": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0456\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0448\u043b\u044e\u0437\u0443." + } + } + } +} \ 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 4e8df0d5e9f..50762fafac1 100644 --- a/homeassistant/components/lutron_caseta/translations/zh-Hant.json +++ b/homeassistant/components/lutron_caseta/translations/zh-Hant.json @@ -2,16 +2,75 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "cannot_connect": "\u9023\u7dda\u5931\u6557" + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "not_lutron_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Lutron \u88dd\u7f6e" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, + "flow_title": "Lutron Cas\u00e9ta {name} ({host})", "step": { "import_failed": { "description": "\u7121\u6cd5\u8a2d\u5b9a\u7531 configuration.yaml \u532f\u5165\u7684 bridge\uff08\u4e3b\u6a5f\uff1a{host}\uff09\u3002", "title": "\u532f\u5165 Cas\u00e9ta bridge \u8a2d\u5b9a\u5931\u6557\u3002" + }, + "link": { + "description": "\u6b32\u8207 {name} ({host}) \u9032\u884c\u914d\u5c0d\uff0c\u65bc\u50b3\u9001\u8868\u683c\u5f8c\u3001\u4e8c\u4e0b Bridge \u5f8c\u65b9\u7684\u9ed1\u8272\u6309\u9215\u3002", + "title": "\u8207 Bridge \u914d\u5c0d" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "description": "\u8f38\u5165\u88dd\u7f6e IP \u4f4d\u5740\u3002", + "title": "\u81ea\u52d5\u9023\u7dda\u81f3 Bridge" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "\u7b2c\u4e00\u500b\u6309\u9215", + "button_2": "\u7b2c\u4e8c\u500b\u6309\u9215", + "button_3": "\u7b2c\u4e09\u500b\u6309\u9215", + "button_4": "\u7b2c\u56db\u500b\u6309\u9215", + "close_1": "\u95dc\u9589 1", + "close_2": "\u95dc\u9589 2", + "close_3": "\u95dc\u9589 3", + "close_4": "\u95dc\u9589 4", + "close_all": "\u5168\u90e8\u95dc\u9589", + "group_1_button_1": "\u7b2c\u4e00\u7d44\u7b2c\u4e00\u500b\u6309\u9215", + "group_1_button_2": "\u7b2c\u4e00\u7d44\u7b2c\u4e8c\u500b\u6309\u9215", + "group_2_button_1": "\u7b2c\u4e8c\u7d44\u7b2c\u4e00\u500b\u6309\u9215", + "group_2_button_2": "\u7b2c\u4e8c\u7d44\u7b2c\u4e8c\u500b\u6309\u9215", + "lower": "\u964d\u4f4e ", + "lower_1": "\u964d\u4f4e 1", + "lower_2": "\u964d\u4f4e 2", + "lower_3": "\u964d\u4f4e 3", + "lower_4": "\u964d\u4f4e 4", + "lower_all": "\u5168\u90e8\u964d\u4f4e", + "off": "\u95dc\u9589", + "on": "\u958b\u555f", + "open_1": "\u958b\u555f 1", + "open_2": "\u958b\u555f 2", + "open_3": "\u958b\u555f 3", + "open_4": "\u958b\u555f 4", + "open_all": "\u5168\u90e8\u958b\u555f", + "raise": "\u62ac\u8d77", + "raise_1": "\u62ac\u8d77 1", + "raise_2": "\u62ac\u8d77 2", + "raise_3": "\u62ac\u8d77 3", + "raise_4": "\u62ac\u8d77 4", + "raise_all": "\u5168\u90e8\u62ac\u8d77", + "stop": "\u505c\u6b62\uff08\u6700\u611b\uff09", + "stop_1": "\u505c\u6b62 1", + "stop_2": "\u505c\u6b62 2", + "stop_3": "\u505c\u6b62 3", + "stop_4": "\u505c\u6b62 4", + "stop_all": "\u5168\u90e8\u505c\u6b62" + }, + "trigger_type": { + "press": "\"{subtype}\" \u6309\u4e0b", + "release": "\"{subtype}\" \u91cb\u653e" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/ca.json b/homeassistant/components/lyric/translations/ca.json new file mode 100644 index 00000000000..195d3d59262 --- /dev/null +++ b/homeassistant/components/lyric/translations/ca.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "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." + }, + "create_entry": { + "default": "Autenticaci\u00f3 exitosa" + }, + "step": { + "pick_implementation": { + "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/cs.json b/homeassistant/components/lyric/translations/cs.json new file mode 100644 index 00000000000..2a54a82f41b --- /dev/null +++ b/homeassistant/components/lyric/translations/cs.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", + "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace." + }, + "create_entry": { + "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno" + }, + "step": { + "pick_implementation": { + "title": "Vyberte metodu ov\u011b\u0159en\u00ed" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/en.json b/homeassistant/components/lyric/translations/en.json index b183398663e..e3849fc17a3 100644 --- a/homeassistant/components/lyric/translations/en.json +++ b/homeassistant/components/lyric/translations/en.json @@ -12,6 +12,5 @@ "title": "Pick Authentication Method" } } - }, - "title": "Honeywell Lyric" + } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/et.json b/homeassistant/components/lyric/translations/et.json new file mode 100644 index 00000000000..c7d46e7e942 --- /dev/null +++ b/homeassistant/components/lyric/translations/et.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp", + "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni." + }, + "create_entry": { + "default": "Tuvastamine \u00f5nnestus" + }, + "step": { + "pick_implementation": { + "title": "Vali tuvastusmeetod" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/it.json b/homeassistant/components/lyric/translations/it.json new file mode 100644 index 00000000000..42536508716 --- /dev/null +++ b/homeassistant/components/lyric/translations/it.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", + "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione." + }, + "create_entry": { + "default": "Autenticazione riuscita" + }, + "step": { + "pick_implementation": { + "title": "Scegli il metodo di autenticazione" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/no.json b/homeassistant/components/lyric/translations/no.json new file mode 100644 index 00000000000..a8f6ce4f9a3 --- /dev/null +++ b/homeassistant/components/lyric/translations/no.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen" + }, + "create_entry": { + "default": "Vellykket godkjenning" + }, + "step": { + "pick_implementation": { + "title": "Velg godkjenningsmetode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/pl.json b/homeassistant/components/lyric/translations/pl.json new file mode 100644 index 00000000000..8c75c11dd7c --- /dev/null +++ b/homeassistant/components/lyric/translations/pl.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." + }, + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono" + }, + "step": { + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelniania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/tr.json b/homeassistant/components/lyric/translations/tr.json new file mode 100644 index 00000000000..773577271d2 --- /dev/null +++ b/homeassistant/components/lyric/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Yetki URL'si olu\u015fturulurken zaman a\u015f\u0131m\u0131 olu\u015ftu.", + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin." + }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131" + }, + "step": { + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/zh-Hant.json b/homeassistant/components/lyric/translations/zh-Hant.json new file mode 100644 index 00000000000..b740fd3e063 --- /dev/null +++ b/homeassistant/components/lyric/translations/zh-Hant.json @@ -0,0 +1,16 @@ +{ + "config": { + "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" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u8a8d\u8b49" + }, + "step": { + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/de.json b/homeassistant/components/mailgun/translations/de.json index f684f822fd5..118192b6516 100644 --- a/homeassistant/components/mailgun/translations/de.json +++ b/homeassistant/components/mailgun/translations/de.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", + "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." + }, "create_entry": { - "default": "Um Ereignisse an den Home Assistant zu senden, musst [Webhooks mit Mailgun]({mailgun_url}) einrichten. \n\n F\u00fclle die folgenden Informationen aus: \n\n - URL: `{webhook_url}` \n - Methode: POST \n - Inhaltstyp: application/json \n\nLies in der [Dokumentation]({docs_url}) wie du Automationen f\u00fcr die Verarbeitung eingehender Daten konfigurierst." + "default": "Um Ereignisse an Home Assistant zu senden, musst du [Webhooks mit Mailgun]({mailgun_url}) einrichten. \n\n F\u00fclle die folgenden Informationen aus: \n\n - URL: `{webhook_url}` \n - Methode: POST \n - Inhaltstyp: application/json \n\nLies in der [Dokumentation]({docs_url}), wie du Automationen f\u00fcr die Verarbeitung eingehender Daten konfigurierst." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/translations/tr.json b/homeassistant/components/mailgun/translations/tr.json new file mode 100644 index 00000000000..84adcdf8225 --- /dev/null +++ b/homeassistant/components/mailgun/translations/tr.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/uk.json b/homeassistant/components/mailgun/translations/uk.json new file mode 100644 index 00000000000..d999b52085a --- /dev/null +++ b/homeassistant/components/mailgun/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f [Mailgun]({mailgun_url}). \n\n\u0417\u0430\u043f\u043e\u0432\u043d\u0456\u0442\u044c \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0456\u0439 \u043f\u043e \u043e\u0431\u0440\u043e\u0431\u0446\u0456 \u0434\u0430\u043d\u0438\u0445, \u0449\u043e \u043d\u0430\u0434\u0445\u043e\u0434\u044f\u0442\u044c." + }, + "step": { + "user": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Mailgun?", + "title": "Mailgun" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/translations/tr.json b/homeassistant/components/media_player/translations/tr.json index 0130b5fb94c..1f46c6a8bc7 100644 --- a/homeassistant/components/media_player/translations/tr.json +++ b/homeassistant/components/media_player/translations/tr.json @@ -1,4 +1,10 @@ { + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} bo\u015fta", + "is_off": "{entity_name} kapal\u0131" + } + }, "state": { "_": { "idle": "Bo\u015fta", diff --git a/homeassistant/components/media_player/translations/uk.json b/homeassistant/components/media_player/translations/uk.json index f475829a524..21c7f2897a3 100644 --- a/homeassistant/components/media_player/translations/uk.json +++ b/homeassistant/components/media_player/translations/uk.json @@ -1,7 +1,16 @@ { + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} \u0432 \u0440\u0435\u0436\u0438\u043c\u0456 \u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f", + "is_off": "{entity_name} \u0443 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_paused": "{entity_name} \u043d\u0430 \u043f\u0430\u0443\u0437\u0456", + "is_playing": "{entity_name} \u0432\u0456\u0434\u0442\u0432\u043e\u0440\u044e\u0454 \u043c\u0435\u0434\u0456\u0430" + } + }, "state": { "_": { - "idle": "\u0411\u0435\u0437\u0434\u0456\u044f\u043b\u044c\u043d\u0456\u0441\u0442\u044c", + "idle": "\u041e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f", "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e", "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e", "paused": "\u041f\u0440\u0438\u0437\u0443\u043f\u0438\u043d\u0435\u043d\u043e", @@ -9,5 +18,5 @@ "standby": "\u041e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f" } }, - "title": "\u041c\u0435\u0434\u0456\u0430 \u043f\u043b\u0435\u0454\u0440" + "title": "\u041c\u0435\u0434\u0456\u0430\u043f\u0440\u043e\u0433\u0440\u0430\u0432\u0430\u0447" } \ No newline at end of file diff --git a/homeassistant/components/melcloud/translations/de.json b/homeassistant/components/melcloud/translations/de.json index 640c96e47c4..54ae78f8680 100644 --- a/homeassistant/components/melcloud/translations/de.json +++ b/homeassistant/components/melcloud/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Die MELCloud-Integration ist bereits f\u00fcr diese E-Mail konfiguriert. Das Zugriffstoken wurde aktualisiert." }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen. Bitte versuchen Sie es erneut.", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/melcloud/translations/tr.json b/homeassistant/components/melcloud/translations/tr.json new file mode 100644 index 00000000000..6bce50f3de6 --- /dev/null +++ b/homeassistant/components/melcloud/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "MELCloud entegrasyonu bu e-posta i\u00e7in zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Eri\u015fim belirteci yenilendi." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "E-posta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/translations/uk.json b/homeassistant/components/melcloud/translations/uk.json new file mode 100644 index 00000000000..001239a8b47 --- /dev/null +++ b/homeassistant/components/melcloud/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f MELCloud \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0430 \u0434\u043b\u044f \u0446\u0456\u0454\u0457 \u0430\u0434\u0440\u0435\u0441\u0438 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438. \u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0439." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + }, + "description": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0456\u0442\u044c\u0441\u044f, \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u044e\u0447\u0438 \u0441\u0432\u0456\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 MELCloud.", + "title": "MELCloud" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/translations/de.json b/homeassistant/components/met/translations/de.json index 901b4fb97b5..e2bb171c749 100644 --- a/homeassistant/components/met/translations/de.json +++ b/homeassistant/components/met/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/met/translations/tr.json b/homeassistant/components/met/translations/tr.json new file mode 100644 index 00000000000..d256711728c --- /dev/null +++ b/homeassistant/components/met/translations/tr.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "latitude": "Enlem", + "longitude": "Boylam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/translations/uk.json b/homeassistant/components/met/translations/uk.json new file mode 100644 index 00000000000..d980db91147 --- /dev/null +++ b/homeassistant/components/met/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "step": { + "user": { + "data": { + "elevation": "\u0412\u0438\u0441\u043e\u0442\u0430", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u041d\u043e\u0440\u0432\u0435\u0437\u044c\u043a\u0438\u0439 \u043c\u0435\u0442\u0435\u043e\u0440\u043e\u043b\u043e\u0433\u0456\u0447\u043d\u0438\u0439 \u0456\u043d\u0441\u0442\u0438\u0442\u0443\u0442.", + "title": "\u041c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/translations/de.json b/homeassistant/components/meteo_france/translations/de.json index 65313f16c41..74637594d5f 100644 --- a/homeassistant/components/meteo_france/translations/de.json +++ b/homeassistant/components/meteo_france/translations/de.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Stadt bereits konfiguriert", - "unknown": "Unbekannter Fehler: Bitte versuchen Sie es sp\u00e4ter erneut" + "already_configured": "Standort ist bereits konfiguriert", + "unknown": "Unerwarteter Fehler" }, "error": { "empty": "Kein Ergebnis bei der Stadtsuche: Bitte \u00fcberpr\u00fcfe das Stadtfeld" diff --git a/homeassistant/components/meteo_france/translations/tr.json b/homeassistant/components/meteo_france/translations/tr.json index 57fc9f76881..59c3886a900 100644 --- a/homeassistant/components/meteo_france/translations/tr.json +++ b/homeassistant/components/meteo_france/translations/tr.json @@ -1,7 +1,28 @@ { "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "unknown": "Beklenmeyen hata" + }, "error": { "empty": "\u015eehir aramas\u0131nda sonu\u00e7 yok: l\u00fctfen \u015fehir alan\u0131n\u0131 kontrol edin" + }, + "step": { + "user": { + "data": { + "city": "\u015eehir" + }, + "title": "M\u00e9t\u00e9o-Fransa" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "Tahmin modu" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/meteo_france/translations/uk.json b/homeassistant/components/meteo_france/translations/uk.json new file mode 100644 index 00000000000..a84c230e218 --- /dev/null +++ b/homeassistant/components/meteo_france/translations/uk.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "empty": "\u041d\u0435\u043c\u0430\u0454 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0456\u0432 \u043f\u043e\u0448\u0443\u043a\u0443. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043f\u043e\u043b\u0435 \"\u041c\u0456\u0441\u0442\u043e\"." + }, + "step": { + "cities": { + "data": { + "city": "\u041c\u0456\u0441\u0442\u043e" + }, + "description": "\u041e\u0431\u0435\u0440\u0456\u0442\u044c \u043c\u0456\u0441\u0442\u043e \u0437\u0456 \u0441\u043f\u0438\u0441\u043a\u0443", + "title": "M\u00e9t\u00e9o-France" + }, + "user": { + "data": { + "city": "\u041c\u0456\u0441\u0442\u043e" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u043e\u0448\u0442\u043e\u0432\u0438\u0439 \u0456\u043d\u0434\u0435\u043a\u0441 (\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0454\u0442\u044c\u0441\u044f \u0442\u0456\u043b\u044c\u043a\u0438 \u0434\u043b\u044f \u0424\u0440\u0430\u043d\u0446\u0456\u0457) \u0430\u0431\u043e \u043d\u0430\u0437\u0432\u0443 \u043c\u0456\u0441\u0442\u0430", + "title": "M\u00e9t\u00e9o-France" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "\u0420\u0435\u0436\u0438\u043c \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0443" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/metoffice/translations/de.json b/homeassistant/components/metoffice/translations/de.json index 74c204b9683..7b92af96c99 100644 --- a/homeassistant/components/metoffice/translations/de.json +++ b/homeassistant/components/metoffice/translations/de.json @@ -4,6 +4,7 @@ "already_configured": "Service ist bereits konfiguriert" }, "error": { + "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/metoffice/translations/tr.json b/homeassistant/components/metoffice/translations/tr.json new file mode 100644 index 00000000000..55064a139ef --- /dev/null +++ b/homeassistant/components/metoffice/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam" + }, + "description": "Enlem ve boylam, en yak\u0131n hava istasyonunu bulmak i\u00e7in kullan\u0131lacakt\u0131r." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/metoffice/translations/uk.json b/homeassistant/components/metoffice/translations/uk.json new file mode 100644 index 00000000000..53ab2115e82 --- /dev/null +++ b/homeassistant/components/metoffice/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430" + }, + "description": "\u0428\u0438\u0440\u043e\u0442\u0430 \u0456 \u0434\u043e\u0432\u0433\u043e\u0442\u0430 \u0431\u0443\u0434\u0443\u0442\u044c \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u0456 \u0434\u043b\u044f \u043f\u043e\u0448\u0443\u043a\u0443 \u043d\u0430\u0439\u0431\u043b\u0438\u0436\u0447\u043e\u0457 \u043c\u0435\u0442\u0435\u043e\u0441\u0442\u0430\u043d\u0446\u0456\u0457.", + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e Met Office UK" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/de.json b/homeassistant/components/mikrotik/translations/de.json index 4211077c82c..82ea47dc4bf 100644 --- a/homeassistant/components/mikrotik/translations/de.json +++ b/homeassistant/components/mikrotik/translations/de.json @@ -1,10 +1,11 @@ { "config": { "abort": { - "already_configured": "Mikrotik ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "name_exists": "Name vorhanden" }, "step": { @@ -25,7 +26,7 @@ "step": { "device_tracker": { "data": { - "arp_ping": "ARP Ping aktivieren", + "arp_ping": "ARP-Ping aktivieren", "force_dhcp": "Erzwingen Sie das Scannen \u00fcber DHCP" } } diff --git a/homeassistant/components/mikrotik/translations/tr.json b/homeassistant/components/mikrotik/translations/tr.json new file mode 100644 index 00000000000..cffcc65151c --- /dev/null +++ b/homeassistant/components/mikrotik/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/uk.json b/homeassistant/components/mikrotik/translations/uk.json new file mode 100644 index 00000000000..b44d5979d13 --- /dev/null +++ b/homeassistant/components/mikrotik/translations/uk.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "name_exists": "\u0426\u044f \u043d\u0430\u0437\u0432\u0430 \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "verify_ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 SSL" + }, + "title": "MikroTik" + } + } + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 ARP-\u043f\u0456\u043d\u0433", + "detection_time": "\u0427\u0430\u0441 \u0432\u0456\u0434 \u043e\u0441\u0442\u0430\u043d\u043d\u044c\u043e\u0433\u043e \u0441\u0435\u0430\u043d\u0441\u0443 \u0437\u0432'\u044f\u0437\u043a\u0443 \u0437 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c (\u0441\u0435\u043a.), \u043f\u043e \u0437\u0430\u043a\u0456\u043d\u0447\u0435\u043d\u043d\u044e \u044f\u043a\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043e\u0442\u0440\u0438\u043c\u0430\u0454 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u041d\u0435 \u0432\u0434\u043e\u043c\u0430\".", + "force_dhcp": "\u041f\u0440\u0438\u043c\u0443\u0441\u043e\u0432\u0435 \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f \u0437 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f\u043c DHCP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/de.json b/homeassistant/components/mill/translations/de.json index 886e7e3c458..63b6b7ea6e9 100644 --- a/homeassistant/components/mill/translations/de.json +++ b/homeassistant/components/mill/translations/de.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Account ist bereits konfiguriert" }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/mill/translations/fr.json b/homeassistant/components/mill/translations/fr.json index e171086a084..ffcff15ade8 100644 --- a/homeassistant/components/mill/translations/fr.json +++ b/homeassistant/components/mill/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9" + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" }, "error": { "cannot_connect": "\u00c9chec de connexion" diff --git a/homeassistant/components/mill/translations/tr.json b/homeassistant/components/mill/translations/tr.json new file mode 100644 index 00000000000..0f14728873a --- /dev/null +++ b/homeassistant/components/mill/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/uk.json b/homeassistant/components/mill/translations/uk.json new file mode 100644 index 00000000000..b8a5aea578e --- /dev/null +++ b/homeassistant/components/mill/translations/uk.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/translations/de.json b/homeassistant/components/minecraft_server/translations/de.json index 484be7bd418..a0bbe60a842 100644 --- a/homeassistant/components/minecraft_server/translations/de.json +++ b/homeassistant/components/minecraft_server/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Der Host ist bereits konfiguriert." + "already_configured": "Der Dienst ist bereits konfiguriert" }, "error": { "cannot_connect": "Verbindung zum Server fehlgeschlagen. Bitte \u00fcberpr\u00fcfe den Host und den Port und versuche es erneut. Stelle au\u00dferdem sicher, dass Du mindestens Minecraft Version 1.7 auf Deinem Server ausf\u00fchrst.", diff --git a/homeassistant/components/minecraft_server/translations/tr.json b/homeassistant/components/minecraft_server/translations/tr.json index 7527294a3c7..422dab32a01 100644 --- a/homeassistant/components/minecraft_server/translations/tr.json +++ b/homeassistant/components/minecraft_server/translations/tr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Host", + "host": "Ana Bilgisayar", "name": "Ad" }, "description": "G\u00f6zetmeye izin vermek i\u00e7in Minecraft server nesnesini ayarla.", diff --git a/homeassistant/components/minecraft_server/translations/uk.json b/homeassistant/components/minecraft_server/translations/uk.json new file mode 100644 index 00000000000..0c8528b2cab --- /dev/null +++ b/homeassistant/components/minecraft_server/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u0427\u0438 \u043d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0456\u0441\u0442\u044c \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0445 \u0434\u0430\u043d\u0438\u0445 \u0456 \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443. \u0422\u0430\u043a\u043e\u0436 \u043f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u043d\u0430 \u0412\u0430\u0448\u043e\u043c\u0443 \u0441\u0435\u0440\u0432\u0435\u0440\u0456 \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0439 Minecraft \u0432\u0435\u0440\u0441\u0456\u0457 1.7, \u0430\u0431\u043e \u0432\u0438\u0449\u0435.", + "invalid_ip": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0430 IP-\u0430\u0434\u0440\u0435\u0441\u0430 (\u043d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438 MAC-\u0430\u0434\u0440\u0435\u0441\u0443).", + "invalid_port": "\u041f\u043e\u0440\u0442 \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0432 \u0434\u0456\u0430\u043f\u0430\u0437\u043e\u043d\u0456 \u0432\u0456\u0434 1024 \u0434\u043e 65535." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0446\u0435\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043b\u044f \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443 \u0412\u0430\u0448\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Minecraft.", + "title": "Minecraft Server" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/tr.json b/homeassistant/components/mobile_app/translations/tr.json new file mode 100644 index 00000000000..10d79751ec1 --- /dev/null +++ b/homeassistant/components/mobile_app/translations/tr.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "action_type": { + "notify": "Bildirim g\u00f6nder" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/uk.json b/homeassistant/components/mobile_app/translations/uk.json index 4a48dd3775d..db471bbdc7f 100644 --- a/homeassistant/components/mobile_app/translations/uk.json +++ b/homeassistant/components/mobile_app/translations/uk.json @@ -1,9 +1,17 @@ { "config": { + "abort": { + "install_app": "\u0412\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u043c\u043e\u0431\u0456\u043b\u044c\u043d\u0438\u0439 \u0434\u043e\u0434\u0430\u0442\u043e\u043a, \u0449\u043e\u0431 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e \u0437 Home Assistant. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({apps_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0441\u043f\u0438\u0441\u043a\u0443 \u0441\u0443\u043c\u0456\u0441\u043d\u0438\u0445 \u0434\u043e\u0434\u0430\u0442\u043a\u0456\u0432." + }, "step": { "confirm": { - "description": "\u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043c\u043e\u0431\u0456\u043b\u044c\u043d\u043e\u0433\u043e \u0434\u043e\u0434\u0430\u0442\u043a\u0430?" + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043c\u043e\u0431\u0456\u043b\u044c\u043d\u0438\u0439 \u0434\u043e\u0434\u0430\u0442\u043e\u043a?" } } + }, + "device_automation": { + "action_type": { + "notify": "\u041d\u0430\u0434\u0456\u0441\u043b\u0430\u0442\u0438 \u0441\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u043d\u044f" + } } } \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/de.json b/homeassistant/components/monoprice/translations/de.json index 820d3a972d3..8f6d1d88196 100644 --- a/homeassistant/components/monoprice/translations/de.json +++ b/homeassistant/components/monoprice/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/monoprice/translations/tr.json b/homeassistant/components/monoprice/translations/tr.json new file mode 100644 index 00000000000..7c622a3cb4a --- /dev/null +++ b/homeassistant/components/monoprice/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "port": "Port", + "source_1": "Kaynak #1 ad\u0131", + "source_2": "Kaynak #2 ad\u0131", + "source_3": "Kaynak #3 ad\u0131", + "source_4": "Kaynak #4 ad\u0131", + "source_5": "Kaynak #5 ad\u0131", + "source_6": "Kaynak #6 ad\u0131" + }, + "title": "Cihaza ba\u011flan\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/uk.json b/homeassistant/components/monoprice/translations/uk.json new file mode 100644 index 00000000000..08857cc26f9 --- /dev/null +++ b/homeassistant/components/monoprice/translations/uk.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442", + "source_1": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #1", + "source_2": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #2", + "source_3": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #3", + "source_4": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #4", + "source_5": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #5", + "source_6": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #6" + }, + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #1", + "source_2": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #2", + "source_3": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #3", + "source_4": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #4", + "source_5": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #5", + "source_6": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #6" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u0436\u0435\u0440\u0435\u043b" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/sensor.uk.json b/homeassistant/components/moon/translations/sensor.uk.json index 71c2d80eb98..f916c03c3a1 100644 --- a/homeassistant/components/moon/translations/sensor.uk.json +++ b/homeassistant/components/moon/translations/sensor.uk.json @@ -4,7 +4,11 @@ "first_quarter": "\u041f\u0435\u0440\u0448\u0430 \u0447\u0432\u0435\u0440\u0442\u044c", "full_moon": "\u041f\u043e\u0432\u043d\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c", "last_quarter": "\u041e\u0441\u0442\u0430\u043d\u043d\u044f \u0447\u0432\u0435\u0440\u0442\u044c", - "new_moon": "\u041d\u043e\u0432\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c" + "new_moon": "\u041d\u043e\u0432\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c", + "waning_crescent": "\u0421\u0442\u0430\u0440\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c", + "waning_gibbous": "\u0421\u043f\u0430\u0434\u0430\u044e\u0447\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c", + "waxing_crescent": "\u041c\u043e\u043b\u043e\u0434\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c", + "waxing_gibbous": "\u041f\u0440\u0438\u0431\u0443\u0432\u0430\u044e\u0447\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c" } } } \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/ca.json b/homeassistant/components/motion_blinds/translations/ca.json index a4bf96457e6..b83746b9ccf 100644 --- a/homeassistant/components/motion_blinds/translations/ca.json +++ b/homeassistant/components/motion_blinds/translations/ca.json @@ -5,14 +5,31 @@ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "connection_error": "Ha fallat la connexi\u00f3" }, + "error": { + "discovery_error": "No s'ha pogut descobrir cap Motion Gateway" + }, "flow_title": "Motion Blinds", "step": { + "connect": { + "data": { + "api_key": "Clau API" + }, + "description": "Necessitar\u00e0s la clau API de 16 car\u00e0cters, consulta les instruccions a https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.", + "title": "Motion Blinds" + }, + "select": { + "data": { + "select_ip": "Adre\u00e7a IP" + }, + "description": "Torna a executar la configuraci\u00f3 si vols connectar m\u00e9s Motion Gateways", + "title": "Selecciona el Motion Gateway que vulguis connectar" + }, "user": { "data": { "api_key": "Clau API", "host": "Adre\u00e7a IP" }, - "description": "Necessitar\u00e0s el token d'API de 16 car\u00e0cters, consulta les instruccions a https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.", + "description": "Connecta el teu Motion Gateway, si no es configura l'adre\u00e7a IP, s'utilitza el descobriment autom\u00e0tic", "title": "Motion Blinds" } } diff --git a/homeassistant/components/motion_blinds/translations/cs.json b/homeassistant/components/motion_blinds/translations/cs.json index 41b5db3c83e..899f04d7cd4 100644 --- a/homeassistant/components/motion_blinds/translations/cs.json +++ b/homeassistant/components/motion_blinds/translations/cs.json @@ -7,12 +7,21 @@ }, "flow_title": "Motion Blinds", "step": { + "connect": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + }, + "select": { + "data": { + "select_ip": "IP adresa" + } + }, "user": { "data": { "api_key": "Kl\u00ed\u010d API", "host": "IP adresa" }, - "description": "Budete pot\u0159ebovat 16m\u00edstn\u00fd API kl\u00ed\u010d, pokyny najdete na https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", "title": "Motion Blinds" } } diff --git a/homeassistant/components/motion_blinds/translations/de.json b/homeassistant/components/motion_blinds/translations/de.json index dd1acc230f1..c1a7ac0bc8d 100644 --- a/homeassistant/components/motion_blinds/translations/de.json +++ b/homeassistant/components/motion_blinds/translations/de.json @@ -1,16 +1,28 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "connection_error": "Verbindung fehlgeschlagen" }, "flow_title": "Jalousien", "step": { + "connect": { + "data": { + "api_key": "API-Schl\u00fcssel" + } + }, + "select": { + "data": { + "select_ip": "IP-Adresse" + } + }, "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", + "description": "Stelle eine Verbindung zu deinem Motion Gateway her. Wenn die IP-Adresse leer bleibt, wird die automatische Erkennung verwendet", "title": "Jalousien" } } diff --git a/homeassistant/components/motion_blinds/translations/en.json b/homeassistant/components/motion_blinds/translations/en.json index b7830a255fc..3a968bc6491 100644 --- a/homeassistant/components/motion_blinds/translations/en.json +++ b/homeassistant/components/motion_blinds/translations/en.json @@ -5,14 +5,31 @@ "already_in_progress": "Configuration flow is already in progress", "connection_error": "Failed to connect" }, + "error": { + "discovery_error": "Failed to discover a Motion Gateway" + }, "flow_title": "Motion Blinds", "step": { + "connect": { + "data": { + "api_key": "API Key" + }, + "description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions", + "title": "Motion Blinds" + }, + "select": { + "data": { + "select_ip": "IP Address" + }, + "description": "Run the setup again if you want to connect additional Motion Gateways", + "title": "Select the Motion Gateway that you wish to connect" + }, "user": { "data": { "api_key": "API Key", "host": "IP Address" }, - "description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions", + "description": "Connect to your Motion Gateway, if the IP address is not set, auto-discovery is used", "title": "Motion Blinds" } } diff --git a/homeassistant/components/motion_blinds/translations/es.json b/homeassistant/components/motion_blinds/translations/es.json index bac5ffddbd3..7d7c6c1510f 100644 --- a/homeassistant/components/motion_blinds/translations/es.json +++ b/homeassistant/components/motion_blinds/translations/es.json @@ -5,14 +5,31 @@ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", "connection_error": "No se pudo conectar" }, + "error": { + "discovery_error": "No se pudo descubrir un detector de movimiento" + }, "flow_title": "Motion Blinds", "step": { + "connect": { + "data": { + "api_key": "Clave API" + }, + "description": "Necesitar\u00e1 la clave de API de 16 caracteres, consulte https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key para obtener instrucciones", + "title": "Estores motorizados" + }, + "select": { + "data": { + "select_ip": "Direcci\u00f3n IP" + }, + "description": "Ejecute la configuraci\u00f3n de nuevo si desea conectar detectores de movimiento adicionales", + "title": "Selecciona el detector de Movimiento que deseas conectar" + }, "user": { "data": { "api_key": "Clave API", "host": "Direcci\u00f3n IP" }, - "description": "Necesitar\u00e1s la Clave API de 16 caracteres, consulta https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key para instrucciones", + "description": "Con\u00e9ctate a tu Motion Gateway, si la direcci\u00f3n IP no est\u00e1 establecida, se utilitzar\u00e1 la detecci\u00f3n autom\u00e1tica", "title": "Motion Blinds" } } diff --git a/homeassistant/components/motion_blinds/translations/et.json b/homeassistant/components/motion_blinds/translations/et.json index b55640d8905..5e585dec1a3 100644 --- a/homeassistant/components/motion_blinds/translations/et.json +++ b/homeassistant/components/motion_blinds/translations/et.json @@ -5,14 +5,31 @@ "already_in_progress": "Seadistamine on juba k\u00e4imas", "connection_error": "\u00dchendamine nurjus" }, + "error": { + "discovery_error": "Motion Gateway avastamine nurjus" + }, "flow_title": "", "step": { + "connect": { + "data": { + "api_key": "API v\u00f5ti" + }, + "description": "On vaja 16-kohalist API-v\u00f5tit, juhiste saamiseks vaata https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", + "title": "" + }, + "select": { + "data": { + "select_ip": "IP aadress" + }, + "description": "K\u00e4ivita seadistamine uuesti kui soovid \u00fchendada t\u00e4iendavaid Motion Gateway sidumisi", + "title": "Vali Motion Gateway, mille soovid \u00fchendada" + }, "user": { "data": { "api_key": "API v\u00f5ti", "host": "IP-aadress" }, - "description": "Vaja on 16-kohalist API-v\u00f5tit. Juhiste saamiseks vt https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", + "description": "\u00dchenda oma Motion Gatewayga. Kui IP-aadress on m\u00e4\u00e4ramata kasutatakse automaatset avastamist", "title": "" } } diff --git a/homeassistant/components/motion_blinds/translations/fr.json b/homeassistant/components/motion_blinds/translations/fr.json new file mode 100644 index 00000000000..86d008b9e6d --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/fr.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "connect": { + "data": { + "api_key": "Cl\u00e9 API" + }, + "description": "Vous aurez besoin de la cl\u00e9 API de 16 caract\u00e8res, voir https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key pour les instructions" + }, + "select": { + "data": { + "select_ip": "Adresse IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/it.json b/homeassistant/components/motion_blinds/translations/it.json index ff56f184ac2..1d79ae28ee5 100644 --- a/homeassistant/components/motion_blinds/translations/it.json +++ b/homeassistant/components/motion_blinds/translations/it.json @@ -5,14 +5,31 @@ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "connection_error": "Impossibile connettersi" }, + "error": { + "discovery_error": "Impossibile rilevare un Motion Gateway" + }, "flow_title": "Tende Motion", "step": { + "connect": { + "data": { + "api_key": "Chiave API" + }, + "description": "Avrai bisogno della chiave API di 16 caratteri, consulta https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key per le istruzioni", + "title": "Motion Blinds" + }, + "select": { + "data": { + "select_ip": "Indirizzo IP" + }, + "description": "Esegui nuovamente l'installazione se desideri collegare altri Motion Gateway", + "title": "Seleziona il Motion Gateway che vorresti collegare" + }, "user": { "data": { "api_key": "Chiave API", "host": "Indirizzo IP" }, - "description": "Avrai bisogno della chiave API di 16 caratteri, consulta https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key per le istruzioni", + "description": "Connetti il tuo Motion Gateway, se l'indirizzo IP non \u00e8 impostato, sar\u00e0 utilizzato il rilevamento automatico", "title": "Tende Motion" } } diff --git a/homeassistant/components/motion_blinds/translations/lb.json b/homeassistant/components/motion_blinds/translations/lb.json index 7a3dcfdbf07..85caeea79e5 100644 --- a/homeassistant/components/motion_blinds/translations/lb.json +++ b/homeassistant/components/motion_blinds/translations/lb.json @@ -5,7 +5,21 @@ "already_in_progress": "Konfiguratioun's Oflaf ass schon am gaang", "connection_error": "Feeler beim verbannen" }, + "error": { + "discovery_error": "Feeler beim Entdecken vun enger Motion Gateway" + }, "step": { + "connect": { + "data": { + "api_key": "API Schl\u00ebssel" + }, + "description": "Du brauchs de 16 stellegen API Schl\u00ebssel, kuck https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key fir w\u00e9ider Instruktiounen" + }, + "select": { + "data": { + "select_ip": "IP Adresse" + } + }, "user": { "data": { "api_key": "API Schl\u00ebssel", diff --git a/homeassistant/components/motion_blinds/translations/no.json b/homeassistant/components/motion_blinds/translations/no.json index 9e406150691..e86da7c1fc4 100644 --- a/homeassistant/components/motion_blinds/translations/no.json +++ b/homeassistant/components/motion_blinds/translations/no.json @@ -5,14 +5,31 @@ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "connection_error": "Tilkobling mislyktes" }, + "error": { + "discovery_error": "Kunne ikke oppdage en Motion Gateway" + }, "flow_title": "Motion Blinds", "step": { + "connect": { + "data": { + "api_key": "API-n\u00f8kkel" + }, + "description": "Du trenger API-n\u00f8kkelen med 16 tegn, se https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instruksjoner", + "title": "" + }, + "select": { + "data": { + "select_ip": "IP adresse" + }, + "description": "Kj\u00f8r oppsettet p\u00e5 nytt hvis du vil koble til flere Motion Gateways", + "title": "Velg Motion Gateway som du vil koble til" + }, "user": { "data": { "api_key": "API-n\u00f8kkel", "host": "IP adresse" }, - "description": "Du trenger API-n\u00f8kkelen med 16 tegn, se https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instruksjoner", + "description": "Koble til Motion Gateway. Hvis IP-adressen ikke er angitt, brukes automatisk oppdagelse", "title": "Motion Blinds" } } diff --git a/homeassistant/components/motion_blinds/translations/pl.json b/homeassistant/components/motion_blinds/translations/pl.json index 8f73496fd1d..1d34d22d65e 100644 --- a/homeassistant/components/motion_blinds/translations/pl.json +++ b/homeassistant/components/motion_blinds/translations/pl.json @@ -5,14 +5,31 @@ "already_in_progress": "Konfiguracja jest ju\u017c w toku", "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, + "error": { + "discovery_error": "Nie uda\u0142o si\u0119 wykry\u0107 bramki ruchu" + }, "flow_title": "Motion Blinds", "step": { + "connect": { + "data": { + "api_key": "Klucz API" + }, + "description": "B\u0119dziesz potrzebowa\u0142 16-znakowego klucza API, instrukcje znajdziesz na https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", + "title": "Motion Blinds" + }, + "select": { + "data": { + "select_ip": "Adres IP" + }, + "description": "Uruchom ponownie konfiguracj\u0119, je\u015bli chcesz pod\u0142\u0105czy\u0107 dodatkowe bramki ruchu", + "title": "Wybierz bram\u0119 ruchu, z kt\u00f3r\u0105 chcesz si\u0119 po\u0142\u0105czy\u0107" + }, "user": { "data": { "api_key": "Klucz API", "host": "Adres IP" }, - "description": "B\u0119dziesz potrzebowa\u0142 16-znakowego klucza API, instrukcje znajdziesz na https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", + "description": "Po\u0142\u0105cz si\u0119 z bram\u0105 ruchu. Je\u015bli adres IP nie jest ustawiony, u\u017cywane jest automatyczne wykrywanie", "title": "Motion Blinds" } } diff --git a/homeassistant/components/motion_blinds/translations/pt.json b/homeassistant/components/motion_blinds/translations/pt.json index fe188057e46..64ccd6061d2 100644 --- a/homeassistant/components/motion_blinds/translations/pt.json +++ b/homeassistant/components/motion_blinds/translations/pt.json @@ -5,12 +5,25 @@ "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "connection_error": "Falha na liga\u00e7\u00e3o" }, + "flow_title": "Cortinas Motion", "step": { + "connect": { + "data": { + "api_key": "API Key" + }, + "title": "Cortinas Motion" + }, + "select": { + "data": { + "select_ip": "Endere\u00e7o IP" + } + }, "user": { "data": { "api_key": "API Key", "host": "Endere\u00e7o IP" - } + }, + "title": "Cortinas Motion" } } } diff --git a/homeassistant/components/motion_blinds/translations/ru.json b/homeassistant/components/motion_blinds/translations/ru.json index 1a249a4fab8..ae2d3229c20 100644 --- a/homeassistant/components/motion_blinds/translations/ru.json +++ b/homeassistant/components/motion_blinds/translations/ru.json @@ -5,14 +5,31 @@ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, + "error": { + "discovery_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0448\u043b\u044e\u0437 Motion." + }, "flow_title": "Motion Blinds", "step": { + "connect": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "description": "\u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c 16-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.", + "title": "Motion Blinds" + }, + "select": { + "data": { + "select_ip": "IP-\u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u0417\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0435\u0449\u0451 \u0440\u0430\u0437, \u0435\u0441\u043b\u0438 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0435\u0449\u0451 \u043e\u0434\u0438\u043d \u0448\u043b\u044e\u0437", + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 Motion" + }, "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", "host": "IP-\u0430\u0434\u0440\u0435\u0441" }, - "description": "\u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c 16-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.", + "description": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0448\u043b\u044e\u0437\u0443 Motion. \u0414\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f \u0448\u043b\u044e\u0437\u0430, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 IP-\u0430\u0434\u0440\u0435\u0441\u0430 \u043f\u0443\u0441\u0442\u044b\u043c.", "title": "Motion Blinds" } } diff --git a/homeassistant/components/motion_blinds/translations/tr.json b/homeassistant/components/motion_blinds/translations/tr.json index 545a3547ffc..194608780c9 100644 --- a/homeassistant/components/motion_blinds/translations/tr.json +++ b/homeassistant/components/motion_blinds/translations/tr.json @@ -1,14 +1,30 @@ { "config": { "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "connection_error": "Ba\u011flanma hatas\u0131" }, + "flow_title": "Hareketli Panjurlar", "step": { + "connect": { + "data": { + "api_key": "API Anahtar\u0131" + } + }, + "select": { + "data": { + "select_ip": "\u0130p Adresi" + }, + "title": "Ba\u011flamak istedi\u011finiz Hareket A\u011f Ge\u00e7idini se\u00e7in" + }, "user": { "data": { "api_key": "API Anahtar\u0131", "host": "IP adresi" - } + }, + "description": "Motion Gateway'inize ba\u011flan\u0131n, IP adresi ayarlanmad\u0131ysa, otomatik ke\u015fif kullan\u0131l\u0131r", + "title": "Hareketli Panjurlar" } } } diff --git a/homeassistant/components/motion_blinds/translations/uk.json b/homeassistant/components/motion_blinds/translations/uk.json new file mode 100644 index 00000000000..99ccb60dc6c --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/uk.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "connection_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "error": { + "discovery_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0438\u044f\u0432\u0438\u0442\u0438 Motion Gateway" + }, + "flow_title": "Motion Blinds", + "step": { + "connect": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "description": "\u0412\u0430\u043c \u043f\u043e\u0442\u0440\u0456\u0431\u0435\u043d 16-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API, \u0434\u0438\u0432. https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0439", + "title": "Motion Blinds" + }, + "select": { + "data": { + "select_ip": "IP-\u0430\u0434\u0440\u0435\u0441\u0430" + }, + "description": "\u0417\u0430\u043f\u0443\u0441\u0442\u0456\u0442\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443 \u0449\u0435 \u0440\u0430\u0437, \u044f\u043a\u0449\u043e \u0432\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 Motion Gateway", + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c Motion Gateway, \u044f\u043a\u0438\u0439 \u0432\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438" + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430" + }, + "description": "\u041f\u0440\u043e \u0442\u0435, \u044f\u043a \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 16-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API, \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0456\u0437\u043d\u0430\u0442\u0438\u0441\u044f \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0457 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.", + "title": "Motion Blinds" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/zh-Hant.json b/homeassistant/components/motion_blinds/translations/zh-Hant.json index 37925ca6288..0f2f9881ebd 100644 --- a/homeassistant/components/motion_blinds/translations/zh-Hant.json +++ b/homeassistant/components/motion_blinds/translations/zh-Hant.json @@ -5,14 +5,31 @@ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "connection_error": "\u9023\u7dda\u5931\u6557" }, + "error": { + "discovery_error": "\u63a2\u7d22 Motion \u9598\u9053\u5668\u5931\u6557" + }, "flow_title": "Motion Blinds", "step": { + "connect": { + "data": { + "api_key": "API \u5bc6\u9470" + }, + "description": "\u5c07\u9700\u8981\u8f38\u5165 16 \u4f4d\u5b57\u5143 API \u5bc6\u9470\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u4ee5\u7372\u5f97\u7372\u53d6\u5bc6\u9470\u7684\u6559\u5b78\u3002", + "title": "Motion Blinds" + }, + "select": { + "data": { + "select_ip": "IP \u4f4d\u5740" + }, + "description": "\u5047\u5982\u6b32\u9023\u7dda\u81f3\u5176\u4ed6 Motion \u9598\u9053\u5668\uff0c\u8acb\u518d\u57f7\u884c\u4e00\u6b21\u8a2d\u5b9a\u6b65\u9a5f", + "title": "\u9078\u64c7\u6240\u8981\u9023\u7dda\u7684 Motion \u7db2\u95dc" + }, "user": { "data": { "api_key": "API \u5bc6\u9470", "host": "IP \u4f4d\u5740" }, - "description": "\u5c07\u9700\u8981\u8f38\u5165 16 \u4f4d\u5b57\u5143 API \u5bc6\u9470\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u4ee5\u7372\u5f97\u7372\u53d6\u5bc6\u9470\u7684\u6559\u5b78\u3002", + "description": "\u9023\u7dda\u81f3 Motion \u9598\u9053\u5668\uff0c\u5047\u5982\u672a\u63d0\u4f9b IP \u4f4d\u5740\uff0c\u5c07\u4f7f\u7528\u81ea\u52d5\u63a2\u7d22", "title": "Motion Blinds" } } diff --git a/homeassistant/components/mqtt/translations/cs.json b/homeassistant/components/mqtt/translations/cs.json index 325e8dde098..60c323d9051 100644 --- a/homeassistant/components/mqtt/translations/cs.json +++ b/homeassistant/components/mqtt/translations/cs.json @@ -38,13 +38,13 @@ "turn_on": "Zapnout" }, "trigger_type": { - "button_double_press": "Dvakr\u00e1t stisknuto \"{subtype}\"", + "button_double_press": "\"{subtype}\" stisknuto dvakr\u00e1t", "button_long_release": "Uvoln\u011bno \"{subtype}\" po dlouh\u00e9m stisku", - "button_quadruple_press": "\u010cty\u0159ikr\u00e1t stisknuto \"{subtype}\"", - "button_quintuple_press": "P\u011btkr\u00e1t stisknuto \"{subtype}\"", - "button_short_press": "Stiknuto \"{subtype}\"", + "button_quadruple_press": "\"{subtype}\" stisknuto \u010dty\u0159ikr\u00e1t", + "button_quintuple_press": "\"{subtype}\" stisknuto \u010dty\u0159ikr\u00e1t", + "button_short_press": "\"{subtype}\" stisknuto", "button_short_release": "Uvoln\u011bno \"{subtype}\"", - "button_triple_press": "T\u0159ikr\u00e1t stisknuto \"{subtype}\"" + "button_triple_press": "\"{subtype}\" stisknuto t\u0159ikr\u00e1t" } }, "options": { diff --git a/homeassistant/components/mqtt/translations/de.json b/homeassistant/components/mqtt/translations/de.json index a92886eb0c6..3346abfd53e 100644 --- a/homeassistant/components/mqtt/translations/de.json +++ b/homeassistant/components/mqtt/translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "single_instance_allowed": "Nur eine einzige Konfiguration von MQTT ist zul\u00e4ssig." + "single_instance_allowed": "Bereits konfiguriert. Es ist nur eine Konfiguration m\u00f6glich." }, "error": { - "cannot_connect": "Es konnte keine Verbindung zum Broker hergestellt werden." + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "broker": { @@ -59,12 +59,15 @@ "password": "Passwort", "port": "Port", "username": "Benutzername" - } + }, + "description": "Bitte gib die Verbindungsinformationen deines MQTT-Brokers ein." }, "options": { "data": { + "discovery": "Erkennung aktivieren", "will_enable": "Letzten Willen aktivieren" - } + }, + "description": "Bitte die MQTT-Einstellungen ausw\u00e4hlen." } } } diff --git a/homeassistant/components/mqtt/translations/no.json b/homeassistant/components/mqtt/translations/no.json index 2a9372b3fb0..12c72603a1f 100644 --- a/homeassistant/components/mqtt/translations/no.json +++ b/homeassistant/components/mqtt/translations/no.json @@ -21,8 +21,8 @@ "data": { "discovery": "Aktiver oppdagelse" }, - "description": "Vil du konfigurere Home Assistant til \u00e5 koble til en MQTT megler som er levert av Hass.io-tillegget {addon}?", - "title": "MQTT megler via Hass.io tillegg" + "description": "Vil du konfigurere Home Assistant til \u00e5 koble til en MQTT megler som er levert av Hass.io-tillegg {addon}?", + "title": "MQTT megler via Hass.io-tillegg" } } }, diff --git a/homeassistant/components/mqtt/translations/pl.json b/homeassistant/components/mqtt/translations/pl.json index ce41d059b24..08b1d2f1974 100644 --- a/homeassistant/components/mqtt/translations/pl.json +++ b/homeassistant/components/mqtt/translations/pl.json @@ -38,14 +38,14 @@ "turn_on": "w\u0142\u0105cznik" }, "trigger_type": { - "button_double_press": "\"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", - "button_long_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", - "button_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", - "button_quadruple_press": "\"{subtype}\" zostanie czterokrotnie naci\u015bni\u0119ty", - "button_quintuple_press": "\"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", - "button_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty", - "button_short_release": "\"{subtype}\" zostanie zwolniony", - "button_triple_press": "\"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" + "button_double_press": "przycisk \"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", + "button_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "button_quadruple_press": "przycisk \"{subtype}\" zostanie czterokrotnie naci\u015bni\u0119ty", + "button_quintuple_press": "przycisk \"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", + "button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", + "button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", + "button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" } }, "options": { diff --git a/homeassistant/components/mqtt/translations/ru.json b/homeassistant/components/mqtt/translations/ru.json index 0079481d6f2..7cc7a84b28c 100644 --- a/homeassistant/components/mqtt/translations/ru.json +++ b/homeassistant/components/mqtt/translations/ru.json @@ -21,8 +21,8 @@ "data": { "discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432" }, - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io \"{addon}\")?", - "title": "\u0411\u0440\u043e\u043a\u0435\u0440 MQTT (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io)" + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant \"{addon}\")?", + "title": "\u0411\u0440\u043e\u043a\u0435\u0440 MQTT (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)" } } }, diff --git a/homeassistant/components/mqtt/translations/tr.json b/homeassistant/components/mqtt/translations/tr.json index 1b73b94d5a4..86dce2b6ea4 100644 --- a/homeassistant/components/mqtt/translations/tr.json +++ b/homeassistant/components/mqtt/translations/tr.json @@ -1,11 +1,52 @@ { "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, "step": { + "broker": { + "data": { + "password": "Parola", + "port": "Port" + } + }, "hassio_confirm": { "data": { "discovery": "Ke\u015ffetmeyi etkinle\u015ftir" } } } + }, + "device_automation": { + "trigger_subtype": { + "turn_off": "Kapat", + "turn_on": "A\u00e7" + }, + "trigger_type": { + "button_double_press": "\" {subtype} \" \u00e7ift t\u0131kland\u0131", + "button_long_press": "\" {subtype} \" s\u00fcrekli olarak bas\u0131ld\u0131", + "button_quadruple_press": "\" {subtype} \" d\u00f6rt kez t\u0131kland\u0131", + "button_quintuple_press": "\" {subtype} \" be\u015fli t\u0131kland\u0131", + "button_short_press": "\" {subtype} \" bas\u0131ld\u0131", + "button_short_release": "\" {subtype} \" yay\u0131nland\u0131", + "button_triple_press": "\" {subtype} \" \u00fc\u00e7 kez t\u0131kland\u0131" + } + }, + "options": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "broker": { + "data": { + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/uk.json b/homeassistant/components/mqtt/translations/uk.json index 747d190a56d..f871db4aa9d 100644 --- a/homeassistant/components/mqtt/translations/uk.json +++ b/homeassistant/components/mqtt/translations/uk.json @@ -1,26 +1,84 @@ { "config": { "abort": { - "single_instance_allowed": "\u0414\u043e\u0437\u0432\u043e\u043b\u0435\u043d\u043e \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e MQTT." + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." }, "error": { - "cannot_connect": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e \u0431\u0440\u043e\u043a\u0435\u0440\u0430." + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" }, "step": { "broker": { "data": { "broker": "\u0411\u0440\u043e\u043a\u0435\u0440", - "discovery": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u043f\u043e\u0448\u0443\u043a", + "discovery": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0410\u0432\u0442\u043e\u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" }, - "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0432\u0430\u0448\u043e\u0433\u043e \u0431\u0440\u043e\u043a\u0435\u0440\u0430 MQTT." + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0437 \u0432\u0430\u0448\u0438\u043c \u0431\u0440\u043e\u043a\u0435\u0440\u043e\u043c MQTT." }, "hassio_confirm": { "data": { - "discovery": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u043f\u043e\u0448\u0443\u043a" - } + "discovery": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0410\u0432\u0442\u043e\u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432" + }, + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0431\u0440\u043e\u043a\u0435\u0440\u0430 MQTT (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io \"{addon}\")?", + "title": "\u0411\u0440\u043e\u043a\u0435\u0440 MQTT (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io)" + } + } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "\u041f\u0435\u0440\u0448\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_2": "\u0414\u0440\u0443\u0433\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_3": "\u0422\u0440\u0435\u0442\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_5": "\u041f'\u044f\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_6": "\u0428\u043e\u0441\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "turn_off": "\u0412\u0438\u043c\u043a\u043d\u0443\u0442\u0438", + "turn_on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438" + }, + "trigger_type": { + "button_double_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0438", + "button_long_press": "{subtype} \u0434\u043e\u0432\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430", + "button_long_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u0434\u043e\u0432\u0433\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f", + "button_quadruple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0447\u043e\u0442\u0438\u0440\u0438 \u0440\u0430\u0437\u0438", + "button_quintuple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u043f'\u044f\u0442\u044c \u0440\u0430\u0437\u0456\u0432", + "button_short_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430", + "button_short_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f", + "button_triple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0438" + } + }, + "options": { + "error": { + "bad_birth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0442\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f.", + "bad_will": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0442\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "broker": { + "data": { + "broker": "\u0411\u0440\u043e\u043a\u0435\u0440", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0437 \u0432\u0430\u0448\u0438\u043c \u0431\u0440\u043e\u043a\u0435\u0440\u043e\u043c MQTT." + }, + "options": { + "data": { + "birth_enable": "\u0412\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u0438 \u0442\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "birth_payload": "\u0417\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0442\u043e\u043f\u0456\u043a\u0430 \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "birth_qos": "QoS \u0442\u043e\u043f\u0456\u043a\u0430 \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "birth_retain": "\u0417\u0431\u0435\u0440\u0456\u0433\u0430\u0442\u0438 \u0442\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "birth_topic": "\u0422\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f (LWT)", + "discovery": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f", + "will_enable": "\u0412\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u0438 \u0442\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "will_payload": "\u0417\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0442\u043e\u043f\u0456\u043a\u0430 \u043f\u0440\u043e \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "will_qos": "QoS \u0442\u043e\u043f\u0456\u043a\u0430 \u043f\u0440\u043e \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "will_retain": "\u0417\u0431\u0435\u0440\u0456\u0433\u0430\u0442\u0438 \u0442\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "will_topic": "\u0422\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f (LWT)" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0438\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 MQTT." } } } diff --git a/homeassistant/components/myq/translations/de.json b/homeassistant/components/myq/translations/de.json index d5c890e4169..fafa38c7817 100644 --- a/homeassistant/components/myq/translations/de.json +++ b/homeassistant/components/myq/translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "MyQ ist bereits konfiguriert" + "already_configured": "Der Dienst ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/myq/translations/tr.json b/homeassistant/components/myq/translations/tr.json new file mode 100644 index 00000000000..7347d18bc34 --- /dev/null +++ b/homeassistant/components/myq/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "title": "MyQ A\u011f Ge\u00e7idine ba\u011flan\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/translations/uk.json b/homeassistant/components/myq/translations/uk.json new file mode 100644 index 00000000000..12f8406de12 --- /dev/null +++ b/homeassistant/components/myq/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "MyQ" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/de.json b/homeassistant/components/neato/translations/de.json index 94fcd3c4cb2..4c2fc456873 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 beachte die Dokumentation.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", "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" }, @@ -20,7 +20,7 @@ "title": "W\u00e4hle die Authentifizierungsmethode" }, "reauth_confirm": { - "title": "Wollen Sie mit der Einrichtung beginnen?" + "title": "M\u00f6chtest du mit der Einrichtung beginnen?" }, "user": { "data": { diff --git a/homeassistant/components/neato/translations/it.json b/homeassistant/components/neato/translations/it.json index 100237c33e6..95866e918c6 100644 --- a/homeassistant/components/neato/translations/it.json +++ b/homeassistant/components/neato/translations/it.json @@ -2,14 +2,14 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "authorize_url_timeout": "Timeout nella generazione dell'URL di autorizzazione.", + "authorize_url_timeout": "Tempo scaduto nel generare l'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" + "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": "La riautenticazione ha avuto successo" }, "create_entry": { - "default": "Autenticato con successo" + "default": "Autenticazione riuscita" }, "error": { "invalid_auth": "Autenticazione non valida", @@ -17,10 +17,10 @@ }, "step": { "pick_implementation": { - "title": "Scegli un metodo di autenticazione" + "title": "Scegli il metodo di autenticazione" }, "reauth_confirm": { - "title": "Vuoi cominciare la configurazione?" + "title": "Vuoi iniziare la configurazione?" }, "user": { "data": { diff --git a/homeassistant/components/neato/translations/lb.json b/homeassistant/components/neato/translations/lb.json index 44d8e4f6811..adc42ae840d 100644 --- a/homeassistant/components/neato/translations/lb.json +++ b/homeassistant/components/neato/translations/lb.json @@ -2,7 +2,10 @@ "config": { "abort": { "already_configured": "Apparat ass scho konfigur\u00e9iert", - "invalid_auth": "Ong\u00eblteg Authentifikatioun" + "authorize_url_timeout": "Z\u00e4itiwwerschreidung beim erstellen vun der Authorisatiouns URL.", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "missing_configuration": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun.", + "reauth_successful": "Re-authentifikatioun war erfollegr\u00e4ich" }, "create_entry": { "default": "Kuckt [Neato Dokumentatioun]({docs_url})." @@ -12,6 +15,12 @@ "unknown": "Onerwaarte Feeler" }, "step": { + "pick_implementation": { + "title": "Authentifikatiouns Method auswielen" + }, + "reauth_confirm": { + "title": "Soll den Ariichtungs Prozess gestart ginn?" + }, "user": { "data": { "password": "Passwuert", @@ -22,5 +31,6 @@ "title": "Neato Kont Informatiounen" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/pt.json b/homeassistant/components/neato/translations/pt.json index 0672c9af33f..48e73c763f0 100644 --- a/homeassistant/components/neato/translations/pt.json +++ b/homeassistant/components/neato/translations/pt.json @@ -2,13 +2,26 @@ "config": { "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "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})", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "create_entry": { + "default": "Autenticado com sucesso" }, "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "title": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/neato/translations/tr.json b/homeassistant/components/neato/translations/tr.json new file mode 100644 index 00000000000..53a8e0503cb --- /dev/null +++ b/homeassistant/components/neato/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7in" + }, + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "title": "Neato Hesap Bilgisi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/uk.json b/homeassistant/components/neato/translations/uk.json new file mode 100644 index 00000000000..58b56a52f6c --- /dev/null +++ b/homeassistant/components/neato/translations/uk.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "reauth_confirm": { + "title": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "vendor": "\u0412\u0438\u0440\u043e\u0431\u043d\u0438\u043a" + }, + "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u043e\u0437\u0448\u0438\u0440\u0435\u043d\u0438\u0445 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u044c.", + "title": "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 2bc328ff8f6..3925b7537b2 100644 --- a/homeassistant/components/nest/translations/de.json +++ b/homeassistant/components/nest/translations/de.json @@ -2,34 +2,43 @@ "config": { "abort": { "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL", - "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL", - "reauth_successful": "Neuathentifizierung erfolgreich", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "unknown_authorize_url_generation": "Beim Generieren einer Authentifizierungs-URL ist ein unbekannter Fehler aufgetreten" }, + "create_entry": { + "default": "Erfolgreich authentifiziert" + }, "error": { "internal_error": "Ein interner Fehler ist aufgetreten", "invalid_pin": "Ung\u00fcltiger PIN-Code", "timeout": "Ein zeit\u00fcberschreitungs Fehler ist aufgetreten", - "unknown": "Ein unbekannter Fehler ist aufgetreten" + "unknown": "Unerwarteter Fehler" }, "step": { "init": { "data": { "flow_impl": "Anbieter" }, - "description": "W\u00e4hlen, \u00fcber welchen Authentifizierungsanbieter du dich bei Nest authentifizieren m\u00f6chtest.", + "description": "W\u00e4hle die Authentifizierungsmethode", "title": "Authentifizierungsanbieter" }, "link": { "data": { - "code": "PIN Code" + "code": "PIN-Code" }, "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" }, + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" + }, "reauth_confirm": { "description": "Die Nest-Integration muss das Konto neu authentifizieren", - "title": "Integration neu authentifizieren" + "title": "Integration erneut authentifizieren" } } }, diff --git a/homeassistant/components/nest/translations/fr.json b/homeassistant/components/nest/translations/fr.json index be006913f65..03b55458e9b 100644 --- a/homeassistant/components/nest/translations/fr.json +++ b/homeassistant/components/nest/translations/fr.json @@ -5,7 +5,8 @@ "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", - "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", + "unknown_authorize_url_generation": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation." }, "create_entry": { "default": "Authentification r\u00e9ussie" diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index 958eaea039a..376437d20f0 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -5,7 +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", + "reauth_successful": "La riautenticazione ha avuto 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." }, @@ -38,7 +38,7 @@ }, "reauth_confirm": { "description": "L'integrazione di Nest deve autenticare nuovamente il tuo account", - "title": "Autentica nuovamente l'integrazione" + "title": "Reautenticare l'integrazione" } } }, diff --git a/homeassistant/components/nest/translations/lb.json b/homeassistant/components/nest/translations/lb.json index 1f0115a429b..612d1f30258 100644 --- a/homeassistant/components/nest/translations/lb.json +++ b/homeassistant/components/nest/translations/lb.json @@ -4,7 +4,12 @@ "authorize_url_fail": "Onbekannte Feeler beim gener\u00e9ieren vun der Autorisatiouns URL.", "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", "missing_configuration": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun.", - "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." + "reauth_successful": "Re-authentifikatioun war erfollegr\u00e4ich", + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech.", + "unknown_authorize_url_generation": "Onbekannte Feeler beim erstellen vun der Authorisatiouns URL." + }, + "create_entry": { + "default": "Erfollegr\u00e4ich authentifiz\u00e9iert" }, "error": { "internal_error": "Interne Feeler beim valid\u00e9ieren vum Code", diff --git a/homeassistant/components/nest/translations/pl.json b/homeassistant/components/nest/translations/pl.json index 63e45df12fa..d1147e03afc 100644 --- a/homeassistant/components/nest/translations/pl.json +++ b/homeassistant/components/nest/translations/pl.json @@ -5,6 +5,7 @@ "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", "unknown_authorize_url_generation": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji" }, @@ -34,6 +35,10 @@ }, "pick_implementation": { "title": "Wybierz metod\u0119 uwierzytelniania" + }, + "reauth_confirm": { + "description": "Integracja Nest wymaga ponownego uwierzytelnienia Twojego konta", + "title": "Ponownie uwierzytelnij integracj\u0119" } } }, diff --git a/homeassistant/components/nest/translations/pt.json b/homeassistant/components/nest/translations/pt.json index 6da647ac29b..33ff857af7e 100644 --- a/homeassistant/components/nest/translations/pt.json +++ b/homeassistant/components/nest/translations/pt.json @@ -2,7 +2,7 @@ "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": "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.", @@ -36,5 +36,10 @@ "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Movimento detectado" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/tr.json b/homeassistant/components/nest/translations/tr.json index 484cdaff6ec..003c1ccc0c2 100644 --- a/homeassistant/components/nest/translations/tr.json +++ b/homeassistant/components/nest/translations/tr.json @@ -1,9 +1,20 @@ { + "config": { + "abort": { + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "unknown_authorize_url_generation": "Yetkilendirme url'si olu\u015fturulurken bilinmeyen hata." + }, + "error": { + "unknown": "Beklenmeyen hata" + } + }, "device_automation": { "trigger_type": { "camera_motion": "Hareket alg\u0131land\u0131", "camera_person": "Ki\u015fi alg\u0131land\u0131", - "camera_sound": "Ses alg\u0131land\u0131" + "camera_sound": "Ses alg\u0131land\u0131", + "doorbell_chime": "Kap\u0131 zili bas\u0131ld\u0131" } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/uk.json b/homeassistant/components/nest/translations/uk.json new file mode 100644 index 00000000000..f2869a76f42 --- /dev/null +++ b/homeassistant/components/nest/translations/uk.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "authorize_url_fail": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "unknown_authorize_url_generation": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457." + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "error": { + "internal_error": "\u0412\u043d\u0443\u0442\u0440\u0456\u0448\u043d\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0438 \u043a\u043e\u0434\u0443.", + "invalid_pin": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 PIN-\u043a\u043e\u0434.", + "timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0438 \u043a\u043e\u0434\u0443.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "init": { + "data": { + "flow_impl": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457", + "title": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "link": { + "data": { + "code": "PIN-\u043a\u043e\u0434" + }, + "description": "[\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c]({url}), \u0449\u043e\u0431 \u043f\u0440\u0438\u0432'\u044f\u0437\u0430\u0442\u0438 \u0441\u0432\u043e\u044e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 Nest. \n \n\u041f\u0456\u0441\u043b\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457 \u0441\u043a\u043e\u043f\u0456\u044e\u0439\u0442\u0435 \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u0438\u0439 PIN-\u043a\u043e\u0434.", + "title": "\u041f\u0440\u0438\u0432'\u044f\u0437\u0430\u0442\u0438 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 Nest" + }, + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "reauth_confirm": { + "description": "\u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 Nest", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e" + } + } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u0440\u0443\u0445", + "camera_person": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c \u043b\u044e\u0434\u0438\u043d\u0438", + "camera_sound": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u0437\u0432\u0443\u043a", + "doorbell_chime": "\u041d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430 \u0434\u0432\u0435\u0440\u043d\u043e\u0433\u043e \u0434\u0437\u0432\u0456\u043d\u043a\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/de.json b/homeassistant/components/netatmo/translations/de.json index 30cfba6dfed..0be425d1e31 100644 --- a/homeassistant/components/netatmo/translations/de.json +++ b/homeassistant/components/netatmo/translations/de.json @@ -1,8 +1,10 @@ { "config": { "abort": { - "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Autorisierungs-URL.", - "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte folgen Sie der Dokumentation." + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "create_entry": { "default": "Erfolgreich authentifiziert." diff --git a/homeassistant/components/netatmo/translations/tr.json b/homeassistant/components/netatmo/translations/tr.json new file mode 100644 index 00000000000..94dd5b3fb0f --- /dev/null +++ b/homeassistant/components/netatmo/translations/tr.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + } + }, + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "Alan\u0131n ad\u0131", + "lat_ne": "Enlem Kuzey-Do\u011fu k\u00f6\u015fesi", + "lat_sw": "Enlem G\u00fcney-Bat\u0131 k\u00f6\u015fesi", + "lon_ne": "Boylam Kuzey-Do\u011fu k\u00f6\u015fesi", + "lon_sw": "Boylam G\u00fcney-Bat\u0131 k\u00f6\u015fesi", + "mode": "Hesaplama", + "show_on_map": "Haritada g\u00f6ster" + }, + "description": "Bir alan i\u00e7in genel hava durumu sens\u00f6r\u00fc yap\u0131land\u0131r\u0131n.", + "title": "Netatmo genel hava durumu sens\u00f6r\u00fc" + }, + "public_weather_areas": { + "data": { + "new_area": "Alan ad\u0131", + "weather_areas": "Hava alanlar\u0131" + }, + "description": "Genel hava durumu sens\u00f6rlerini yap\u0131land\u0131r\u0131n.", + "title": "Netatmo genel hava durumu sens\u00f6r\u00fc" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/uk.json b/homeassistant/components/netatmo/translations/uk.json new file mode 100644 index 00000000000..b8c439edfde --- /dev/null +++ b/homeassistant/components/netatmo/translations/uk.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "step": { + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + } + } + }, + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "\u041d\u0430\u0437\u0432\u0430 \u043e\u0431\u043b\u0430\u0441\u0442\u0456", + "lat_ne": "\u0428\u0438\u0440\u043e\u0442\u0430 (\u043f\u0456\u0432\u043d\u0456\u0447\u043d\u043e-\u0441\u0445\u0456\u0434\u043d\u0438\u0439 \u043a\u0443\u0442)", + "lat_sw": "\u0428\u0438\u0440\u043e\u0442\u0430 (\u044e\u0433\u043e-\u0437\u0430\u043f\u0430\u0434\u043d\u044b\u0439 \u0443\u0433\u043e\u043b)", + "lon_ne": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430 (\u043f\u0456\u0432\u043d\u0456\u0447\u043d\u043e-\u0441\u0445\u0456\u0434\u043d\u0438\u0439 \u043a\u0443\u0442)", + "lon_sw": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430 (\u043f\u0456\u0432\u0434\u0435\u043d\u043d\u043e-\u0437\u0430\u0445\u0456\u0434\u043d\u0438\u0439 \u043a\u0443\u0442)", + "mode": "\u0420\u043e\u0437\u0440\u0430\u0445\u0443\u043d\u043e\u043a", + "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u043d\u0430 \u043c\u0430\u043f\u0456" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0437\u0430\u0433\u0430\u043b\u044c\u043d\u043e\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0434\u0430\u0442\u0447\u0438\u043a \u043f\u043e\u0433\u043e\u0434\u0438 \u0434\u043b\u044f \u043e\u0431\u043b\u0430\u0441\u0442\u0456", + "title": "\u0417\u0430\u0433\u0430\u043b\u044c\u043d\u043e\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0434\u0430\u0442\u0447\u0438\u043a \u043f\u043e\u0433\u043e\u0434\u0438 Netatmo" + }, + "public_weather_areas": { + "data": { + "new_area": "\u041d\u0430\u0437\u0432\u0430 \u043e\u0431\u043b\u0430\u0441\u0442\u0456", + "weather_areas": "\u041f\u043e\u0433\u043e\u0434\u043d\u0456 \u043e\u0431\u043b\u0430\u0441\u0442\u0456" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u0430\u0433\u0430\u043b\u044c\u043d\u043e\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u0434\u0430\u0442\u0447\u0438\u043a\u0456\u0432 \u043f\u043e\u0433\u043e\u0434\u0438", + "title": "\u0417\u0430\u0433\u0430\u043b\u044c\u043d\u043e\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0434\u0430\u0442\u0447\u0438\u043a \u043f\u043e\u0433\u043e\u0434\u0438 Netatmo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/de.json b/homeassistant/components/nexia/translations/de.json index 0ff4da3b2e1..f2220f828e8 100644 --- a/homeassistant/components/nexia/translations/de.json +++ b/homeassistant/components/nexia/translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Dieses Nexia Home ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/nexia/translations/tr.json b/homeassistant/components/nexia/translations/tr.json new file mode 100644 index 00000000000..47f3d931c46 --- /dev/null +++ b/homeassistant/components/nexia/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "title": "Mynexia.com'a ba\u011flan\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/uk.json b/homeassistant/components/nexia/translations/uk.json new file mode 100644 index 00000000000..8cb2aec836a --- /dev/null +++ b/homeassistant/components/nexia/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e mynexia.com" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/de.json b/homeassistant/components/nightscout/translations/de.json index 8581b04099d..510d57ce45f 100644 --- a/homeassistant/components/nightscout/translations/de.json +++ b/homeassistant/components/nightscout/translations/de.json @@ -1,12 +1,18 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "flow_title": "Nightscout", "step": { "user": { "data": { + "api_key": "API-Schl\u00fcssel", "url": "URL" } } diff --git a/homeassistant/components/nightscout/translations/no.json b/homeassistant/components/nightscout/translations/no.json index db7b8f811ca..d68fe45c684 100644 --- a/homeassistant/components/nightscout/translations/no.json +++ b/homeassistant/components/nightscout/translations/no.json @@ -15,7 +15,7 @@ "api_key": "API-n\u00f8kkel", "url": "URL" }, - "description": "- URL: adressen til din nattscout-forekomst. Dvs: https://myhomeassistant.duckdns.org:5423 \n - API-n\u00f8kkel (valgfritt): Bruk bare hvis forekomsten din er beskyttet (auth_default_roles! = Lesbar).", + "description": "- URL: Adressen til din nattscout-forekomst. F. Eks: https://myhomeassistant.duckdns.org:5423 \n- API-n\u00f8kkel (valgfritt): Bruk bare hvis forekomsten din er beskyttet (auth_default_roles! = readable).", "title": "Skriv inn informasjon om Nightscout-serveren." } } diff --git a/homeassistant/components/nightscout/translations/tr.json b/homeassistant/components/nightscout/translations/tr.json index 585aace899d..95f36a4d124 100644 --- a/homeassistant/components/nightscout/translations/tr.json +++ b/homeassistant/components/nightscout/translations/tr.json @@ -1,11 +1,18 @@ { "config": { - "error": { - "cannot_connect": "Ba\u011flan\u0131lamad\u0131" + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, + "error": { + "cannot_connect": "Ba\u011flan\u0131lamad\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "Nightscout", "step": { "user": { "data": { + "api_key": "API Anahtar\u0131", "url": "URL" } } diff --git a/homeassistant/components/nightscout/translations/uk.json b/homeassistant/components/nightscout/translations/uk.json new file mode 100644 index 00000000000..6504b00eb88 --- /dev/null +++ b/homeassistant/components/nightscout/translations/uk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "Nightscout", + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430" + }, + "description": "- URL: \u0430\u0434\u0440\u0435\u0441\u0430 \u0412\u0430\u0448\u043e\u0433\u043e Nightscout. \u041d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: https://myhomeassistant.duckdns.org:5423\n - \u041a\u043b\u044e\u0447 API (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e): \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435, \u043b\u0438\u0448\u0435 \u044f\u043a\u0449\u043e \u0412\u0430\u0448 Nightcout \u0437\u0430\u0445\u0438\u0449\u0435\u043d\u0438\u0439 (auth_default_roles != readable).", + "title": "Nightscout" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notify/translations/uk.json b/homeassistant/components/notify/translations/uk.json index 86821a3e50f..d87752255d5 100644 --- a/homeassistant/components/notify/translations/uk.json +++ b/homeassistant/components/notify/translations/uk.json @@ -1,3 +1,3 @@ { - "title": "\u041f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u043d\u044f" + "title": "\u0421\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u043d\u044f" } \ No newline at end of file diff --git a/homeassistant/components/notion/translations/de.json b/homeassistant/components/notion/translations/de.json index f322826c45b..0b421911aa7 100644 --- a/homeassistant/components/notion/translations/de.json +++ b/homeassistant/components/notion/translations/de.json @@ -1,9 +1,10 @@ { "config": { "abort": { - "already_configured": "Dieser Benutzername wird bereits benutzt." + "already_configured": "Konto wurde bereits konfiguriert" }, "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", "no_devices": "Keine Ger\u00e4te im Konto gefunden" }, "step": { diff --git a/homeassistant/components/notion/translations/tr.json b/homeassistant/components/notion/translations/tr.json index 8966b79df1b..f89e3fb7533 100644 --- a/homeassistant/components/notion/translations/tr.json +++ b/homeassistant/components/notion/translations/tr.json @@ -1,7 +1,19 @@ { "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "no_devices": "Hesapta cihaz bulunamad\u0131" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/notion/translations/uk.json b/homeassistant/components/notion/translations/uk.json new file mode 100644 index 00000000000..6dc969c3609 --- /dev/null +++ b/homeassistant/components/notion/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "no_devices": "\u041d\u0435\u043c\u0430\u0454 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432, \u043f\u043e\u0432'\u044f\u0437\u0430\u043d\u0438\u0445 \u0437 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u043c \u0437\u0430\u043f\u0438\u0441\u043e\u043c." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "Notion" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/de.json b/homeassistant/components/nuheat/translations/de.json index 52c30681efc..8599f7fe1b5 100644 --- a/homeassistant/components/nuheat/translations/de.json +++ b/homeassistant/components/nuheat/translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Der Thermostat ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "invalid_thermostat": "Die Seriennummer des Thermostats ist ung\u00fcltig.", "unknown": "Unerwarteter Fehler" diff --git a/homeassistant/components/nuheat/translations/tr.json b/homeassistant/components/nuheat/translations/tr.json new file mode 100644 index 00000000000..5123f1c7d9a --- /dev/null +++ b/homeassistant/components/nuheat/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_thermostat": "Termostat seri numaras\u0131 ge\u00e7ersiz.", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "serial_number": "Termostat\u0131n seri numaras\u0131.", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/uk.json b/homeassistant/components/nuheat/translations/uk.json new file mode 100644 index 00000000000..21be3968eb7 --- /dev/null +++ b/homeassistant/components/nuheat/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "invalid_thermostat": "\u0421\u0435\u0440\u0456\u0439\u043d\u0438\u0439 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u0430 \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "serial_number": "\u0421\u0435\u0440\u0456\u0439\u043d\u0438\u0439 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u0430", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0441\u0435\u0440\u0456\u0439\u043d\u0438\u0439 \u043d\u043e\u043c\u0435\u0440 \u0430\u0431\u043e ID \u0412\u0430\u0448\u043e\u0433\u043e \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u0430, \u043d\u0430 \u0441\u0430\u0439\u0442\u0456 https://MyNuHeat.com.", + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/ca.json b/homeassistant/components/nuki/translations/ca.json new file mode 100644 index 00000000000..a08308e7897 --- /dev/null +++ b/homeassistant/components/nuki/translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port", + "token": "Token d'acc\u00e9s" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/cs.json b/homeassistant/components/nuki/translations/cs.json new file mode 100644 index 00000000000..349c92805cf --- /dev/null +++ b/homeassistant/components/nuki/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port", + "token": "P\u0159\u00edstupov\u00fd token" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/en.json b/homeassistant/components/nuki/translations/en.json index 70ae9c6a1fe..135e8de2b2f 100644 --- a/homeassistant/components/nuki/translations/en.json +++ b/homeassistant/components/nuki/translations/en.json @@ -2,15 +2,15 @@ "config": { "error": { "cannot_connect": "Failed to connect", - "invalid_auth": "Could not login with provided token", - "unknown": "Unknown error" + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" }, "step": { "user": { "data": { - "token": "Access Token", "host": "Host", - "port": "Port" + "port": "Port", + "token": "Access Token" } } } diff --git a/homeassistant/components/nuki/translations/et.json b/homeassistant/components/nuki/translations/et.json new file mode 100644 index 00000000000..750afff003c --- /dev/null +++ b/homeassistant/components/nuki/translations/et.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port", + "token": "Juurdep\u00e4\u00e4sut\u00f5end" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/it.json b/homeassistant/components/nuki/translations/it.json new file mode 100644 index 00000000000..899093e1f41 --- /dev/null +++ b/homeassistant/components/nuki/translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta", + "token": "Token di accesso" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/no.json b/homeassistant/components/nuki/translations/no.json new file mode 100644 index 00000000000..8cdbac230d7 --- /dev/null +++ b/homeassistant/components/nuki/translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "port": "Port", + "token": "Tilgangstoken" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/pl.json b/homeassistant/components/nuki/translations/pl.json new file mode 100644 index 00000000000..77a7c31ee34 --- /dev/null +++ b/homeassistant/components/nuki/translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port", + "token": "Token dost\u0119pu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/ru.json b/homeassistant/components/nuki/translations/ru.json new file mode 100644 index 00000000000..bad9f35c076 --- /dev/null +++ b/homeassistant/components/nuki/translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", + "token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/tr.json b/homeassistant/components/nuki/translations/tr.json new file mode 100644 index 00000000000..ba6a496fa4c --- /dev/null +++ b/homeassistant/components/nuki/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port", + "token": "Eri\u015fim Belirteci" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/zh-Hant.json b/homeassistant/components/nuki/translations/zh-Hant.json new file mode 100644 index 00000000000..662d7ed6ed9 --- /dev/null +++ b/homeassistant/components/nuki/translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0", + "token": "\u5b58\u53d6\u5bc6\u9470" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/ca.json b/homeassistant/components/number/translations/ca.json new file mode 100644 index 00000000000..0058f01aac0 --- /dev/null +++ b/homeassistant/components/number/translations/ca.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "Estableix el valor de {entity_name}" + } + }, + "title": "N\u00famero" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/cs.json b/homeassistant/components/number/translations/cs.json new file mode 100644 index 00000000000..a6810f08c61 --- /dev/null +++ b/homeassistant/components/number/translations/cs.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "Nastavit hodnotu pro {entity_name}" + } + }, + "title": "\u010c\u00edslo" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/en.json b/homeassistant/components/number/translations/en.json new file mode 100644 index 00000000000..4e3fe6536b3 --- /dev/null +++ b/homeassistant/components/number/translations/en.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "Set value for {entity_name}" + } + }, + "title": "Number" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/et.json b/homeassistant/components/number/translations/et.json new file mode 100644 index 00000000000..36958c0fc77 --- /dev/null +++ b/homeassistant/components/number/translations/et.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "Olemi {entity_name} v\u00e4\u00e4rtuse m\u00e4\u00e4ramine" + } + }, + "title": "Number" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/it.json b/homeassistant/components/number/translations/it.json new file mode 100644 index 00000000000..135467cea9b --- /dev/null +++ b/homeassistant/components/number/translations/it.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "Imposta il valore per {entity_name}" + } + }, + "title": "Numero" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/no.json b/homeassistant/components/number/translations/no.json new file mode 100644 index 00000000000..ad82c4ac6d1 --- /dev/null +++ b/homeassistant/components/number/translations/no.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "Angi verdi for {entity_name}" + } + }, + "title": "Nummer" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/pl.json b/homeassistant/components/number/translations/pl.json new file mode 100644 index 00000000000..93d5dd04599 --- /dev/null +++ b/homeassistant/components/number/translations/pl.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "ustaw warto\u015b\u0107 dla {entity_name}" + } + }, + "title": "Number" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/ru.json b/homeassistant/components/number/translations/ru.json new file mode 100644 index 00000000000..5e250b4e2db --- /dev/null +++ b/homeassistant/components/number/translations/ru.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0434\u043b\u044f {entity_name}" + } + }, + "title": "\u0427\u0438\u0441\u043b\u043e" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/tr.json b/homeassistant/components/number/translations/tr.json new file mode 100644 index 00000000000..dfdbd905317 --- /dev/null +++ b/homeassistant/components/number/translations/tr.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "{entity_name} i\u00e7in de\u011fer ayarlay\u0131n" + } + }, + "title": "Numara" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/zh-Hant.json b/homeassistant/components/number/translations/zh-Hant.json new file mode 100644 index 00000000000..d36f751682d --- /dev/null +++ b/homeassistant/components/number/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "{entity_name} \u8a2d\u5b9a\u503c" + } + }, + "title": "\u865f\u78bc" +} \ No newline at end of file diff --git a/homeassistant/components/nut/translations/de.json b/homeassistant/components/nut/translations/de.json index 793ab5bfa7c..50d37fa8ec4 100644 --- a/homeassistant/components/nut/translations/de.json +++ b/homeassistant/components/nut/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/nut/translations/tr.json b/homeassistant/components/nut/translations/tr.json new file mode 100644 index 00000000000..b383d765619 --- /dev/null +++ b/homeassistant/components/nut/translations/tr.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "resources": { + "data": { + "resources": "Kaynaklar" + } + }, + "ups": { + "data": { + "alias": "Takma ad" + } + }, + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "resources": "Kaynaklar" + }, + "description": "Sens\u00f6r Kaynaklar\u0131'n\u0131 se\u00e7in." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/translations/uk.json b/homeassistant/components/nut/translations/uk.json new file mode 100644 index 00000000000..b25fe854560 --- /dev/null +++ b/homeassistant/components/nut/translations/uk.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "resources": { + "data": { + "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u0438" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0440\u0435\u0441\u0443\u0440\u0441\u0438 \u0434\u043b\u044f \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443" + }, + "ups": { + "data": { + "alias": "\u041f\u0441\u0435\u0432\u0434\u043e\u043d\u0456\u043c", + "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u0438" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c UPS \u0434\u043b\u044f \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 NUT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u0438", + "scan_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0440\u0435\u0441\u0443\u0440\u0441\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0456\u0432." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/translations/de.json b/homeassistant/components/nws/translations/de.json index 1461d86b2e5..3d409bf885b 100644 --- a/homeassistant/components/nws/translations/de.json +++ b/homeassistant/components/nws/translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Der Dienst ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/nws/translations/tr.json b/homeassistant/components/nws/translations/tr.json new file mode 100644 index 00000000000..8f51593aedb --- /dev/null +++ b/homeassistant/components/nws/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam" + }, + "description": "Bir METAR istasyon kodu belirtilmezse, en yak\u0131n istasyonu bulmak i\u00e7in enlem ve boylam kullan\u0131lacakt\u0131r. \u015eimdilik bir API Anahtar\u0131 herhangi bir \u015fey olabilir. Ge\u00e7erli bir e-posta adresi kullanman\u0131z tavsiye edilir." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/translations/uk.json b/homeassistant/components/nws/translations/uk.json new file mode 100644 index 00000000000..1e6886540ae --- /dev/null +++ b/homeassistant/components/nws/translations/uk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", + "station": "\u041a\u043e\u0434 \u0441\u0442\u0430\u043d\u0446\u0456\u0457 METAR" + }, + "description": "\u042f\u043a\u0449\u043e \u043a\u043e\u0434 \u0441\u0442\u0430\u043d\u0446\u0456\u0457 METAR \u043d\u0435 \u0432\u043a\u0430\u0437\u0430\u043d\u043e, \u0434\u043b\u044f \u043f\u043e\u0448\u0443\u043a\u0443 \u043d\u0430\u0439\u0431\u043b\u0438\u0436\u0447\u043e\u0457 \u0441\u0442\u0430\u043d\u0446\u0456\u0457 \u0431\u0443\u0434\u0443\u0442\u044c \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0448\u0438\u0440\u043e\u0442\u0430 \u0456 \u0434\u043e\u0432\u0433\u043e\u0442\u0430. \u041d\u0430 \u0434\u0430\u043d\u0438\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u043a\u043b\u044e\u0447 API \u043c\u043e\u0436\u0435 \u0431\u0443\u0442\u0438 \u0431\u0443\u0434\u044c-\u044f\u043a\u0438\u043c. \u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0454\u0442\u044c\u0441\u044f \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0434\u0456\u044e\u0447\u0443 \u0430\u0434\u0440\u0435\u0441\u0443 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438.", + "title": "National Weather Service" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/de.json b/homeassistant/components/nzbget/translations/de.json index 018f3870c58..529eff3d9a2 100644 --- a/homeassistant/components/nzbget/translations/de.json +++ b/homeassistant/components/nzbget/translations/de.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "unknown": "Unerwarteter Fehler" }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, "flow_title": "NZBGet: {name}", "step": { "user": { @@ -11,7 +15,9 @@ "name": "Name", "password": "Passwort", "port": "Port", - "username": "Benutzername" + "ssl": "Nutzt ein SSL-Zertifikat", + "username": "Benutzername", + "verify_ssl": "SSL-Zertifikat verfizieren" }, "title": "Mit NZBGet verbinden" } diff --git a/homeassistant/components/nzbget/translations/tr.json b/homeassistant/components/nzbget/translations/tr.json new file mode 100644 index 00000000000..63b6c489018 --- /dev/null +++ b/homeassistant/components/nzbget/translations/tr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "G\u00fcncelle\u015ftirme s\u0131kl\u0131\u011f\u0131 (saniye)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/uk.json b/homeassistant/components/nzbget/translations/uk.json new file mode 100644 index 00000000000..eba15cca19c --- /dev/null +++ b/homeassistant/components/nzbget/translations/uk.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "flow_title": "NZBGet: {name}", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" + }, + "title": "NZBGet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/de.json b/homeassistant/components/omnilogic/translations/de.json index 38215675701..4378d39912d 100644 --- a/homeassistant/components/omnilogic/translations/de.json +++ b/homeassistant/components/omnilogic/translations/de.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/omnilogic/translations/tr.json b/homeassistant/components/omnilogic/translations/tr.json new file mode 100644 index 00000000000..ab93b71de84 --- /dev/null +++ b/homeassistant/components/omnilogic/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/uk.json b/homeassistant/components/omnilogic/translations/uk.json new file mode 100644 index 00000000000..21ebf6f4faf --- /dev/null +++ b/homeassistant/components/omnilogic/translations/uk.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "polling_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onboarding/translations/uk.json b/homeassistant/components/onboarding/translations/uk.json new file mode 100644 index 00000000000..595726cbd34 --- /dev/null +++ b/homeassistant/components/onboarding/translations/uk.json @@ -0,0 +1,7 @@ +{ + "area": { + "bedroom": "\u0421\u043f\u0430\u043b\u044c\u043d\u044f", + "kitchen": "\u041a\u0443\u0445\u043d\u044f", + "living_room": "\u0412\u0456\u0442\u0430\u043b\u044c\u043d\u044f" + } +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/ca.json b/homeassistant/components/ondilo_ico/translations/ca.json new file mode 100644 index 00000000000..77453bda398 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/ca.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "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." + }, + "create_entry": { + "default": "Autenticaci\u00f3 exitosa" + }, + "step": { + "pick_implementation": { + "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" + } + } + }, + "title": "Ondilo ICO" +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/cs.json b/homeassistant/components/ondilo_ico/translations/cs.json new file mode 100644 index 00000000000..bcb8849839c --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/cs.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", + "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace." + }, + "create_entry": { + "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno" + }, + "step": { + "pick_implementation": { + "title": "Vyberte metodu ov\u011b\u0159en\u00ed" + } + } + }, + "title": "Ondilo ICO" +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/de.json b/homeassistant/components/ondilo_ico/translations/de.json new file mode 100644 index 00000000000..5bab6ed132b --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/de.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen." + }, + "create_entry": { + "default": "Erfolgreich authentifiziert" + }, + "step": { + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/en.json b/homeassistant/components/ondilo_ico/translations/en.json index e3849fc17a3..c88a152ef81 100644 --- a/homeassistant/components/ondilo_ico/translations/en.json +++ b/homeassistant/components/ondilo_ico/translations/en.json @@ -12,5 +12,6 @@ "title": "Pick Authentication Method" } } - } + }, + "title": "Ondilo ICO" } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/es.json b/homeassistant/components/ondilo_ico/translations/es.json new file mode 100644 index 00000000000..2394c610796 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/es.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "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." + }, + "create_entry": { + "default": "Autenticado correctamente" + }, + "step": { + "pick_implementation": { + "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" + } + } + }, + "title": "Ondilo ICO" +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/et.json b/homeassistant/components/ondilo_ico/translations/et.json new file mode 100644 index 00000000000..132e9849cf1 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/et.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp", + "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni." + }, + "create_entry": { + "default": "Tuvastamine \u00f5nnestus" + }, + "step": { + "pick_implementation": { + "title": "Vali tuvastusmeetod" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/it.json b/homeassistant/components/ondilo_ico/translations/it.json new file mode 100644 index 00000000000..cd75684a437 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/it.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", + "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione." + }, + "create_entry": { + "default": "Autenticazione riuscita" + }, + "step": { + "pick_implementation": { + "title": "Scegli il metodo di autenticazione" + } + } + }, + "title": "Ondilo ICO" +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/lb.json b/homeassistant/components/ondilo_ico/translations/lb.json new file mode 100644 index 00000000000..d9a5cc7482a --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/lb.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Z\u00e4itiwwerschreidung beim erstellen vun der Authorisatiouns URL.", + "missing_configuration": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun." + }, + "create_entry": { + "default": "Erfollegr\u00e4ich authentifiz\u00e9iert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/no.json b/homeassistant/components/ondilo_ico/translations/no.json new file mode 100644 index 00000000000..4a06b93d045 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/no.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen" + }, + "create_entry": { + "default": "Vellykket godkjenning" + }, + "step": { + "pick_implementation": { + "title": "Velg godkjenningsmetode" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/pl.json b/homeassistant/components/ondilo_ico/translations/pl.json new file mode 100644 index 00000000000..f3aa08a250f --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/pl.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." + }, + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono" + }, + "step": { + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelniania" + } + } + }, + "title": "Ondilo ICO" +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/ru.json b/homeassistant/components/ondilo_ico/translations/ru.json new file mode 100644 index 00000000000..56bb2d342b7 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/ru.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "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." + }, + "create_entry": { + "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "step": { + "pick_implementation": { + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + } + } + }, + "title": "Ondilo ICO" +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/tr.json b/homeassistant/components/ondilo_ico/translations/tr.json new file mode 100644 index 00000000000..96722757365 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/tr.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7in" + } + } + }, + "title": "Ondilo ICO" +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/uk.json b/homeassistant/components/ondilo_ico/translations/uk.json new file mode 100644 index 00000000000..31e5834b027 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438." + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "step": { + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + } + } + }, + "title": "Ondilo ICO" +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/zh-Hant.json b/homeassistant/components/ondilo_ico/translations/zh-Hant.json new file mode 100644 index 00000000000..ea1902b3295 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/zh-Hant.json @@ -0,0 +1,17 @@ +{ + "config": { + "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" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u8a8d\u8b49" + }, + "step": { + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + } + } + }, + "title": "Ondilo ICO" +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/de.json b/homeassistant/components/onewire/translations/de.json index 3cc9f9cfc68..d3ed8137da3 100644 --- a/homeassistant/components/onewire/translations/de.json +++ b/homeassistant/components/onewire/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_path": "Verzeichnis nicht gefunden." diff --git a/homeassistant/components/onewire/translations/tr.json b/homeassistant/components/onewire/translations/tr.json new file mode 100644 index 00000000000..f59da2ab7e7 --- /dev/null +++ b/homeassistant/components/onewire/translations/tr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_path": "Dizin bulunamad\u0131." + }, + "step": { + "owserver": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + }, + "user": { + "data": { + "type": "Ba\u011flant\u0131 t\u00fcr\u00fc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/uk.json b/homeassistant/components/onewire/translations/uk.json new file mode 100644 index 00000000000..9c9705d2993 --- /dev/null +++ b/homeassistant/components/onewire/translations/uk.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_path": "\u041a\u0430\u0442\u0430\u043b\u043e\u0433 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e." + }, + "step": { + "owserver": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e owserver" + }, + "user": { + "data": { + "type": "\u0422\u0438\u043f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + }, + "title": "1-Wire" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/de.json b/homeassistant/components/onvif/translations/de.json index 25984ecf3e1..5289f6479cc 100644 --- a/homeassistant/components/onvif/translations/de.json +++ b/homeassistant/components/onvif/translations/de.json @@ -1,12 +1,15 @@ { "config": { "abort": { - "already_configured": "Das ONVIF-Ger\u00e4t ist bereits konfiguriert.", - "already_in_progress": "Der Konfigurationsfluss f\u00fcr das ONVIF-Ger\u00e4t wird bereits ausgef\u00fchrt.", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "no_h264": "Es waren keine H264-Streams verf\u00fcgbar. \u00dcberpr\u00fcfen Sie die Profilkonfiguration auf Ihrem Ger\u00e4t.", "no_mac": "Die eindeutige ID f\u00fcr das ONVIF-Ger\u00e4t konnte nicht konfiguriert werden.", "onvif_error": "Fehler beim Einrichten des ONVIF-Ger\u00e4ts. \u00dcberpr\u00fcfen Sie die Protokolle auf weitere Informationen." }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, "step": { "auth": { "data": { diff --git a/homeassistant/components/onvif/translations/tr.json b/homeassistant/components/onvif/translations/tr.json index 4e3ad18a60d..683dfbe7b92 100644 --- a/homeassistant/components/onvif/translations/tr.json +++ b/homeassistant/components/onvif/translations/tr.json @@ -1,8 +1,42 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, "step": { + "auth": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + }, + "configure_profile": { + "data": { + "include": "Kamera varl\u0131\u011f\u0131 olu\u015ftur" + }, + "title": "Profilleri Yap\u0131land\u0131r" + }, + "device": { + "data": { + "host": "Ke\u015ffedilen ONVIF cihaz\u0131n\u0131 se\u00e7in" + }, + "title": "ONVIF cihaz\u0131n\u0131 se\u00e7in" + }, + "manual_input": { + "data": { + "host": "Ana Bilgisayar", + "name": "Ad", + "port": "Port" + }, + "title": "ONVIF cihaz\u0131n\u0131 yap\u0131land\u0131r\u0131n" + }, "user": { - "description": "G\u00f6nder d\u00fc\u011fmesine t\u0131klad\u0131\u011f\u0131n\u0131zda, Profil S'yi destekleyen ONVIF cihazlar\u0131 i\u00e7in a\u011f\u0131n\u0131zda arama yapaca\u011f\u0131z. \n\n Baz\u0131 \u00fcreticiler varsay\u0131lan olarak ONVIF'i devre d\u0131\u015f\u0131 b\u0131rakmaya ba\u015flad\u0131. L\u00fctfen kameran\u0131z\u0131n yap\u0131land\u0131rmas\u0131nda ONVIF'in etkinle\u015ftirildi\u011finden emin olun." + "description": "G\u00f6nder d\u00fc\u011fmesine t\u0131klad\u0131\u011f\u0131n\u0131zda, Profil S'yi destekleyen ONVIF cihazlar\u0131 i\u00e7in a\u011f\u0131n\u0131zda arama yapaca\u011f\u0131z. \n\n Baz\u0131 \u00fcreticiler varsay\u0131lan olarak ONVIF'i devre d\u0131\u015f\u0131 b\u0131rakmaya ba\u015flad\u0131. L\u00fctfen kameran\u0131z\u0131n yap\u0131land\u0131rmas\u0131nda ONVIF'in etkinle\u015ftirildi\u011finden emin olun.", + "title": "ONVIF cihaz kurulumu" } } }, diff --git a/homeassistant/components/onvif/translations/uk.json b/homeassistant/components/onvif/translations/uk.json new file mode 100644 index 00000000000..82a816add04 --- /dev/null +++ b/homeassistant/components/onvif/translations/uk.json @@ -0,0 +1,59 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "no_h264": "\u041d\u0435\u043c\u0430\u0454 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u043f\u043e\u0442\u043e\u043a\u0456\u0432 H264. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043d\u0430 \u0441\u0432\u043e\u0454\u043c\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457.", + "no_mac": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0443\u043d\u0456\u043a\u0430\u043b\u044c\u043d\u0438\u0439 \u0456\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0434\u043b\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e.", + "onvif_error": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043b\u043e\u0433\u0438 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "auth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "\u0423\u0432\u0456\u0439\u0442\u0438" + }, + "configure_profile": { + "data": { + "include": "\u0421\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043e\u0431'\u0454\u043a\u0442 \u043a\u0430\u043c\u0435\u0440\u0438" + }, + "description": "\u0421\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043e\u0431'\u0454\u043a\u0442 \u043a\u0430\u043c\u0435\u0440\u0438 \u0434\u043b\u044f {profile} \u0437 \u0440\u043e\u0437\u0434\u0456\u043b\u044c\u043d\u043e\u044e \u0437\u0434\u0430\u0442\u043d\u0456\u0441\u0442\u044e {resolution}?", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u043e\u0444\u0456\u043b\u0456\u0432" + }, + "device": { + "data": { + "host": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 ONVIF" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 ONVIF" + }, + "manual_input": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e ONVIF" + }, + "user": { + "description": "\u041a\u043e\u043b\u0438 \u0412\u0438 \u043d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u041d\u0430\u0434\u0456\u0441\u043b\u0430\u0442\u0438, \u043f\u043e\u0447\u043d\u0435\u0442\u044c\u0441\u044f \u043f\u043e\u0448\u0443\u043a \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 ONVIF, \u044f\u043a\u0456 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u044e\u0442\u044c Profile S. \n\n\u0414\u0435\u044f\u043a\u0456 \u0432\u0438\u0440\u043e\u0431\u043d\u0438\u043a\u0438 \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0437\u0430 \u0443\u043c\u043e\u0432\u0447\u0430\u043d\u043d\u044f\u043c \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0430\u044e\u0442\u044c ONVIF. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e ONVIF \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e \u0432 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u0445 \u0412\u0430\u0448\u043e\u0457 \u043a\u0430\u043c\u0435\u0440\u0438.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e ONVIF" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "\u0414\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u0438 FFMPEG", + "rtsp_transport": "\u0422\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u0438\u0439 \u043c\u0435\u0445\u0430\u043d\u0456\u0437\u043c RTSP" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e ONVIF" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/de.json b/homeassistant/components/opentherm_gw/translations/de.json index 6e8d02bc792..36b76592945 100644 --- a/homeassistant/components/opentherm_gw/translations/de.json +++ b/homeassistant/components/opentherm_gw/translations/de.json @@ -1,7 +1,7 @@ { "config": { "error": { - "already_configured": "Gateway bereits konfiguriert", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen", "id_exists": "Gateway-ID ist bereits vorhanden" }, diff --git a/homeassistant/components/opentherm_gw/translations/tr.json b/homeassistant/components/opentherm_gw/translations/tr.json new file mode 100644 index 00000000000..507b71ede5b --- /dev/null +++ b/homeassistant/components/opentherm_gw/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "init": { + "data": { + "device": "Yol veya URL" + }, + "title": "OpenTherm A\u011f Ge\u00e7idi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/uk.json b/homeassistant/components/opentherm_gw/translations/uk.json new file mode 100644 index 00000000000..af769927113 --- /dev/null +++ b/homeassistant/components/opentherm_gw/translations/uk.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "id_exists": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0448\u043b\u044e\u0437\u0443 \u0432\u0436\u0435 \u0456\u0441\u043d\u0443\u0454." + }, + "step": { + "init": { + "data": { + "device": "\u0428\u043b\u044f\u0445 \u0430\u0431\u043e URL-\u0430\u0434\u0440\u0435\u0441\u0430", + "id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "title": "OpenTherm" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043f\u0456\u0434\u043b\u043e\u0433\u0438", + "precision": "\u0422\u043e\u0447\u043d\u0456\u0441\u0442\u044c" + }, + "description": "\u0414\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0434\u043b\u044f \u0448\u043b\u044e\u0437\u0443 Opentherm" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/de.json b/homeassistant/components/openuv/translations/de.json index fae3f0f0620..88f9e69a5b6 100644 --- a/homeassistant/components/openuv/translations/de.json +++ b/homeassistant/components/openuv/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Diese Koordinaten sind bereits registriert." + "already_configured": "Standort ist bereits konfiguriert" }, "error": { "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" diff --git a/homeassistant/components/openuv/translations/tr.json b/homeassistant/components/openuv/translations/tr.json new file mode 100644 index 00000000000..241c588f691 --- /dev/null +++ b/homeassistant/components/openuv/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/uk.json b/homeassistant/components/openuv/translations/uk.json index fef350a3f3c..bd29fe692e1 100644 --- a/homeassistant/components/openuv/translations/uk.json +++ b/homeassistant/components/openuv/translations/uk.json @@ -1,13 +1,20 @@ { "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + }, + "error": { + "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API" + }, "step": { "user": { "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", "elevation": "\u0412\u0438\u0441\u043e\u0442\u0430", "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430" }, - "title": "\u0417\u0430\u043f\u043e\u0432\u043d\u0456\u0442\u044c \u0432\u0430\u0448\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e" + "title": "OpenUV" } } } diff --git a/homeassistant/components/openweathermap/translations/de.json b/homeassistant/components/openweathermap/translations/de.json index 239b47e2d3e..cac601b71d3 100644 --- a/homeassistant/components/openweathermap/translations/de.json +++ b/homeassistant/components/openweathermap/translations/de.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "already_configured": "Standort ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" }, "step": { "user": { diff --git a/homeassistant/components/openweathermap/translations/tr.json b/homeassistant/components/openweathermap/translations/tr.json new file mode 100644 index 00000000000..0f845a4df73 --- /dev/null +++ b/homeassistant/components/openweathermap/translations/tr.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam", + "mode": "Mod" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "Mod" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/uk.json b/homeassistant/components/openweathermap/translations/uk.json new file mode 100644 index 00000000000..7a39cfa078e --- /dev/null +++ b/homeassistant/components/openweathermap/translations/uk.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API" + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "language": "\u041c\u043e\u0432\u0430", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", + "mode": "\u0420\u0435\u0436\u0438\u043c", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 OpenWeatherMap. \u0414\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u043a\u043b\u044e\u0447\u0430 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043d\u0430 https://openweathermap.org/appid.", + "title": "OpenWeatherMap" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "\u041c\u043e\u0432\u0430", + "mode": "\u0420\u0435\u0436\u0438\u043c" + } + } + } + } +} \ 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 3bd083e4839..761f6a7d247 100644 --- a/homeassistant/components/ovo_energy/translations/de.json +++ b/homeassistant/components/ovo_energy/translations/de.json @@ -1,8 +1,11 @@ { "config": { "error": { - "cannot_connect": "Verbindungsfehler" + "already_configured": "Konto wurde bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, + "flow_title": "OVO Energy: {username}", "step": { "reauth": { "data": { @@ -13,7 +16,8 @@ "data": { "password": "Passwort", "username": "Benutzername" - } + }, + "title": "Ovo Energy Account hinzuf\u00fcgen" } } } diff --git a/homeassistant/components/ovo_energy/translations/fr.json b/homeassistant/components/ovo_energy/translations/fr.json index 86719e87df4..351e20641aa 100644 --- a/homeassistant/components/ovo_energy/translations/fr.json +++ b/homeassistant/components/ovo_energy/translations/fr.json @@ -1,11 +1,16 @@ { "config": { "error": { - "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9", + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide" }, "step": { + "reauth": { + "data": { + "password": "Mot de passe" + } + }, "user": { "data": { "password": "Mot de passe", diff --git a/homeassistant/components/ovo_energy/translations/lb.json b/homeassistant/components/ovo_energy/translations/lb.json index 0e007924b6a..b27b7d9702c 100644 --- a/homeassistant/components/ovo_energy/translations/lb.json +++ b/homeassistant/components/ovo_energy/translations/lb.json @@ -6,6 +6,11 @@ "invalid_auth": "Ong\u00eblteg Authentifikatioun" }, "step": { + "reauth": { + "data": { + "password": "Passwuert" + } + }, "user": { "data": { "password": "Passwuert", diff --git a/homeassistant/components/ovo_energy/translations/tr.json b/homeassistant/components/ovo_energy/translations/tr.json index f3784f6de87..714daac3253 100644 --- a/homeassistant/components/ovo_energy/translations/tr.json +++ b/homeassistant/components/ovo_energy/translations/tr.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, "flow_title": "OVO Enerji: {username}", "step": { "reauth": { @@ -8,6 +13,12 @@ }, "description": "OVO Energy i\u00e7in kimlik do\u011frulama ba\u015far\u0131s\u0131z oldu. L\u00fctfen mevcut kimlik bilgilerinizi girin.", "title": "Yeniden kimlik do\u011frulama" + }, + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } } } } diff --git a/homeassistant/components/ovo_energy/translations/uk.json b/homeassistant/components/ovo_energy/translations/uk.json new file mode 100644 index 00000000000..8a5f8e2a8ba --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/uk.json @@ -0,0 +1,27 @@ +{ + "config": { + "error": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "flow_title": "OVO Energy: {username}", + "step": { + "reauth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457. \u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0412\u0430\u0448\u0456 \u043f\u043e\u0442\u043e\u0447\u043d\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 OVO Energy.", + "title": "OVO Energy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/translations/de.json b/homeassistant/components/owntracks/translations/de.json index 9d832cc264a..0bc533c0469 100644 --- a/homeassistant/components/owntracks/translations/de.json +++ b/homeassistant/components/owntracks/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, "create_entry": { "default": "\n\n\u00d6ffnen unter Android [die OwnTracks-App]({android_url}) und gehe zu {android_url} - > Verbindung. \u00c4nder die folgenden Einstellungen: \n - Modus: Privates HTTP \n - Host: {webhook_url} \n - Identifizierung: \n - Benutzername: `''` \n - Ger\u00e4te-ID: `''` \n\n\u00d6ffnen unter iOS [die OwnTracks-App]({ios_url}) und tippe auf das Symbol (i) oben links - > Einstellungen. \u00c4nder die folgenden Einstellungen: \n - Modus: HTTP \n - URL: {webhook_url} \n - Aktivieren Sie die Authentifizierung \n - UserID: `''`\n\n {secret} \n \n Weitere Informationen findest du in der [Dokumentation]({docs_url})." }, diff --git a/homeassistant/components/owntracks/translations/tr.json b/homeassistant/components/owntracks/translations/tr.json new file mode 100644 index 00000000000..a152eb19468 --- /dev/null +++ b/homeassistant/components/owntracks/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/translations/uk.json b/homeassistant/components/owntracks/translations/uk.json index f1f31864242..e6a6fc26068 100644 --- a/homeassistant/components/owntracks/translations/uk.json +++ b/homeassistant/components/owntracks/translations/uk.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "create_entry": { + "default": "\u042f\u043a\u0449\u043e \u0412\u0430\u0448 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0430\u0446\u044e\u0454 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0439\u043d\u0456\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0456 Android, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a [OwnTracks]({android_url}), \u043f\u043e\u0442\u0456\u043c preferences - > connection. \u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0442\u0430\u043a, \u044f\u043a \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043e \u043d\u0438\u0436\u0447\u0435:\n- Mode: Private HTTP\n- Host: {webhook_url}\n- Identification:\n- Username: ``\n- Device ID: `` \n\n\u042f\u043a\u0449\u043e \u0412\u0430\u0448 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0430\u0446\u044e\u0454 \u043d\u0430 iOS, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a [OwnTracks]({ios_url}), \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0456\u0432\u043e\u043c\u0443 \u0432\u0435\u0440\u0445\u043d\u044c\u043e\u043c\u0443 \u043a\u0443\u0442\u043a\u0443 - > settings. \u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0442\u0430\u043a, \u044f\u043a \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043e \u043d\u0438\u0436\u0447\u0435:\n- Mode: HTTP\n- URL: {webhook_url}\n- Turn on authentication\n- UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457." + }, "step": { "user": { "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 OwnTracks?", diff --git a/homeassistant/components/ozw/translations/ca.json b/homeassistant/components/ozw/translations/ca.json index 4553589e36d..835c16eb449 100644 --- a/homeassistant/components/ozw/translations/ca.json +++ b/homeassistant/components/ozw/translations/ca.json @@ -26,7 +26,7 @@ "data": { "use_addon": "Utilitza el complement OpenZWave Supervisor" }, - "description": "Voleu utilitzar el complement OpenZWave Supervisor?", + "description": "Vols utilitzar el complement Supervisor d'OpenZWave?", "title": "Selecciona el m\u00e8tode de connexi\u00f3" }, "start_addon": { diff --git a/homeassistant/components/ozw/translations/de.json b/homeassistant/components/ozw/translations/de.json index 70eaaaf18df..afa26fb7e03 100644 --- a/homeassistant/components/ozw/translations/de.json +++ b/homeassistant/components/ozw/translations/de.json @@ -3,7 +3,8 @@ "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" + "mqtt_required": "Die MQTT-Integration ist nicht eingerichtet", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "progress": { "install_addon": "Bitte warten, bis die Installation des OpenZWave-Add-Ons abgeschlossen ist. Dies kann einige Minuten dauern." @@ -14,6 +15,15 @@ }, "install_addon": { "title": "Die Installation des OpenZWave-Add-On wurde gestartet" + }, + "on_supervisor": { + "title": "Verbindungstyp ausw\u00e4hlen" + }, + "start_addon": { + "data": { + "network_key": "Netzwerk-Schl\u00fcssel", + "usb_path": "USB-Ger\u00e4te-Pfad" + } } } } diff --git a/homeassistant/components/ozw/translations/lb.json b/homeassistant/components/ozw/translations/lb.json index f97f026d38b..33de9a44953 100644 --- a/homeassistant/components/ozw/translations/lb.json +++ b/homeassistant/components/ozw/translations/lb.json @@ -1,8 +1,17 @@ { "config": { "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "already_in_progress": "Konfiguratioun's Oflaf ass schon am gaang", "mqtt_required": "MQTT Integratioun ass net ageriicht", "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech." + }, + "step": { + "start_addon": { + "data": { + "network_key": "Netzwierk Schl\u00ebssel" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/no.json b/homeassistant/components/ozw/translations/no.json index 89563ff3533..652e28fe3fc 100644 --- a/homeassistant/components/ozw/translations/no.json +++ b/homeassistant/components/ozw/translations/no.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "addon_info_failed": "Kunne ikke hente OpenZWave-tilleggsinfo", - "addon_install_failed": "Kunne ikke installere OpenZWave-tillegget", + "addon_info_failed": "Kunne ikke hente informasjon om OpenZWave-tillegg", + "addon_install_failed": "Kunne ikke installere OpenZWave-tillegg", "addon_set_config_failed": "Kunne ikke angi OpenZWave-konfigurasjon", "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", @@ -10,23 +10,23 @@ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { - "addon_start_failed": "Kunne ikke starte OpenZWave-tillegget. Sjekk konfigurasjonen." + "addon_start_failed": "Kunne ikke starte OpenZWave-tillegg. Sjekk konfigurasjonen." }, "progress": { - "install_addon": "Vent mens OpenZWave-tilleggsinstallasjonen er ferdig. Dette kan ta flere minutter." + "install_addon": "Vent mens installasjonen av OpenZWave-tillegg er ferdig. Dette kan ta flere minutter." }, "step": { "hassio_confirm": { - "title": "Sett opp OpenZWave-integrasjon med OpenZWave-tillegget" + "title": "Sett opp OpenZWave-integrasjon med OpenZWave-tillegg" }, "install_addon": { - "title": "Installasjonen av tilleggsprogrammet OpenZWave har startet" + "title": "Installasjonen av OpenZWave-tillegg har startet" }, "on_supervisor": { "data": { - "use_addon": "Bruk OpenZWave Supervisor-tillegget" + "use_addon": "Bruk OpenZWave Supervisor-tillegg" }, - "description": "\u00d8nsker du \u00e5 bruke OpenZWave Supervisor-tillegget?", + "description": "\u00d8nsker du \u00e5 bruke OpenZWave Supervisor-tillegg?", "title": "Velg tilkoblingsmetode" }, "start_addon": { @@ -34,7 +34,7 @@ "network_key": "Nettverksn\u00f8kkel", "usb_path": "USB enhetsbane" }, - "title": "Angi OpenZWave-tilleggskonfigurasjonen" + "title": "Angi konfigurasjon for OpenZWave-tillegg" } } } diff --git a/homeassistant/components/ozw/translations/tr.json b/homeassistant/components/ozw/translations/tr.json index d0a70d57752..99eda8b8311 100644 --- a/homeassistant/components/ozw/translations/tr.json +++ b/homeassistant/components/ozw/translations/tr.json @@ -2,7 +2,12 @@ "config": { "abort": { "addon_info_failed": "OpenZWave eklenti bilgileri al\u0131namad\u0131.", - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "addon_install_failed": "OpenZWave eklentisi y\u00fcklenemedi.", + "addon_set_config_failed": "OpenZWave yap\u0131land\u0131rmas\u0131 ayarlanamad\u0131.", + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "mqtt_required": "MQTT entegrasyonu kurulmam\u0131\u015f", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, "progress": { "install_addon": "OpenZWave eklenti kurulumu bitene kadar l\u00fctfen bekleyin. Bu birka\u00e7 dakika s\u00fcrebilir." @@ -10,6 +15,18 @@ "step": { "install_addon": { "title": "OpenZWave eklenti kurulumu ba\u015flad\u0131" + }, + "on_supervisor": { + "data": { + "use_addon": "OpenZWave Supervisor eklentisini kullan\u0131n" + }, + "description": "OpenZWave Supervisor eklentisini kullanmak istiyor musunuz?", + "title": "Ba\u011flant\u0131 y\u00f6ntemini se\u00e7in" + }, + "start_addon": { + "data": { + "network_key": "A\u011f Anahtar\u0131" + } } } } diff --git a/homeassistant/components/ozw/translations/uk.json b/homeassistant/components/ozw/translations/uk.json new file mode 100644 index 00000000000..f8fb161aa1c --- /dev/null +++ b/homeassistant/components/ozw/translations/uk.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "addon_info_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u0434\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f OpenZWave.", + "addon_install_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0438 OpenZWave.", + "addon_set_config_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0438 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e OpenZWave.", + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "mqtt_required": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f MQTT \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0430.", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "error": { + "addon_start_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 OpenZWave. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "progress": { + "install_addon": "\u0417\u0430\u0447\u0435\u043a\u0430\u0439\u0442\u0435, \u043f\u043e\u043a\u0438 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0434\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f OpenZWave. \u0426\u0435 \u043c\u043e\u0436\u0435 \u0437\u0430\u0439\u043d\u044f\u0442\u0438 \u043a\u0456\u043b\u044c\u043a\u0430 \u0445\u0432\u0438\u043b\u0438\u043d." + }, + "step": { + "hassio_confirm": { + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0434\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f OpenZWave" + }, + "install_addon": { + "title": "\u0420\u043e\u0437\u043f\u043e\u0447\u0430\u0442\u043e \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0434\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f Open'Wave" + }, + "on_supervisor": { + "data": { + "use_addon": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Supervisor OpenZWave" + }, + "description": "\u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Supervisor OpenZWave?", + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + }, + "start_addon": { + "data": { + "network_key": "\u041a\u043b\u044e\u0447 \u043c\u0435\u0440\u0435\u0436\u0456", + "usb_path": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 Open'Wave" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/de.json b/homeassistant/components/panasonic_viera/translations/de.json index 4b2c14be9d6..71090830714 100644 --- a/homeassistant/components/panasonic_viera/translations/de.json +++ b/homeassistant/components/panasonic_viera/translations/de.json @@ -1,20 +1,20 @@ { "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." + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" }, "error": { - "cannot_connect": "Verbindungsfehler", - "invalid_pin_code": "Der von Ihnen eingegebene PIN-Code war ung\u00fcltig" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_pin_code": "Der eingegebene PIN-Code war ung\u00fcltig" }, "step": { "pairing": { "data": { - "pin": "PIN" + "pin": "PIN-Code" }, - "description": "Geben Sie die auf Ihrem Fernseher angezeigte PIN ein", + "description": "Gib den auf deinem TV angezeigten PIN-Code ein", "title": "Kopplung" }, "user": { @@ -22,7 +22,7 @@ "host": "IP-Adresse", "name": "Name" }, - "description": "Geben Sie die IP-Adresse Ihres Panasonic Viera TV ein", + "description": "Gib die IP-Adresse deines Panasonic Viera TV ein", "title": "Richten Sie Ihr Fernsehger\u00e4t ein" } } diff --git a/homeassistant/components/panasonic_viera/translations/tr.json b/homeassistant/components/panasonic_viera/translations/tr.json new file mode 100644 index 00000000000..d0e573fdcf9 --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "\u0130p Adresi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/uk.json b/homeassistant/components/panasonic_viera/translations/uk.json new file mode 100644 index 00000000000..9722b19ece9 --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/uk.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_pin_code": "\u0412\u0432\u0435\u0434\u0435\u043d\u0438\u0439 PIN-\u043a\u043e\u0434 \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439." + }, + "step": { + "pairing": { + "data": { + "pin": "PIN-\u043a\u043e\u0434" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c PIN-\u043a\u043e\u0434 , \u0449\u043e \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0454\u0442\u044c\u0441\u044f \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430", + "title": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "user": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441\u0430 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 Panasonic Viera", + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/person/translations/uk.json b/homeassistant/components/person/translations/uk.json index 0dba7914da0..5e6b186e38c 100644 --- a/homeassistant/components/person/translations/uk.json +++ b/homeassistant/components/person/translations/uk.json @@ -2,7 +2,7 @@ "state": { "_": { "home": "\u0412\u0434\u043e\u043c\u0430", - "not_home": "\u0412\u0456\u0434\u0441\u0443\u0442\u043d\u0456\u0439" + "not_home": "\u041d\u0435 \u0432\u0434\u043e\u043c\u0430" } }, "title": "\u041b\u044e\u0434\u0438\u043d\u0430" diff --git a/homeassistant/components/pi_hole/translations/ca.json b/homeassistant/components/pi_hole/translations/ca.json index 37d4e890ef4..eb15fa7bf97 100644 --- a/homeassistant/components/pi_hole/translations/ca.json +++ b/homeassistant/components/pi_hole/translations/ca.json @@ -7,6 +7,11 @@ "cannot_connect": "Ha fallat la connexi\u00f3" }, "step": { + "api_key": { + "data": { + "api_key": "Clau API" + } + }, "user": { "data": { "api_key": "Clau API", @@ -15,6 +20,7 @@ "name": "Nom", "port": "Port", "ssl": "Utilitza un certificat SSL", + "statistics_only": "Nom\u00e9s les estad\u00edstiques", "verify_ssl": "Verifica el certificat SSL" } } diff --git a/homeassistant/components/pi_hole/translations/cs.json b/homeassistant/components/pi_hole/translations/cs.json index a9057ceabab..fa90fbdb2a0 100644 --- a/homeassistant/components/pi_hole/translations/cs.json +++ b/homeassistant/components/pi_hole/translations/cs.json @@ -7,6 +7,11 @@ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "step": { + "api_key": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + }, "user": { "data": { "api_key": "Kl\u00ed\u010d API", diff --git a/homeassistant/components/pi_hole/translations/de.json b/homeassistant/components/pi_hole/translations/de.json index f74c5acb635..34198fcfebe 100644 --- a/homeassistant/components/pi_hole/translations/de.json +++ b/homeassistant/components/pi_hole/translations/de.json @@ -4,17 +4,17 @@ "already_configured": "Service ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung konnte nicht hergestellt werden" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { "data": { - "api_key": "API-Schl\u00fcssel (optional)", + "api_key": "API-Schl\u00fcssel", "host": "Host", "location": "Org", "name": "Name", "port": "Port", - "ssl": "SSL verwenden", + "ssl": "Nutzt ein SSL-Zertifikat", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" } } diff --git a/homeassistant/components/pi_hole/translations/en.json b/homeassistant/components/pi_hole/translations/en.json index 858e7c230ac..9053a70c18f 100644 --- a/homeassistant/components/pi_hole/translations/en.json +++ b/homeassistant/components/pi_hole/translations/en.json @@ -7,6 +7,11 @@ "cannot_connect": "Failed to connect" }, "step": { + "api_key": { + "data": { + "api_key": "API Key" + } + }, "user": { "data": { "api_key": "API Key", @@ -15,6 +20,7 @@ "name": "Name", "port": "Port", "ssl": "Uses an SSL certificate", + "statistics_only": "Statistics Only", "verify_ssl": "Verify SSL certificate" } } diff --git a/homeassistant/components/pi_hole/translations/es.json b/homeassistant/components/pi_hole/translations/es.json index 48708d68104..35597af49f2 100644 --- a/homeassistant/components/pi_hole/translations/es.json +++ b/homeassistant/components/pi_hole/translations/es.json @@ -7,6 +7,11 @@ "cannot_connect": "No se pudo conectar" }, "step": { + "api_key": { + "data": { + "api_key": "Clave API" + } + }, "user": { "data": { "api_key": "Clave API", @@ -15,6 +20,7 @@ "name": "Nombre", "port": "Puerto", "ssl": "Usar SSL", + "statistics_only": "S\u00f3lo las estad\u00edsticas", "verify_ssl": "Verificar certificado SSL" } } diff --git a/homeassistant/components/pi_hole/translations/et.json b/homeassistant/components/pi_hole/translations/et.json index c68d52c0c10..4ff0fdd0ba8 100644 --- a/homeassistant/components/pi_hole/translations/et.json +++ b/homeassistant/components/pi_hole/translations/et.json @@ -7,6 +7,11 @@ "cannot_connect": "\u00dchendamine nurjus" }, "step": { + "api_key": { + "data": { + "api_key": "API v\u00f5ti" + } + }, "user": { "data": { "api_key": "API v\u00f5ti", @@ -15,6 +20,7 @@ "name": "Nimi", "port": "", "ssl": "Kasuatb SSL serti", + "statistics_only": "Ainult statistika", "verify_ssl": "Kontrolli SSL sertifikaati" } } diff --git a/homeassistant/components/pi_hole/translations/it.json b/homeassistant/components/pi_hole/translations/it.json index 34590ee77bb..7d355caf985 100644 --- a/homeassistant/components/pi_hole/translations/it.json +++ b/homeassistant/components/pi_hole/translations/it.json @@ -7,6 +7,11 @@ "cannot_connect": "Impossibile connettersi" }, "step": { + "api_key": { + "data": { + "api_key": "Chiave API" + } + }, "user": { "data": { "api_key": "Chiave API", @@ -15,6 +20,7 @@ "name": "Nome", "port": "Porta", "ssl": "Utilizza un certificato SSL", + "statistics_only": "Solo Statistiche", "verify_ssl": "Verificare il certificato SSL" } } diff --git a/homeassistant/components/pi_hole/translations/no.json b/homeassistant/components/pi_hole/translations/no.json index 71c815ecd36..7d005fa6516 100644 --- a/homeassistant/components/pi_hole/translations/no.json +++ b/homeassistant/components/pi_hole/translations/no.json @@ -7,6 +7,11 @@ "cannot_connect": "Tilkobling mislyktes" }, "step": { + "api_key": { + "data": { + "api_key": "API-n\u00f8kkel" + } + }, "user": { "data": { "api_key": "API-n\u00f8kkel", @@ -15,6 +20,7 @@ "name": "Navn", "port": "Port", "ssl": "Bruker et SSL-sertifikat", + "statistics_only": "Bare statistikk", "verify_ssl": "Verifisere SSL-sertifikat" } } diff --git a/homeassistant/components/pi_hole/translations/pl.json b/homeassistant/components/pi_hole/translations/pl.json index add788ef916..ee4b6eadd87 100644 --- a/homeassistant/components/pi_hole/translations/pl.json +++ b/homeassistant/components/pi_hole/translations/pl.json @@ -7,6 +7,11 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "step": { + "api_key": { + "data": { + "api_key": "Klucz API" + } + }, "user": { "data": { "api_key": "Klucz API", @@ -15,6 +20,7 @@ "name": "Nazwa", "port": "Port", "ssl": "Certyfikat SSL", + "statistics_only": "Tylko statystyki", "verify_ssl": "Weryfikacja certyfikatu SSL" } } diff --git a/homeassistant/components/pi_hole/translations/ru.json b/homeassistant/components/pi_hole/translations/ru.json index eb3cfa62c62..eed9596c907 100644 --- a/homeassistant/components/pi_hole/translations/ru.json +++ b/homeassistant/components/pi_hole/translations/ru.json @@ -7,6 +7,11 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "step": { + "api_key": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + } + }, "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", @@ -15,6 +20,7 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "port": "\u041f\u043e\u0440\u0442", "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL", + "statistics_only": "\u0422\u043e\u043b\u044c\u043a\u043e \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0430", "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" } } diff --git a/homeassistant/components/pi_hole/translations/tr.json b/homeassistant/components/pi_hole/translations/tr.json new file mode 100644 index 00000000000..a14e020d360 --- /dev/null +++ b/homeassistant/components/pi_hole/translations/tr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "api_key": { + "data": { + "api_key": "API Anahtar\u0131" + } + }, + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "host": "Ana Bilgisayar", + "location": "Konum", + "port": "Port", + "statistics_only": "Yaln\u0131zca \u0130statistikler" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/uk.json b/homeassistant/components/pi_hole/translations/uk.json new file mode 100644 index 00000000000..93413f9abff --- /dev/null +++ b/homeassistant/components/pi_hole/translations/uk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "host": "\u0425\u043e\u0441\u0442", + "location": "\u041c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f", + "name": "\u041d\u0430\u0437\u0432\u0430", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/zh-Hant.json b/homeassistant/components/pi_hole/translations/zh-Hant.json index 1cea5a87f4b..1527b48f580 100644 --- a/homeassistant/components/pi_hole/translations/zh-Hant.json +++ b/homeassistant/components/pi_hole/translations/zh-Hant.json @@ -7,6 +7,11 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "step": { + "api_key": { + "data": { + "api_key": "API \u5bc6\u9470" + } + }, "user": { "data": { "api_key": "API \u5bc6\u9470", @@ -15,6 +20,7 @@ "name": "\u540d\u7a31", "port": "\u901a\u8a0a\u57e0", "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49", + "statistics_only": "\u50c5\u7d71\u8a08\u8cc7\u8a0a", "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" } } diff --git a/homeassistant/components/plaato/translations/ca.json b/homeassistant/components/plaato/translations/ca.json index 1dbe125d50d..c4669b219ab 100644 --- a/homeassistant/components/plaato/translations/ca.json +++ b/homeassistant/components/plaato/translations/ca.json @@ -1,16 +1,53 @@ { "config": { "abort": { + "already_configured": "El compte ja ha estat configurat", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", "webhook_not_internet_accessible": "La teva inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per poder rebre missatges webhook." }, "create_entry": { - "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar l'opci\u00f3 webhook de Plaato Airlock.\n\nCompleta la seg\u00fcent informaci\u00f3:\n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n\nConsulta la [documentaci\u00f3]({docs_url}) per a m\u00e9s detalls." + "default": "El dispositiu Plaato {device_type} amb nom **{device_name}** s'ha configurat correctament!" + }, + "error": { + "invalid_webhook_device": "Has seleccionat un dispositiu que no admet l'enviament de dades a un webhook. Nom\u00e9s est\u00e0 disponible per a Airlock", + "no_api_method": "Has d'afegir un token d'autenticaci\u00f3 o seleccionar webhook", + "no_auth_token": "Has d'afegir un token d'autenticaci\u00f3" }, "step": { + "api_method": { + "data": { + "token": "Enganxa el token d'autenticaci\u00f3 aqu\u00ed", + "use_webhook": "Utilitza webhook" + }, + "description": "Per poder consultar l'API, cal un `auth_token` que es pot obtenir seguint aquestes [instruccions](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token)\n\n Dispositiu seleccionat: **{device_type}** \n\n Si prefereixes utilitzar el m\u00e8tode webhook integrat (nom\u00e9s per Airlock), marca la casella seg\u00fcent i deixa el token d'autenticaci\u00f3 en blanc", + "title": "Selecciona el m\u00e8tode API" + }, "user": { + "data": { + "device_name": "Posa un nom al dispositiu", + "device_type": "Tipus de dispositiu Plaato" + }, "description": "Vols comen\u00e7ar la configuraci\u00f3?", - "title": "Configuraci\u00f3 del Webhook de Plaato" + "title": "Configura dispositius Plaato" + }, + "webhook": { + "description": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar la opci\u00f3 webhook de Plaato Airlock.\n\nCompleta la seg\u00fcent informaci\u00f3:\n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n\nConsulta la [documentaci\u00f3]({docs_url}) per a m\u00e9s detalls.", + "title": "Webhook a utilitzar" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "Interval d'actualitzaci\u00f3 (minuts)" + }, + "description": "Estableix l'interval d'actualitzaci\u00f3 (minuts)", + "title": "Opcions de Plaato" + }, + "webhook": { + "description": "Informaci\u00f3 del webhook: \n\n - URL: `{webhook_url}`\n - M\u00e8tode: POST\n\n", + "title": "Opcions de Plaato Airlock" } } } diff --git a/homeassistant/components/plaato/translations/de.json b/homeassistant/components/plaato/translations/de.json index f97fe4875f7..5171baab654 100644 --- a/homeassistant/components/plaato/translations/de.json +++ b/homeassistant/components/plaato/translations/de.json @@ -1,11 +1,15 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", + "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." + }, "create_entry": { "default": "Um Ereignisse an Home Assistant zu senden, muss das Webhook Feature in Plaato Airlock konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})." }, "step": { "user": { - "description": "Soll Plaato Airlock wirklich eingerichtet werden?", + "description": "M\u00f6chtest du mit der Einrichtung beginnen?", "title": "Plaato Webhook einrichten" } } diff --git a/homeassistant/components/plaato/translations/et.json b/homeassistant/components/plaato/translations/et.json index 75c7a2182ef..ec7b7e4b1a4 100644 --- a/homeassistant/components/plaato/translations/et.json +++ b/homeassistant/components/plaato/translations/et.json @@ -1,16 +1,53 @@ { "config": { "abort": { + "already_configured": "Kasutaja on juba seadistatud", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.", "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 pead seadistama Plaatoo Airlock'i veebihaagi. \n\n Sisesta j\u00e4rgmine teave: \n\n - URL: \" {webhook_url} \" \n - Meetod: POST \n \n Lisateavet leiad [documentation] ( {docs_url} )." + "default": "{device_type} Plaato seade nimega **{device_name}** on edukalt seadistatud!" + }, + "error": { + "invalid_webhook_device": "Oled valinud seadme, mis ei toeta andmete saatmist veebihaagile. See on saadaval ainult Airlocki jaoks", + "no_api_method": "Pead lisama autentimisloa v\u00f5i valima veebihaagi", + "no_auth_token": "Pead lisama autentimisloa" }, "step": { + "api_method": { + "data": { + "token": "Aseta Auth Token siia", + "use_webhook": "Kasuta veebihaaki" + }, + "description": "API p\u00e4ringu esitamiseks on vajalik \"auth_token\", mille saad j\u00e4rgides [neid] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) juhiseid \n\n Valitud seade: ** {device_type} ** \n\n Kui kasutad pigem sisseehitatud veebihaagi meetodit (ainult Airlock), m\u00e4rgi palun allolev ruut ja j\u00e4ta Auth Token t\u00fchjaks", + "title": "Vali API meetod" + }, "user": { + "data": { + "device_name": "Pang oma seadmele nimi", + "device_type": "Plaato seadme t\u00fc\u00fcp" + }, "description": "Kas alustan seadistamist?", - "title": "Plaato Webhooki seadistamine" + "title": "Plaato seadmete h\u00e4\u00e4lestamine" + }, + "webhook": { + "description": "S\u00fcndmuste saatmiseks Home Assistanti pead seadistama Plaato Airlocki veebihaagi. \n\n Sisesta j\u00e4rgmine teave: \n\n - URL: \" {webhook_url} \"\n - Meetod: POST \n\n Lisateavet leiad [dokumentatsioonist] ( {docs_url} ).", + "title": "Kasutatav veebihaak" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "V\u00e4rskendamise intervall (minutites)" + }, + "description": "M\u00e4\u00e4ra v\u00e4rskendamise intervall (minutites)", + "title": "Plaato valikud" + }, + "webhook": { + "description": "Veebihaagi teave: \n\n - URL: `{webhook_url}`\n - Meetod: POST\n\n", + "title": "Plaato Airlocki valikud" } } } diff --git a/homeassistant/components/plaato/translations/no.json b/homeassistant/components/plaato/translations/no.json index 1e2da1bfb12..8873399aaa4 100644 --- a/homeassistant/components/plaato/translations/no.json +++ b/homeassistant/components/plaato/translations/no.json @@ -1,16 +1,48 @@ { "config": { "abort": { + "already_configured": "Kontoen er allerede konfigurert", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", "webhook_not_internet_accessible": "Home Assistant forekomsten din m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta webhook meldinger" }, "create_entry": { "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i Plaato Airlock. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer." }, + "error": { + "invalid_webhook_device": "Du har valgt en enhet som ikke st\u00f8tter sending av data til en webhook. Den er kun tilgjengelig for Airlock", + "no_api_method": "Du m\u00e5 legge til et godkjenningstoken eller velge webhook", + "no_auth_token": "Du m\u00e5 legge til et godkjenningstoken" + }, "step": { + "api_method": { + "title": "Velg API-metode" + }, "user": { + "data": { + "device_name": "Navngi enheten din", + "device_type": "Type Platon-enhet" + }, "description": "Vil du starte oppsettet?", "title": "Sett opp Plaato Webhook" + }, + "webhook": { + "description": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i Plaato Airlock. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer.", + "title": "Webhook \u00e5 bruke" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "Oppdateringsintervall (minutter)" + }, + "description": "Still inn oppdateringsintervallet (minutter)", + "title": "Alternativer for Plaato" + }, + "webhook": { + "description": "Webhook info:\n\n- URL-adresse: {webhook_url}\n- Metode: POST\n\n", + "title": "Alternativer for Plaato Airlock" } } } diff --git a/homeassistant/components/plaato/translations/pl.json b/homeassistant/components/plaato/translations/pl.json index 1f7c8141aa5..c849f574c9c 100644 --- a/homeassistant/components/plaato/translations/pl.json +++ b/homeassistant/components/plaato/translations/pl.json @@ -1,16 +1,53 @@ { "config": { "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", "webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook" }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistanta, musisz skonfigurowa\u0107 webhook w Plaato Airlock. \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." + "default": "Tw\u00f3j {device_type} Plaato o nazwie *{device_name}* zosta\u0142o pomy\u015blnie skonfigurowane!" + }, + "error": { + "invalid_webhook_device": "Wybra\u0142e\u015b urz\u0105dzenie, kt\u00f3re nie obs\u0142uguje wysy\u0142ania danych do webhooka. Opcja dost\u0119pna tylko w areometrze Airlock", + "no_api_method": "Musisz doda\u0107 token uwierzytelniania lub wybra\u0107 webhook", + "no_auth_token": "Musisz doda\u0107 token autoryzacji" }, "step": { + "api_method": { + "data": { + "token": "Wklej token autoryzacji", + "use_webhook": "U\u017cyj webhook" + }, + "description": "Aby m\u00f3c przesy\u0142a\u0107 zapytania do API, wymagany jest \u201etoken autoryzacji\u201d, kt\u00f3ry mo\u017cna uzyska\u0107, post\u0119puj\u0105c zgodnie z [t\u0105] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instrukcj\u0105\n\nWybrane urz\u0105dzenie: *{device_type}* \n\nJe\u015bli wolisz u\u017cywa\u0107 wbudowanej metody webhook (tylko areomierz Airlock), zaznacz poni\u017csze pole i pozostaw token autoryzacji pusty", + "title": "Wybierz metod\u0119 API" + }, "user": { + "data": { + "device_name": "Nazwij swoje urz\u0105dzenie", + "device_type": "Rodzaj urz\u0105dzenia Plaato" + }, "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?", - "title": "Konfiguracja Plaato Webhook" + "title": "Konfiguracja urz\u0105dze\u0144 Plaato" + }, + "webhook": { + "description": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistanta, musisz skonfigurowa\u0107 webhook w Plaato Airlock. \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y.", + "title": "Webhook do u\u017cycia" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (w minutach)" + }, + "description": "Ustaw cz\u0119stotliwo\u015bci aktualizacji (w minutach)", + "title": "Opcje dla Plaato" + }, + "webhook": { + "description": "Informacje o webhook: \n\n - URL: `{webhook_url}`\n - Metoda: POST \n\n", + "title": "Opcje dla areomierza Plaato Airlock" } } } diff --git a/homeassistant/components/plaato/translations/tr.json b/homeassistant/components/plaato/translations/tr.json new file mode 100644 index 00000000000..1f21b08ec81 --- /dev/null +++ b/homeassistant/components/plaato/translations/tr.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + }, + "error": { + "no_auth_token": "Bir kimlik do\u011frulama jetonu eklemeniz gerekiyor" + }, + "step": { + "api_method": { + "data": { + "use_webhook": "Webhook kullan" + }, + "title": "API y\u00f6ntemini se\u00e7in" + }, + "user": { + "data": { + "device_name": "Cihaz\u0131n\u0131z\u0131 adland\u0131r\u0131n", + "device_type": "Plaato cihaz\u0131n\u0131n t\u00fcr\u00fc" + } + }, + "webhook": { + "title": "Webhook kullanmak i\u00e7in" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "G\u00fcncelle\u015ftirme aral\u0131\u011f\u0131 (dakika)" + }, + "description": "G\u00fcncelleme aral\u0131\u011f\u0131n\u0131 ayarlay\u0131n (dakika)", + "title": "Plaato i\u00e7in se\u00e7enekler" + }, + "webhook": { + "title": "Plaato Airlock i\u00e7in se\u00e7enekler" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/uk.json b/homeassistant/components/plaato/translations/uk.json new file mode 100644 index 00000000000..a4f7de7c6be --- /dev/null +++ b/homeassistant/components/plaato/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0432\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f Plaato Airlock. \n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST \n\n \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457." + }, + "step": { + "user": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?", + "title": "Plaato Airlock" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/zh-Hant.json b/homeassistant/components/plaato/translations/zh-Hant.json index aec745ea38b..2890c5c31c6 100644 --- a/homeassistant/components/plaato/translations/zh-Hant.json +++ b/homeassistant/components/plaato/translations/zh-Hant.json @@ -1,16 +1,53 @@ { "config": { "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "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": { - "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Plaato Airlock \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" + "default": "\u540d\u7a31\u70ba **{device_name}** \u7684 Plaato {device_type} \u5df2\u6210\u529f\u8a2d\u5b9a\uff01" + }, + "error": { + "invalid_webhook_device": "\u6240\u9078\u64c7\u7684\u88dd\u7f6e\u4e0d\u652f\u63f4\u50b3\u9001\u8cc7\u6599\u81f3 Webhook\u3001AirLock \u50c5\u652f\u63f4\u6b64\u985e\u578b", + "no_api_method": "\u9700\u8981\u65b0\u589e\u6388\u6b0a\u5bc6\u9470\u6216\u9078\u64c7 Webhook", + "no_auth_token": "\u9700\u8981\u65b0\u589e\u6388\u6b0a\u5bc6\u9470" }, "step": { + "api_method": { + "data": { + "token": "\u65bc\u6b64\u8cbc\u4e0a\u6388\u6b0a\u5bc6\u9470", + "use_webhook": "\u4f7f\u7528 Webhook" + }, + "description": "\u9700\u8981\u6388\u6b0a\u5bc6\u8981 `auth_token` \u65b9\u80fd\u67e5\u8a62 API\u3002\u7372\u5f97\u7684\u65b9\u6cd5\u8acb [\u53c3\u95b1](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) \u6559\u5b78\n\n\u9078\u64c7\u7684\u88dd\u7f6e\uff1a**{device_type}** \n\n\u5047\u5982\u9078\u64c7\u5167\u5efa Webhook \u65b9\u6cd5\uff08Airlock \u552f\u4e00\u652f\u63f4\uff09\uff0c\u8acb\u6aa2\u67e5\u4e0b\u65b9\u6838\u9078\u76d2\u4e26\u78ba\u5b9a\u4fdd\u6301\u6388\u6b0a\u5bc6\u9470\u6b04\u4f4d\u7a7a\u767d", + "title": "\u9078\u64c7 API \u65b9\u5f0f" + }, "user": { + "data": { + "device_name": "\u88dd\u7f6e\u540d\u7a31", + "device_type": "Plaato \u88dd\u7f6e\u985e\u578b" + }, "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f", - "title": "\u8a2d\u5b9a Plaato Webhook" + "title": "\u8a2d\u5b9a Plaato \u88dd\u7f6e" + }, + "webhook": { + "description": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Plaato Airlock \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002", + "title": "\u4f7f\u7528\u4e4b Webhook" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "\u66f4\u65b0\u983b\u7387\uff08\u5206\uff09" + }, + "description": "\u8a2d\u5b9a\u66f4\u65b0\u983b\u7387\uff08\u5206\uff09", + "title": "Plaato \u9078\u9805" + }, + "webhook": { + "description": "Webhook \u8a0a\u606f\uff1a\n\n- URL\uff1a`{webhook_url}`\n- \u65b9\u5f0f\uff1aPOST\n\n", + "title": "Plaato Airlock \u9078\u9805" } } } diff --git a/homeassistant/components/plant/translations/uk.json b/homeassistant/components/plant/translations/uk.json index 3204c42a714..25f24b43b80 100644 --- a/homeassistant/components/plant/translations/uk.json +++ b/homeassistant/components/plant/translations/uk.json @@ -1,8 +1,8 @@ { "state": { "_": { - "ok": "\u0422\u0410\u041a", - "problem": "\u0425\u0430\u043b\u0435\u043f\u0430" + "ok": "\u041e\u041a", + "problem": "\u041f\u0440\u043e\u0431\u043b\u0435\u043c\u0430" } }, "title": "\u0420\u043e\u0441\u043b\u0438\u043d\u0430" diff --git a/homeassistant/components/plex/translations/de.json b/homeassistant/components/plex/translations/de.json index 961ad4b3ed6..2ba14e65f85 100644 --- a/homeassistant/components/plex/translations/de.json +++ b/homeassistant/components/plex/translations/de.json @@ -3,9 +3,10 @@ "abort": { "all_configured": "Alle verkn\u00fcpften Server sind bereits konfiguriert", "already_configured": "Dieser Plex-Server ist bereits konfiguriert", - "already_in_progress": "Plex wird konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "token_request_timeout": "Zeit\u00fcberschreitung beim Erhalt des Tokens", - "unknown": "Aus unbekanntem Grund fehlgeschlagen" + "unknown": "Unerwarteter Fehler" }, "error": { "faulty_credentials": "Autorisierung fehlgeschlagen, Token \u00fcberpr\u00fcfen", diff --git a/homeassistant/components/plex/translations/tr.json b/homeassistant/components/plex/translations/tr.json new file mode 100644 index 00000000000..93f8cc85eae --- /dev/null +++ b/homeassistant/components/plex/translations/tr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Bu Plex sunucusu zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "unknown": "Beklenmeyen hata" + }, + "step": { + "manual_setup": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + }, + "user": { + "title": "Plex Medya Sunucusu" + }, + "user_advanced": { + "data": { + "setup_method": "Kurulum y\u00f6ntemi" + }, + "title": "Plex Medya Sunucusu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/translations/uk.json b/homeassistant/components/plex/translations/uk.json new file mode 100644 index 00000000000..20351cf735a --- /dev/null +++ b/homeassistant/components/plex/translations/uk.json @@ -0,0 +1,62 @@ +{ + "config": { + "abort": { + "all_configured": "\u0412\u0441\u0456 \u043f\u043e\u0432'\u044f\u0437\u0430\u043d\u0456 \u0441\u0435\u0440\u0432\u0435\u0440\u0438 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0456.", + "already_configured": "\u0426\u0435\u0439 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e", + "token_request_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0442\u043e\u043a\u0435\u043d\u0430.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "faulty_credentials": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0422\u043e\u043a\u0435\u043d.", + "host_or_token": "\u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0432\u043a\u0430\u0437\u0430\u0442\u0438 \u0425\u043e\u0441\u0442 \u0430\u0431\u043e \u0422\u043e\u043a\u0435\u043d.", + "no_servers": "\u041d\u0435\u043c\u0430\u0454 \u0441\u0435\u0440\u0432\u0435\u0440\u0456\u0432, \u043f\u043e\u0432'\u044f\u0437\u0430\u043d\u0438\u0445 \u0437 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u043c \u0437\u0430\u043f\u0438\u0441\u043e\u043c.", + "not_found": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e.", + "ssl_error": "\u041f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u0437 SSL \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u043c." + }, + "flow_title": "{name} ({host})", + "step": { + "manual_setup": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL", + "token": "\u0422\u043e\u043a\u0435\u043d (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" + }, + "title": "\u0420\u0443\u0447\u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Plex" + }, + "select_server": { + "data": { + "server": "\u0421\u0435\u0440\u0432\u0435\u0440" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u0434\u0438\u043d \u0437 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u0441\u0435\u0440\u0432\u0435\u0440\u0456\u0432:", + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440 Plex" + }, + "user": { + "description": "\u041f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043d\u0430 [plex.tv](https://plex.tv), \u0449\u043e\u0431 \u043f\u0440\u0438\u0432'\u044f\u0437\u0430\u0442\u0438 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0434\u043e Home Assistant.", + "title": "Plex Media Server" + }, + "user_advanced": { + "data": { + "setup_method": "\u0421\u043f\u043e\u0441\u0456\u0431 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f" + }, + "title": "Plex Media Server" + } + } + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "ignore_new_shared_users": "\u0406\u0433\u043d\u043e\u0440\u0443\u0432\u0430\u0442\u0438 \u043d\u043e\u0432\u0438\u0445 \u043a\u0435\u0440\u043e\u0432\u0430\u043d\u0438\u0445 / \u0437\u0430\u0433\u0430\u043b\u044c\u043d\u0438\u0445 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0456\u0432", + "ignore_plex_web_clients": "\u0406\u0433\u043d\u043e\u0440\u0443\u0432\u0430\u0442\u0438 \u0432\u0435\u0431-\u043a\u043b\u0456\u0454\u043d\u0442\u0438 Plex", + "monitored_users": "\u0412\u0456\u0434\u0441\u0442\u0435\u0436\u0443\u0432\u0430\u043d\u0456 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0456", + "use_episode_art": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u043e\u0431\u043a\u043b\u0430\u0434\u0438\u043d\u043a\u0438 \u0435\u043f\u0456\u0437\u043e\u0434\u0456\u0432" + }, + "description": "\u0414\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json index 2282e3584fc..685cd6fb9ae 100644 --- a/homeassistant/components/plugwise/translations/de.json +++ b/homeassistant/components/plugwise/translations/de.json @@ -4,7 +4,8 @@ "already_configured": "Service ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "flow_title": "Smile: {name}", @@ -17,6 +18,8 @@ }, "user_gateway": { "data": { + "host": "IP-Adresse", + "password": "Smile ID", "port": "Port" }, "description": "Bitte eingeben" diff --git a/homeassistant/components/plugwise/translations/lb.json b/homeassistant/components/plugwise/translations/lb.json index 4ce9f8b0145..a3618bc911e 100644 --- a/homeassistant/components/plugwise/translations/lb.json +++ b/homeassistant/components/plugwise/translations/lb.json @@ -21,7 +21,8 @@ "data": { "host": "IP Adress", "password": "Smile ID", - "port": "Port" + "port": "Port", + "username": "Smile Benotzernumm" }, "title": "Mam Smile verbannen" } diff --git a/homeassistant/components/plugwise/translations/tr.json b/homeassistant/components/plugwise/translations/tr.json index d25f1975cf7..60d6b1f92be 100644 --- a/homeassistant/components/plugwise/translations/tr.json +++ b/homeassistant/components/plugwise/translations/tr.json @@ -1,10 +1,23 @@ { "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "Smile: {name}", "step": { "user_gateway": { "data": { + "host": "\u0130p Adresi", + "password": "G\u00fcl\u00fcmseme Kimli\u011fi", + "port": "Port", "username": "Smile Kullan\u0131c\u0131 Ad\u0131" - } + }, + "description": "L\u00fctfen girin" } } } diff --git a/homeassistant/components/plugwise/translations/uk.json b/homeassistant/components/plugwise/translations/uk.json new file mode 100644 index 00000000000..6c6f54612b1 --- /dev/null +++ b/homeassistant/components/plugwise/translations/uk.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "Smile: {name}", + "step": { + "user": { + "data": { + "flow_type": "\u0422\u0438\u043f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + }, + "description": "\u041f\u0440\u043e\u0434\u0443\u043a\u0442:", + "title": "\u0422\u0438\u043f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Plugwise" + }, + "user_gateway": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "password": "Smile ID", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041b\u043e\u0433\u0456\u043d Smile" + }, + "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0432\u0435\u0434\u0456\u0442\u044c:", + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e Smile" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Plugwise" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/de.json b/homeassistant/components/plum_lightpad/translations/de.json index accee16a6f5..c94bf9aadab 100644 --- a/homeassistant/components/plum_lightpad/translations/de.json +++ b/homeassistant/components/plum_lightpad/translations/de.json @@ -1,7 +1,10 @@ { "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/plum_lightpad/translations/tr.json b/homeassistant/components/plum_lightpad/translations/tr.json new file mode 100644 index 00000000000..f0dab20775f --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "E-posta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/uk.json b/homeassistant/components/plum_lightpad/translations/uk.json new file mode 100644 index 00000000000..96b14f79375 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/uk.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/translations/de.json b/homeassistant/components/point/translations/de.json index 8ee83eab727..343c02055d5 100644 --- a/homeassistant/components/point/translations/de.json +++ b/homeassistant/components/point/translations/de.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_setup": "Du kannst nur ein Point-Konto konfigurieren.", + "already_setup": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "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": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", "unknown_authorize_url_generation": "Beim Generieren einer Authentifizierungs-URL ist ein unbekannter Fehler aufgetreten" }, "create_entry": { @@ -17,15 +17,15 @@ }, "step": { "auth": { - "description": "Folge dem Link unten und Akzeptiere Zugriff auf dei Minut-Konto. Kehre dann zur\u00fcck und dr\u00fccke unten auf Senden . \n\n [Link]({authorization_url})", + "description": "Folge dem Link unten und **Best\u00e4tige** den Zugriff auf dein Minut-Konto. Kehre dann zur\u00fcck und dr\u00fccke unten auf **Senden**. \n\n [Link]({authorization_url})", "title": "Point authentifizieren" }, "user": { "data": { "flow_impl": "Anbieter" }, - "description": "W\u00e4hle \u00fcber welchen Authentifizierungsanbieter du sich mit Point authentifizieren m\u00f6chtest.", - "title": "Authentifizierungsanbieter" + "description": "M\u00f6chtest du mit der Einrichtung beginnen?", + "title": "W\u00e4hle die Authentifizierungsmethode" } } } diff --git a/homeassistant/components/point/translations/fr.json b/homeassistant/components/point/translations/fr.json index 141af3545ba..ab9cd7af34e 100644 --- a/homeassistant/components/point/translations/fr.json +++ b/homeassistant/components/point/translations/fr.json @@ -5,7 +5,8 @@ "authorize_url_fail": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation.", "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", "external_setup": "Point correctement configur\u00e9 \u00e0 partir d\u2019un autre flux.", - "no_flows": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation." + "no_flows": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", + "unknown_authorize_url_generation": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation." }, "create_entry": { "default": "Authentification r\u00e9ussie" diff --git a/homeassistant/components/point/translations/tr.json b/homeassistant/components/point/translations/tr.json new file mode 100644 index 00000000000..5a4849fad07 --- /dev/null +++ b/homeassistant/components/point/translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_setup": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "unknown_authorize_url_generation": "Yetkilendirme url'si olu\u015fturulurken bilinmeyen hata." + }, + "error": { + "no_token": "Eri\u015fim Belirteci" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/translations/uk.json b/homeassistant/components/point/translations/uk.json new file mode 100644 index 00000000000..6b66a39a291 --- /dev/null +++ b/homeassistant/components/point/translations/uk.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_setup": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "authorize_url_fail": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "external_setup": "Point \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439 \u0437 \u0456\u043d\u0448\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0443.", + "no_flows": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "unknown_authorize_url_generation": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457." + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "error": { + "follow_link": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0437\u0430 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c \u0456 \u043f\u0440\u043e\u0439\u0434\u0456\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e, \u043f\u0435\u0440\u0448 \u043d\u0456\u0436 \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0438 \"\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438\".", + "no_token": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443." + }, + "step": { + "auth": { + "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0437\u0430 [\u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c]({authorization_url}) \u0456 ** \u0414\u043e\u0437\u0432\u043e\u043b\u044c\u0442\u0435 ** \u0434\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0432\u0430\u0448\u043e\u0433\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 Minut, \u043f\u043e\u0442\u0456\u043c \u043f\u043e\u0432\u0435\u0440\u043d\u0456\u0442\u044c\u0441\u044f \u0441\u044e\u0434\u0438 \u0442\u0430 \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **.", + "title": "Minut Point" + }, + "user": { + "data": { + "flow_impl": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440" + }, + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?", + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/de.json b/homeassistant/components/poolsense/translations/de.json index c7dfe6d02b2..5869da61c9c 100644 --- a/homeassistant/components/poolsense/translations/de.json +++ b/homeassistant/components/poolsense/translations/de.json @@ -3,13 +3,16 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, "step": { "user": { "data": { "email": "E-Mail", "password": "Passwort" }, - "description": "Wollen Sie mit der Einrichtung beginnen?" + "description": "M\u00f6chtest du mit der Einrichtung beginnen?" } } } diff --git a/homeassistant/components/poolsense/translations/tr.json b/homeassistant/components/poolsense/translations/tr.json new file mode 100644 index 00000000000..1e2e9d0c5b8 --- /dev/null +++ b/homeassistant/components/poolsense/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "email": "E-posta", + "password": "Parola" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/uk.json b/homeassistant/components/poolsense/translations/uk.json new file mode 100644 index 00000000000..6ac3b97f741 --- /dev/null +++ b/homeassistant/components/poolsense/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "user": { + "data": { + "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?", + "title": "PoolSense" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/ca.json b/homeassistant/components/powerwall/translations/ca.json index 2c9becb1795..4b176fff686 100644 --- a/homeassistant/components/powerwall/translations/ca.json +++ b/homeassistant/components/powerwall/translations/ca.json @@ -8,6 +8,7 @@ "unknown": "Error inesperat", "wrong_version": "El teu Powerwall utilitza una versi\u00f3 de programari no compatible. L'hauries d'actualitzar o informar d'aquest problema perqu\u00e8 sigui solucionat." }, + "flow_title": "Tesla Powerwall ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/cs.json b/homeassistant/components/powerwall/translations/cs.json index b64eabcf33b..698934ad10e 100644 --- a/homeassistant/components/powerwall/translations/cs.json +++ b/homeassistant/components/powerwall/translations/cs.json @@ -8,6 +8,7 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba", "wrong_version": "Powerwall pou\u017e\u00edv\u00e1 verzi softwaru, kter\u00e1 nen\u00ed podporov\u00e1na. Zva\u017ete upgrade nebo nahlaste probl\u00e9m, aby mohl b\u00fdt vy\u0159e\u0161en." }, + "flow_title": "Tesla Powerwall ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/de.json b/homeassistant/components/powerwall/translations/de.json index f5317e3046a..c30286d8744 100644 --- a/homeassistant/components/powerwall/translations/de.json +++ b/homeassistant/components/powerwall/translations/de.json @@ -1,12 +1,13 @@ { "config": { "abort": { - "already_configured": "Die Powerwall ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, + "flow_title": "Tesla Powerwall ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/en.json b/homeassistant/components/powerwall/translations/en.json index ac0d9568154..6eb0b77708d 100644 --- a/homeassistant/components/powerwall/translations/en.json +++ b/homeassistant/components/powerwall/translations/en.json @@ -1,21 +1,21 @@ { - "config": { - "flow_title": "Tesla Powerwall ({ip_address})", - "step": { - "user": { - "title": "Connect to the powerwall", - "data": { - "ip_address": "[%key:common::config_flow::data::ip%]" + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error", + "wrong_version": "Your powerwall uses a software version that is not supported. Please consider upgrading or reporting this issue so it can be resolved." + }, + "flow_title": "Tesla Powerwall ({ip_address})", + "step": { + "user": { + "data": { + "ip_address": "IP Address" + }, + "title": "Connect to the powerwall" + } } - } - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "wrong_version": "Your powerwall uses a software version that is not supported. Please consider upgrading or reporting this issue so it can be resolved.", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/es.json b/homeassistant/components/powerwall/translations/es.json index 76835e81480..373bf29f8ba 100644 --- a/homeassistant/components/powerwall/translations/es.json +++ b/homeassistant/components/powerwall/translations/es.json @@ -8,6 +8,7 @@ "unknown": "Error inesperado", "wrong_version": "Tu powerwall utiliza una versi\u00f3n de software que no es compatible. Considera actualizar o informar de este problema para que pueda resolverse." }, + "flow_title": "Powerwall de Tesla ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/et.json b/homeassistant/components/powerwall/translations/et.json index 996b0ea4b30..b10dca9b08b 100644 --- a/homeassistant/components/powerwall/translations/et.json +++ b/homeassistant/components/powerwall/translations/et.json @@ -8,6 +8,7 @@ "unknown": "Ootamatu t\u00f5rge", "wrong_version": "Teie Powerwall kasutab tarkvaraversiooni, mida ei toetata. Kaaluge tarkvara uuendamist v\u00f5i probleemist teavitamist, et see saaks lahendatud." }, + "flow_title": "Tesla Powerwall ( {ip_address} )", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/it.json b/homeassistant/components/powerwall/translations/it.json index 422a28b6936..376168f8616 100644 --- a/homeassistant/components/powerwall/translations/it.json +++ b/homeassistant/components/powerwall/translations/it.json @@ -8,6 +8,7 @@ "unknown": "Errore imprevisto", "wrong_version": "Il tuo powerwall utilizza una versione del software non supportata. Si prega di considerare l'aggiornamento o la segnalazione di questo problema in modo che possa essere risolto." }, + "flow_title": "Tesla Powerwall ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/no.json b/homeassistant/components/powerwall/translations/no.json index 7ddab100358..cdc04a006ad 100644 --- a/homeassistant/components/powerwall/translations/no.json +++ b/homeassistant/components/powerwall/translations/no.json @@ -8,6 +8,7 @@ "unknown": "Uventet feil", "wrong_version": "Powerwall bruker en programvareversjon som ikke st\u00f8ttes. Vennligst vurder \u00e5 oppgradere eller rapportere dette problemet, s\u00e5 det kan l\u00f8ses." }, + "flow_title": "", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/pl.json b/homeassistant/components/powerwall/translations/pl.json index 8532066608c..dfd4fa21a37 100644 --- a/homeassistant/components/powerwall/translations/pl.json +++ b/homeassistant/components/powerwall/translations/pl.json @@ -8,6 +8,7 @@ "unknown": "Nieoczekiwany b\u0142\u0105d", "wrong_version": "Powerwall u\u017cywa wersji oprogramowania, kt\u00f3ra nie jest obs\u0142ugiwana. Rozwa\u017c uaktualnienie lub zg\u0142oszenie tego problemu, aby mo\u017cna go by\u0142o rozwi\u0105za\u0107." }, + "flow_title": "Tesla UPS ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/ru.json b/homeassistant/components/powerwall/translations/ru.json index a8713bcd04a..faabf2d0ede 100644 --- a/homeassistant/components/powerwall/translations/ru.json +++ b/homeassistant/components/powerwall/translations/ru.json @@ -8,6 +8,7 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", "wrong_version": "\u0412\u0430\u0448 powerwall \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0432\u0435\u0440\u0441\u0438\u044e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u043e\u0433\u043e \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u0441\u043e\u043e\u0431\u0449\u0438\u0442\u0435 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0435, \u0447\u0442\u043e\u0431\u044b \u0435\u0435 \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0440\u0435\u0448\u0438\u0442\u044c." }, + "flow_title": "Tesla Powerwall ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/tr.json b/homeassistant/components/powerwall/translations/tr.json new file mode 100644 index 00000000000..dd09a83a78c --- /dev/null +++ b/homeassistant/components/powerwall/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "Tesla Powerwall ( {ip_address} )", + "step": { + "user": { + "data": { + "ip_address": "\u0130p Adresi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/uk.json b/homeassistant/components/powerwall/translations/uk.json new file mode 100644 index 00000000000..9b397138c52 --- /dev/null +++ b/homeassistant/components/powerwall/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430", + "wrong_version": "\u0412\u0430\u0448 Powerwall \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454 \u0432\u0435\u0440\u0441\u0456\u044e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043d\u043e\u0433\u043e \u0437\u0430\u0431\u0435\u0437\u043f\u0435\u0447\u0435\u043d\u043d\u044f, \u044f\u043a\u0430 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0440\u043e\u0437\u0433\u043b\u044f\u043d\u044c\u0442\u0435 \u043c\u043e\u0436\u043b\u0438\u0432\u0456\u0441\u0442\u044c \u043f\u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0430\u0431\u043e \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u0442\u0435 \u043f\u0440\u043e \u0446\u044e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443, \u0449\u043e\u0431 \u0457\u0457 \u043c\u043e\u0436\u043d\u0430 \u0431\u0443\u043b\u043e \u0432\u0438\u0440\u0456\u0448\u0438\u0442\u0438." + }, + "step": { + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430" + }, + "title": "Tesla Powerwall" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/zh-Hant.json b/homeassistant/components/powerwall/translations/zh-Hant.json index 45edbf2d88e..ec0d2e278b6 100644 --- a/homeassistant/components/powerwall/translations/zh-Hant.json +++ b/homeassistant/components/powerwall/translations/zh-Hant.json @@ -8,6 +8,7 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4", "wrong_version": "\u4e0d\u652f\u63f4\u60a8\u6240\u4f7f\u7528\u7684 Powerwall \u7248\u672c\u3002\u8acb\u8003\u616e\u9032\u884c\u5347\u7d1a\u6216\u56de\u5831\u6b64\u554f\u984c\u3001\u4ee5\u671f\u554f\u984c\u53ef\u4ee5\u7372\u5f97\u89e3\u6c7a\u3002" }, + "flow_title": "Tesla Powerwall ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/profiler/translations/de.json b/homeassistant/components/profiler/translations/de.json new file mode 100644 index 00000000000..7137cd2ee4e --- /dev/null +++ b/homeassistant/components/profiler/translations/de.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "user": { + "description": "M\u00f6chtest du mit der Einrichtung beginnen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/profiler/translations/tr.json b/homeassistant/components/profiler/translations/tr.json new file mode 100644 index 00000000000..a152eb19468 --- /dev/null +++ b/homeassistant/components/profiler/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/profiler/translations/uk.json b/homeassistant/components/profiler/translations/uk.json new file mode 100644 index 00000000000..5594895456e --- /dev/null +++ b/homeassistant/components/profiler/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "step": { + "user": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/de.json b/homeassistant/components/progettihwsw/translations/de.json index 2e5bed4b668..0f773e03c1d 100644 --- a/homeassistant/components/progettihwsw/translations/de.json +++ b/homeassistant/components/progettihwsw/translations/de.json @@ -1,7 +1,10 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/progettihwsw/translations/tr.json b/homeassistant/components/progettihwsw/translations/tr.json new file mode 100644 index 00000000000..1d3d77584dd --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/tr.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "relay_modes": { + "data": { + "relay_1": "R\u00f6le 1", + "relay_10": "R\u00f6le 10", + "relay_11": "R\u00f6le 11", + "relay_12": "R\u00f6le 12", + "relay_13": "R\u00f6le 13", + "relay_14": "R\u00f6le 14", + "relay_15": "R\u00f6le 15", + "relay_16": "R\u00f6le 16", + "relay_2": "R\u00f6le 2", + "relay_3": "R\u00f6le 3", + "relay_4": "R\u00f6le 4", + "relay_5": "R\u00f6le 5", + "relay_6": "R\u00f6le 6", + "relay_7": "R\u00f6le 7", + "relay_8": "R\u00f6le 8", + "relay_9": "R\u00f6le 9" + }, + "title": "R\u00f6leleri kur" + }, + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + }, + "title": "Panoyu kur" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/uk.json b/homeassistant/components/progettihwsw/translations/uk.json new file mode 100644 index 00000000000..7918db8e158 --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/uk.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "relay_modes": { + "data": { + "relay_1": "\u0420\u0435\u043b\u0435 1", + "relay_10": "\u0420\u0435\u043b\u0435 10", + "relay_11": "\u0420\u0435\u043b\u0435 11", + "relay_12": "\u0420\u0435\u043b\u0435 12", + "relay_13": "\u0420\u0435\u043b\u0435 13", + "relay_14": "\u0420\u0435\u043b\u0435 14", + "relay_15": "\u0420\u0435\u043b\u0435 15", + "relay_16": "\u0420\u0435\u043b\u0435 16", + "relay_2": "\u0420\u0435\u043b\u0435 2", + "relay_3": "\u0420\u0435\u043b\u0435 3", + "relay_4": "\u0420\u0435\u043b\u0435 4", + "relay_5": "\u0420\u0435\u043b\u0435 5", + "relay_6": "\u0420\u0435\u043b\u0435 6", + "relay_7": "\u0420\u0435\u043b\u0435 7", + "relay_8": "\u0420\u0435\u043b\u0435 8", + "relay_9": "\u0420\u0435\u043b\u0435 9" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0440\u0435\u043b\u0435" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043b\u0430\u0442\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/translations/de.json b/homeassistant/components/ps4/translations/de.json index 5dd638a717c..d5aa867f1db 100644 --- a/homeassistant/components/ps4/translations/de.json +++ b/homeassistant/components/ps4/translations/de.json @@ -1,15 +1,16 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "credential_error": "Fehler beim Abrufen der Anmeldeinformationen.", "no_devices_found": "Es wurden keine PlayStation 4 im Netzwerk gefunden.", "port_987_bind_error": "Konnte sich nicht an Port 987 binden. Weitere Informationen findest du in der [Dokumentation] (https://www.home-assistant.io/components/ps4/).", "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", + "cannot_connect": "Verbindung fehlgeschlagen", "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.", + "login_failed": "Fehler beim Koppeln mit der PlayStation 4. \u00dcberpr\u00fcfe, ob der PIN-Code korrekt ist.", "no_ipaddress": "Gib die IP-Adresse der PlayStation 4 ein, die konfiguriert werden soll." }, "step": { @@ -19,7 +20,7 @@ }, "link": { "data": { - "code": "PIN", + "code": "PIN-Code", "ip_address": "IP-Adresse", "name": "Name", "region": "Region" diff --git a/homeassistant/components/ps4/translations/tr.json b/homeassistant/components/ps4/translations/tr.json new file mode 100644 index 00000000000..4e3e0b53445 --- /dev/null +++ b/homeassistant/components/ps4/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "link": { + "data": { + "ip_address": "\u0130p Adresi" + } + }, + "mode": { + "data": { + "ip_address": "\u0130p Adresi (Otomatik Bulma kullan\u0131l\u0131yorsa bo\u015f b\u0131rak\u0131n)." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/translations/uk.json b/homeassistant/components/ps4/translations/uk.json new file mode 100644 index 00000000000..696a46bf8d7 --- /dev/null +++ b/homeassistant/components/ps4/translations/uk.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "credential_error": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0445 \u0434\u0430\u043d\u0438\u0445.", + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "port_987_bind_error": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 \u043f\u043e\u0440\u0442\u043e\u043c 987. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438](https://www.home-assistant.io/components/ps4/).", + "port_997_bind_error": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 \u043f\u043e\u0440\u0442\u043e\u043c 997. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438](https://www.home-assistant.io/components/ps4/)." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "credential_timeout": "\u0427\u0430\u0441 \u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f \u043c\u0438\u043d\u0443\u0432. \u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **, \u0449\u043e\u0431 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0438 \u0441\u043f\u0440\u043e\u0431\u0443.", + "login_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043f\u0430\u0440\u0443 \u0437 PlayStation 4. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e PIN-\u043a\u043e\u0434 \u0432\u0432\u0435\u0434\u0435\u043d\u0438\u0439 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e.", + "no_ipaddress": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441\u0443 PlayStation 4." + }, + "step": { + "creds": { + "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **, \u0430 \u043f\u043e\u0442\u0456\u043c \u0432 \u0434\u043e\u0434\u0430\u0442\u043a\u0443 'PS4 Second Screen' \u043e\u043d\u043e\u0432\u0456\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 \u0456 \u0432\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 'Home-Assistant'.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN-\u043a\u043e\u0434", + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430", + "region": "\u0420\u0435\u0433\u0456\u043e\u043d" + }, + "description": "\u0414\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f PIN-\u043a\u043e\u0434\u0443 \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0434\u043e \u043f\u0443\u043d\u043a\u0442\u0443 ** \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f ** \u043d\u0430 \u043a\u043e\u043d\u0441\u043e\u043b\u0456 PlayStation 4. \u041f\u043e\u0442\u0456\u043c \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 ** \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043c\u043e\u0431\u0456\u043b\u044c\u043d\u043e\u0433\u043e \u0434\u043e\u0434\u0430\u0442\u043a\u0430 ** \u0456 \u0432\u0438\u0431\u0435\u0440\u0456\u0442\u044c ** \u0414\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 ** . \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438](https://www.home-assistant.io/components/ps4/) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457.", + "title": "PlayStation 4" + }, + "mode": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430 (\u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c \u043f\u0440\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u0456 \u0440\u0435\u0436\u0438\u043c\u0443 \u0430\u0432\u0442\u043e\u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f)", + "mode": "\u0420\u0435\u0436\u0438\u043c" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0438\u043f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u041f\u043e\u043b\u0435 'IP-\u0430\u0434\u0440\u0435\u0441\u0430' \u043c\u043e\u0436\u043d\u0430 \u0437\u0430\u043b\u0438\u0448\u0438\u0442\u0438 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c, \u044f\u043a\u0449\u043e \u0432\u0438\u0431\u0440\u0430\u043d\u043e 'Auto Discovery', \u043e\u0441\u043a\u0456\u043b\u044c\u043a\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u0431\u0443\u0434\u0443\u0442\u044c \u0434\u043e\u0434\u0430\u043d\u0456 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e.", + "title": "PlayStation 4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/de.json b/homeassistant/components/pvpc_hourly_pricing/translations/de.json index 8e3e9b68e42..1b5c4d37658 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/de.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Die Integration ist bereits mit einem vorhandenen Sensor mit diesem Tarif konfiguriert" + "already_configured": "Der Dienst ist bereits konfiguriert" }, "step": { "user": { diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/tr.json b/homeassistant/components/pvpc_hourly_pricing/translations/tr.json new file mode 100644 index 00000000000..394f876401b --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/translations/tr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "name": "Sens\u00f6r Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/uk.json b/homeassistant/components/pvpc_hourly_pricing/translations/uk.json new file mode 100644 index 00000000000..da2136d7765 --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430", + "tariff": "\u041a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u043d\u0438\u0439 \u0442\u0430\u0440\u0438\u0444 (1, 2 \u0430\u0431\u043e 3 \u043f\u0435\u0440\u0456\u043e\u0434\u0438)" + }, + "description": "\u0426\u0435\u0439 \u0441\u0435\u043d\u0441\u043e\u0440 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454 \u043e\u0444\u0456\u0446\u0456\u0439\u043d\u0438\u0439 API \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f [\u043f\u043e\u0433\u043e\u0434\u0438\u043d\u043d\u043e\u0457 \u0446\u0456\u043d\u0438 \u0437\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u0435\u043d\u0435\u0440\u0433\u0456\u044e (PVPC)] (https://www.esios.ree.es/es/pvpc) \u0432 \u0406\u0441\u043f\u0430\u043d\u0456\u0457.\n\u0414\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u0435\u0442\u0430\u043b\u044c\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044c \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0430\u0440\u0438\u0444, \u0437\u0430\u0441\u043d\u043e\u0432\u0430\u043d\u0438\u0439 \u043d\u0430 \u043a\u0456\u043b\u044c\u043a\u043e\u0441\u0442\u0456 \u0440\u043e\u0437\u0440\u0430\u0445\u0443\u043d\u043a\u043e\u0432\u0438\u0445 \u043f\u0435\u0440\u0456\u043e\u0434\u0456\u0432 \u0432 \u0434\u0435\u043d\u044c:\n- 1 \u043f\u0435\u0440\u0456\u043e\u0434: normal\n- 2 \u043f\u0435\u0440\u0456\u043e\u0434\u0438: discrimination (nightly rate)\n- 3 \u043f\u0435\u0440\u0456\u043e\u0434\u0438: electric car (nightly rate of 3 periods)", + "title": "\u0412\u0438\u0431\u0456\u0440 \u0442\u0430\u0440\u0438\u0444\u0443" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/translations/de.json b/homeassistant/components/rachio/translations/de.json index e6a4d73cde1..9acd92ce40d 100644 --- a/homeassistant/components/rachio/translations/de.json +++ b/homeassistant/components/rachio/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, @@ -13,7 +13,7 @@ "data": { "api_key": "API-Schl\u00fcssel" }, - "description": "Sie ben\u00f6tigen den API-Schl\u00fcssel von https://app.rach.io/. W\u00e4hlen Sie \"Kontoeinstellungen\" und klicken Sie dann auf \"API-SCHL\u00dcSSEL ERHALTEN\".", + "description": "Du ben\u00f6tigst den API-Schl\u00fcssel von https://app.rach.io/. Gehe in die Einstellungen und klicke auf \"API-SCHL\u00dcSSEL ANFORDERN\".", "title": "Stellen Sie eine Verbindung zu Ihrem Rachio-Ger\u00e4t her" } } @@ -22,7 +22,7 @@ "step": { "init": { "data": { - "manual_run_mins": "Wie lange, in Minuten, um eine Station einzuschalten, wenn der Schalter aktiviert ist." + "manual_run_mins": "Wie viele Minuten es laufen soll, wenn ein Zonen-Schalter aktiviert wird" } } } diff --git a/homeassistant/components/rachio/translations/tr.json b/homeassistant/components/rachio/translations/tr.json new file mode 100644 index 00000000000..8bbc4eb1e49 --- /dev/null +++ b/homeassistant/components/rachio/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/translations/uk.json b/homeassistant/components/rachio/translations/uk.json new file mode 100644 index 00000000000..af5d7cd39d9 --- /dev/null +++ b/homeassistant/components/rachio/translations/uk.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "description": "\u0414\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0442\u0440\u0456\u0431\u0435\u043d \u043a\u043b\u044e\u0447 API \u0437 \u0441\u0430\u0439\u0442\u0443 https://app.rach.io/. \u041f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0432 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f, \u0430 \u043f\u043e\u0442\u0456\u043c \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c 'GET API KEY'.", + "title": "Rachio" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "\u0422\u0440\u0438\u0432\u0430\u043b\u0456\u0441\u0442\u044c \u0440\u043e\u0431\u043e\u0442\u0438 \u043f\u0440\u0438 \u0430\u043a\u0442\u0438\u0432\u0430\u0446\u0456\u0457 \u043f\u0435\u0440\u0435\u043c\u0438\u043a\u0430\u0447\u0430 \u0437\u043e\u043d\u0438 (\u0432 \u0445\u0432\u0438\u043b\u0438\u043d\u0430\u0445)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/de.json b/homeassistant/components/rainmachine/translations/de.json index 92df52bb148..511d85b36b6 100644 --- a/homeassistant/components/rainmachine/translations/de.json +++ b/homeassistant/components/rainmachine/translations/de.json @@ -1,7 +1,10 @@ { "config": { "abort": { - "already_configured": "Dieser RainMachine-Kontroller ist bereits konfiguriert." + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { "user": { diff --git a/homeassistant/components/rainmachine/translations/tr.json b/homeassistant/components/rainmachine/translations/tr.json index 20f74cae994..80cfc05e568 100644 --- a/homeassistant/components/rainmachine/translations/tr.json +++ b/homeassistant/components/rainmachine/translations/tr.json @@ -1,4 +1,21 @@ { + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "ip_address": "Ana makine ad\u0131 veya IP adresi", + "password": "Parola", + "port": "Port" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/uk.json b/homeassistant/components/rainmachine/translations/uk.json new file mode 100644 index 00000000000..ff8d7089cec --- /dev/null +++ b/homeassistant/components/rainmachine/translations/uk.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "user": { + "data": { + "ip_address": "\u0414\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "RainMachine" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "zone_run_time": "\u0427\u0430\u0441 \u0440\u043e\u0431\u043e\u0442\u0438 \u0437\u043e\u043d\u0438 \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f RainMachine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/de.json b/homeassistant/components/recollect_waste/translations/de.json new file mode 100644 index 00000000000..7cbcea1b25e --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "step": { + "user": { + "data": { + "place_id": "Platz-ID", + "service_id": "Dienst-ID" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "Recollect Waste konfigurieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/es.json b/homeassistant/components/recollect_waste/translations/es.json index 2fdeb991bfd..69a39d435eb 100644 --- a/homeassistant/components/recollect_waste/translations/es.json +++ b/homeassistant/components/recollect_waste/translations/es.json @@ -20,7 +20,8 @@ "init": { "data": { "friendly_name": "Utilizar nombres descriptivos para los tipos de recogida (cuando sea posible)" - } + }, + "title": "Configurar la recogida de residuos" } } } diff --git a/homeassistant/components/recollect_waste/translations/lb.json b/homeassistant/components/recollect_waste/translations/lb.json new file mode 100644 index 00000000000..4e312bb0f23 --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/lb.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "invalid_place_or_service_id": "Ong\u00eblteg Place oder Service ID" + }, + "step": { + "user": { + "data": { + "place_id": "Place ID", + "service_id": "Service ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/pl.json b/homeassistant/components/recollect_waste/translations/pl.json index 013d0028790..cc0342e93d7 100644 --- a/homeassistant/components/recollect_waste/translations/pl.json +++ b/homeassistant/components/recollect_waste/translations/pl.json @@ -14,5 +14,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "U\u017cywaj przyjaznych nazw dla typu odbioru (je\u015bli to mo\u017cliwe)" + }, + "title": "Konfiguracja Recollect Waste" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/tr.json b/homeassistant/components/recollect_waste/translations/tr.json new file mode 100644 index 00000000000..5307276a71d --- /dev/null +++ b/homeassistant/components/recollect_waste/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/recollect_waste/translations/uk.json b/homeassistant/components/recollect_waste/translations/uk.json new file mode 100644 index 00000000000..db47699f1ba --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/uk.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "invalid_place_or_service_id": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 ID \u0440\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f \u0430\u0431\u043e \u0441\u043b\u0443\u0436\u0431\u0438." + }, + "step": { + "user": { + "data": { + "place_id": "ID \u0440\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f", + "service_id": "ID \u0441\u043b\u0443\u0436\u0431\u0438" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0437\u0440\u043e\u0437\u0443\u043c\u0456\u043b\u0456 \u0456\u043c\u0435\u043d\u0430 \u0434\u043b\u044f \u0442\u0438\u043f\u0456\u0432 \u0432\u0438\u0431\u043e\u0440\u0443 (\u044f\u043a\u0449\u043e \u043c\u043e\u0436\u043b\u0438\u0432\u043e)" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Recollect Waste" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/remote/translations/tr.json b/homeassistant/components/remote/translations/tr.json index cdc40c6268b..5359c99a78a 100644 --- a/homeassistant/components/remote/translations/tr.json +++ b/homeassistant/components/remote/translations/tr.json @@ -1,4 +1,14 @@ { + "device_automation": { + "action_type": { + "turn_off": "{entity_name} kapat", + "turn_on": "{entity_name} a\u00e7\u0131n" + }, + "trigger_type": { + "turned_off": "{entity_name} kapat\u0131ld\u0131", + "turned_on": "{entity_name} a\u00e7\u0131ld\u0131" + } + }, "state": { "_": { "off": "Kapal\u0131", diff --git a/homeassistant/components/remote/translations/uk.json b/homeassistant/components/remote/translations/uk.json index 2feda4928e5..1f275f5f2eb 100644 --- a/homeassistant/components/remote/translations/uk.json +++ b/homeassistant/components/remote/translations/uk.json @@ -1,5 +1,14 @@ { "device_automation": { + "action_type": { + "toggle": "{entity_name}: \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u0438", + "turn_off": "{entity_name}: \u0432\u0438\u043c\u043a\u043d\u0443\u0442\u0438", + "turn_on": "{entity_name}: \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438" + }, + "condition_type": { + "is_off": "{entity_name} \u0443 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456" + }, "trigger_type": { "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e", "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u043e" diff --git a/homeassistant/components/rfxtrx/translations/ca.json b/homeassistant/components/rfxtrx/translations/ca.json index 6c4e920df02..d7db4107e3b 100644 --- a/homeassistant/components/rfxtrx/translations/ca.json +++ b/homeassistant/components/rfxtrx/translations/ca.json @@ -64,7 +64,8 @@ "off_delay": "Retard OFF", "off_delay_enabled": "Activa el retard OFF", "replace_device": "Selecciona el dispositiu a substituir", - "signal_repetitions": "Nombre de repeticions del senyal" + "signal_repetitions": "Nombre de repeticions del senyal", + "venetian_blind_mode": "Mode persiana veneciana" }, "title": "Configuraci\u00f3 de les opcions del dispositiu" } diff --git a/homeassistant/components/rfxtrx/translations/de.json b/homeassistant/components/rfxtrx/translations/de.json index 1979a10cb8a..b1e4197c0f1 100644 --- a/homeassistant/components/rfxtrx/translations/de.json +++ b/homeassistant/components/rfxtrx/translations/de.json @@ -2,13 +2,17 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert. Nur eine Konfiguration m\u00f6glich.", - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "setup_network": { + "data": { + "host": "Host", + "port": "Port" + }, "title": "Verbindungsadresse ausw\u00e4hlen" }, "setup_serial": { @@ -18,6 +22,9 @@ "title": "Ger\u00e4t" }, "setup_serial_manual_path": { + "data": { + "device": "USB-Ger\u00e4te-Pfad" + }, "title": "Pfad" }, "user": { @@ -30,6 +37,7 @@ }, "options": { "error": { + "already_configured_device": "Ger\u00e4t ist bereits konfiguriert", "unknown": "Unerwarteter Fehler" }, "step": { @@ -37,6 +45,13 @@ "data": { "debug": "Debugging aktivieren" } + }, + "set_device_options": { + "data": { + "off_delay": "Ausschaltverz\u00f6gerung", + "off_delay_enabled": "Ausschaltverz\u00f6gerung aktivieren", + "replace_device": "W\u00e4hle ein Ger\u00e4t aus, das ersetzt werden soll" + } } } } diff --git a/homeassistant/components/rfxtrx/translations/en.json b/homeassistant/components/rfxtrx/translations/en.json index 2d73ac56810..5e3f551e0cf 100644 --- a/homeassistant/components/rfxtrx/translations/en.json +++ b/homeassistant/components/rfxtrx/translations/en.json @@ -64,8 +64,8 @@ "off_delay": "Off delay", "off_delay_enabled": "Enable off delay", "replace_device": "Select device to replace", - "venetian_blind_mode": "Venetian blind mode (tilt by: US - long press, EU - short press)", - "signal_repetitions": "Number of signal repetitions" + "signal_repetitions": "Number of signal repetitions", + "venetian_blind_mode": "Venetian blind mode" }, "title": "Configure device options" } diff --git a/homeassistant/components/rfxtrx/translations/es.json b/homeassistant/components/rfxtrx/translations/es.json index 86bad8f096f..c1c4d72735c 100644 --- a/homeassistant/components/rfxtrx/translations/es.json +++ b/homeassistant/components/rfxtrx/translations/es.json @@ -64,7 +64,8 @@ "off_delay": "Retraso de apagado", "off_delay_enabled": "Activar retardo de apagado", "replace_device": "Seleccione el dispositivo que desea reemplazar", - "signal_repetitions": "N\u00famero de repeticiones de la se\u00f1al" + "signal_repetitions": "N\u00famero de repeticiones de la se\u00f1al", + "venetian_blind_mode": "Modo de persiana veneciana" }, "title": "Configurar las opciones del dispositivo" } diff --git a/homeassistant/components/rfxtrx/translations/et.json b/homeassistant/components/rfxtrx/translations/et.json index 1ade1f112c2..662664b4454 100644 --- a/homeassistant/components/rfxtrx/translations/et.json +++ b/homeassistant/components/rfxtrx/translations/et.json @@ -64,7 +64,8 @@ "off_delay": "V\u00e4ljal\u00fclitamise viivitus", "off_delay_enabled": "Luba v\u00e4ljal\u00fclitusviivitus", "replace_device": "Vali asendav seade", - "signal_repetitions": "Signaali korduste arv" + "signal_repetitions": "Signaali korduste arv", + "venetian_blind_mode": "Ribikardinate juhtimine" }, "title": "Seadista seadme valikud" } diff --git a/homeassistant/components/rfxtrx/translations/it.json b/homeassistant/components/rfxtrx/translations/it.json index ff705fdd0a2..938c471e992 100644 --- a/homeassistant/components/rfxtrx/translations/it.json +++ b/homeassistant/components/rfxtrx/translations/it.json @@ -64,7 +64,8 @@ "off_delay": "Ritardo di spegnimento", "off_delay_enabled": "Attivare il ritardo di spegnimento", "replace_device": "Selezionare il dispositivo da sostituire", - "signal_repetitions": "Numero di ripetizioni del segnale" + "signal_repetitions": "Numero di ripetizioni del segnale", + "venetian_blind_mode": "Modalit\u00e0 veneziana" }, "title": "Configurare le opzioni del dispositivo" } diff --git a/homeassistant/components/rfxtrx/translations/no.json b/homeassistant/components/rfxtrx/translations/no.json index 752136dac7f..3eb9c9b83df 100644 --- a/homeassistant/components/rfxtrx/translations/no.json +++ b/homeassistant/components/rfxtrx/translations/no.json @@ -64,7 +64,8 @@ "off_delay": "Av forsinkelse", "off_delay_enabled": "Aktiver av forsinkelse", "replace_device": "Velg enheten du vil erstatte", - "signal_repetitions": "Antall signalrepetisjoner" + "signal_repetitions": "Antall signalrepetisjoner", + "venetian_blind_mode": "Persiennemodus" }, "title": "Konfigurer enhetsalternativer" } diff --git a/homeassistant/components/rfxtrx/translations/pl.json b/homeassistant/components/rfxtrx/translations/pl.json index bf17d6c5166..e0e69b2a64a 100644 --- a/homeassistant/components/rfxtrx/translations/pl.json +++ b/homeassistant/components/rfxtrx/translations/pl.json @@ -75,7 +75,8 @@ "off_delay": "Op\u00f3\u017anienie stanu \"off\"", "off_delay_enabled": "W\u0142\u0105cz op\u00f3\u017anienie stanu \"off\"", "replace_device": "Wybierz urz\u0105dzenie do zast\u0105pienia", - "signal_repetitions": "Liczba powt\u00f3rze\u0144 sygna\u0142u" + "signal_repetitions": "Liczba powt\u00f3rze\u0144 sygna\u0142u", + "venetian_blind_mode": "Tryb \u017caluzji weneckich" }, "title": "Konfiguracja opcji urz\u0105dzenia" } diff --git a/homeassistant/components/rfxtrx/translations/ru.json b/homeassistant/components/rfxtrx/translations/ru.json index 361b051ac2c..5a635766d3f 100644 --- a/homeassistant/components/rfxtrx/translations/ru.json +++ b/homeassistant/components/rfxtrx/translations/ru.json @@ -64,7 +64,8 @@ "off_delay": "\u0417\u0430\u0434\u0435\u0440\u0436\u043a\u0430 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", "off_delay_enabled": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0437\u0430\u0434\u0435\u0440\u0436\u043a\u0443 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", "replace_device": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u0437\u0430\u043c\u0435\u043d\u044b", - "signal_repetitions": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u043e\u0432 \u0441\u0438\u0433\u043d\u0430\u043b\u0430" + "signal_repetitions": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u043e\u0432 \u0441\u0438\u0433\u043d\u0430\u043b\u0430", + "venetian_blind_mode": "\u0420\u0435\u0436\u0438\u043c \u0432\u0435\u043d\u0435\u0446\u0438\u0430\u043d\u0441\u043a\u0438\u0445 \u0436\u0430\u043b\u044e\u0437\u0438" }, "title": "\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" } diff --git a/homeassistant/components/rfxtrx/translations/tr.json b/homeassistant/components/rfxtrx/translations/tr.json new file mode 100644 index 00000000000..1c3ad8b9e05 --- /dev/null +++ b/homeassistant/components/rfxtrx/translations/tr.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "setup_network": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + } + } + }, + "options": { + "error": { + "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "unknown": "Beklenmeyen hata" + }, + "step": { + "set_device_options": { + "data": { + "venetian_blind_mode": "Jaluzi modu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/uk.json b/homeassistant/components/rfxtrx/translations/uk.json new file mode 100644 index 00000000000..1b0938b8b70 --- /dev/null +++ b/homeassistant/components/rfxtrx/translations/uk.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "setup_network": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + }, + "setup_serial": { + "data": { + "device": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + }, + "title": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + }, + "setup_serial_manual_path": { + "data": { + "device": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "title": "\u0428\u043b\u044f\u0445" + }, + "user": { + "data": { + "type": "\u0422\u0438\u043f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0438\u043f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + } + } + }, + "options": { + "error": { + "already_configured_device": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "invalid_event_code": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043e\u0434 \u043f\u043e\u0434\u0456\u0457.", + "invalid_input_2262_off": "\u041d\u0435\u0432\u0456\u0440\u043d\u0456 \u0434\u0430\u043d\u0456 \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u0438 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043d\u044f.", + "invalid_input_2262_on": "\u041d\u0435\u0432\u0456\u0440\u043d\u0456 \u0434\u0430\u043d\u0456 \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u0438 \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u044f.", + "invalid_input_off_delay": "\u041d\u0435\u0432\u0456\u0440\u043d\u0456 \u0434\u0430\u043d\u0456 \u0434\u043b\u044f \u0437\u0430\u0442\u0440\u0438\u043c\u043a\u0438 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043d\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "prompt_options": { + "data": { + "automatic_add": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0435 \u0434\u043e\u0434\u0430\u0432\u0430\u043d\u043d\u044f", + "debug": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0440\u0435\u0436\u0438\u043c \u043d\u0430\u043b\u0430\u0433\u043e\u0434\u0436\u0435\u043d\u043d\u044f", + "device": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f", + "event_code": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u0434\u0456\u0457", + "remove_device": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0434\u043b\u044f \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043d\u044f" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438" + }, + "set_device_options": { + "data": { + "command_off": "\u0417\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0431\u0456\u0442\u0456\u0432 \u0434\u0430\u043d\u0438\u0445 \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u0438 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043d\u044f", + "command_on": "\u0417\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0431\u0456\u0442\u0456\u0432 \u0434\u0430\u043d\u0438\u0445 \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u0438 \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u044f", + "data_bit": "\u041a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u0431\u0456\u0442\u0456\u0432 \u0434\u0430\u043d\u0438\u0445", + "fire_event": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u043f\u043e\u0434\u0456\u0457 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", + "off_delay": "\u0417\u0430\u0442\u0440\u0438\u043c\u043a\u0430 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043d\u044f", + "off_delay_enabled": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0437\u0430\u0442\u0440\u0438\u043c\u043a\u0443 \u0432\u0438\u043c\u0438\u043a\u0430\u043d\u043d\u044f", + "replace_device": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0434\u043b\u044f \u0437\u0430\u043c\u0456\u043d\u0438", + "signal_repetitions": "\u041a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u0456\u0432 \u0441\u0438\u0433\u043d\u0430\u043b\u0443", + "venetian_blind_mode": "\u0420\u0435\u0436\u0438\u043c \u0436\u0430\u043b\u044e\u0437\u0456" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/zh-Hant.json b/homeassistant/components/rfxtrx/translations/zh-Hant.json index 3da2e5f5384..24e5ee56d76 100644 --- a/homeassistant/components/rfxtrx/translations/zh-Hant.json +++ b/homeassistant/components/rfxtrx/translations/zh-Hant.json @@ -64,7 +64,8 @@ "off_delay": "\u5ef6\u9072", "off_delay_enabled": "\u958b\u555f\u5ef6\u9072", "replace_device": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u53d6\u4ee3", - "signal_repetitions": "\u8a0a\u865f\u91cd\u8907\u6b21\u6578" + "signal_repetitions": "\u8a0a\u865f\u91cd\u8907\u6b21\u6578", + "venetian_blind_mode": "\u767e\u8449\u7a97\u6a21\u5f0f" }, "title": "\u8a2d\u5b9a\u88dd\u7f6e\u9078\u9805" } diff --git a/homeassistant/components/ring/translations/tr.json b/homeassistant/components/ring/translations/tr.json new file mode 100644 index 00000000000..caba385d7fa --- /dev/null +++ b/homeassistant/components/ring/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/translations/uk.json b/homeassistant/components/ring/translations/uk.json new file mode 100644 index 00000000000..8d40cdf0d23 --- /dev/null +++ b/homeassistant/components/ring/translations/uk.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "2fa": { + "data": { + "2fa": "\u041a\u043e\u0434 \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "title": "\u0414\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "Ring" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/de.json b/homeassistant/components/risco/translations/de.json index ad863f7ff79..36d808bd6de 100644 --- a/homeassistant/components/risco/translations/de.json +++ b/homeassistant/components/risco/translations/de.json @@ -1,14 +1,18 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { "user": { "data": { "password": "Passwort", - "pin": "PIN Code", + "pin": "PIN-Code", "username": "Benutzername" } } @@ -16,6 +20,12 @@ }, "options": { "step": { + "init": { + "data": { + "code_arm_required": "PIN-Code zum Entsperren vorgeben", + "code_disarm_required": "PIN-Code zum Entsperren vorgeben" + } + }, "risco_to_ha": { "data": { "A": "Gruppe A", diff --git a/homeassistant/components/risco/translations/lb.json b/homeassistant/components/risco/translations/lb.json index 197dd78c403..ae136cb1843 100644 --- a/homeassistant/components/risco/translations/lb.json +++ b/homeassistant/components/risco/translations/lb.json @@ -39,7 +39,9 @@ "A": "Grupp A", "B": "Grupp B", "C": "Grupp C", - "D": "Grupp D" + "D": "Grupp D", + "arm": "Aktiv\u00e9iert (\u00cbNNERWEE)", + "partial_arm": "Deelweis Aktiv\u00e9iert (DOHEEM)" } } } diff --git a/homeassistant/components/risco/translations/tr.json b/homeassistant/components/risco/translations/tr.json new file mode 100644 index 00000000000..02a3b505f84 --- /dev/null +++ b/homeassistant/components/risco/translations/tr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "Se\u00e7enekleri yap\u0131land\u0131r\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/uk.json b/homeassistant/components/risco/translations/uk.json new file mode 100644 index 00000000000..53b64344f2e --- /dev/null +++ b/homeassistant/components/risco/translations/uk.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "pin": "PIN-\u043a\u043e\u0434", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + }, + "options": { + "step": { + "ha_to_risco": { + "data": { + "armed_away": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (\u041d\u0435 \u0432\u0434\u043e\u043c\u0430)", + "armed_custom_bypass": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 \u0437 \u0432\u0438\u043d\u044f\u0442\u043a\u0430\u043c\u0438", + "armed_home": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (\u0412\u0434\u043e\u043c\u0430)", + "armed_night": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (\u043d\u0456\u0447)" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0442\u0430\u043d \u0441\u0438\u0433\u043d\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u0457 Risco \u043f\u0440\u0438 \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u0456 \u0441\u0438\u0433\u043d\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u0457 Home Assistant", + "title": "\u0417\u0456\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044f \u0441\u0442\u0430\u043d\u0456\u0432 Home Assistant \u0456 Risco" + }, + "init": { + "data": { + "code_arm_required": "\u0412\u0438\u043c\u0430\u0433\u0430\u0442\u0438 PIN-\u043a\u043e\u0434 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0443", + "code_disarm_required": "\u0412\u0438\u043c\u0430\u0433\u0430\u0442\u0438 PIN-\u043a\u043e\u0434 \u0434\u043b\u044f \u0437\u043d\u044f\u0442\u0442\u044f \u0437 \u043e\u0445\u043e\u0440\u043e\u043d\u0438", + "scan_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438" + }, + "risco_to_ha": { + "data": { + "A": "\u0413\u0440\u0443\u043f\u0430 \u0410", + "B": "\u0413\u0440\u0443\u043f\u0430 B", + "C": "\u0413\u0440\u0443\u043f\u0430 C", + "D": "\u0413\u0440\u0443\u043f\u0430 D", + "arm": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (AWAY)", + "partial_arm": "\u0427\u0430\u0441\u0442\u043a\u043e\u0432\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0430 (STAY)" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0442\u0430\u043d \u0441\u0438\u0433\u043d\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u0457 Home Assistant \u043f\u0440\u0438 \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u0456 \u0441\u0438\u0433\u043d\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u0457 Risco", + "title": "\u0417\u0456\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044f \u0441\u0442\u0430\u043d\u0456\u0432 Home Assistant \u0456 Risco" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/ca.json b/homeassistant/components/roku/translations/ca.json index e9ab61575b5..eb0564b5bde 100644 --- a/homeassistant/components/roku/translations/ca.json +++ b/homeassistant/components/roku/translations/ca.json @@ -9,6 +9,10 @@ }, "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "description": "Vols configurar {name}?", + "title": "Roku" + }, "ssdp_confirm": { "description": "Vols configurar {name}?", "title": "Roku" diff --git a/homeassistant/components/roku/translations/cs.json b/homeassistant/components/roku/translations/cs.json index 7a83973a6f7..89ca523af47 100644 --- a/homeassistant/components/roku/translations/cs.json +++ b/homeassistant/components/roku/translations/cs.json @@ -9,6 +9,10 @@ }, "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "description": "Chcete nastavit {name}?", + "title": "Roku" + }, "ssdp_confirm": { "description": "Chcete nastavit {name}?", "title": "Roku" diff --git a/homeassistant/components/roku/translations/de.json b/homeassistant/components/roku/translations/de.json index 9899aeba427..4bfb3c7503d 100644 --- a/homeassistant/components/roku/translations/de.json +++ b/homeassistant/components/roku/translations/de.json @@ -9,6 +9,10 @@ }, "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "description": "M\u00f6chtest du {name} einrichten?", + "title": "Roku" + }, "ssdp_confirm": { "data": { "one": "eins", diff --git a/homeassistant/components/roku/translations/en.json b/homeassistant/components/roku/translations/en.json index 6facd1f3a7c..08db89f3677 100644 --- a/homeassistant/components/roku/translations/en.json +++ b/homeassistant/components/roku/translations/en.json @@ -9,6 +9,10 @@ }, "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "description": "Do you want to set up {name}?", + "title": "Roku" + }, "ssdp_confirm": { "description": "Do you want to set up {name}?", "title": "Roku" diff --git a/homeassistant/components/roku/translations/es.json b/homeassistant/components/roku/translations/es.json index 78fb2580927..95e42643379 100644 --- a/homeassistant/components/roku/translations/es.json +++ b/homeassistant/components/roku/translations/es.json @@ -9,6 +9,10 @@ }, "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "description": "\u00bfQuieres configurar {name} ?", + "title": "Roku" + }, "ssdp_confirm": { "description": "\u00bfQuieres configurar {name}?", "title": "Roku" diff --git a/homeassistant/components/roku/translations/et.json b/homeassistant/components/roku/translations/et.json index e4869d044c8..6727f539f57 100644 --- a/homeassistant/components/roku/translations/et.json +++ b/homeassistant/components/roku/translations/et.json @@ -9,6 +9,10 @@ }, "flow_title": "", "step": { + "discovery_confirm": { + "description": "Kas soovid seadistada {name}?", + "title": "" + }, "ssdp_confirm": { "description": "Kas soovid seadistada {name}?", "title": "" diff --git a/homeassistant/components/roku/translations/it.json b/homeassistant/components/roku/translations/it.json index 007be91d155..100d9992472 100644 --- a/homeassistant/components/roku/translations/it.json +++ b/homeassistant/components/roku/translations/it.json @@ -9,6 +9,10 @@ }, "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "description": "Vuoi configurare {name}?", + "title": "Roku" + }, "ssdp_confirm": { "description": "Vuoi impostare {name}?", "title": "Roku" diff --git a/homeassistant/components/roku/translations/lb.json b/homeassistant/components/roku/translations/lb.json index 3aa8e5fa642..04ad814c6b4 100644 --- a/homeassistant/components/roku/translations/lb.json +++ b/homeassistant/components/roku/translations/lb.json @@ -9,6 +9,10 @@ }, "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "description": "Soll {name} konfigur\u00e9iert ginn?", + "title": "Roku" + }, "ssdp_confirm": { "description": "Soll {name} konfigur\u00e9iert ginn?", "title": "Roku" diff --git a/homeassistant/components/roku/translations/no.json b/homeassistant/components/roku/translations/no.json index 029220b5859..dd4ce418141 100644 --- a/homeassistant/components/roku/translations/no.json +++ b/homeassistant/components/roku/translations/no.json @@ -9,6 +9,10 @@ }, "flow_title": "", "step": { + "discovery_confirm": { + "description": "Vil du konfigurere {name}?", + "title": "" + }, "ssdp_confirm": { "description": "Vil du sette opp {name} ?", "title": "" diff --git a/homeassistant/components/roku/translations/pl.json b/homeassistant/components/roku/translations/pl.json index 3231d6c4bb7..1d193acc0ff 100644 --- a/homeassistant/components/roku/translations/pl.json +++ b/homeassistant/components/roku/translations/pl.json @@ -9,6 +9,16 @@ }, "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "data": { + "few": "kilka", + "many": "wiele", + "one": "jeden", + "other": "inne" + }, + "description": "Czy chcesz skonfigurowa\u0107 {name}?", + "title": "Roku" + }, "ssdp_confirm": { "data": { "few": "kilka", diff --git a/homeassistant/components/roku/translations/ru.json b/homeassistant/components/roku/translations/ru.json index b5dcddbe555..f7f36f41b27 100644 --- a/homeassistant/components/roku/translations/ru.json +++ b/homeassistant/components/roku/translations/ru.json @@ -9,6 +9,10 @@ }, "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?", + "title": "Roku" + }, "ssdp_confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?", "title": "Roku" diff --git a/homeassistant/components/roku/translations/tr.json b/homeassistant/components/roku/translations/tr.json new file mode 100644 index 00000000000..0dca1a028b2 --- /dev/null +++ b/homeassistant/components/roku/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "discovery_confirm": { + "description": "{name} kurmak istiyor musunuz?", + "title": "Roku" + }, + "ssdp_confirm": { + "description": "{name} kurmak istiyor musunuz?" + }, + "user": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/uk.json b/homeassistant/components/roku/translations/uk.json new file mode 100644 index 00000000000..b7db8875f8e --- /dev/null +++ b/homeassistant/components/roku/translations/uk.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "flow_title": "Roku: {name}", + "step": { + "discovery_confirm": { + "description": "\u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name}?", + "title": "Roku" + }, + "ssdp_confirm": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name}?", + "title": "Roku" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e Roku." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/zh-Hant.json b/homeassistant/components/roku/translations/zh-Hant.json index cfa3a4aa3b4..4b0566d66b0 100644 --- a/homeassistant/components/roku/translations/zh-Hant.json +++ b/homeassistant/components/roku/translations/zh-Hant.json @@ -9,6 +9,10 @@ }, "flow_title": "Roku\uff1a{name}", "step": { + "discovery_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f", + "title": "Roku" + }, "ssdp_confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f", "title": "Roku" diff --git a/homeassistant/components/roomba/translations/ca.json b/homeassistant/components/roomba/translations/ca.json index af358678144..b2fe68c876c 100644 --- a/homeassistant/components/roomba/translations/ca.json +++ b/homeassistant/components/roomba/translations/ca.json @@ -1,9 +1,41 @@ { "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", + "not_irobot_device": "El dispositiu descobert no \u00e9s un dispositiu iRobot" + }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, + "flow_title": "iRobot {name} ({host})", "step": { + "init": { + "data": { + "host": "Amfitri\u00f3" + }, + "description": "Selecciona un/a Roomba o Braava.", + "title": "Connecta't al dispositiu autom\u00e0ticament" + }, + "link": { + "description": "Mant\u00e9 premut el bot\u00f3 d'inici a {name} fins que el dispositiu emeti un so (aproximadament dos segons).", + "title": "Recupera la contrasenya" + }, + "link_manual": { + "data": { + "password": "Contrasenya" + }, + "description": "No s'ha pogut obtenir la contrasenya del dispositiu autom\u00e0ticament. Segueix els passos de la seg\u00fcent documentaci\u00f3: {auth_help_url}", + "title": "Introdueix contrasenya" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "Amfitri\u00f3" + }, + "description": "No s'ha descobert cap Roomba ni cap Braava a la teva xarxa. El BLID \u00e9s la part del nom d'amfitri\u00f3 del dispositiu despr\u00e9s de `iRobot-`. Segueix els passos de la seg\u00fcent documentaci\u00f3: {auth_help_url}", + "title": "Connecta't al dispositiu manualment" + }, "user": { "data": { "blid": "BLID", diff --git a/homeassistant/components/roomba/translations/cs.json b/homeassistant/components/roomba/translations/cs.json index fdf4aff22c9..d94d39f8136 100644 --- a/homeassistant/components/roomba/translations/cs.json +++ b/homeassistant/components/roomba/translations/cs.json @@ -1,9 +1,29 @@ { "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, + "flow_title": "iRobot {name} ({host})", "step": { + "init": { + "data": { + "host": "Hostitel" + } + }, + "link_manual": { + "data": { + "password": "Heslo" + } + }, + "manual": { + "data": { + "host": "Hostitel" + } + }, "user": { "data": { "delay": "Zpo\u017ed\u011bn\u00ed", diff --git a/homeassistant/components/roomba/translations/de.json b/homeassistant/components/roomba/translations/de.json index 2f6ef37d13c..780d406bcaf 100644 --- a/homeassistant/components/roomba/translations/de.json +++ b/homeassistant/components/roomba/translations/de.json @@ -1,9 +1,40 @@ { "config": { - "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut" + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", + "not_irobot_device": "Das erkannte Ger\u00e4t ist kein iRobot-Ger\u00e4t" }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "flow_title": "iRobot {name} ({host})", "step": { + "init": { + "data": { + "host": "Host" + }, + "description": "W\u00e4hle einen Roomba oder Braava aus.", + "title": "Automatisch mit dem Ger\u00e4t verbinden" + }, + "link": { + "description": "Halte die Home-Taste von {name} gedr\u00fcckt, bis das Ger\u00e4t einen Ton erzeugt (ca. zwei Sekunden).", + "title": "Passwort abrufen" + }, + "link_manual": { + "data": { + "password": "Passwort" + }, + "description": "Das Passwort konnte nicht automatisch vom Ger\u00e4t abgerufen werden. Bitte die in der Dokumentation beschriebenen Schritte unter {auth_help_url} befolgen", + "title": "Passwort eingeben" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "Host" + }, + "title": "Manuell mit dem Ger\u00e4t verbinden" + }, "user": { "data": { "blid": "BLID", diff --git a/homeassistant/components/roomba/translations/en.json b/homeassistant/components/roomba/translations/en.json index 276ab4a92a2..8d449e18815 100644 --- a/homeassistant/components/roomba/translations/en.json +++ b/homeassistant/components/roomba/translations/en.json @@ -1,51 +1,62 @@ { - "config": { - "flow_title": "iRobot {name} ({host})", - "step": { - "init": { - "title": "Automaticlly connect to the device", - "description": "Select a Roomba or Braava.", - "data": { - "host": "[%key:common::config_flow::data::host%]" + "config": { + "abort": { + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect", + "not_irobot_device": "Discovered device is not an iRobot device" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "flow_title": "iRobot {name} ({host})", + "step": { + "init": { + "data": { + "host": "Host" + }, + "description": "Select a Roomba or Braava.", + "title": "Automaticlly connect to the device" + }, + "link": { + "description": "Press and hold the Home button on {name} until the device generates a sound (about two seconds).", + "title": "Retrieve Password" + }, + "link_manual": { + "data": { + "password": "Password" + }, + "description": "The password could not be retrivied from the device automatically. Please follow the steps outlined in the documentation at: {auth_help_url}", + "title": "Enter Password" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "Host" + }, + "description": "No Roomba or Braava have been discovered on your network. The BLID is the portion of the device hostname after `iRobot-`. Please follow the steps outlined in the documentation at: {auth_help_url}", + "title": "Manually connect to the device" + }, + "user": { + "data": { + "blid": "BLID", + "continuous": "Continuous", + "delay": "Delay", + "host": "Host", + "password": "Password" + }, + "description": "Currently retrieving the BLID and password is a manual process. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "title": "Connect to the device" + } } - }, - "manual": { - "title": "Manually connect to the device", - "description": "No Roomba or Braava have been discovered on your network. The BLID is the portion of the device hostname after `iRobot-`. Please follow the steps outlined in the documentation at: {auth_help_url}", - "data": { - "host": "[%key:common::config_flow::data::host%]", - "blid": "BLID" - } - }, - "link": { - "title": "Retrieve Password", - "description": "Press and hold the Home button on {name} until the device generates a sound (about two seconds)." - }, - "link_manual": { - "title": "Enter Password", - "description": "The password could not be retrivied from the device automatically. Please follow the steps outlined in the documentation at: {auth_help_url}", - "data": { - "password": "[%key:common::config_flow::data::password%]" - } - } }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" - }, - "abort": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "not_irobot_device": "Discovered device not an iRobot device" - } - }, - "options": { - "step": { - "init": { - "data": { - "continuous": "Continuous", - "delay": "Delay" + "options": { + "step": { + "init": { + "data": { + "continuous": "Continuous", + "delay": "Delay" + } + } } - } } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/es.json b/homeassistant/components/roomba/translations/es.json index a49022c2d3d..29f0b47a655 100644 --- a/homeassistant/components/roomba/translations/es.json +++ b/homeassistant/components/roomba/translations/es.json @@ -1,9 +1,41 @@ { "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar", + "not_irobot_device": "El dispositivo descubierto no es un dispositivo iRobot" + }, "error": { "cannot_connect": "No se pudo conectar" }, + "flow_title": "iRobot {name} ({host})", "step": { + "init": { + "data": { + "host": "Host" + }, + "description": "Selecciona una Roomba o Braava.", + "title": "Conectar autom\u00e1ticamente con el dispositivo" + }, + "link": { + "description": "Mant\u00e9n pulsado el bot\u00f3n Inicio en {name} hasta que el dispositivo genere un sonido (aproximadamente dos segundos).", + "title": "Recuperar la contrase\u00f1a" + }, + "link_manual": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "No se pudo recuperar la contrase\u00f1a desde el dispositivo de forma autom\u00e1tica. Por favor, sigue los pasos descritos en la documentaci\u00f3n en: {auth_help_url}", + "title": "Escribe la contrase\u00f1a" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "Host" + }, + "description": "No se ha descubierto ning\u00fan dispositivo Roomba ni Braava en tu red. El BLID es la parte del nombre de host del dispositivo despu\u00e9s de 'iRobot-'. Por favor, sigue los pasos descritos en la documentaci\u00f3n en: {auth_help_url}", + "title": "Conectar manualmente con el dispositivo" + }, "user": { "data": { "blid": "BLID", diff --git a/homeassistant/components/roomba/translations/et.json b/homeassistant/components/roomba/translations/et.json index 92da58fa146..e038257c12d 100644 --- a/homeassistant/components/roomba/translations/et.json +++ b/homeassistant/components/roomba/translations/et.json @@ -1,9 +1,41 @@ { "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "cannot_connect": "\u00dchendamine nurjus", + "not_irobot_device": "Leitud seade ei ole iRoboti seade" + }, "error": { "cannot_connect": "\u00dchendamine nurjus" }, + "flow_title": "iRobot {name} ( {host} )", "step": { + "init": { + "data": { + "host": "Host" + }, + "description": "Vali Roomba v\u00f5i Braava seade.", + "title": "\u00dchendu seadmega automaatselt" + }, + "link": { + "description": "Vajuta ja hoia all seadme {name} nuppu Home kuni seade teeb piiksu (umbes kaks sekundit).", + "title": "Hangi salas\u00f5na" + }, + "link_manual": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Salas\u00f5na ei \u00f5nnestunud seadmest automaatselt hankida. J\u00e4rgi dokumentatsioonis toodud juhiseid: {auth_help_url}", + "title": "Sisesta salas\u00f5na" + }, + "manual": { + "data": { + "blid": "", + "host": "Host" + }, + "description": "V\u00f5rgus ei tuvastatud \u00fchtegi Roomba ega Braava seadet. BLID on seadme hostinime osa p\u00e4rast iRobot-`. J\u00e4rgi dokumentatsioonis toodud juhiseid: {auth_help_url}", + "title": "\u00dchenda seadmega k\u00e4sitsi" + }, "user": { "data": { "blid": "", diff --git a/homeassistant/components/roomba/translations/fr.json b/homeassistant/components/roomba/translations/fr.json index 1ec97dd3842..8142d3acf13 100644 --- a/homeassistant/components/roomba/translations/fr.json +++ b/homeassistant/components/roomba/translations/fr.json @@ -1,9 +1,38 @@ { "config": { + "abort": { + "cannot_connect": "Echec de connection" + }, "error": { "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer" }, "step": { + "init": { + "data": { + "host": "H\u00f4te" + }, + "description": "S\u00e9lectionnez un Roomba ou un Braava.", + "title": "Se connecter automatiquement \u00e0 l'appareil" + }, + "link": { + "description": "Appuyez sur le bouton Accueil et maintenez-le enfonc\u00e9 jusqu'\u00e0 ce que l'appareil \u00e9mette un son (environ deux secondes).", + "title": "R\u00e9cup\u00e9rer le mot de passe" + }, + "link_manual": { + "data": { + "password": "Mot de passe" + }, + "description": "Le mot de passe n'a pas pu \u00eatre r\u00e9cup\u00e9r\u00e9 automatiquement \u00e0 partir de l'appareil. Veuillez suivre les \u00e9tapes d\u00e9crites dans la documentation \u00e0 {auth_help_url}", + "title": "Entrer le mot de passe" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "H\u00f4te" + }, + "description": "Aucun Roomba ou Braava d\u00e9couvert sur votre r\u00e9seau. Le BLID est la partie du nom d'h\u00f4te du p\u00e9riph\u00e9rique apr\u00e8s `iRobot-`. Veuillez suivre les \u00e9tapes d\u00e9crites dans la documentation \u00e0 {auth_help_url}", + "title": "Se connecter manuellement \u00e0 l'appareil" + }, "user": { "data": { "blid": "BLID", diff --git a/homeassistant/components/roomba/translations/it.json b/homeassistant/components/roomba/translations/it.json index d109aa8bcc0..b9e01faf16c 100644 --- a/homeassistant/components/roomba/translations/it.json +++ b/homeassistant/components/roomba/translations/it.json @@ -1,9 +1,41 @@ { "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi", + "not_irobot_device": "Il dispositivo rilevato non \u00e8 un dispositivo iRobot" + }, "error": { "cannot_connect": "Impossibile connettersi" }, + "flow_title": "iRobot {name} ({host})", "step": { + "init": { + "data": { + "host": "Host" + }, + "description": "Seleziona un Roomba o un Braava.", + "title": "Connettiti automaticamente al dispositivo" + }, + "link": { + "description": "Tieni premuto il pulsante Home su {name} fino a quando il dispositivo non genera un suono (circa due secondi).", + "title": "Recupera password" + }, + "link_manual": { + "data": { + "password": "Password" + }, + "description": "La password non pu\u00f2 essere recuperata automaticamente dal dispositivo. Segui le istruzioni indicate sulla documentazione a: {auth_help_url}", + "title": "Inserisci la password" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "Host" + }, + "description": "Non sono stati trovati Roomba o Braava all'interno della tua rete. Il BLID \u00e8 la porzione del nome host del dispositivo dopo `iRobot-`. Segui le istruzioni indicate sulla documentazione a: {auth_help_url}", + "title": "Connettiti manualmente al dispositivo" + }, "user": { "data": { "blid": "BLID", diff --git a/homeassistant/components/roomba/translations/lb.json b/homeassistant/components/roomba/translations/lb.json index d3fc631f5df..500aa4fbee6 100644 --- a/homeassistant/components/roomba/translations/lb.json +++ b/homeassistant/components/roomba/translations/lb.json @@ -1,9 +1,35 @@ { "config": { + "abort": { + "cannot_connect": "Feeler beim verbannen" + }, "error": { "cannot_connect": "Feeler beim verbannen" }, "step": { + "init": { + "data": { + "host": "Host" + }, + "description": "Ee Roomba oder Bravaa auswielen.", + "title": "Automatesch mam Apparat verbannen" + }, + "link": { + "title": "Passwuert ausliesen" + }, + "link_manual": { + "data": { + "password": "Passwuert" + }, + "title": "Passwuert aginn" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "Host" + }, + "title": "Manuell mam Apparat verbannen" + }, "user": { "data": { "blid": "BLID", diff --git a/homeassistant/components/roomba/translations/no.json b/homeassistant/components/roomba/translations/no.json index adf13cb57af..2bfe9f774d1 100644 --- a/homeassistant/components/roomba/translations/no.json +++ b/homeassistant/components/roomba/translations/no.json @@ -1,9 +1,41 @@ { "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes", + "not_irobot_device": "Oppdaget enhet er ikke en iRobot-enhet" + }, "error": { "cannot_connect": "Tilkobling mislyktes" }, + "flow_title": "", "step": { + "init": { + "data": { + "host": "Vert" + }, + "description": "Velg en Roomba eller Braava", + "title": "Koble automatisk til enheten" + }, + "link": { + "description": "Trykk og hold inne Hjem-knappen p\u00e5 {name} til enheten genererer en lyd (omtrent to sekunder)", + "title": "Hent passord" + }, + "link_manual": { + "data": { + "password": "Passord" + }, + "description": "Passordet kunne ikke hentes automatisk fra enheten. F\u00f8lg trinnene som er beskrevet i dokumentasjonen p\u00e5: {auth_help_url}", + "title": "Skriv inn passord" + }, + "manual": { + "data": { + "blid": "", + "host": "Vert" + }, + "description": "Ingen Roomba eller Braava har blitt oppdaget i nettverket ditt. BLID er delen av enhetens vertsnavn etter `iRobot-`. F\u00f8lg trinnene som er beskrevet i dokumentasjonen p\u00e5: {auth_help_url}", + "title": "Koble til enheten manuelt" + }, "user": { "data": { "blid": "Blid", diff --git a/homeassistant/components/roomba/translations/pl.json b/homeassistant/components/roomba/translations/pl.json index b2a4ab89cbe..e4951a366dd 100644 --- a/homeassistant/components/roomba/translations/pl.json +++ b/homeassistant/components/roomba/translations/pl.json @@ -1,9 +1,41 @@ { "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "not_irobot_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem iRobot" + }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, + "flow_title": "iRobot {name} ({host})", "step": { + "init": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "description": "Wybierz Roomb\u0119 lub Braava", + "title": "Po\u0142\u0105cz si\u0119 automatycznie z urz\u0105dzeniem" + }, + "link": { + "description": "Naci\u015bnij i przytrzymaj przycisk Home na {name} a\u017c urz\u0105dzenie wygeneruje d\u017awi\u0119k (oko\u0142o dwie sekundy).", + "title": "Odzyskiwanie has\u0142a" + }, + "link_manual": { + "data": { + "password": "Has\u0142o" + }, + "description": "Nie mo\u017cna automatycznie pobra\u0107 has\u0142a z urz\u0105dzenia. Post\u0119puj zgodnie z instrukcjami podanymi w dokumentacji pod adresem: {auth_help_url}", + "title": "Wprowad\u017a has\u0142o" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "Nazwa hosta lub adres IP" + }, + "description": "W Twojej sieci nie wykryto urz\u0105dzenia Roomba ani Braava. BLID to cz\u0119\u015b\u0107 nazwy hosta urz\u0105dzenia po `iRobot-`. Post\u0119puj zgodnie z instrukcjami podanymi w dokumentacji pod adresem: {auth_help_url}", + "title": "R\u0119czne po\u0142\u0105czenie z urz\u0105dzeniem" + }, "user": { "data": { "blid": "BLID", diff --git a/homeassistant/components/roomba/translations/pt.json b/homeassistant/components/roomba/translations/pt.json index 0156fd48a62..6036e870e6c 100644 --- a/homeassistant/components/roomba/translations/pt.json +++ b/homeassistant/components/roomba/translations/pt.json @@ -1,9 +1,16 @@ { "config": { + "abort": { + "not_irobot_device": "O dispositivo descoberto n\u00e3o \u00e9 um dispositivo iRobot" + }, "error": { "cannot_connect": "Falha ao conectar, tente novamente" }, + "flow_title": "iRobot {name} ({host})", "step": { + "link": { + "title": "Recuperar Palavra-passe" + }, "user": { "data": { "continuous": "Cont\u00ednuo", diff --git a/homeassistant/components/roomba/translations/ru.json b/homeassistant/components/roomba/translations/ru.json index ee1192f69ec..979bb9bc70f 100644 --- a/homeassistant/components/roomba/translations/ru.json +++ b/homeassistant/components/roomba/translations/ru.json @@ -1,9 +1,41 @@ { "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.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "not_irobot_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 iRobot." + }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, + "flow_title": "iRobot {name} ({host})", "step": { + "init": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u044b\u043b\u0435\u0441\u043e\u0441 \u0438\u0437 \u043c\u043e\u0434\u0435\u043b\u0435\u0439 Roomba \u0438\u043b\u0438 Braava.", + "title": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, + "link": { + "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u0438 \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0439\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 Home \u043d\u0430 {name}, \u043f\u043e\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0438\u0437\u0434\u0430\u0441\u0442 \u0437\u0432\u0443\u043a (\u043e\u043a\u043e\u043b\u043e \u0434\u0432\u0443\u0445 \u0441\u0435\u043a\u0443\u043d\u0434).", + "title": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u043f\u0430\u0440\u043e\u043b\u044f" + }, + "link_manual": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \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: {auth_help_url}.", + "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0412 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u043f\u044b\u043b\u0435\u0441\u043e\u0441\u043e\u0432 Roomba \u0438\u043b\u0438 Braava. BLID - \u044d\u0442\u043e \u0447\u0430\u0441\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0433\u043e \u0438\u043c\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u043f\u043e\u0441\u043b\u0435 \"iRobot-\". \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: {auth_help_url}.", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 \u0432\u0440\u0443\u0447\u043d\u0443\u044e" + }, "user": { "data": { "blid": "BLID", diff --git a/homeassistant/components/roomba/translations/tr.json b/homeassistant/components/roomba/translations/tr.json new file mode 100644 index 00000000000..3d85144c188 --- /dev/null +++ b/homeassistant/components/roomba/translations/tr.json @@ -0,0 +1,60 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "not_irobot_device": "Bulunan cihaz bir iRobot cihaz\u0131 de\u011fil" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "flow_title": "iRobot {name} ( {host} )", + "step": { + "init": { + "data": { + "host": "Ana Bilgisayar" + }, + "description": "Roomba veya Braava'y\u0131 se\u00e7in.", + "title": "Cihaza otomatik olarak ba\u011flan" + }, + "link": { + "description": "Cihaz bir ses olu\u015fturana kadar (yakla\u015f\u0131k iki saniye) {name} \u00fczerindeki Ana Sayfa d\u00fc\u011fmesini bas\u0131l\u0131 tutun.", + "title": "\u015eifre Al" + }, + "link_manual": { + "data": { + "password": "\u015eifre" + }, + "description": "Parola ayg\u0131ttan otomatik olarak al\u0131namad\u0131. L\u00fctfen belgelerde belirtilen ad\u0131mlar\u0131 izleyin: {auth_help_url}", + "title": "\u015eifre Girin" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "Ana Bilgisayar" + }, + "title": "Cihaza manuel olarak ba\u011flan\u0131n" + }, + "user": { + "data": { + "continuous": "S\u00fcrekli", + "delay": "Gecikme", + "host": "Ana Bilgisayar", + "password": "Parola" + }, + "description": "\u015eu anda BLID ve parola alma manuel bir i\u015flemdir. L\u00fctfen a\u015fa\u011f\u0131daki belgelerde belirtilen ad\u0131mlar\u0131 izleyin: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "title": "Cihaza ba\u011flan\u0131n" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "continuous": "S\u00fcrekli", + "delay": "Gecikme" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/uk.json b/homeassistant/components/roomba/translations/uk.json new file mode 100644 index 00000000000..833a35f62f3 --- /dev/null +++ b/homeassistant/components/roomba/translations/uk.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "blid": "BLID", + "continuous": "\u0411\u0435\u0437\u043f\u0435\u0440\u0435\u0440\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c", + "delay": "\u0417\u0430\u0442\u0440\u0438\u043c\u043a\u0430 (\u0441\u0435\u043a.)", + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438, \u0449\u043e\u0431 \u0434\u0456\u0437\u043d\u0430\u0442\u0438\u0441\u044f \u044f\u043a \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 BLID \u0456 \u043f\u0430\u0440\u043e\u043b\u044c:\nhttps://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "continuous": "\u0411\u0435\u0437\u043f\u0435\u0440\u0435\u0440\u0432\u043d\u043e", + "delay": "\u0417\u0430\u0442\u0440\u0438\u043c\u043a\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/zh-Hant.json b/homeassistant/components/roomba/translations/zh-Hant.json index 932e5cadd75..790eba79c03 100644 --- a/homeassistant/components/roomba/translations/zh-Hant.json +++ b/homeassistant/components/roomba/translations/zh-Hant.json @@ -1,9 +1,41 @@ { "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "not_irobot_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e iRobot \u88dd\u7f6e" + }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, + "flow_title": "iRobot {name} ({host})", "step": { + "init": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "description": "\u9078\u64c7 Roomba \u6216 Braava\u3002", + "title": "\u81ea\u52d5\u9023\u7dda\u81f3\u88dd\u7f6e" + }, + "link": { + "description": "\u8acb\u6309\u4f4f {name} \u4e0a\u7684 Home \u9375\u76f4\u5230\u88dd\u7f6e\u767c\u51fa\u8072\u97f3\uff08\u7d04\u5169\u79d2\uff09\u3002", + "title": "\u91cd\u7f6e\u5bc6\u78bc" + }, + "link_manual": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "\u5bc6\u78bc\u53ef\u81ea\u52d5\u81ea\u88dd\u7f6e\u4e0a\u53d6\u5f97\u3002\u8acb\u53c3\u95b1\u4ee5\u4e0b\u6587\u4ef6\u7684\u6b65\u9a5f\u9032\u884c\u8a2d\u5b9a\uff1a{auth_help_url}", + "title": "\u8f38\u5165\u5bc6\u78bc" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "\u4e3b\u6a5f\u7aef" + }, + "description": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Roomba \u6216 Braava\u3002BLID \u88dd\u7f6e\u65bc\u4e3b\u6a5f\u7aef\u7684\u90e8\u5206\u540d\u7a31\u70ba `iRobot-` \u958b\u982d\u3002\u8acb\u53c3\u95b1\u4ee5\u4e0b\u6587\u4ef6\u7684\u6b65\u9a5f\u9032\u884c\u8a2d\u5b9a\uff1a{auth_help_url}", + "title": "\u624b\u52d5\u9023\u7dda\u81f3\u88dd\u7f6e" + }, "user": { "data": { "blid": "BLID", diff --git a/homeassistant/components/roon/translations/ca.json b/homeassistant/components/roon/translations/ca.json index 3a1de2208b6..ef32dd00e75 100644 --- a/homeassistant/components/roon/translations/ca.json +++ b/homeassistant/components/roon/translations/ca.json @@ -17,7 +17,7 @@ "data": { "host": "Amfitri\u00f3" }, - "description": "Introdueix el nom d'amfitri\u00f3 o la IP del servidor Roon" + "description": "No s'ha pogut descobrir el servidor Roon, introdueix el nom d'amfitri\u00f3 o la IP." } } } diff --git a/homeassistant/components/roon/translations/cs.json b/homeassistant/components/roon/translations/cs.json index a15e75066a9..fd01ed1cd25 100644 --- a/homeassistant/components/roon/translations/cs.json +++ b/homeassistant/components/roon/translations/cs.json @@ -16,8 +16,7 @@ "user": { "data": { "host": "Hostitel" - }, - "description": "Zadejte pros\u00edm n\u00e1zev hostitele nebo IP adresu va\u0161eho Roon serveru." + } } } } diff --git a/homeassistant/components/roon/translations/de.json b/homeassistant/components/roon/translations/de.json index 9918e38670a..4416589a23e 100644 --- a/homeassistant/components/roon/translations/de.json +++ b/homeassistant/components/roon/translations/de.json @@ -1,8 +1,19 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { "duplicate_entry": "Dieser Host wurde bereits hinzugef\u00fcgt.", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/roon/translations/en.json b/homeassistant/components/roon/translations/en.json index 99f2b65bd13..b763fbb1e0c 100644 --- a/homeassistant/components/roon/translations/en.json +++ b/homeassistant/components/roon/translations/en.json @@ -17,7 +17,7 @@ "data": { "host": "Host" }, - "description": "Please enter your Roon server Hostname or IP." + "description": "Could not discover Roon server, please enter your the Hostname or IP." } } } diff --git a/homeassistant/components/roon/translations/et.json b/homeassistant/components/roon/translations/et.json index dfe3ad53f48..e29b1ccc6c6 100644 --- a/homeassistant/components/roon/translations/et.json +++ b/homeassistant/components/roon/translations/et.json @@ -17,7 +17,7 @@ "data": { "host": "" }, - "description": "Sisesta oma Rooni serveri hostinimi v\u00f5i IP." + "description": "Rooni serverit ei leitud. Sisesta oma Rooni serveri hostinimi v\u00f5i IP." } } } diff --git a/homeassistant/components/roon/translations/it.json b/homeassistant/components/roon/translations/it.json index 5f63482c3c3..e0450af9d39 100644 --- a/homeassistant/components/roon/translations/it.json +++ b/homeassistant/components/roon/translations/it.json @@ -17,7 +17,7 @@ "data": { "host": "Host" }, - "description": "Inserisci il nome host o l'IP del tuo server Roon." + "description": "Impossibile individuare il server Roon, inserire l'hostname o l'IP." } } } diff --git a/homeassistant/components/roon/translations/no.json b/homeassistant/components/roon/translations/no.json index 9067e2c6f53..e872e03a69d 100644 --- a/homeassistant/components/roon/translations/no.json +++ b/homeassistant/components/roon/translations/no.json @@ -17,7 +17,7 @@ "data": { "host": "Vert" }, - "description": "Vennligst skriv inn Roon-serverens vertsnavn eller IP." + "description": "Kunne ikke oppdage Roon-serveren. Angi vertsnavnet eller IP-adressen." } } } diff --git a/homeassistant/components/roon/translations/pl.json b/homeassistant/components/roon/translations/pl.json index e63c5f6b55c..d763fc12bd2 100644 --- a/homeassistant/components/roon/translations/pl.json +++ b/homeassistant/components/roon/translations/pl.json @@ -17,7 +17,7 @@ "data": { "host": "Nazwa hosta lub adres IP" }, - "description": "Wprowad\u017a nazw\u0119 hosta lub adres IP swojego serwera Roon." + "description": "Nie wykryto serwera Roon, wprowad\u017a nazw\u0119 hosta lub adres IP." } } } diff --git a/homeassistant/components/roon/translations/ru.json b/homeassistant/components/roon/translations/ru.json index abfbea2ccde..187151affe2 100644 --- a/homeassistant/components/roon/translations/ru.json +++ b/homeassistant/components/roon/translations/ru.json @@ -17,7 +17,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Roon" + "description": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440 Roon, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." } } } diff --git a/homeassistant/components/roon/translations/tr.json b/homeassistant/components/roon/translations/tr.json new file mode 100644 index 00000000000..97241919c9b --- /dev/null +++ b/homeassistant/components/roon/translations/tr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "duplicate_entry": "Bu ana bilgisayar zaten eklendi.", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "link": { + "description": "Roon'da HomeAssistant\u0131 yetkilendirmelisiniz. G\u00f6nder'e t\u0131klad\u0131ktan sonra, Roon Core uygulamas\u0131na gidin, Ayarlar'\u0131 a\u00e7\u0131n ve Uzant\u0131lar sekmesinde HomeAssistant'\u0131 etkinle\u015ftirin.", + "title": "Roon'da HomeAssistant'\u0131 Yetkilendirme" + }, + "user": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/uk.json b/homeassistant/components/roon/translations/uk.json new file mode 100644 index 00000000000..91a530787ae --- /dev/null +++ b/homeassistant/components/roon/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "duplicate_entry": "\u0426\u0435\u0439 \u0445\u043e\u0441\u0442 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0438\u0439.", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "link": { + "description": "\u041f\u0456\u0441\u043b\u044f \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f \u043a\u043d\u043e\u043f\u043a\u0438 \u00ab\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438\u00bb \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0432 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Roon Core, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u00ab\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u00bb \u0456 \u0443\u0432\u0456\u043c\u043a\u043d\u0456\u0442\u044c HomeAssistant \u043d\u0430 \u0432\u043a\u043b\u0430\u0434\u0446\u0456 \u00ab\u0420\u043e\u0437\u0448\u0438\u0440\u0435\u043d\u043d\u044f\u00bb.", + "title": "Roon" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043d\u0430\u0437\u0432\u0443 \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Roon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/zh-Hant.json b/homeassistant/components/roon/translations/zh-Hant.json index f34bce445f7..39099753f39 100644 --- a/homeassistant/components/roon/translations/zh-Hant.json +++ b/homeassistant/components/roon/translations/zh-Hant.json @@ -17,7 +17,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u8acb\u8f38\u5165 Roon \u4f3a\u670d\u5668\u4e3b\u6a5f\u540d\u7a31\u6216 IP\u3002" + "description": "\u627e\u4e0d\u5230 Roon \u4f3a\u670d\u5668\uff0c\u8acb\u8f38\u5165\u4e3b\u6a5f\u540d\u7a31\u6216 IP\u3002" } } } diff --git a/homeassistant/components/rpi_power/translations/de.json b/homeassistant/components/rpi_power/translations/de.json new file mode 100644 index 00000000000..9f3851f0c2b --- /dev/null +++ b/homeassistant/components/rpi_power/translations/de.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "confirm": { + "description": "M\u00f6chtest du mit der Einrichtung beginnen?" + } + } + }, + "title": "Raspberry Pi Stromversorgungspr\u00fcfer" +} \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/lb.json b/homeassistant/components/rpi_power/translations/lb.json index 3e145432bae..e4bb7389339 100644 --- a/homeassistant/components/rpi_power/translations/lb.json +++ b/homeassistant/components/rpi_power/translations/lb.json @@ -3,6 +3,11 @@ "abort": { "no_devices_found": "Kann d\u00e9i Systemklass fir d\u00ebs noutwendeg Komponent net fannen, stell s\u00e9cher dass de Kernel rezent ass an d'Hardware \u00ebnnerst\u00ebtzt g\u00ebtt.", "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." + }, + "step": { + "confirm": { + "description": "Soll den Ariichtungs Prozess gestart ginn?" + } } }, "title": "Raspberry Pi Netzdeel Checker" diff --git a/homeassistant/components/rpi_power/translations/tr.json b/homeassistant/components/rpi_power/translations/tr.json new file mode 100644 index 00000000000..f1dfcf16667 --- /dev/null +++ b/homeassistant/components/rpi_power/translations/tr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + } + } + }, + "title": "Raspberry Pi G\u00fc\u00e7 Kayna\u011f\u0131 Denetleyicisi" +} \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/uk.json b/homeassistant/components/rpi_power/translations/uk.json new file mode 100644 index 00000000000..b60160e1c4e --- /dev/null +++ b/homeassistant/components/rpi_power/translations/uk.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u043d\u0430\u0439\u0442\u0438 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u0438\u0439 \u043a\u043b\u0430\u0441, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0438\u0439 \u0434\u043b\u044f \u0440\u043e\u0431\u043e\u0442\u0438 \u0446\u044c\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0443 \u0412\u0430\u0441 \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u043d\u0430\u0439\u043d\u043e\u0432\u0456\u0448\u0435 \u044f\u0434\u0440\u043e \u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0435 \u043e\u0431\u043b\u0430\u0434\u043d\u0430\u043d\u043d\u044f.", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "step": { + "confirm": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?" + } + } + }, + "title": "Raspberry Pi power supply checker" +} \ 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 ae15ec058b5..625c7372347 100644 --- a/homeassistant/components/ruckus_unleashed/translations/de.json +++ b/homeassistant/components/ruckus_unleashed/translations/de.json @@ -4,12 +4,14 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { "user": { "data": { + "host": "Host", "password": "Passwort", "username": "Benutzername" } diff --git a/homeassistant/components/ruckus_unleashed/translations/tr.json b/homeassistant/components/ruckus_unleashed/translations/tr.json new file mode 100644 index 00000000000..40c9c39b967 --- /dev/null +++ b/homeassistant/components/ruckus_unleashed/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruckus_unleashed/translations/uk.json b/homeassistant/components/ruckus_unleashed/translations/uk.json new file mode 100644 index 00000000000..2df11f74455 --- /dev/null +++ b/homeassistant/components/ruckus_unleashed/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/de.json b/homeassistant/components/samsungtv/translations/de.json index e3354267630..3ba569c87db 100644 --- a/homeassistant/components/samsungtv/translations/de.json +++ b/homeassistant/components/samsungtv/translations/de.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "Dieser Samsung TV ist bereits konfiguriert", - "already_in_progress": "Der Konfigurationsablauf f\u00fcr Samsung TV wird bereits ausgef\u00fchrt.", + "already_in_progress": "Der Konfigurationsablauf 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", + "cannot_connect": "Verbindung fehlgeschlagen", "not_supported": "Dieses Samsung TV-Ger\u00e4t wird derzeit nicht unterst\u00fctzt." }, "flow_title": "Samsung TV: {model}", diff --git a/homeassistant/components/samsungtv/translations/tr.json b/homeassistant/components/samsungtv/translations/tr.json index 50e6b21d120..6b3900e9aa5 100644 --- a/homeassistant/components/samsungtv/translations/tr.json +++ b/homeassistant/components/samsungtv/translations/tr.json @@ -4,13 +4,17 @@ "already_configured": "Bu Samsung TV zaten ayarlanm\u0131\u015f.", "already_in_progress": "Samsung TV ayar\u0131 zaten s\u00fcr\u00fcyor.", "auth_missing": "Home Assistant'\u0131n bu Samsung TV'ye ba\u011flanma izni yok. Home Assistant'\u0131 yetkilendirmek i\u00e7in l\u00fctfen TV'nin ayarlar\u0131n\u0131 kontrol et.", + "cannot_connect": "Ba\u011flanma hatas\u0131", "not_supported": "Bu Samsung TV cihaz\u0131 \u015fu anda desteklenmiyor." }, "flow_title": "Samsung TV: {model}", "step": { + "confirm": { + "title": "Samsung TV" + }, "user": { "data": { - "host": "Host veya IP adresi", + "host": "Ana Bilgisayar", "name": "Ad" }, "description": "Samsung TV bilgilerini gir. Daha \u00f6nce hi\u00e7 Home Assistant'a ba\u011flamad\u0131ysan, TV'nde izin isteyen bir pencere g\u00f6receksindir." diff --git a/homeassistant/components/samsungtv/translations/uk.json b/homeassistant/components/samsungtv/translations/uk.json new file mode 100644 index 00000000000..83bb18e76f1 --- /dev/null +++ b/homeassistant/components/samsungtv/translations/uk.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "auth_missing": "Home Assistant \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u0438\u0439 \u0434\u043b\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0446\u044c\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "not_supported": "\u0426\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 \u0432 \u0434\u0430\u043d\u0438\u0439 \u0447\u0430\u0441 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f." + }, + "flow_title": "Samsung TV: {model}", + "step": { + "confirm": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 Samsung {model}? \u042f\u043a\u0449\u043e \u0446\u0435\u0439 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0440\u0430\u043d\u0456\u0448\u0435 \u043d\u0435 \u0431\u0443\u0432 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 \u0434\u043e Home Assistant, \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 \u043c\u0430\u0454 \u0437'\u044f\u0432\u0438\u0442\u0438\u0441\u044f \u0441\u043f\u043b\u0438\u0432\u0430\u044e\u0447\u0435 \u0432\u0456\u043a\u043d\u043e \u0456\u0437 \u0437\u0430\u043f\u0438\u0442\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457. \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430, \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0456 \u0432\u0440\u0443\u0447\u043d\u0443, \u0431\u0443\u0434\u0443\u0442\u044c \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u0430\u043d\u0456.", + "title": "\u0422\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 Samsung" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 Samsung. \u042f\u043a\u0449\u043e \u0446\u0435\u0439 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0440\u0430\u043d\u0456\u0448\u0435 \u043d\u0435 \u0431\u0443\u0432 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 \u0434\u043e Home Assistant, \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 \u043c\u0430\u0454 \u0437'\u044f\u0432\u0438\u0442\u0438\u0441\u044f \u0441\u043f\u043b\u0438\u0432\u0430\u044e\u0447\u0435 \u0432\u0456\u043a\u043d\u043e \u0456\u0437 \u0437\u0430\u043f\u0438\u0442\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/script/translations/uk.json b/homeassistant/components/script/translations/uk.json index bfff0258c66..ee494e264ae 100644 --- a/homeassistant/components/script/translations/uk.json +++ b/homeassistant/components/script/translations/uk.json @@ -5,5 +5,5 @@ "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" } }, - "title": "\u0421\u0446\u0435\u043d\u0430\u0440\u0456\u0439" + "title": "\u0421\u043a\u0440\u0438\u043f\u0442" } \ No newline at end of file diff --git a/homeassistant/components/season/translations/sensor.uk.json b/homeassistant/components/season/translations/sensor.uk.json index 2c694e287b1..fa79d3cff07 100644 --- a/homeassistant/components/season/translations/sensor.uk.json +++ b/homeassistant/components/season/translations/sensor.uk.json @@ -1,5 +1,11 @@ { "state": { + "season__season": { + "autumn": "\u041e\u0441\u0456\u043d\u044c", + "spring": "\u0412\u0435\u0441\u043d\u0430", + "summer": "\u041b\u0456\u0442\u043e", + "winter": "\u0417\u0438\u043c\u0430" + }, "season__season__": { "autumn": "\u041e\u0441\u0456\u043d\u044c", "spring": "\u0412\u0435\u0441\u043d\u0430", diff --git a/homeassistant/components/sense/translations/de.json b/homeassistant/components/sense/translations/de.json index de9e6877f25..9d4845ece79 100644 --- a/homeassistant/components/sense/translations/de.json +++ b/homeassistant/components/sense/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/sense/translations/tr.json b/homeassistant/components/sense/translations/tr.json new file mode 100644 index 00000000000..0e335265325 --- /dev/null +++ b/homeassistant/components/sense/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "email": "E-posta", + "password": "Parola" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/uk.json b/homeassistant/components/sense/translations/uk.json new file mode 100644 index 00000000000..8eac9c9d4ab --- /dev/null +++ b/homeassistant/components/sense/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "title": "Sense Energy Monitor" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/tr.json b/homeassistant/components/sensor/translations/tr.json index 3bf1ba6f368..feca40991ee 100644 --- a/homeassistant/components/sensor/translations/tr.json +++ b/homeassistant/components/sensor/translations/tr.json @@ -1,4 +1,31 @@ { + "device_automation": { + "condition_type": { + "is_current": "Mevcut {entity_name} ak\u0131m\u0131", + "is_energy": "Mevcut {entity_name} enerjisi", + "is_power_factor": "Mevcut {entity_name} g\u00fc\u00e7 fakt\u00f6r\u00fc", + "is_signal_strength": "Mevcut {entity_name} sinyal g\u00fcc\u00fc", + "is_temperature": "Mevcut {entity_name} s\u0131cakl\u0131\u011f\u0131", + "is_timestamp": "Mevcut {entity_name} zaman damgas\u0131", + "is_value": "Mevcut {entity_name} de\u011feri", + "is_voltage": "Mevcut {entity_name} voltaj\u0131" + }, + "trigger_type": { + "battery_level": "{entity_name} pil seviyesi de\u011fi\u015fiklikleri", + "current": "{entity_name} ak\u0131m de\u011fi\u015fiklikleri", + "energy": "{entity_name} enerji de\u011fi\u015fiklikleri", + "humidity": "{entity_name} nem de\u011fi\u015fiklikleri", + "illuminance": "{entity_name} ayd\u0131nlatma de\u011fi\u015fiklikleri", + "power": "{entity_name} g\u00fc\u00e7 de\u011fi\u015fiklikleri", + "power_factor": "{entity_name} g\u00fc\u00e7 fakt\u00f6r\u00fc de\u011fi\u015fiklikleri", + "pressure": "{entity_name} bas\u0131n\u00e7 de\u011fi\u015fiklikleri", + "signal_strength": "{entity_name} sinyal g\u00fcc\u00fc de\u011fi\u015fiklikleri", + "temperature": "{entity_name} s\u0131cakl\u0131k de\u011fi\u015fiklikleri", + "timestamp": "{entity_name} zaman damgas\u0131 de\u011fi\u015fiklikleri", + "value": "{entity_name} de\u011fer de\u011fi\u015fiklikleri", + "voltage": "{entity_name} voltaj de\u011fi\u015fiklikleri" + } + }, "state": { "_": { "off": "Kapal\u0131", diff --git a/homeassistant/components/sensor/translations/uk.json b/homeassistant/components/sensor/translations/uk.json index 391415409f5..9e6148c3b8c 100644 --- a/homeassistant/components/sensor/translations/uk.json +++ b/homeassistant/components/sensor/translations/uk.json @@ -1,7 +1,34 @@ { "device_automation": { "condition_type": { - "is_battery_level": "\u041f\u043e\u0442\u043e\u0447\u043d\u0438\u0439 \u0440\u0456\u0432\u0435\u043d\u044c \u0437\u0430\u0440\u044f\u0434\u0443 \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440\u0430 {entity_name}" + "is_battery_level": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "is_current": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0441\u0438\u043b\u0438 \u0441\u0442\u0440\u0443\u043c\u0443", + "is_energy": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u043e\u0442\u0443\u0436\u043d\u043e\u0441\u0442\u0456", + "is_humidity": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "is_illuminance": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "is_power": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "is_power_factor": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043a\u043e\u0435\u0444\u0456\u0446\u0456\u0454\u043d\u0442\u0430 \u043f\u043e\u0442\u0443\u0436\u043d\u043e\u0441\u0442\u0456", + "is_pressure": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "is_signal_strength": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "is_temperature": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "is_timestamp": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "is_value": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "is_voltage": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043d\u0430\u043f\u0440\u0443\u0433\u0438" + }, + "trigger_type": { + "battery_level": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "current": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0441\u0438\u043b\u0438 \u0441\u0442\u0440\u0443\u043c\u0443", + "energy": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u043e\u0442\u0443\u0436\u043d\u043e\u0441\u0442\u0456", + "humidity": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "illuminance": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "power": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "power_factor": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u043a\u043e\u0435\u0444\u0456\u0446\u0456\u0454\u043d\u0442 \u043f\u043e\u0442\u0443\u0436\u043d\u043e\u0441\u0442\u0456", + "pressure": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "signal_strength": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "temperature": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "timestamp": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "value": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "voltage": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043d\u0430\u043f\u0440\u0443\u0433\u0438" } }, "state": { @@ -10,5 +37,5 @@ "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" } }, - "title": "\u0414\u0430\u0442\u0447\u0438\u043a" + "title": "\u0421\u0435\u043d\u0441\u043e\u0440" } \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/de.json b/homeassistant/components/sentry/translations/de.json index c36bbf258b0..8fbcfc1eaa2 100644 --- a/homeassistant/components/sentry/translations/de.json +++ b/homeassistant/components/sentry/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, "error": { "bad_dsn": "Ung\u00fcltiger DSN", "unknown": "Unerwarteter Fehler" diff --git a/homeassistant/components/sentry/translations/tr.json b/homeassistant/components/sentry/translations/tr.json new file mode 100644 index 00000000000..4dab23fbd94 --- /dev/null +++ b/homeassistant/components/sentry/translations/tr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "bad_dsn": "Ge\u00e7ersiz DSN", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "dsn": "DSN" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "environment": "Ortam\u0131n iste\u011fe ba\u011fl\u0131 ad\u0131." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/uk.json b/homeassistant/components/sentry/translations/uk.json new file mode 100644 index 00000000000..01da0308851 --- /dev/null +++ b/homeassistant/components/sentry/translations/uk.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "error": { + "bad_dsn": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 DSN.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "dsn": "DSN" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0412\u0430\u0448 DSN Sentry", + "title": "Sentry" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "environment": "\u041d\u0430\u0437\u0432\u0430", + "event_custom_components": "\u0412\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u0438 \u043f\u043e\u0434\u0456\u0457 \u0437 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0446\u044c\u043a\u0438\u0445 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0456\u0432", + "event_handled": "\u0412\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u0438 \u043e\u0431\u0440\u043e\u0431\u043b\u0435\u043d\u0456 \u043f\u043e\u0434\u0456\u0457", + "event_third_party_packages": "\u0412\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u0438 \u043f\u043e\u0434\u0456\u0457 \u0437 \u0441\u0442\u043e\u0440\u043e\u043d\u043d\u0456\u0445 \u043f\u0430\u043a\u0435\u0442\u0456\u0432", + "logging_event_level": "\u0417\u0430\u043f\u0438\u0441\u0443\u0432\u0430\u0442\u0438 \u0436\u0443\u0440\u043d\u0430\u043b\u0438 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u043e\u0432\u0430\u043d\u0438\u0445 \u043f\u043e\u0434\u0456\u0439", + "logging_level": "\u0417\u0430\u043f\u0438\u0441\u0443\u0432\u0430\u0442\u0438 \u0436\u0443\u0440\u043d\u0430\u043b\u0438 \u0443 \u0432\u0438\u0433\u043b\u044f\u0434\u0456 \u043d\u0430\u0432\u0456\u0433\u0430\u0446\u0456\u0439\u043d\u0438\u0445 \u043b\u0430\u043d\u0446\u044e\u0436\u043a\u0456\u0432", + "tracing": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u0456", + "tracing_sample_rate": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u0434\u0438\u0441\u043a\u0440\u0435\u0442\u0438\u0437\u0430\u0446\u0456\u0457 \u0442\u0440\u0430\u0441\u0443\u0432\u0430\u043d\u043d\u044f; \u0432\u0456\u0434 0,0 \u0434\u043e 1,0 (1,0 = 100%)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/de.json b/homeassistant/components/sharkiq/translations/de.json index 2294960d6f2..8a6f9b14747 100644 --- a/homeassistant/components/sharkiq/translations/de.json +++ b/homeassistant/components/sharkiq/translations/de.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "cannot_connect": "Verbindungsfehler", + "already_configured": "Konto wurde bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "unknown": "Unerwarteter Fehler" }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/sharkiq/translations/fr.json b/homeassistant/components/sharkiq/translations/fr.json index 5f05292ec2a..6fa3ba7707c 100644 --- a/homeassistant/components/sharkiq/translations/fr.json +++ b/homeassistant/components/sharkiq/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9", + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", "reauth_successful": "Jeton d'acc\u00e8s mis \u00e0 jour avec succ\u00e8s", "unknown": "Erreur inattendue" diff --git a/homeassistant/components/sharkiq/translations/tr.json b/homeassistant/components/sharkiq/translations/tr.json new file mode 100644 index 00000000000..c82f1e8bf05 --- /dev/null +++ b/homeassistant/components/sharkiq/translations/tr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "reauth": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + }, + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/uk.json b/homeassistant/components/sharkiq/translations/uk.json new file mode 100644 index 00000000000..0f78c62fa7e --- /dev/null +++ b/homeassistant/components/sharkiq/translations/uk.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "reauth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/ca.json b/homeassistant/components/shelly/translations/ca.json index 2bf17c2ba77..c2df82c0b16 100644 --- a/homeassistant/components/shelly/translations/ca.json +++ b/homeassistant/components/shelly/translations/ca.json @@ -27,5 +27,21 @@ "description": "Abans de configurar-lo, els dispositius amb bateria s'han de desperar prement el bot\u00f3 del dispositiu." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Bot\u00f3", + "button1": "Primer bot\u00f3", + "button2": "Segon bot\u00f3", + "button3": "Tercer bot\u00f3" + }, + "trigger_type": { + "double": "{subtype} clicat dues vegades", + "long": "{subtype} clicat durant una estona", + "long_single": "{subtype} clicat durant una estona i despr\u00e9s r\u00e0pid", + "single": "{subtype} clicat una vegada", + "single_long": "{subtype} clicat r\u00e0pid i, despr\u00e9s, durant una estona", + "triple": "{subtype} clicat tres vegades" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/cs.json b/homeassistant/components/shelly/translations/cs.json index 41ca338ab9e..afdfe7c8f56 100644 --- a/homeassistant/components/shelly/translations/cs.json +++ b/homeassistant/components/shelly/translations/cs.json @@ -27,5 +27,21 @@ "description": "P\u0159ed nastaven\u00edm mus\u00ed b\u00fdt za\u0159\u00edzen\u00ed nap\u00e1jen\u00e9 z baterie probuzeno stisknut\u00edm tla\u010d\u00edtka na dan\u00e9m za\u0159\u00edzen\u00ed." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Tla\u010d\u00edtko", + "button1": "Prvn\u00ed tla\u010d\u00edtko", + "button2": "Druh\u00e9 tla\u010d\u00edtko", + "button3": "T\u0159et\u00ed tla\u010d\u00edtko" + }, + "trigger_type": { + "double": "\"{subtype}\" stisknuto dvakr\u00e1t", + "long": "\"{subtype}\" stisknuto dlouze", + "long_single": "\"{subtype}\" stisknuto dlouze a pak jednou", + "single": "\"{subtype}\" stisknuto jednou", + "single_long": "\"{subtype}\" stisknuto jednou a pak dlouze", + "triple": "\"{subtype}\" stisknuto t\u0159ikr\u00e1t" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/da.json b/homeassistant/components/shelly/translations/da.json new file mode 100644 index 00000000000..08631bc39e1 --- /dev/null +++ b/homeassistant/components/shelly/translations/da.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "trigger_subtype": { + "button": "Knap", + "button1": "F\u00f8rste knap", + "button2": "Anden knap", + "button3": "Tredje knap" + }, + "trigger_type": { + "double": "{subtype} dobbelt klik", + "long": "{subtype} langt klik", + "long_single": "{subtype} langt klik og derefter enkelt klik", + "single": "{subtype} enkelt klik", + "single_long": "{subtype} enkelt klik og derefter langt klik" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/de.json b/homeassistant/components/shelly/translations/de.json index 74d0f831c8b..4764936a41b 100644 --- a/homeassistant/components/shelly/translations/de.json +++ b/homeassistant/components/shelly/translations/de.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "flow_title": "Shelly: {name}", @@ -15,7 +19,8 @@ "user": { "data": { "host": "Host" - } + }, + "description": "Vor der Einrichtung m\u00fcssen batteriebetriebene Ger\u00e4te durch Dr\u00fccken der Taste am Ger\u00e4t aufgeweckt werden." } } } diff --git a/homeassistant/components/shelly/translations/en.json b/homeassistant/components/shelly/translations/en.json index a1fa6b72598..a9ad6092a08 100644 --- a/homeassistant/components/shelly/translations/en.json +++ b/homeassistant/components/shelly/translations/en.json @@ -31,17 +31,17 @@ "device_automation": { "trigger_subtype": { "button": "Button", - "button1": "First button", + "button1": "First button", "button2": "Second button", "button3": "Third button" }, - "trigger_type": { - "single": "{subtype} single clicked", - "double": "{subtype} double clicked", - "triple": "{subtype} triple clicked", - "long":" {subtype} long clicked", - "single_long": "{subtype} single clicked and then long clicked", - "long_single": "{subtype} long clicked and then single clicked" + "trigger_type": { + "double": "{subtype} double clicked", + "long": " {subtype} long clicked", + "long_single": "{subtype} long clicked and then single clicked", + "single": "{subtype} single clicked", + "single_long": "{subtype} single clicked and then long clicked", + "triple": "{subtype} triple clicked" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/es.json b/homeassistant/components/shelly/translations/es.json index 38c72f21dca..09cc3f51378 100644 --- a/homeassistant/components/shelly/translations/es.json +++ b/homeassistant/components/shelly/translations/es.json @@ -27,5 +27,21 @@ "description": "Antes de configurarlo, el dispositivo que funciona con bater\u00eda debe despertarse presionando el bot\u00f3n del dispositivo." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Bot\u00f3n", + "button1": "Primer bot\u00f3n", + "button2": "Segundo bot\u00f3n", + "button3": "Tercer bot\u00f3n" + }, + "trigger_type": { + "double": "Pulsaci\u00f3n doble de {subtype}", + "long": "Pulsaci\u00f3n larga de {subtype}", + "long_single": "Pulsaci\u00f3n larga de {subtype} seguida de una pulsaci\u00f3n simple", + "single": "Pulsaci\u00f3n simple de {subtype}", + "single_long": "Pulsaci\u00f3n simple de {subtype} seguida de una pulsaci\u00f3n larga", + "triple": "Pulsaci\u00f3n triple de {subtype}" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/et.json b/homeassistant/components/shelly/translations/et.json index 12a662f6560..d2514876a81 100644 --- a/homeassistant/components/shelly/translations/et.json +++ b/homeassistant/components/shelly/translations/et.json @@ -27,5 +27,21 @@ "description": "Enne seadistamist tuleb akutoitega seade \u00e4ratada vajutades seadme nuppu." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Nupp", + "button1": "Esimene nupp", + "button2": "Teine nupp", + "button3": "Kolmas nupp" + }, + "trigger_type": { + "double": "Nuppu {subtype} topeltkl\u00f5psati", + "long": "Nuppu \"{subtype}\" hoiti all", + "long_single": "Nuppu {subtype} hoiti all ja seej\u00e4rel kl\u00f5psati", + "single": "Nuppu {subtype} kl\u00f5psati", + "single_long": "Nuppu {subtype} kl\u00f5psati \u00fcks kord ja seej\u00e4rel hoiti all", + "triple": "Nuppu {subtype} kl\u00f5psati kolm korda" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/it.json b/homeassistant/components/shelly/translations/it.json index 61f2f8ccd09..4d486a8f2fa 100644 --- a/homeassistant/components/shelly/translations/it.json +++ b/homeassistant/components/shelly/translations/it.json @@ -27,5 +27,21 @@ "description": "Prima della configurazione, i dispositivi alimentati a batteria devono essere riattivati premendo il pulsante sul dispositivo." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Pulsante", + "button1": "Primo pulsante", + "button2": "Secondo pulsante", + "button3": "Terzo pulsante" + }, + "trigger_type": { + "double": "{subtype} premuto due volte", + "long": "{subtype} premuto a lungo", + "long_single": "{subtype} premuto a lungo e poi singolarmente", + "single": "{subtype} premuto singolarmente", + "single_long": "{subtype} premuto singolarmente e poi a lungo", + "triple": "{subtype} premuto tre volte" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/lb.json b/homeassistant/components/shelly/translations/lb.json index b358c1c7282..e6c5d8330c6 100644 --- a/homeassistant/components/shelly/translations/lb.json +++ b/homeassistant/components/shelly/translations/lb.json @@ -27,5 +27,13 @@ "description": "Virum ariichten muss dat Batterie bedriwwen Ger\u00e4t aktiv\u00e9iert ginn andeems de Kn\u00e4ppchen um Apparat gedr\u00e9ckt g\u00ebtt." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Kn\u00e4ppchen", + "button1": "\u00c9ischte Kn\u00e4ppchen", + "button2": "Zweete Kn\u00e4ppchen", + "button3": "Dr\u00ebtte Kn\u00e4ppchen" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/no.json b/homeassistant/components/shelly/translations/no.json index 705c494a4c1..1606a1acbb1 100644 --- a/homeassistant/components/shelly/translations/no.json +++ b/homeassistant/components/shelly/translations/no.json @@ -27,5 +27,21 @@ "description": "F\u00f8r du setter opp, m\u00e5 batteridrevne enheter vekkes ved \u00e5 trykke p\u00e5 knappen p\u00e5 enheten." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Knapp", + "button1": "F\u00f8rste knapp", + "button2": "Andre knapp", + "button3": "Tredje knapp" + }, + "trigger_type": { + "double": "{subtype} dobbeltklikket", + "long": "{subtype} lenge klikket", + "long_single": "{subtype} lengre klikk og deretter et enkeltklikk", + "single": "{subtype} enkeltklikket", + "single_long": "{subtype} enkeltklikket og deretter et lengre klikk", + "triple": "{subtype} trippelklikket" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/pl.json b/homeassistant/components/shelly/translations/pl.json index ebf6041d4ba..cd8ffac7138 100644 --- a/homeassistant/components/shelly/translations/pl.json +++ b/homeassistant/components/shelly/translations/pl.json @@ -27,5 +27,21 @@ "description": "Przed skonfigurowaniem urz\u0105dzenia zasilane bateryjnie nale\u017cy, wybudzi\u0107 naciskaj\u0105c przycisk na urz\u0105dzeniu." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Przycisk", + "button1": "pierwszy", + "button2": "drugi", + "button3": "trzeci" + }, + "trigger_type": { + "double": "przycisk \"{subtype}\" zostanie dwukrotnie naci\u015bni\u0119ty", + "long": "przycisk \"{subtype}\" zostanie d\u0142ugo naci\u015bni\u0119ty", + "long_single": "przycisk \"{subtype}\" zostanie d\u0142ugo naci\u015bni\u0119ty, a nast\u0119pnie pojedynczo naci\u015bni\u0119ty", + "single": "przycisk \"{subtype}\" zostanie pojedynczo naci\u015bni\u0119ty", + "single_long": "przycisk \"{subtype}\" pojedynczo naci\u015bni\u0119ty, a nast\u0119pnie d\u0142ugo naci\u015bni\u0119ty", + "triple": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/ru.json b/homeassistant/components/shelly/translations/ru.json index 508a189b849..5a3a40ac9f8 100644 --- a/homeassistant/components/shelly/translations/ru.json +++ b/homeassistant/components/shelly/translations/ru.json @@ -27,5 +27,21 @@ "description": "\u041f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0449\u0438\u0435 \u043e\u0442 \u0431\u0430\u0442\u0430\u0440\u0435\u0438, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u0432\u0435\u0441\u0442\u0438 \u0438\u0437 \u0441\u043f\u044f\u0449\u0435\u0433\u043e \u0440\u0435\u0436\u0438\u043c\u0430, \u043d\u0430\u0436\u0430\u0432 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "\u041a\u043d\u043e\u043f\u043a\u0430", + "button1": "\u041f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430" + }, + "trigger_type": { + "double": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", + "long": "{subtype} \u0434\u043e\u043b\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0430", + "long_single": "{subtype} \u0434\u043e\u043b\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0430 \u0438 \u0437\u0430\u0442\u0435\u043c \u043d\u0430\u0436\u0430\u0442\u0430 \u043e\u0434\u0438\u043d \u0440\u0430\u0437", + "single": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u043e\u0434\u0438\u043d \u0440\u0430\u0437", + "single_long": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u043e\u0434\u0438\u043d \u0440\u0430\u0437 \u0438 \u0437\u0430\u0442\u0435\u043c \u0434\u043e\u043b\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0430", + "triple": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/tr.json b/homeassistant/components/shelly/translations/tr.json new file mode 100644 index 00000000000..f577c73787f --- /dev/null +++ b/homeassistant/components/shelly/translations/tr.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "{name}", + "step": { + "credentials": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + }, + "user": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + }, + "device_automation": { + "trigger_subtype": { + "button": "D\u00fc\u011fme", + "button1": "\u0130lk d\u00fc\u011fme", + "button2": "\u0130kinci d\u00fc\u011fme", + "button3": "\u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fme" + }, + "trigger_type": { + "double": "{subtype} \u00e7ift t\u0131kland\u0131", + "long": "{subtype} uzun t\u0131kland\u0131", + "long_single": "{subtype} uzun t\u0131kland\u0131 ve ard\u0131ndan tek t\u0131kland\u0131", + "single": "{subtype} tek t\u0131kland\u0131", + "triple": "{subtype} \u00fc\u00e7 kez t\u0131kland\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/uk.json b/homeassistant/components/shelly/translations/uk.json new file mode 100644 index 00000000000..7ad70b0f0da --- /dev/null +++ b/homeassistant/components/shelly/translations/uk.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "unsupported_firmware": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454 \u043d\u0435\u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0443 \u0432\u0435\u0440\u0441\u0456\u044e \u043c\u0456\u043a\u0440\u043e\u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0438." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 {model} ({host})? \n\n\u041f\u0435\u0440\u0435\u0434 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457, \u0449\u043e \u043f\u0440\u0430\u0446\u044e\u044e\u0442\u044c \u0432\u0456\u0434 \u0431\u0430\u0442\u0430\u0440\u0435\u0457, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0432\u0438\u0432\u0435\u0441\u0442\u0438 \u0437\u0456 \u0441\u043f\u043b\u044f\u0447\u043e\u0433\u043e \u0440\u0435\u0436\u0438\u043c\u0443, \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0432\u0448\u0438 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457." + }, + "credentials": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u041f\u0435\u0440\u0435\u0434 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457, \u0449\u043e \u043f\u0440\u0430\u0446\u044e\u044e\u0442\u044c \u0432\u0456\u0434 \u0431\u0430\u0442\u0430\u0440\u0435\u0457, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0432\u0438\u0432\u0435\u0441\u0442\u0438 \u0437\u0456 \u0441\u043f\u043b\u044f\u0447\u043e\u0433\u043e \u0440\u0435\u0436\u0438\u043c\u0443, \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0432\u0448\u0438 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457." + } + } + }, + "device_automation": { + "trigger_subtype": { + "button": "\u041a\u043d\u043e\u043f\u043a\u0430", + "button1": "\u041f\u0435\u0440\u0448\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button2": "\u0414\u0440\u0443\u0433\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button3": "\u0422\u0440\u0435\u0442\u044f \u043a\u043d\u043e\u043f\u043a\u0430" + }, + "trigger_type": { + "double": "{subtype} \u043f\u043e\u0434\u0432\u0456\u0439\u043d\u0438\u0439 \u043a\u043b\u0456\u043a", + "long": "{subtype} \u0434\u043e\u0432\u0433\u0438\u0439 \u043a\u043b\u0456\u043a", + "long_single": "{subtype} \u0434\u043e\u0432\u0433\u0438\u0439 \u043a\u043b\u0456\u043a, \u0430 \u043f\u043e\u0442\u0456\u043c \u043e\u0434\u0438\u043d \u043a\u043b\u0456\u043a", + "single": "{subtype} \u043e\u0434\u0438\u043d\u0430\u0440\u043d\u0438\u0439 \u043a\u043b\u0456\u043a", + "single_long": "{subtype} \u043e\u0434\u0438\u043d\u0430\u0440\u043d\u0438\u0439 \u043a\u043b\u0456\u043a, \u043f\u043e\u0442\u0456\u043c \u0434\u043e\u0432\u0433\u0438\u0439 \u043a\u043b\u0456\u043a", + "triple": "{subtype} \u043f\u043e\u0442\u0440\u0456\u0439\u043d\u0438\u0439 \u043a\u043b\u0456\u043a" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/zh-Hant.json b/homeassistant/components/shelly/translations/zh-Hant.json index bf0150523b3..8f315208135 100644 --- a/homeassistant/components/shelly/translations/zh-Hant.json +++ b/homeassistant/components/shelly/translations/zh-Hant.json @@ -27,5 +27,21 @@ "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" } } + }, + "device_automation": { + "trigger_subtype": { + "button": "\u6309\u9215", + "button1": "\u7b2c\u4e00\u500b\u6309\u9215", + "button2": "\u7b2c\u4e8c\u500b\u6309\u9215", + "button3": "\u7b2c\u4e09\u500b\u6309\u9215" + }, + "trigger_type": { + "double": "{subtype} \u96d9\u64ca", + "long": "{subtype} \u9577\u6309", + "long_single": "{subtype} \u9577\u6309\u5f8c\u55ae\u64ca", + "single": "{subtype} \u55ae\u64ca", + "single_long": "{subtype} \u55ae\u64ca\u5f8c\u9577\u6309", + "triple": "{subtype} \u4e09\u9023\u64ca" + } } } \ No newline at end of file diff --git a/homeassistant/components/shopping_list/translations/de.json b/homeassistant/components/shopping_list/translations/de.json index d2d6a42fe24..68372e9f4ac 100644 --- a/homeassistant/components/shopping_list/translations/de.json +++ b/homeassistant/components/shopping_list/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Die Einkaufsliste ist bereits konfiguriert." + "already_configured": "Der Dienst ist bereits konfiguriert" }, "step": { "user": { diff --git a/homeassistant/components/shopping_list/translations/tr.json b/homeassistant/components/shopping_list/translations/tr.json new file mode 100644 index 00000000000..d139d2f6399 --- /dev/null +++ b/homeassistant/components/shopping_list/translations/tr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "description": "Al\u0131\u015fveri\u015f listesini yap\u0131land\u0131rmak istiyor musunuz?", + "title": "Al\u0131\u015fveri\u015f listesi" + } + } + }, + "title": "Al\u0131\u015fveri\u015f listesi" +} \ No newline at end of file diff --git a/homeassistant/components/shopping_list/translations/uk.json b/homeassistant/components/shopping_list/translations/uk.json new file mode 100644 index 00000000000..b73bd6c702a --- /dev/null +++ b/homeassistant/components/shopping_list/translations/uk.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "step": { + "user": { + "description": "\u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u0441\u043f\u0438\u0441\u043e\u043a \u043f\u043e\u043a\u0443\u043f\u043e\u043a?", + "title": "\u0421\u043f\u0438\u0441\u043e\u043a \u043f\u043e\u043a\u0443\u043f\u043e\u043a" + } + } + }, + "title": "\u0421\u043f\u0438\u0441\u043e\u043a \u043f\u043e\u043a\u0443\u043f\u043e\u043a" +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index ab05cf649d8..5914e8f680c 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -1,17 +1,21 @@ { "config": { "abort": { - "already_configured": "Dieses SimpliSafe-Konto wird bereits verwendet." + "already_configured": "Dieses SimpliSafe-Konto wird bereits verwendet.", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "identifier_exists": "Konto bereits registriert", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { "reauth_confirm": { "data": { "password": "Passwort" - } + }, + "description": "Dein Zugriffstoken ist abgelaufen oder wurde widerrufen. Gib dein Passwort ein, um dein Konto erneut zu verkn\u00fcpfen.", + "title": "Integration erneut authentifizieren" }, "user": { "data": { diff --git a/homeassistant/components/simplisafe/translations/tr.json b/homeassistant/components/simplisafe/translations/tr.json index ec84b1b7c1c..94506fb426b 100644 --- a/homeassistant/components/simplisafe/translations/tr.json +++ b/homeassistant/components/simplisafe/translations/tr.json @@ -1,6 +1,26 @@ { "config": { + "abort": { + "already_configured": "Bu SimpliSafe hesab\u0131 zaten kullan\u0131mda.", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "still_awaiting_mfa": "Hala MFA e-posta t\u0131klamas\u0131 bekleniyor", + "unknown": "Beklenmeyen hata" + }, "step": { + "mfa": { + "description": "SimpliSafe'den bir ba\u011flant\u0131 i\u00e7in e-postan\u0131z\u0131 kontrol edin. Ba\u011flant\u0131y\u0131 do\u011frulad\u0131ktan sonra, entegrasyonun kurulumunu tamamlamak i\u00e7in buraya geri d\u00f6n\u00fcn.", + "title": "SimpliSafe \u00c7ok Fakt\u00f6rl\u00fc Kimlik Do\u011frulama" + }, + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "description": "Eri\u015fim kodunuzun s\u00fcresi doldu veya iptal edildi. Hesab\u0131n\u0131z\u0131 yeniden ba\u011flamak i\u00e7in parolan\u0131z\u0131 girin.", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "password": "Parola", diff --git a/homeassistant/components/simplisafe/translations/uk.json b/homeassistant/components/simplisafe/translations/uk.json index 376fb4468db..0a51f129e5f 100644 --- a/homeassistant/components/simplisafe/translations/uk.json +++ b/homeassistant/components/simplisafe/translations/uk.json @@ -1,18 +1,45 @@ { "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "error": { + "identifier_exists": "\u041e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u043e\u0432\u0430\u043d\u043e.", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "still_awaiting_mfa": "\u041e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f, \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u043e \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0456\u0439 \u043f\u043e\u0448\u0442\u0456.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, "step": { + "mfa": { + "description": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0441\u0432\u043e\u044e \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0443 \u043f\u043e\u0448\u0442\u0443 \u043d\u0430 \u043d\u0430\u044f\u0432\u043d\u0456\u0441\u0442\u044c \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0432\u0456\u0434 SimpliSafe. \u041f\u0456\u0441\u043b\u044f \u0442\u043e\u0433\u043e \u044f\u043a \u0432\u0456\u0434\u043a\u0440\u0438\u0454\u0442\u0435 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f, \u043f\u043e\u0432\u0435\u0440\u043d\u0456\u0442\u044c\u0441\u044f \u0441\u044e\u0434\u0438, \u0449\u043e\u0431 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457.", + "title": "\u0414\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f SimpliSafe" + }, "reauth_confirm": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c" - } + }, + "description": "\u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u0437\u0430\u043a\u0456\u043d\u0447\u0438\u0432\u0441\u044f \u0430\u0431\u043e \u0431\u0443\u0432 \u0430\u043d\u0443\u043b\u044c\u043e\u0432\u0430\u043d\u0438\u0439. \u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c, \u0449\u043e\u0431 \u0437\u0430\u043d\u043e\u0432\u043e \u043f\u0440\u0438\u0432'\u044f\u0437\u0430\u0442\u0438 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e" }, "user": { "data": { + "code": "\u041a\u043e\u0434 (\u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0456 Home Assistant)", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" }, "title": "\u0417\u0430\u043f\u043e\u0432\u043d\u0456\u0442\u044c \u0432\u0430\u0448\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e" } } + }, + "options": { + "step": { + "init": { + "data": { + "code": "\u041a\u043e\u0434 (\u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0456 Home Assistant)" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f SimpliSafe" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/de.json b/homeassistant/components/smappee/translations/de.json index a609492f428..15fd8d6cd22 100644 --- a/homeassistant/components/smappee/translations/de.json +++ b/homeassistant/components/smappee/translations/de.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "cannot_connect": "Verbindungsfehler" + "already_configured_device": "Ger\u00e4t ist bereits konfiguriert", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "cannot_connect": "Verbindung fehlgeschlagen", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url})." }, "flow_title": "Smappee: {name}", "step": { @@ -9,6 +13,14 @@ "data": { "environment": "Umgebung" } + }, + "local": { + "data": { + "host": "Host" + } + }, + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" } } } diff --git a/homeassistant/components/smappee/translations/tr.json b/homeassistant/components/smappee/translations/tr.json new file mode 100644 index 00000000000..4ba8a4da9a6 --- /dev/null +++ b/homeassistant/components/smappee/translations/tr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_configured_local_device": "Yerel ayg\u0131t (lar) zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. L\u00fctfen bir bulut cihaz\u0131n\u0131 yap\u0131land\u0131rmadan \u00f6nce bunlar\u0131 kald\u0131r\u0131n.", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_mdns": "Smappee entegrasyonu i\u00e7in desteklenmeyen cihaz." + }, + "flow_title": "Smappee: {name}", + "step": { + "environment": { + "data": { + "environment": "\u00c7evre" + } + }, + "local": { + "data": { + "host": "Ana Bilgisayar" + } + }, + "zeroconf_confirm": { + "title": "Smappee cihaz\u0131 bulundu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/uk.json b/homeassistant/components/smappee/translations/uk.json new file mode 100644 index 00000000000..a268fa82eac --- /dev/null +++ b/homeassistant/components/smappee/translations/uk.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_configured_local_device": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435 \u0434\u043b\u044f \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432. \u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u0457\u0445 \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0445\u043c\u0430\u0440\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e.", + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_mdns": "\u041d\u0435\u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439.", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443." + }, + "flow_title": "Smappee: {name}", + "step": { + "environment": { + "data": { + "environment": "\u041e\u0442\u043e\u0447\u0435\u043d\u043d\u044f" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Smappee." + }, + "local": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0430\u0434\u0440\u0435\u0441\u0443 \u0445\u043e\u0441\u0442\u0430, \u0449\u043e\u0431 \u043f\u043e\u0447\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e \u0437 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0438\u043c \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c Smappee" + }, + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "zeroconf_confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Smappee \u0437 \u0441\u0435\u0440\u0456\u0439\u043d\u0438\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c `{serialnumber}`?", + "title": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Smappee" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/de.json b/homeassistant/components/smart_meter_texas/translations/de.json index 38215675701..0eee2778d05 100644 --- a/homeassistant/components/smart_meter_texas/translations/de.json +++ b/homeassistant/components/smart_meter_texas/translations/de.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/smart_meter_texas/translations/tr.json b/homeassistant/components/smart_meter_texas/translations/tr.json new file mode 100644 index 00000000000..6ed28a58c79 --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/uk.json b/homeassistant/components/smart_meter_texas/translations/uk.json new file mode 100644 index 00000000000..49bceaa3f6e --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/de.json b/homeassistant/components/smarthab/translations/de.json index 2c76c4d56db..18bb2c77047 100644 --- a/homeassistant/components/smarthab/translations/de.json +++ b/homeassistant/components/smarthab/translations/de.json @@ -1,6 +1,7 @@ { "config": { "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/smarthab/translations/tr.json b/homeassistant/components/smarthab/translations/tr.json new file mode 100644 index 00000000000..98da6384f8d --- /dev/null +++ b/homeassistant/components/smarthab/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "email": "E-posta", + "password": "Parola" + }, + "title": "SmartHab'\u0131 kurun" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/uk.json b/homeassistant/components/smarthab/translations/uk.json new file mode 100644 index 00000000000..036ec0a78d4 --- /dev/null +++ b/homeassistant/components/smarthab/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "service": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0441\u043f\u0440\u043e\u0431\u0456 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e SmartHab. \u0421\u0435\u0440\u0432\u0456\u0441 \u043c\u043e\u0436\u0435 \u0431\u0443\u0442\u0438 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0417 \u0442\u0435\u0445\u043d\u0456\u0447\u043d\u0438\u0445 \u043f\u0440\u0438\u0447\u0438\u043d \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0438\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0434\u043b\u044f Home Assistant. \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u0457\u0457 \u0432 \u0434\u043e\u0434\u0430\u0442\u043a\u0443 SmartHab.", + "title": "SmartHab" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/translations/tr.json b/homeassistant/components/smartthings/translations/tr.json new file mode 100644 index 00000000000..5e7463c1c74 --- /dev/null +++ b/homeassistant/components/smartthings/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "webhook_error": "SmartThings, webhook URL'sini do\u011frulayamad\u0131. L\u00fctfen webhook URL'sinin internetten eri\u015filebilir oldu\u011fundan emin olun ve tekrar deneyin." + }, + "step": { + "pat": { + "data": { + "access_token": "Eri\u015fim Belirteci" + } + }, + "select_location": { + "title": "Konum Se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/translations/uk.json b/homeassistant/components/smartthings/translations/uk.json new file mode 100644 index 00000000000..6f8a0ed4744 --- /dev/null +++ b/homeassistant/components/smartthings/translations/uk.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "invalid_webhook_url": "Webhook URL, \u0432\u043a\u0430\u0437\u0430\u043d\u0438\u0439 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u044c \u0432\u0456\u0434 SmartThings, \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439:\n > {webhook_url} \n\n\u041e\u043d\u043e\u0432\u0456\u0442\u044c \u0432\u0430\u0448\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u043d\u043e \u0434\u043e [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0439] ({component_url}), \u0430 \u043f\u0456\u0441\u043b\u044f \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a\u0443 Home Assistant \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437.", + "no_available_locations": "\u041d\u0435\u043c\u0430\u0454 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043c\u0456\u0441\u0446\u044c \u0440\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f SmartThings." + }, + "error": { + "app_setup_error": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 SmartApp. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437.", + "token_forbidden": "\u0422\u043e\u043a\u0435\u043d \u043d\u0435 \u043c\u043e\u0436\u0435 \u0431\u0443\u0442\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u0438\u0439 \u0434\u043b\u044f OAuth.", + "token_invalid_format": "\u0422\u043e\u043a\u0435\u043d \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 UID / GUID.", + "token_unauthorized": "\u0422\u043e\u043a\u0435\u043d \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439 \u0430\u0431\u043e \u0431\u0456\u043b\u044c\u0448\u0435 \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u0438\u0439.", + "webhook_error": "SmartThings \u043d\u0435 \u043c\u043e\u0436\u0435 \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0438\u0442\u0438 Webhook URL. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0432\u043a\u0430\u0437\u0430\u043d\u0438\u0439 Webhook URL \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0456\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0456 \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437." + }, + "step": { + "authorize": { + "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f Home Assistant" + }, + "pat": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c [\u041e\u0441\u043e\u0431\u0438\u0441\u0442\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 SmartThings] ({token_url}), \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u0438\u0439 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u043d\u043e \u0434\u043e [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0457] ({component_url}).", + "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443" + }, + "select_location": { + "data": { + "location_id": "\u041c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043c\u0456\u0441\u0446\u0435 \u0440\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f SmartThings, \u044f\u043a\u0438\u0439 \u0432\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 \u0432 Home Assistant. \u041f\u0456\u0441\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u0432\u0456\u0434\u043a\u0440\u0438\u0454\u0442\u044c\u0441\u044f \u043d\u043e\u0432\u0435 \u0432\u0456\u043a\u043d\u043e, \u0434\u0435 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0431\u0443\u0434\u0435 \u0443\u0432\u0456\u0439\u0442\u0438 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u0442\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0432\u0430\u0442\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 Home Assistant \u0432 \u043e\u0431\u0440\u0430\u043d\u043e\u043c\u0443 \u043c\u0456\u0441\u0446\u0456 \u0440\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f.", + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f" + }, + "user": { + "description": "SmartThings \u0431\u0443\u0434\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439 \u0434\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 push-\u043e\u043d\u043e\u0432\u043b\u0435\u043d\u044c \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e:\n> {webhook_url} \n\n\u042f\u043a\u0449\u043e \u0446\u0435 \u043d\u0435 \u0442\u0430\u043a, \u043e\u043d\u043e\u0432\u0456\u0442\u044c \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e, \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0456\u0442\u044c Home Assistant \u0456 \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437.", + "title": "\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f Callback URL" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/translations/uk.json b/homeassistant/components/smhi/translations/uk.json new file mode 100644 index 00000000000..24af32172ba --- /dev/null +++ b/homeassistant/components/smhi/translations/uk.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "name_exists": "\u0426\u044f \u043d\u0430\u0437\u0432\u0430 \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f.", + "wrong_location": "\u0422\u0456\u043b\u044c\u043a\u0438 \u0434\u043b\u044f \u0428\u0432\u0435\u0446\u0456\u0457." + }, + "step": { + "user": { + "data": { + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "title": "\u041c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432 \u0428\u0432\u0435\u0446\u0456\u0457" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sms/translations/de.json b/homeassistant/components/sms/translations/de.json index 1252313a438..b262df1486d 100644 --- a/homeassistant/components/sms/translations/de.json +++ b/homeassistant/components/sms/translations/de.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/sms/translations/tr.json b/homeassistant/components/sms/translations/tr.json new file mode 100644 index 00000000000..1ef2efb8121 --- /dev/null +++ b/homeassistant/components/sms/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "title": "Modeme ba\u011flan\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sms/translations/uk.json b/homeassistant/components/sms/translations/uk.json new file mode 100644 index 00000000000..be271a2b6e4 --- /dev/null +++ b/homeassistant/components/sms/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + }, + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/translations/fr.json b/homeassistant/components/solaredge/translations/fr.json index f8aec1fa230..6fa6fdf264f 100644 --- a/homeassistant/components/solaredge/translations/fr.json +++ b/homeassistant/components/solaredge/translations/fr.json @@ -4,7 +4,9 @@ "site_exists": "Ce site est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "site_exists": "Ce site est d\u00e9j\u00e0 configur\u00e9" + "invalid_api_key": "Cl\u00e9 API invalide", + "site_exists": "Ce site est d\u00e9j\u00e0 configur\u00e9", + "site_not_active": "The site n'est pas actif" }, "step": { "user": { diff --git a/homeassistant/components/solaredge/translations/lb.json b/homeassistant/components/solaredge/translations/lb.json index 4f2f698a6ca..709a57f070b 100644 --- a/homeassistant/components/solaredge/translations/lb.json +++ b/homeassistant/components/solaredge/translations/lb.json @@ -1,10 +1,13 @@ { "config": { "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", "site_exists": "D\u00ebs site_id ass scho konfigur\u00e9iert" }, "error": { - "site_exists": "D\u00ebs site_id ass scho konfigur\u00e9iert" + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "site_exists": "D\u00ebs site_id ass scho konfigur\u00e9iert", + "site_not_active": "De Site ass net aktiv" }, "step": { "user": { diff --git a/homeassistant/components/solaredge/translations/tr.json b/homeassistant/components/solaredge/translations/tr.json index 5307276a71d..b8159be58b4 100644 --- a/homeassistant/components/solaredge/translations/tr.json +++ b/homeassistant/components/solaredge/translations/tr.json @@ -2,6 +2,19 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "could_not_connect": "Solaredge API'ye ba\u011flan\u0131lamad\u0131", + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", + "site_not_active": "Site aktif de\u011fil" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/solaredge/translations/uk.json b/homeassistant/components/solaredge/translations/uk.json new file mode 100644 index 00000000000..5ad67d87680 --- /dev/null +++ b/homeassistant/components/solaredge/translations/uk.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "site_exists": "\u0426\u0435\u0439 site_id \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439." + }, + "error": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "could_not_connect": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 API Solaredge.", + "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API", + "site_exists": "\u0426\u0435\u0439 site_id \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439.", + "site_not_active": "\u0421\u0430\u0439\u0442 \u043d\u0435 \u0430\u043a\u0442\u0438\u0432\u043d\u0438\u0439." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "name": "\u041d\u0430\u0437\u0432\u0430", + "site_id": "site-id" + }, + "title": "SolarEdge" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/translations/de.json b/homeassistant/components/solarlog/translations/de.json index 58e691b733d..008e1058681 100644 --- a/homeassistant/components/solarlog/translations/de.json +++ b/homeassistant/components/solarlog/translations/de.json @@ -5,7 +5,7 @@ }, "error": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "cannot_connect": "Verbindung fehlgeschlagen. \u00dcberpr\u00fcfe die Host-Adresse" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/solarlog/translations/tr.json b/homeassistant/components/solarlog/translations/tr.json new file mode 100644 index 00000000000..a11d3815eed --- /dev/null +++ b/homeassistant/components/solarlog/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/translations/uk.json b/homeassistant/components/solarlog/translations/uk.json new file mode 100644 index 00000000000..f4fca695032 --- /dev/null +++ b/homeassistant/components/solarlog/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041f\u0440\u0435\u0444\u0456\u043a\u0441, \u044f\u043a\u0438\u0439 \u0431\u0443\u0434\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0434\u043b\u044f \u0441\u0435\u043d\u0441\u043e\u0440\u0456\u0432 Solar-Log" + }, + "title": "Solar-Log" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/translations/tr.json b/homeassistant/components/soma/translations/tr.json new file mode 100644 index 00000000000..21a477c75a7 --- /dev/null +++ b/homeassistant/components/soma/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/translations/uk.json b/homeassistant/components/soma/translations/uk.json new file mode 100644 index 00000000000..0ec98301d62 --- /dev/null +++ b/homeassistant/components/soma/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_setup": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435.", + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "connection_error": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 SOMA Connect.", + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Soma \u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "result_error": "SOMA Connect \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0432 \u0437\u0456 \u0441\u0442\u0430\u0442\u0443\u0441\u043e\u043c \u043f\u043e\u043c\u0438\u043b\u043a\u0438." + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e SOMA Connect.", + "title": "SOMA Connect" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/de.json b/homeassistant/components/somfy/translations/de.json index 6b76e2f61be..29a959f48ce 100644 --- a/homeassistant/components/somfy/translations/de.json +++ b/homeassistant/components/somfy/translations/de.json @@ -2,10 +2,12 @@ "config": { "abort": { "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", - "missing_configuration": "Die Somfy-Komponente ist nicht konfiguriert. Folge bitte der Dokumentation." + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "create_entry": { - "default": "Erfolgreich mit Somfy authentifiziert." + "default": "Erfolgreich authentifiziert" }, "step": { "pick_implementation": { diff --git a/homeassistant/components/somfy/translations/tr.json b/homeassistant/components/somfy/translations/tr.json new file mode 100644 index 00000000000..a152eb19468 --- /dev/null +++ b/homeassistant/components/somfy/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/uk.json b/homeassistant/components/somfy/translations/uk.json new file mode 100644 index 00000000000..ebf7e41044e --- /dev/null +++ b/homeassistant/components/somfy/translations/uk.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "step": { + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/ca.json b/homeassistant/components/somfy_mylink/translations/ca.json new file mode 100644 index 00000000000..93ae58ca2bf --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/ca.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "flow_title": "Somfy MyLink {mac} ({ip})", + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port", + "system_id": "ID del sistema" + }, + "description": "L'ID de sistema es pot obtenir des de l'aplicaci\u00f3 MyLink dins de Integraci\u00f3, seleccionant qualsevol servei que no sigui al n\u00favol." + } + } + }, + "options": { + "abort": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "entity_config": { + "data": { + "reverse": "La coberta est\u00e0 invertida" + }, + "description": "Opcions de configuraci\u00f3 de `{entity_id}`", + "title": "Configura l'entitat" + }, + "init": { + "data": { + "default_reverse": "Estat d'inversi\u00f3 predeterminat per a cobertes sense configurar", + "entity_id": "Configura una entitat espec\u00edfica.", + "target_id": "Opcions de configuraci\u00f3 de la coberta." + }, + "title": "Configura opcions de MyLink" + }, + "target_config": { + "data": { + "reverse": "La coberta est\u00e0 invertida" + }, + "description": "Opcions de configuraci\u00f3 de `{target_name}`", + "title": "Configura coberta MyLink" + } + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/cs.json b/homeassistant/components/somfy_mylink/translations/cs.json new file mode 100644 index 00000000000..71e05b51544 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/cs.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "flow_title": "Somfy MyLink {mac} ({ip})", + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + } + } + } + }, + "options": { + "abort": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/de.json b/homeassistant/components/somfy_mylink/translations/de.json new file mode 100644 index 00000000000..522e185af5d --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/de.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "Somfy MyLink {mac} ({ip})", + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port", + "system_id": "System-ID" + } + } + } + }, + "options": { + "abort": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "entity_config": { + "title": "Entit\u00e4t konfigurieren" + }, + "init": { + "title": "MyLink-Optionen konfigurieren" + }, + "target_config": { + "description": "Konfiguriere die Optionen f\u00fcr `{target_name}`", + "title": "MyLink-Cover konfigurieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/en.json b/homeassistant/components/somfy_mylink/translations/en.json index ca3d83e402b..13115b36e5c 100644 --- a/homeassistant/components/somfy_mylink/translations/en.json +++ b/homeassistant/components/somfy_mylink/translations/en.json @@ -1,44 +1,53 @@ { - "title": "Somfy MyLink", - "config": { - "flow_title": "Somfy MyLink {mac} ({ip})", - "step": { - "user": { - "description": "The System ID can be obtained in the MyLink app under Integration by selecting any non-Cloud service.", - "data": { - "host": "[%key:common::config_flow::data::host%]", - "port": "[%key:common::config_flow::data::port%]", - "system_id": "System ID" + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "flow_title": "Somfy MyLink {mac} ({ip})", + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port", + "system_id": "System ID" + }, + "description": "The System ID can be obtained in the MyLink app under Integration by selecting any non-Cloud service." + } } - } }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" - } - }, - "options": { - "abort": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" - }, - "step": { - "init": { - "title": "Configure MyLink Options", - "data": { - "target_id": "Configure options for a cover." + "options": { + "abort": { + "cannot_connect": "Failed to connect" + }, + "step": { + "entity_config": { + "data": { + "reverse": "Cover is reversed" + }, + "description": "Configure options for `{entity_id}`", + "title": "Configure Entity" + }, + "init": { + "data": { + "default_reverse": "Default reversal status for unconfigured covers", + "entity_id": "Configure a specific entity.", + "target_id": "Configure options for a cover." + }, + "title": "Configure MyLink Options" + }, + "target_config": { + "data": { + "reverse": "Cover is reversed" + }, + "description": "Configure options for `{target_name}`", + "title": "Configure MyLink Cover" + } } - }, - "target_config": { - "title": "Configure MyLink Cover", - "description": "Configure options for `{target_name}`", - "data": { - "reverse": "Cover is reversed" - } - } - } - } + }, + "title": "Somfy MyLink" } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/es.json b/homeassistant/components/somfy_mylink/translations/es.json new file mode 100644 index 00000000000..40d82a4522a --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/es.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "flow_title": "Somfy MyLink {mac} ({ip})", + "step": { + "user": { + "data": { + "host": "Host", + "port": "Puerto", + "system_id": "ID del sistema" + }, + "description": "El ID del sistema se puede obtener en la aplicaci\u00f3n MyLink en Integraci\u00f3n seleccionando cualquier servicio que no sea de la nube." + } + } + }, + "options": { + "abort": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "entity_config": { + "data": { + "reverse": "La cubierta est\u00e1 invertida" + }, + "description": "Configurar opciones para `{entity_id}`", + "title": "Configurar entidad" + }, + "init": { + "data": { + "default_reverse": "Estado de inversi\u00f3n predeterminado para cubiertas no configuradas", + "entity_id": "Configurar una entidad espec\u00edfica.", + "target_id": "Configurar opciones para una cubierta." + }, + "title": "Configurar opciones de MyLink" + }, + "target_config": { + "data": { + "reverse": "La cubierta est\u00e1 invertida" + }, + "description": "Configurar opciones para `{target_name}`", + "title": "Configurar la cubierta MyLink" + } + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/et.json b/homeassistant/components/somfy_mylink/translations/et.json new file mode 100644 index 00000000000..6d965220d7e --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/et.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine", + "unknown": "Ootamatu t\u00f5rge" + }, + "flow_title": "Somfy MyLink {mac} ( {ip} )", + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port", + "system_id": "S\u00fcsteemi ID" + }, + "description": "S\u00fcsteemi ID saab rakenduse MyLink sidumise alt valides mis tahes mitte- pilveteenuse." + } + } + }, + "options": { + "abort": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "entity_config": { + "data": { + "reverse": "(Akna)kate t\u00f6\u00f6tab vastupidi" + }, + "description": "Olemi {entity_id} suvandite seadmine", + "title": "Seadista olem" + }, + "init": { + "data": { + "default_reverse": "Seadistamata (akna)katete vaikep\u00f6\u00f6rduse olek", + "entity_id": "Seadista konkreetne olem.", + "target_id": "Seadista (akna)katte suvandid" + }, + "title": "Seadista MyLinki suvandid" + }, + "target_config": { + "data": { + "reverse": "(Akna)kate liigub vastupidi" + }, + "description": "Seadme `{target_name}` suvandite seadmine", + "title": "Seadista MyLink Cover" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/fr.json b/homeassistant/components/somfy_mylink/translations/fr.json new file mode 100644 index 00000000000..96904b6038d --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/fr.json @@ -0,0 +1,43 @@ +{ + "config": { + "error": { + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te" + } + } + } + }, + "options": { + "abort": { + "cannot_connect": "Echec de connection" + }, + "step": { + "entity_config": { + "data": { + "reverse": "La couverture est invers\u00e9e" + }, + "title": "Configurez une entit\u00e9 sp\u00e9cifique" + }, + "init": { + "data": { + "default_reverse": "Statut d'inversion par d\u00e9faut pour les couvertures non configur\u00e9es", + "entity_id": "Configurez une entit\u00e9 sp\u00e9cifique.", + "target_id": "Configurez les options pour la couverture." + }, + "title": "Configurer les options MyLink" + }, + "target_config": { + "data": { + "reverse": "La couverture est invers\u00e9e" + }, + "description": "Configurer les options pour \u00ab {target_name} \u00bb", + "title": "Configurer la couverture MyLink" + } + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/it.json b/homeassistant/components/somfy_mylink/translations/it.json new file mode 100644 index 00000000000..ce049782c43 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/it.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "flow_title": "Somfy MyLink {mac} ({ip})", + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta", + "system_id": "ID sistema" + }, + "description": "L'ID sistema pu\u00f2 essere ottenuto nell'app MyLink alla voce Integrazione selezionando qualsiasi servizio non-Cloud." + } + } + }, + "options": { + "abort": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "entity_config": { + "data": { + "reverse": "La tapparella \u00e8 invertita" + }, + "description": "Configura le opzioni per `{entity_id}`", + "title": "Configura entit\u00e0" + }, + "init": { + "data": { + "default_reverse": "Stato d'inversione predefinito per le tapparelle non configurate", + "entity_id": "Configura un'entit\u00e0 specifica.", + "target_id": "Configura opzioni per una tapparella" + }, + "title": "Configura le opzioni MyLink" + }, + "target_config": { + "data": { + "reverse": "La tapparella \u00e8 invertita" + }, + "description": "Configura le opzioni per `{target_name}`", + "title": "Configura tapparelle MyLink" + } + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/lb.json b/homeassistant/components/somfy_mylink/translations/lb.json new file mode 100644 index 00000000000..efaba3ab497 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/lb.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port", + "system_id": "System ID" + } + } + } + }, + "options": { + "abort": { + "cannot_connect": "Feeler beim verbannen" + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/no.json b/homeassistant/components/somfy_mylink/translations/no.json new file mode 100644 index 00000000000..5b9b6608c25 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/no.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "flow_title": "", + "step": { + "user": { + "data": { + "host": "Vert", + "port": "Port", + "system_id": "" + }, + "description": "System-ID-en kan f\u00e5s i MyLink-appen under Integrasjon ved \u00e5 velge en hvilken som helst ikke-Cloud-tjeneste." + } + } + }, + "options": { + "abort": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "entity_config": { + "data": { + "reverse": "Rullegardinet reverseres" + }, + "description": "Konfigurer alternativer for \"{entity_id}\"", + "title": "Konfigurer enhet" + }, + "init": { + "data": { + "default_reverse": "Standard tilbakef\u00f8ringsstatus for ukonfigurerte rullegardiner", + "entity_id": "Konfigurer en bestemt enhet.", + "target_id": "Konfigurer alternativer for et rullgardin" + }, + "title": "Konfigurere MyLink-alternativer" + }, + "target_config": { + "data": { + "reverse": "Rullegardinet reverseres" + }, + "description": "Konfigurer alternativer for \"{target_name}\"", + "title": "Konfigurer MyLink-deksel" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/pl.json b/homeassistant/components/somfy_mylink/translations/pl.json new file mode 100644 index 00000000000..7e49ecb2bca --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/pl.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "Somfy MyLink {mac} ({ip})", + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port", + "system_id": "Identyfikator systemu" + }, + "description": "Identyfikator systemu mo\u017cna uzyska\u0107 w aplikacji MyLink w sekcji Integracja, wybieraj\u0105c dowoln\u0105 us\u0142ug\u0119 spoza chmury." + } + } + }, + "options": { + "abort": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "entity_config": { + "data": { + "reverse": "Roleta/pokrywa jest odwr\u00f3cona" + }, + "description": "Konfiguracja opcji dla \"{entity_id}\"", + "title": "Konfigurowanie encji" + }, + "init": { + "data": { + "default_reverse": "Domy\u015blny stan odwr\u00f3cenia nieskonfigurowanych rolet/pokryw", + "entity_id": "Skonfiguruj okre\u015blon\u0105 encj\u0119.", + "target_id": "Konfiguracja opcji rolety" + }, + "title": "Konfiguracja opcji MyLink" + }, + "target_config": { + "data": { + "reverse": "Roleta/pokrywa jest odwr\u00f3cona" + }, + "description": "Konfiguracja opcji dla \"{target_name}\"", + "title": "Konfiguracja rolety MyLink" + } + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/ru.json b/homeassistant/components/somfy_mylink/translations/ru.json new file mode 100644 index 00000000000..e4cc7b71712 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/ru.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "Somfy MyLink {mac} ({ip})", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", + "system_id": "System ID" + }, + "description": "System ID \u043c\u043e\u0436\u043d\u043e \u0443\u0437\u043d\u0430\u0442\u044c \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 MyLink \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \u00ab\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u00bb, \u0432\u044b\u0431\u0440\u0430\u0432 \u043b\u044e\u0431\u0443\u044e \u043d\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u0443\u044e \u0441\u043b\u0443\u0436\u0431\u0443." + } + } + }, + "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." + }, + "step": { + "entity_config": { + "data": { + "reverse": "\u0418\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0434\u043b\u044f \u0448\u0442\u043e\u0440 \u0438 \u0436\u0430\u043b\u044e\u0437\u0438" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0434\u043b\u044f `{entity_id}`", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043e\u0431\u044a\u0435\u043a\u0442\u0430" + }, + "init": { + "data": { + "default_reverse": "\u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0438\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0434\u043b\u044f \u0448\u0442\u043e\u0440 \u0438 \u0436\u0430\u043b\u044e\u0437\u0438", + "entity_id": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430", + "target_id": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0448\u0442\u043e\u0440 \u0438\u043b\u0438 \u0436\u0430\u043b\u044e\u0437\u0438." + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 MyLink" + }, + "target_config": { + "data": { + "reverse": "\u0418\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0434\u043b\u044f \u0448\u0442\u043e\u0440 \u0438 \u0436\u0430\u043b\u044e\u0437\u0438" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0434\u043b\u044f `{target_name}`", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 MyLink Cover" + } + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/tr.json b/homeassistant/components/somfy_mylink/translations/tr.json new file mode 100644 index 00000000000..29530b65659 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/tr.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "Somfy MyLink {mac} ( {ip} )", + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port", + "system_id": "Sistem ID" + }, + "description": "Sistem Kimli\u011fi, MyLink uygulamas\u0131nda Entegrasyon alt\u0131nda Bulut d\u0131\u015f\u0131 herhangi bir hizmet se\u00e7ilerek elde edilebilir." + } + } + }, + "options": { + "abort": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "entity_config": { + "data": { + "reverse": "Kapak ters \u00e7evrildi" + }, + "description": "'{entity_id}' i\u00e7in se\u00e7enekleri yap\u0131land\u0131r\u0131n", + "title": "Varl\u0131\u011f\u0131 Yap\u0131land\u0131r" + }, + "init": { + "data": { + "default_reverse": "Yap\u0131land\u0131r\u0131lmam\u0131\u015f kapaklar i\u00e7in varsay\u0131lan geri alma durumu", + "entity_id": "Belirli bir varl\u0131\u011f\u0131 yap\u0131land\u0131r\u0131n.", + "target_id": "Kapak i\u00e7in se\u00e7enekleri yap\u0131land\u0131r\u0131n." + }, + "title": "MyLink Se\u00e7eneklerini Yap\u0131land\u0131r\u0131n" + }, + "target_config": { + "data": { + "reverse": "Kapak ters \u00e7evrildi" + }, + "description": "'{target_name}' i\u00e7in se\u00e7enekleri yap\u0131land\u0131r\u0131n", + "title": "MyLink Kapa\u011f\u0131n\u0131 Yap\u0131land\u0131r\u0131n" + } + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/uk.json b/homeassistant/components/somfy_mylink/translations/uk.json new file mode 100644 index 00000000000..2d251531340 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/uk.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", + "system_id": "System ID" + }, + "description": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0441\u0438\u0441\u0442\u0435\u043c\u0438 \u043c\u043e\u0436\u043d\u0430 \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0432 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0456 MyLink \u0443 \u0440\u043e\u0437\u0434\u0456\u043b\u0456 \u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f, \u0432\u0438\u0431\u0440\u0430\u0432\u0448\u0438 \u0431\u0443\u0434\u044c-\u044f\u043a\u0443 \u043d\u0435\u0445\u043c\u0430\u0440\u043d\u0443 \u0441\u043b\u0443\u0436\u0431\u0443." + } + } + }, + "options": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "entity_config": { + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 \u0434\u043b\u044f \"{entity_id}\"", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c" + }, + "init": { + "data": { + "entity_id": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0441\u043f\u0435\u0446\u0438\u0444\u0456\u0447\u043d\u043e\u0457 \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0456." + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0435\u0439 MyLink" + } + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/zh-Hant.json b/homeassistant/components/somfy_mylink/translations/zh-Hant.json new file mode 100644 index 00000000000..2abb6a64f7c --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/zh-Hant.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "Somfy MyLink {mac} ({ip})", + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0", + "system_id": "\u7cfb\u7d71 ID" + }, + "description": "\u7cfb\u7d71 ID \u53ef\u4ee5\u65bc\u6574\u5408\u5167\u7684 MyLink app \u9078\u64c7\u975e\u96f2\u7aef\u670d\u52d9\u4e2d\u627e\u5230\u3002" + } + } + }, + "options": { + "abort": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "entity_config": { + "data": { + "reverse": "\u7a97\u7c3e\u53cd\u5411" + }, + "description": "`{entity_id}` \u8a2d\u5b9a\u9078\u9805", + "title": "\u8a2d\u5b9a\u5be6\u9ad4" + }, + "init": { + "data": { + "default_reverse": "\u672a\u8a2d\u5b9a\u7a97\u7c3e\u9810\u8a2d\u70ba\u53cd\u5411", + "entity_id": "\u8a2d\u5b9a\u7279\u5b9a\u5be6\u9ad4\u3002", + "target_id": "\u7a97\u7c3e\u8a2d\u5b9a\u9078\u9805\u3002" + }, + "title": "MyLink \u8a2d\u5b9a\u9078\u9805" + }, + "target_config": { + "data": { + "reverse": "\u7a97\u7c3e\u53cd\u5411" + }, + "description": "`{target_name}` \u8a2d\u5b9a\u9078\u9805", + "title": "\u8a2d\u5b9a MyLink \u7a97\u7c3e" + } + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/de.json b/homeassistant/components/sonarr/translations/de.json index 3abc6b45ef3..19a37dbcc4f 100644 --- a/homeassistant/components/sonarr/translations/de.json +++ b/homeassistant/components/sonarr/translations/de.json @@ -1,19 +1,27 @@ { "config": { "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "unknown": "Unerwateter Fehler" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "flow_title": "Sonarr: {name}", "step": { + "reauth_confirm": { + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "api_key": "API Schl\u00fcssel", "base_path": "Pfad zur API", "host": "Host", - "port": "Port" + "port": "Port", + "ssl": "Verwendet ein SSL-Zertifikat", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" } } } diff --git a/homeassistant/components/sonarr/translations/tr.json b/homeassistant/components/sonarr/translations/tr.json new file mode 100644 index 00000000000..eadf0100045 --- /dev/null +++ b/homeassistant/components/sonarr/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "host": "Ana Bilgisayar", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/uk.json b/homeassistant/components/sonarr/translations/uk.json new file mode 100644 index 00000000000..0b6b7acf26d --- /dev/null +++ b/homeassistant/components/sonarr/translations/uk.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "flow_title": "Sonarr: {name}", + "step": { + "reauth_confirm": { + "description": "\u041f\u043e\u0442\u0440\u0456\u0431\u043d\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e API Sonarr \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e: {host}", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e" + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "base_path": "\u0428\u043b\u044f\u0445 \u0434\u043e API", + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "\u041a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u043c\u0430\u0439\u0431\u0443\u0442\u043d\u0456\u0445 \u0434\u043d\u0456\u0432 \u0434\u043b\u044f \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u043d\u044f", + "wanted_max_items": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430 \u043a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u0435\u043b\u0435\u043c\u0435\u043d\u0442\u0456\u0432 \u0434\u043b\u044f \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u043d\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/tr.json b/homeassistant/components/songpal/translations/tr.json new file mode 100644 index 00000000000..ab90d4b1067 --- /dev/null +++ b/homeassistant/components/songpal/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "endpoint": "Biti\u015f noktas\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/uk.json b/homeassistant/components/songpal/translations/uk.json new file mode 100644 index 00000000000..893077a826d --- /dev/null +++ b/homeassistant/components/songpal/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "not_songpal_device": "\u0426\u0435 \u043d\u0435 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Songpal." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "flow_title": "Sony Songpal {name} ({host})", + "step": { + "init": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name} ({host})?" + }, + "user": { + "data": { + "endpoint": "\u041a\u0456\u043d\u0446\u0435\u0432\u0430 \u0442\u043e\u0447\u043a\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/translations/de.json b/homeassistant/components/sonos/translations/de.json index 93b25cf0b97..5d66c168116 100644 --- a/homeassistant/components/sonos/translations/de.json +++ b/homeassistant/components/sonos/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Keine Sonos Ger\u00e4te im Netzwerk gefunden.", - "single_instance_allowed": "Nur eine einzige Konfiguration von Sonos ist notwendig." + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { "confirm": { diff --git a/homeassistant/components/sonos/translations/tr.json b/homeassistant/components/sonos/translations/tr.json new file mode 100644 index 00000000000..42bd46ce7c0 --- /dev/null +++ b/homeassistant/components/sonos/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "Sonos'u kurmak istiyor musunuz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/translations/uk.json b/homeassistant/components/sonos/translations/uk.json new file mode 100644 index 00000000000..aff6c9f59b1 --- /dev/null +++ b/homeassistant/components/sonos/translations/uk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "step": { + "confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Sonos?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/de.json b/homeassistant/components/speedtestdotnet/translations/de.json index 56b1a91a89e..3b5ef0b26e1 100644 --- a/homeassistant/components/speedtestdotnet/translations/de.json +++ b/homeassistant/components/speedtestdotnet/translations/de.json @@ -1,11 +1,12 @@ { "config": { "abort": { - "wrong_server_id": "Server ID ist ung\u00fcltig" + "single_instance_allowed": "Bereits konfiguriert. Es ist nur eine Konfiguration m\u00f6glich.", + "wrong_server_id": "Server-ID ist ung\u00fcltig" }, "step": { "user": { - "description": "Einrichtung beginnen?" + "description": "M\u00f6chtest du mit der Einrichtung beginnen?" } } }, @@ -13,7 +14,9 @@ "step": { "init": { "data": { - "manual": "Automatische Updates deaktivieren" + "manual": "Automatische Updates deaktivieren", + "scan_interval": "Aktualisierungsfrequenz (Minuten)", + "server_name": "Testserver ausw\u00e4hlen" } } } diff --git a/homeassistant/components/speedtestdotnet/translations/tr.json b/homeassistant/components/speedtestdotnet/translations/tr.json new file mode 100644 index 00000000000..b13be7c5e0c --- /dev/null +++ b/homeassistant/components/speedtestdotnet/translations/tr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "wrong_server_id": "Sunucu kimli\u011fi ge\u00e7erli de\u011fil" + }, + "step": { + "user": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "manual": "Otomatik g\u00fcncellemeyi devre d\u0131\u015f\u0131 b\u0131rak\u0131n", + "scan_interval": "G\u00fcncelleme s\u0131kl\u0131\u011f\u0131 (dakika)", + "server_name": "Test sunucusunu se\u00e7in" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/uk.json b/homeassistant/components/speedtestdotnet/translations/uk.json new file mode 100644 index 00000000000..89ef24440d1 --- /dev/null +++ b/homeassistant/components/speedtestdotnet/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "wrong_server_id": "\u041d\u0435\u043f\u0440\u0438\u043f\u0443\u0441\u0442\u0438\u043c\u0438\u0439 \u0456\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0441\u0435\u0440\u0432\u0435\u0440\u0430." + }, + "step": { + "user": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "manual": "\u0412\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0435 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f", + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f (\u0443 \u0445\u0432\u0438\u043b\u0438\u043d\u0430\u0445)", + "server_name": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0443\u0432\u0430\u043d\u043d\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spider/translations/de.json b/homeassistant/components/spider/translations/de.json index 6f398062876..c57e55e9d2e 100644 --- a/homeassistant/components/spider/translations/de.json +++ b/homeassistant/components/spider/translations/de.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/spider/translations/tr.json b/homeassistant/components/spider/translations/tr.json new file mode 100644 index 00000000000..9bcc6bb1c41 --- /dev/null +++ b/homeassistant/components/spider/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spider/translations/uk.json b/homeassistant/components/spider/translations/uk.json new file mode 100644 index 00000000000..b8be2a14887 --- /dev/null +++ b/homeassistant/components/spider/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "\u0412\u0445\u0456\u0434 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 mijn.ithodaalderop.nl" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/de.json b/homeassistant/components/spotify/translations/de.json index bfd393bbbb8..281803ec66e 100644 --- a/homeassistant/components/spotify/translations/de.json +++ b/homeassistant/components/spotify/translations/de.json @@ -2,14 +2,18 @@ "config": { "abort": { "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", - "missing_configuration": "Die Spotify-Integration ist nicht konfiguriert. Bitte folgen Sie der Dokumentation." + "missing_configuration": "Die Spotify-Integration ist nicht konfiguriert. Bitte folgen Sie der Dokumentation.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url})." }, "create_entry": { "default": "Erfolgreich mit Spotify authentifiziert." }, "step": { "pick_implementation": { - "title": "Authentifizierungsmethode ausw\u00e4hlen" + "title": "W\u00e4hle die Authentifizierungsmethode" + }, + "reauth_confirm": { + "title": "Integration erneut authentifizieren" } } }, diff --git a/homeassistant/components/spotify/translations/lb.json b/homeassistant/components/spotify/translations/lb.json index d7b5dcec0be..92e323d6c4d 100644 --- a/homeassistant/components/spotify/translations/lb.json +++ b/homeassistant/components/spotify/translations/lb.json @@ -17,5 +17,10 @@ "title": "Integratioun re-authentifiz\u00e9ieren" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Spotify API Endpunkt ereechbar" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/uk.json b/homeassistant/components/spotify/translations/uk.json new file mode 100644 index 00000000000..fda84b310a5 --- /dev/null +++ b/homeassistant/components/spotify/translations/uk.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "missing_configuration": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f Spotify \u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0430. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044c \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0454\u044e.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", + "reauth_account_mismatch": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u043e\u0432\u0430\u043d\u0438\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u043d\u0435 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u0430\u0454 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441\u0443, \u0449\u043e \u0432\u0438\u043c\u0430\u0433\u0430\u0454 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457." + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "step": { + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "reauth_confirm": { + "description": "\u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u0432 Spotify \u0434\u043b\u044f \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443: {account}", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e" + } + } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e API Spotify" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/de.json b/homeassistant/components/squeezebox/translations/de.json index 667bf6dbd12..742210f3dc6 100644 --- a/homeassistant/components/squeezebox/translations/de.json +++ b/homeassistant/components/squeezebox/translations/de.json @@ -1,11 +1,17 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" }, "step": { "edit": { "data": { + "host": "Host", "password": "Passwort", "port": "Port", "username": "Benutzername" diff --git a/homeassistant/components/squeezebox/translations/tr.json b/homeassistant/components/squeezebox/translations/tr.json new file mode 100644 index 00000000000..ff249aafa14 --- /dev/null +++ b/homeassistant/components/squeezebox/translations/tr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "edit": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + }, + "user": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/uk.json b/homeassistant/components/squeezebox/translations/uk.json new file mode 100644 index 00000000000..50cd135f6f3 --- /dev/null +++ b/homeassistant/components/squeezebox/translations/uk.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "no_server_found": "\u0421\u0435\u0440\u0432\u0435\u0440 LMS \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "no_server_found": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0432\u0438\u044f\u0432\u0438\u0442\u0438 \u0441\u0435\u0440\u0432\u0435\u0440.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "Logitech Squeezebox: {host}", + "step": { + "edit": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "\u0406\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044f \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/de.json b/homeassistant/components/srp_energy/translations/de.json index 23fe89c73b4..302233d2923 100644 --- a/homeassistant/components/srp_energy/translations/de.json +++ b/homeassistant/components/srp_energy/translations/de.json @@ -1,14 +1,21 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Anmeldung", + "unknown": "Unerwarteter Fehler" }, "step": { "user": { "data": { - "password": "Passwort" + "password": "Passwort", + "username": "Benutzername" } } } - } + }, + "title": "SRP Energy" } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/es.json b/homeassistant/components/srp_energy/translations/es.json index de15bb80551..849c5019d3b 100644 --- a/homeassistant/components/srp_energy/translations/es.json +++ b/homeassistant/components/srp_energy/translations/es.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { "cannot_connect": "No se pudo conectar", "invalid_account": "El ID de la cuenta debe ser un n\u00famero de 9 d\u00edgitos", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { @@ -15,7 +15,7 @@ "id": "ID de la cuenta", "is_tou": "Es el plan de tiempo de uso", "password": "Contrase\u00f1a", - "username": "Nombre de usuario" + "username": "Usuario" } } } diff --git a/homeassistant/components/srp_energy/translations/fr.json b/homeassistant/components/srp_energy/translations/fr.json new file mode 100644 index 00000000000..0cc85aff649 --- /dev/null +++ b/homeassistant/components/srp_energy/translations/fr.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/lb.json b/homeassistant/components/srp_energy/translations/lb.json new file mode 100644 index 00000000000..1affdcc31e6 --- /dev/null +++ b/homeassistant/components/srp_energy/translations/lb.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/tr.json b/homeassistant/components/srp_energy/translations/tr.json index 1b08426f631..ead8238d82c 100644 --- a/homeassistant/components/srp_energy/translations/tr.json +++ b/homeassistant/components/srp_energy/translations/tr.json @@ -1,7 +1,13 @@ { "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, "error": { - "invalid_account": "Hesap kimli\u011fi 9 haneli bir say\u0131 olmal\u0131d\u0131r" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_account": "Hesap kimli\u011fi 9 haneli bir say\u0131 olmal\u0131d\u0131r", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" }, "step": { "user": { diff --git a/homeassistant/components/srp_energy/translations/uk.json b/homeassistant/components/srp_energy/translations/uk.json new file mode 100644 index 00000000000..5267aa2a575 --- /dev/null +++ b/homeassistant/components/srp_energy/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_account": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 \u043c\u0430\u0454 \u0431\u0443\u0442\u0438 9-\u0437\u043d\u0430\u0447\u043d\u0438\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c.", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443", + "is_tou": "\u041f\u043b\u0430\u043d \u0447\u0430\u0441\u0443 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + }, + "title": "SRP Energy" +} \ No newline at end of file diff --git a/homeassistant/components/starline/translations/tr.json b/homeassistant/components/starline/translations/tr.json new file mode 100644 index 00000000000..9d52f589e98 --- /dev/null +++ b/homeassistant/components/starline/translations/tr.json @@ -0,0 +1,33 @@ +{ + "config": { + "error": { + "error_auth_user": "Yanl\u0131\u015f kullan\u0131c\u0131 ad\u0131 ya da parola" + }, + "step": { + "auth_app": { + "title": "Uygulama kimlik bilgileri" + }, + "auth_captcha": { + "data": { + "captcha_code": "G\u00f6r\u00fcnt\u00fcden kod" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS kodu" + }, + "description": "{phone_number} telefona g\u00f6nderilen kodu girin", + "title": "\u0130ki fakt\u00f6rl\u00fc yetkilendirme" + }, + "auth_user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "StarLine hesab\u0131 e-postas\u0131 ve parolas\u0131" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/translations/uk.json b/homeassistant/components/starline/translations/uk.json new file mode 100644 index 00000000000..8a263044284 --- /dev/null +++ b/homeassistant/components/starline/translations/uk.json @@ -0,0 +1,41 @@ +{ + "config": { + "error": { + "error_auth_app": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0456\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0434\u043e\u0434\u0430\u0442\u043a\u0430 \u0430\u0431\u043e \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u0438\u0439 \u043a\u043e\u0434.", + "error_auth_mfa": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043e\u0434.", + "error_auth_user": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u043b\u043e\u0433\u0456\u043d \u0430\u0431\u043e \u043f\u0430\u0440\u043e\u043b\u044c." + }, + "step": { + "auth_app": { + "data": { + "app_id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0434\u043e\u0434\u0430\u0442\u043a\u0430", + "app_secret": "\u0421\u0435\u043a\u0440\u0435\u0442\u043d\u0438\u0439 \u043a\u043e\u0434" + }, + "description": "ID \u0434\u043e\u0434\u0430\u0442\u043a\u0430 \u0456 \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u0438\u0439 \u043a\u043e\u0434 [\u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 \u0440\u043e\u0437\u0440\u043e\u0431\u043d\u0438\u043a\u0430 StarLine] (https://my.starline.ru/developer)", + "title": "\u041e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456 \u0434\u043e\u0434\u0430\u0442\u043a\u0430" + }, + "auth_captcha": { + "data": { + "captcha_code": "\u041a\u043e\u0434 \u0437 \u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u043d\u044f" + }, + "description": "{captcha_img}", + "title": "CAPTCHA" + }, + "auth_mfa": { + "data": { + "mfa_code": "\u041a\u043e\u0434 \u0437 SMS" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043e\u0434, \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0439 \u043d\u0430 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0443 {phone_number}", + "title": "\u0414\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + }, + "auth_user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438 \u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u044c \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 StarLine", + "title": "\u041e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sun/translations/pl.json b/homeassistant/components/sun/translations/pl.json index fb90b9bd232..1f00babd1fd 100644 --- a/homeassistant/components/sun/translations/pl.json +++ b/homeassistant/components/sun/translations/pl.json @@ -1,7 +1,7 @@ { "state": { "_": { - "above_horizon": "powy\u017cej horyzontu", + "above_horizon": "nad horyzontem", "below_horizon": "poni\u017cej horyzontu" } }, diff --git a/homeassistant/components/switch/translations/uk.json b/homeassistant/components/switch/translations/uk.json index bee9eb957d5..26b85b3a873 100644 --- a/homeassistant/components/switch/translations/uk.json +++ b/homeassistant/components/switch/translations/uk.json @@ -1,5 +1,14 @@ { "device_automation": { + "action_type": { + "toggle": "{entity_name}: \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u0438", + "turn_off": "{entity_name}: \u0432\u0438\u043c\u043a\u043d\u0443\u0442\u0438", + "turn_on": "{entity_name}: \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438" + }, + "condition_type": { + "is_off": "{entity_name} \u0443 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456" + }, "trigger_type": { "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e", "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" diff --git a/homeassistant/components/syncthru/translations/tr.json b/homeassistant/components/syncthru/translations/tr.json new file mode 100644 index 00000000000..942457958f8 --- /dev/null +++ b/homeassistant/components/syncthru/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "confirm": { + "data": { + "url": "Web aray\u00fcz\u00fc URL'si" + } + }, + "user": { + "data": { + "name": "Ad", + "url": "Web aray\u00fcz\u00fc URL'si" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthru/translations/uk.json b/homeassistant/components/syncthru/translations/uk.json new file mode 100644 index 00000000000..74cccc7ef5a --- /dev/null +++ b/homeassistant/components/syncthru/translations/uk.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "invalid_url": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0430 URL-\u0430\u0434\u0440\u0435\u0441\u0430.", + "syncthru_not_supported": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454 SyncThru.", + "unknown_state": "\u0421\u0442\u0430\u043d \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430 \u043d\u0435\u0432\u0456\u0434\u043e\u043c\u0438\u0439, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 URL-\u0430\u0434\u0440\u0435\u0441\u0443 \u0442\u0430 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043c\u0435\u0440\u0435\u0436\u0456." + }, + "flow_title": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440 Samsung SyncThru: {name}", + "step": { + "confirm": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430", + "url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0432\u0435\u0431-\u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443" + } + }, + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430", + "url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0432\u0435\u0431-\u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/de.json b/homeassistant/components/synology_dsm/translations/de.json index 303321ea94c..f0d274c3bfe 100644 --- a/homeassistant/components/synology_dsm/translations/de.json +++ b/homeassistant/components/synology_dsm/translations/de.json @@ -1,13 +1,14 @@ { "config": { "abort": { - "already_configured": "Host bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "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" + "unknown": "Unerwarteter Fehler" }, "flow_title": "Synology DSM {name} ({host})", "step": { @@ -21,7 +22,7 @@ "data": { "password": "Passwort", "port": "Port", - "ssl": "Verwenden Sie SSL/TLS, um eine Verbindung zu Ihrem NAS herzustellen", + "ssl": "Verwendet ein SSL-Zertifikat", "username": "Benutzername", "verify_ssl": "SSL Zertifikat verifizieren" }, @@ -33,7 +34,7 @@ "host": "Host", "password": "Passwort", "port": "Port", - "ssl": "Verwenden Sie SSL/TLS, um eine Verbindung zu Ihrem NAS herzustellen", + "ssl": "Verwendet ein SSL-Zertifikat", "username": "Benutzername", "verify_ssl": "SSL Zertifikat verifizieren" }, diff --git a/homeassistant/components/synology_dsm/translations/tr.json b/homeassistant/components/synology_dsm/translations/tr.json index a7598bb3438..681d85d2ef5 100644 --- a/homeassistant/components/synology_dsm/translations/tr.json +++ b/homeassistant/components/synology_dsm/translations/tr.json @@ -1,15 +1,31 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, "step": { "link": { "data": { + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131", "verify_ssl": "SSL sertifikalar\u0131n\u0131 do\u011frula" } }, "user": { "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131", "verify_ssl": "SSL sertifikalar\u0131n\u0131 do\u011frula" - } + }, + "title": "Synology DSM" } } } diff --git a/homeassistant/components/synology_dsm/translations/uk.json b/homeassistant/components/synology_dsm/translations/uk.json new file mode 100644 index 00000000000..4d80350989f --- /dev/null +++ b/homeassistant/components/synology_dsm/translations/uk.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "missing_data": "\u0412\u0456\u0434\u0441\u0443\u0442\u043d\u0456 \u0434\u0430\u043d\u0456: \u043f\u043e\u0432\u0442\u043e\u0440\u0456\u0442\u044c \u0441\u043f\u0440\u043e\u0431\u0443 \u043f\u0456\u0437\u043d\u0456\u0448\u0435 \u0430\u0431\u043e \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0456\u043d\u0448\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "otp_failed": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437 \u0437 \u043d\u043e\u0432\u0438\u043c \u043f\u0430\u0440\u043e\u043b\u0435\u043c.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "Synology DSM {name} ({host})", + "step": { + "2sa": { + "data": { + "otp_code": "\u041a\u043e\u0434" + }, + "title": "Synology DSM: \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + }, + "link": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" + }, + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name} ({host})?", + "title": "Synology DSM" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" + }, + "title": "Synology DSM" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043c\u0456\u0436 \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f\u043c\u0438 (\u0445\u0432.)", + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/system_health/translations/uk.json b/homeassistant/components/system_health/translations/uk.json index 267fcb83a61..61f30782f04 100644 --- a/homeassistant/components/system_health/translations/uk.json +++ b/homeassistant/components/system_health/translations/uk.json @@ -1,3 +1,3 @@ { - "title": "\u0411\u0435\u0437\u043f\u0435\u043a\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0438" + "title": "\u0421\u0442\u0430\u043d \u0441\u0438\u0441\u0442\u0435\u043c\u0438" } \ No newline at end of file diff --git a/homeassistant/components/tado/translations/de.json b/homeassistant/components/tado/translations/de.json index ffab091f726..9dc410b670e 100644 --- a/homeassistant/components/tado/translations/de.json +++ b/homeassistant/components/tado/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "no_homes": "Es sind keine Standorte mit diesem Tado-Konto verkn\u00fcpft.", "unknown": "Unerwarteter Fehler" @@ -15,7 +15,7 @@ "password": "Passwort", "username": "Benutzername" }, - "title": "Stellen Sie eine Verbindung zu Ihrem Tado-Konto her" + "title": "Stellen eine Verbindung zu deinem Tado-Konto her" } } }, @@ -23,10 +23,10 @@ "step": { "init": { "data": { - "fallback": "Aktivieren Sie den Fallback-Modus." + "fallback": "Aktivieren den Fallback-Modus." }, "description": "Der Fallback-Modus wechselt beim n\u00e4chsten Zeitplanwechsel nach dem manuellen Anpassen einer Zone zu Smart Schedule.", - "title": "Passen Sie die Tado-Optionen an." + "title": "Passe die Tado-Optionen an." } } } diff --git a/homeassistant/components/tado/translations/tr.json b/homeassistant/components/tado/translations/tr.json new file mode 100644 index 00000000000..09ffbf8a7d1 --- /dev/null +++ b/homeassistant/components/tado/translations/tr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "fallback": "Geri d\u00f6n\u00fc\u015f modunu etkinle\u015ftirin." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tado/translations/uk.json b/homeassistant/components/tado/translations/uk.json new file mode 100644 index 00000000000..f1dcf4d575b --- /dev/null +++ b/homeassistant/components/tado/translations/uk.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "no_homes": "\u0427\u0438 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0431\u0443\u0434\u0438\u043d\u043a\u0456\u0432, \u043f\u043e\u0432'\u044f\u0437\u0430\u043d\u0438\u0445 \u0437 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u043c \u0437\u0430\u043f\u0438\u0441\u043e\u043c.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "Tado" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "fallback": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0440\u0435\u0436\u0438\u043c Fallback" + }, + "description": "\u0420\u0435\u0436\u0438\u043c Fallback \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043d\u0430 Smart Schedule \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u043e\u0433\u043e \u0440\u0430\u0437\u0443 \u043f\u0456\u0441\u043b\u044f \u0440\u0443\u0447\u043d\u043e\u0433\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u043e\u043d\u0438.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Tado" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tag/translations/uk.json b/homeassistant/components/tag/translations/uk.json new file mode 100644 index 00000000000..fdac700612d --- /dev/null +++ b/homeassistant/components/tag/translations/uk.json @@ -0,0 +1,3 @@ +{ + "title": "Tag" +} \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/de.json b/homeassistant/components/tasmota/translations/de.json new file mode 100644 index 00000000000..30874708839 --- /dev/null +++ b/homeassistant/components/tasmota/translations/de.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "config": { + "description": "Bitte die Tasmota-Konfiguration einstellen.", + "title": "Tasmota" + }, + "confirm": { + "description": "M\u00f6chtest du Tasmota einrichten?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/tr.json b/homeassistant/components/tasmota/translations/tr.json new file mode 100644 index 00000000000..a559d0911ee --- /dev/null +++ b/homeassistant/components/tasmota/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "config": { + "description": "L\u00fctfen Tasmota yap\u0131land\u0131rmas\u0131n\u0131 girin.", + "title": "Tasmota" + }, + "confirm": { + "description": "Tasmota'y\u0131 kurmak istiyor musunuz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/uk.json b/homeassistant/components/tasmota/translations/uk.json new file mode 100644 index 00000000000..6639a9c9626 --- /dev/null +++ b/homeassistant/components/tasmota/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "error": { + "invalid_discovery_topic": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043f\u0440\u0435\u0444\u0456\u043a\u0441 \u0442\u0435\u043c\u0438 \u0430\u0432\u0442\u043e\u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f." + }, + "step": { + "config": { + "data": { + "discovery_prefix": "\u041f\u0440\u0435\u0444\u0456\u043a\u0441 \u0442\u0435\u043c\u0438 \u0430\u0432\u0442\u043e\u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Tasmota.", + "title": "Tasmota" + }, + "confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Tasmota?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/de.json b/homeassistant/components/tellduslive/translations/de.json index a1f6f595a04..098ad9c17be 100644 --- a/homeassistant/components/tellduslive/translations/de.json +++ b/homeassistant/components/tellduslive/translations/de.json @@ -4,9 +4,12 @@ "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": "Unerwarteter Fehler", "unknown_authorize_url_generation": "Beim Generieren einer Authentifizierungs-URL ist ein unbekannter Fehler aufgetreten" }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, "step": { "auth": { "description": "So verkn\u00fcpfest du dein TelldusLive-Konto: \n 1. Klicke auf den Link unten \n 2. Melde dich bei Telldus Live an \n 3. Autorisiere ** {app_name} ** (klicke auf ** Yes **). \n 4. Komme hierher zur\u00fcck und klicke auf ** SUBMIT **. \n\n [Link TelldusLive-Konto]({auth_url})", diff --git a/homeassistant/components/tellduslive/translations/fr.json b/homeassistant/components/tellduslive/translations/fr.json index cde9d9c2c68..ef4d7bc44dd 100644 --- a/homeassistant/components/tellduslive/translations/fr.json +++ b/homeassistant/components/tellduslive/translations/fr.json @@ -4,7 +4,8 @@ "already_configured": "TelldusLive est d\u00e9j\u00e0 configur\u00e9", "authorize_url_fail": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation.", "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", - "unknown": "Une erreur inconnue s'est produite" + "unknown": "Une erreur inconnue s'est produite", + "unknown_authorize_url_generation": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation." }, "error": { "invalid_auth": "Authentification invalide" diff --git a/homeassistant/components/tellduslive/translations/lb.json b/homeassistant/components/tellduslive/translations/lb.json index 5e733c2294d..2b809050677 100644 --- a/homeassistant/components/tellduslive/translations/lb.json +++ b/homeassistant/components/tellduslive/translations/lb.json @@ -4,7 +4,8 @@ "already_configured": "Service ass scho konfigur\u00e9iert", "authorize_url_fail": "Onbekannte Feeler beim gener\u00e9ieren vun der Autorisatiouns URL.", "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", - "unknown": "Onerwaarte Feeler" + "unknown": "Onerwaarte Feeler", + "unknown_authorize_url_generation": "Onbekannte Feeler beim erstellen vun der Authorisatiouns URL." }, "error": { "invalid_auth": "Ong\u00eblteg Authentifikatioun" diff --git a/homeassistant/components/tellduslive/translations/tr.json b/homeassistant/components/tellduslive/translations/tr.json new file mode 100644 index 00000000000..300fad68391 --- /dev/null +++ b/homeassistant/components/tellduslive/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "unknown": "Beklenmeyen hata", + "unknown_authorize_url_generation": "Yetkilendirme url'si olu\u015fturulurken bilinmeyen hata." + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/uk.json b/homeassistant/components/tellduslive/translations/uk.json new file mode 100644 index 00000000000..ff7b3337bb9 --- /dev/null +++ b/homeassistant/components/tellduslive/translations/uk.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant.", + "authorize_url_fail": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430", + "unknown_authorize_url_generation": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "auth": { + "description": "\u0414\u043b\u044f \u0442\u043e\u0433\u043e, \u0449\u043e\u0431 \u043f\u0440\u0438\u0432'\u044f\u0437\u0430\u0442\u0438 \u0430\u043a\u0430\u0443\u043d\u0442 Telldus Live:\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043f\u043e \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044e, \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043e\u043c\u0443 \u043d\u0438\u0436\u0447\u0435\n2. \u0423\u0432\u0456\u0439\u0434\u0456\u0442\u044c \u0432 Telldus Live\n3. Authorize ** {app_name} ** (\u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** Yes **).\n4. \u041f\u043e\u0432\u0435\u0440\u043d\u0456\u0442\u044c\u0441\u044f \u0441\u044e\u0434\u0438 \u0442\u0430 \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **. \n\n[\u041f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u043d\u0430 Telldus Live]({auth_url})", + "title": "Telldus Live" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u043f\u043e\u0440\u043e\u0436\u043d\u044c\u043e", + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043a\u0456\u043d\u0446\u0435\u0432\u0443 \u0442\u043e\u0447\u043a\u0443." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/translations/de.json b/homeassistant/components/tesla/translations/de.json index 09100c355c2..558209af411 100644 --- a/homeassistant/components/tesla/translations/de.json +++ b/homeassistant/components/tesla/translations/de.json @@ -1,7 +1,9 @@ { "config": { "error": { - "cannot_connect": "Verbindungsfehler" + "already_configured": "Konto wurde bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { "user": { @@ -18,6 +20,7 @@ "step": { "init": { "data": { + "enable_wake_on_start": "Aufwachen des Autos beim Start erzwingen", "scan_interval": "Sekunden zwischen den Scans" } } diff --git a/homeassistant/components/tesla/translations/fr.json b/homeassistant/components/tesla/translations/fr.json index c8efc8b4fb5..6134ff25f6b 100644 --- a/homeassistant/components/tesla/translations/fr.json +++ b/homeassistant/components/tesla/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "error": { - "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9", + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide" }, diff --git a/homeassistant/components/tesla/translations/tr.json b/homeassistant/components/tesla/translations/tr.json new file mode 100644 index 00000000000..cf0d144c1ed --- /dev/null +++ b/homeassistant/components/tesla/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "E-posta" + }, + "description": "L\u00fctfen bilgilerinizi giriniz." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/translations/uk.json b/homeassistant/components/tesla/translations/uk.json new file mode 100644 index 00000000000..90d47ec2ff5 --- /dev/null +++ b/homeassistant/components/tesla/translations/uk.json @@ -0,0 +1,29 @@ +{ + "config": { + "error": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0434\u0430\u043d\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443.", + "title": "Tesla" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "enable_wake_on_start": "\u041f\u0440\u0438\u043c\u0443\u0441\u043e\u0432\u043e \u0440\u043e\u0437\u0431\u0443\u0434\u0438\u0442\u0438 \u043c\u0430\u0448\u0438\u043d\u0443 \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0443", + "scan_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043c\u0456\u0436 \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f\u043c\u0438 (\u0441\u0435\u043a.)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/de.json b/homeassistant/components/tibber/translations/de.json index 670f57df8ba..8d49c9d9e61 100644 --- a/homeassistant/components/tibber/translations/de.json +++ b/homeassistant/components/tibber/translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Ein Tibber-Konto ist bereits konfiguriert." + "already_configured": "Der Dienst ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token", "timeout": "Zeit\u00fcberschreitung beim Verbinden mit Tibber" }, diff --git a/homeassistant/components/tibber/translations/tr.json b/homeassistant/components/tibber/translations/tr.json new file mode 100644 index 00000000000..5f8e72986b2 --- /dev/null +++ b/homeassistant/components/tibber/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_access_token": "Ge\u00e7ersiz eri\u015fim belirteci" + }, + "step": { + "user": { + "data": { + "access_token": "Eri\u015fim Belirteci" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/uk.json b/homeassistant/components/tibber/translations/uk.json new file mode 100644 index 00000000000..b1240116856 --- /dev/null +++ b/homeassistant/components/tibber/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_access_token": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443.", + "timeout": "\u0427\u0430\u0441 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043c\u0438\u043d\u0443\u0432." + }, + "step": { + "user": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443, \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u0438\u0439 \u043d\u0430 \u0441\u0430\u0439\u0442\u0456 https://developer.tibber.com/settings/accesstoken", + "title": "Tibber" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tile/translations/de.json b/homeassistant/components/tile/translations/de.json index 59f48253a18..1c2af82aa63 100644 --- a/homeassistant/components/tile/translations/de.json +++ b/homeassistant/components/tile/translations/de.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Konto ist bereits konfiguriert" }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/tile/translations/tr.json b/homeassistant/components/tile/translations/tr.json new file mode 100644 index 00000000000..8a04e2f4bbf --- /dev/null +++ b/homeassistant/components/tile/translations/tr.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "E-posta" + }, + "title": "Karoyu Yap\u0131land\u0131r" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_inactive": "Etkin Olmayan Karolar\u0131 G\u00f6ster" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tile/translations/uk.json b/homeassistant/components/tile/translations/uk.json new file mode 100644 index 00000000000..dc28164fd93 --- /dev/null +++ b/homeassistant/components/tile/translations/uk.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + }, + "title": "Tile" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_inactive": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u043d\u0435\u0430\u043a\u0442\u0438\u0432\u043d\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457" + }, + "title": "Tile" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/timer/translations/uk.json b/homeassistant/components/timer/translations/uk.json index df690bded93..ce937735406 100644 --- a/homeassistant/components/timer/translations/uk.json +++ b/homeassistant/components/timer/translations/uk.json @@ -1,9 +1,9 @@ { "state": { "_": { - "active": "\u0430\u043a\u0442\u0438\u0432\u043d\u0438\u0439", - "idle": "\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f", - "paused": "\u043d\u0430 \u043f\u0430\u0443\u0437\u0456" + "active": "\u0410\u043a\u0442\u0438\u0432\u043d\u0438\u0439", + "idle": "\u041e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f", + "paused": "\u041f\u0440\u0438\u0437\u0443\u043f\u0438\u043d\u0435\u043d\u043e" } } } \ No newline at end of file diff --git a/homeassistant/components/toon/translations/de.json b/homeassistant/components/toon/translations/de.json index d9060a719d8..c04f3a5f4bb 100644 --- a/homeassistant/components/toon/translations/de.json +++ b/homeassistant/components/toon/translations/de.json @@ -1,7 +1,10 @@ { "config": { "abort": { + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", "no_agreements": "Dieses Konto hat keine Toon-Anzeigen.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", "unknown_authorize_url_generation": "Beim Generieren einer Authentifizierungs-URL ist ein unbekannter Fehler aufgetreten" } } diff --git a/homeassistant/components/toon/translations/fr.json b/homeassistant/components/toon/translations/fr.json index caeed852d0a..3fa6059a58f 100644 --- a/homeassistant/components/toon/translations/fr.json +++ b/homeassistant/components/toon/translations/fr.json @@ -6,7 +6,8 @@ "authorize_url_timeout": "Timout de g\u00e9n\u00e9ration de l'URL d'autorisation.", "missing_configuration": "The composant n'est pas configur\u00e9. Veuillez vous r\u00e9f\u00e9rer \u00e0 la documentation.", "no_agreements": "Ce compte n'a pas d'affichages Toon.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )" + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", + "unknown_authorize_url_generation": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation." }, "step": { "agreement": { diff --git a/homeassistant/components/toon/translations/lb.json b/homeassistant/components/toon/translations/lb.json index 6491c666738..e21dfb0c996 100644 --- a/homeassistant/components/toon/translations/lb.json +++ b/homeassistant/components/toon/translations/lb.json @@ -6,7 +6,8 @@ "authorize_url_timeout": "Z\u00e4itiwwerschraidung beim erstellen vun der Autorisatioun's URL.", "missing_configuration": "Komponent ass net konfigur\u00e9iert. Folleg der Dokumentatioun.", "no_agreements": "D\u00ebse Kont huet keen Toon Ecran.", - "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})" + "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})", + "unknown_authorize_url_generation": "Onbekannte Feeler beim erstellen vun der Authorisatiouns URL." }, "step": { "agreement": { diff --git a/homeassistant/components/toon/translations/tr.json b/homeassistant/components/toon/translations/tr.json new file mode 100644 index 00000000000..97765a99a7f --- /dev/null +++ b/homeassistant/components/toon/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Se\u00e7ilen anla\u015fma zaten yap\u0131land\u0131r\u0131lm\u0131\u015f.", + "unknown_authorize_url_generation": "Yetkilendirme url'si olu\u015fturulurken bilinmeyen hata." + }, + "step": { + "agreement": { + "data": { + "agreement": "Anla\u015fma" + }, + "description": "Eklemek istedi\u011finiz anla\u015fma adresini se\u00e7in.", + "title": "Anla\u015fman\u0131z\u0131 se\u00e7in" + }, + "pick_implementation": { + "title": "Kimlik do\u011frulamak i\u00e7in kirac\u0131n\u0131z\u0131 se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/uk.json b/homeassistant/components/toon/translations/uk.json new file mode 100644 index 00000000000..51aa28f3984 --- /dev/null +++ b/homeassistant/components/toon/translations/uk.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u041e\u0431\u0440\u0430\u043d\u0430 \u0443\u0433\u043e\u0434\u0430 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0430.", + "authorize_url_fail": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "no_agreements": "\u0423 \u0446\u044c\u043e\u043c\u0443 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441\u0456 \u043d\u0435\u043c\u0430\u0454 \u0434\u0438\u0441\u043f\u043b\u0435\u0457\u0432 Toon.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", + "unknown_authorize_url_generation": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457." + }, + "step": { + "agreement": { + "data": { + "agreement": "\u0423\u0433\u043e\u0434\u0430" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0430\u0434\u0440\u0435\u0441\u0443 \u0443\u0433\u043e\u0434\u0438, \u044f\u043a\u0443 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438.", + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0412\u0430\u0448\u0443 \u0443\u0433\u043e\u0434\u0443" + }, + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u0440\u0435\u043d\u0434\u0430\u0440\u044f \u0434\u043b\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/de.json b/homeassistant/components/totalconnect/translations/de.json index 25069635cca..530fef95af2 100644 --- a/homeassistant/components/totalconnect/translations/de.json +++ b/homeassistant/components/totalconnect/translations/de.json @@ -1,7 +1,10 @@ { "config": { "abort": { - "already_configured": "Konto bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { "user": { diff --git a/homeassistant/components/totalconnect/translations/tr.json b/homeassistant/components/totalconnect/translations/tr.json new file mode 100644 index 00000000000..f941db5ab89 --- /dev/null +++ b/homeassistant/components/totalconnect/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/uk.json b/homeassistant/components/totalconnect/translations/uk.json new file mode 100644 index 00000000000..f34a279d598 --- /dev/null +++ b/homeassistant/components/totalconnect/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "Total Connect" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/de.json b/homeassistant/components/tplink/translations/de.json index 64bdfc9bf77..48571158085 100644 --- a/homeassistant/components/tplink/translations/de.json +++ b/homeassistant/components/tplink/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Es wurden keine TP-Link-Ger\u00e4te im Netzwerk gefunden.", - "single_instance_allowed": "Es ist nur eine einzige Konfiguration erforderlich." + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { "confirm": { diff --git a/homeassistant/components/tplink/translations/tr.json b/homeassistant/components/tplink/translations/tr.json new file mode 100644 index 00000000000..e8f7a5aaf6d --- /dev/null +++ b/homeassistant/components/tplink/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "TP-Link ak\u0131ll\u0131 cihazlar\u0131 kurmak istiyor musunuz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/uk.json b/homeassistant/components/tplink/translations/uk.json new file mode 100644 index 00000000000..cfeaf049675 --- /dev/null +++ b/homeassistant/components/tplink/translations/uk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "step": { + "confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 TP-Link Smart Home?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/translations/de.json b/homeassistant/components/traccar/translations/de.json index 5d5969b2d51..7e253c1d05f 100644 --- a/homeassistant/components/traccar/translations/de.json +++ b/homeassistant/components/traccar/translations/de.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", + "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." + }, "create_entry": { - "default": "Um Ereignisse an den Heimassistenten zu senden, m\u00fcssen die Webhook-Funktionen in Traccar eingerichtet werden.\n\nVerwende die folgende URL: `{webhook_url}}`\n\nSiehe [die Dokumentation]( {docs_url} ) f\u00fcr weitere Details." + "default": "Um Ereignisse an den Heimassistenten zu senden, m\u00fcssen die Webhook-Funktionen in Traccar eingerichtet werden.\n\nVerwende die folgende URL: `{webhook_url}`\n\nSiehe [Dokumentation]({docs_url}) f\u00fcr weitere Details." }, "step": { "user": { diff --git a/homeassistant/components/traccar/translations/lb.json b/homeassistant/components/traccar/translations/lb.json index d7295252005..9e7d16fec3f 100644 --- a/homeassistant/components/traccar/translations/lb.json +++ b/homeassistant/components/traccar/translations/lb.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech." + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech.", + "webhook_not_internet_accessible": "Deng Home Assistant Instanz muss iwwert Internet accessibel si fir Webhook Noriichten z'empf\u00e4nken." }, "create_entry": { "default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, muss den Webhook Feature am Traccar ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider Informatiounen." diff --git a/homeassistant/components/traccar/translations/tr.json b/homeassistant/components/traccar/translations/tr.json index 7d044949a6e..9a2b1a119cd 100644 --- a/homeassistant/components/traccar/translations/tr.json +++ b/homeassistant/components/traccar/translations/tr.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + }, "step": { "user": { "title": "Traccar'\u0131 kur" diff --git a/homeassistant/components/traccar/translations/uk.json b/homeassistant/components/traccar/translations/uk.json new file mode 100644 index 00000000000..5bfb1714a79 --- /dev/null +++ b/homeassistant/components/traccar/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f Traccar. \n\n\u0414\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}` \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457" + }, + "step": { + "user": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Traccar?", + "title": "Traccar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/translations/de.json b/homeassistant/components/tradfri/translations/de.json index 3e55cb701d5..b1ebb2aff0b 100644 --- a/homeassistant/components/tradfri/translations/de.json +++ b/homeassistant/components/tradfri/translations/de.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "Bridge ist bereits konfiguriert.", - "already_in_progress": "Der Konfigurationsablauf f\u00fcr die Bridge wird bereits ausgef\u00fchrt." + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt" }, "error": { - "cannot_connect": "Verbindung zum Gateway nicht m\u00f6glich.", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_key": "Registrierung mit angegebenem Schl\u00fcssel fehlgeschlagen. Wenn dies weiterhin geschieht, starte den Gateway neu.", "timeout": "Timeout bei der \u00dcberpr\u00fcfung des Codes." }, diff --git a/homeassistant/components/tradfri/translations/tr.json b/homeassistant/components/tradfri/translations/tr.json new file mode 100644 index 00000000000..e4483536b12 --- /dev/null +++ b/homeassistant/components/tradfri/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "auth": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/translations/uk.json b/homeassistant/components/tradfri/translations/uk.json index a163a4680e3..abd25d04b6b 100644 --- a/homeassistant/components/tradfri/translations/uk.json +++ b/homeassistant/components/tradfri/translations/uk.json @@ -1,14 +1,22 @@ { "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454." + }, "error": { - "cannot_connect": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e \u0448\u043b\u044e\u0437\u0443." + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_key": "\u0427\u0438 \u043d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0437 \u0432\u043a\u0430\u0437\u0430\u043d\u0438\u043c \u043a\u043b\u044e\u0447\u0435\u043c. \u042f\u043a\u0449\u043e \u0446\u0435 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u044c\u0441\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0438\u0442\u0438 \u0448\u043b\u044e\u0437.", + "timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0438 \u043a\u043e\u0434\u0443." }, "step": { "auth": { "data": { + "host": "\u0425\u043e\u0441\u0442", "security_code": "\u041a\u043e\u0434 \u0431\u0435\u0437\u043f\u0435\u043a\u0438" }, - "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043e\u0434 \u0431\u0435\u0437\u043f\u0435\u043a\u0438" + "description": "\u041a\u043e\u0434 \u0431\u0435\u0437\u043f\u0435\u043a\u0438 \u043c\u043e\u0436\u043d\u0430 \u0437\u043d\u0430\u0439\u0442\u0438 \u043d\u0430 \u0437\u0430\u0434\u043d\u0456\u0439 \u043f\u0430\u043d\u0435\u043b\u0456 \u0448\u043b\u044e\u0437\u0443.", + "title": "IKEA TR\u00c5DFRI" } } } diff --git a/homeassistant/components/transmission/translations/de.json b/homeassistant/components/transmission/translations/de.json index a133cd363e0..2355905d1f7 100644 --- a/homeassistant/components/transmission/translations/de.json +++ b/homeassistant/components/transmission/translations/de.json @@ -1,10 +1,11 @@ { "config": { "abort": { - "already_configured": "Host ist bereits konfiguriert." + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung zum Host nicht m\u00f6glich", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "name_exists": "Name existiert bereits" }, "step": { diff --git a/homeassistant/components/transmission/translations/tr.json b/homeassistant/components/transmission/translations/tr.json new file mode 100644 index 00000000000..cffcc65151c --- /dev/null +++ b/homeassistant/components/transmission/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/uk.json b/homeassistant/components/transmission/translations/uk.json new file mode 100644 index 00000000000..5bc74f7da2a --- /dev/null +++ b/homeassistant/components/transmission/translations/uk.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "name_exists": "\u0426\u044f \u043d\u0430\u0437\u0432\u0430 \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "Transmission" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "limit": "\u041e\u0431\u043c\u0435\u0436\u0435\u043d\u043d\u044f", + "order": "\u041f\u043e\u0440\u044f\u0434\u043e\u043a", + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/de.json b/homeassistant/components/tuya/translations/de.json index 4cdcdfced79..67a61f81a1c 100644 --- a/homeassistant/components/tuya/translations/de.json +++ b/homeassistant/components/tuya/translations/de.json @@ -1,9 +1,13 @@ { "config": { "abort": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, "flow_title": "Tuya Konfiguration", "step": { "user": { @@ -11,7 +15,7 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "Geben Sie Ihre Tuya-Anmeldeinformationen ein.", + "description": "Gib deine Tuya-Anmeldeinformationen ein.", "title": "Tuya" } } diff --git a/homeassistant/components/tuya/translations/lb.json b/homeassistant/components/tuya/translations/lb.json index 884eb328fe4..0000f9ef6e6 100644 --- a/homeassistant/components/tuya/translations/lb.json +++ b/homeassistant/components/tuya/translations/lb.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "Feeler beim verbannen" + }, "error": { "dev_multi_type": "Multiple ausgewielte Ger\u00e4ter fir ze konfigur\u00e9ieren musse vum selwechten Typ sinn", "dev_not_config": "Typ vun Apparat net konfigur\u00e9ierbar", diff --git a/homeassistant/components/tuya/translations/tr.json b/homeassistant/components/tuya/translations/tr.json index 5a4de08033c..2edf3276b6c 100644 --- a/homeassistant/components/tuya/translations/tr.json +++ b/homeassistant/components/tuya/translations/tr.json @@ -1,11 +1,39 @@ { + "config": { + "abort": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "flow_title": "Tuya yap\u0131land\u0131rmas\u0131", + "step": { + "user": { + "data": { + "country_code": "Hesap \u00fclke kodunuz (\u00f6r. ABD i\u00e7in 1 veya \u00c7in i\u00e7in 86)", + "password": "Parola", + "platform": "Hesab\u0131n\u0131z\u0131n kay\u0131tl\u0131 oldu\u011fu uygulama", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Tuya kimlik bilgilerinizi girin.", + "title": "Tuya" + } + } + }, "options": { "abort": { "cannot_connect": "Ba\u011flanma hatas\u0131" }, + "error": { + "dev_not_config": "Cihaz t\u00fcr\u00fc yap\u0131land\u0131r\u0131lamaz", + "dev_not_found": "Cihaz bulunamad\u0131" + }, "step": { "device": { "data": { + "brightness_range_mode": "Cihaz\u0131n kulland\u0131\u011f\u0131 parlakl\u0131k aral\u0131\u011f\u0131", "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)", diff --git a/homeassistant/components/tuya/translations/uk.json b/homeassistant/components/tuya/translations/uk.json new file mode 100644 index 00000000000..1d2709d260a --- /dev/null +++ b/homeassistant/components/tuya/translations/uk.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "flow_title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Tuya", + "step": { + "user": { + "data": { + "country_code": "\u041a\u043e\u0434 \u043a\u0440\u0430\u0457\u043d\u0438 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 (1 \u0434\u043b\u044f \u0421\u0428\u0410 \u0430\u0431\u043e 86 \u0434\u043b\u044f \u041a\u0438\u0442\u0430\u044e)", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "platform": "\u0414\u043e\u0434\u0430\u0442\u043e\u043a, \u0432 \u044f\u043a\u043e\u043c\u0443 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u043e\u0432\u0430\u043d\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Tuya.", + "title": "Tuya" + } + } + }, + "options": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "error": { + "dev_multi_type": "\u041a\u0456\u043b\u044c\u043a\u0430 \u043e\u0431\u0440\u0430\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0431\u0443\u0442\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0442\u0438\u043f\u0443.", + "dev_not_config": "\u0422\u0438\u043f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f.", + "dev_not_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e." + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "\u0414\u0456\u0430\u043f\u0430\u0437\u043e\u043d \u044f\u0441\u043a\u0440\u0430\u0432\u043e\u0441\u0442\u0456, \u044f\u043a\u0438\u0439 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c", + "curr_temp_divider": "\u0414\u0456\u043b\u044c\u043d\u0438\u043a \u043f\u043e\u0442\u043e\u0447\u043d\u043e\u0433\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0438 (0 = \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c)", + "max_kelvin": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0430 \u043a\u043e\u043b\u0456\u0440\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0432 \u043a\u0435\u043b\u044c\u0432\u0456\u043d\u0430\u0445)", + "max_temp": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430 \u0446\u0456\u043b\u044c\u043e\u0432\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 min \u0456 max = 0)", + "min_kelvin": "\u041c\u0456\u043d\u0456\u043c\u0430\u043b\u044c\u043d\u0430 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0430 \u043a\u043e\u043b\u0456\u0440\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0432 \u043a\u0435\u043b\u044c\u0432\u0456\u043d\u0430\u0445)", + "min_temp": "\u041c\u0456\u043d\u0456\u043c\u0430\u043b\u044c\u043d\u0430 \u0446\u0456\u043b\u044c\u043e\u0432\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 min \u0456 max = 0)", + "support_color": "\u041f\u0440\u0438\u043c\u0443\u0441\u043e\u0432\u0430 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u043a\u0430 \u043a\u043e\u043b\u044c\u043e\u0440\u0443", + "temp_divider": "\u0414\u0456\u043b\u044c\u043d\u0438\u043a \u0437\u043d\u0430\u0447\u0435\u043d\u044c \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0438 (0 = \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c)", + "tuya_max_coltemp": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430 \u043a\u043e\u043b\u0456\u0440\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430, \u044f\u043a\u0430 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u044f\u0454\u0442\u044c\u0441\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c", + "unit_of_measurement": "\u041e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u0443 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0438, \u044f\u043a\u0430 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c" + }, + "description": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u0434\u0438\u043c\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u0434\u043b\u044f {device_type} \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e '{device_name}'", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Tuya" + }, + "init": { + "data": { + "discovery_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", + "list_devices": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0430\u0431\u043e \u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c \u0434\u043b\u044f \u0437\u0431\u0435\u0440\u0435\u0436\u0435\u043d\u043d\u044f \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457", + "query_device": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u044f\u043a\u0438\u0439 \u0431\u0443\u0434\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430\u043f\u0438\u0442\u0443 \u0434\u043b\u044f \u0431\u0456\u043b\u044c\u0448 \u0448\u0432\u0438\u0434\u043a\u043e\u0433\u043e \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0441\u0442\u0430\u0442\u0443\u0441\u0443", + "query_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + }, + "description": "\u041d\u0435 \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u044e\u0439\u0442\u0435 \u0437\u0430\u043d\u0430\u0434\u0442\u043e \u043d\u0438\u0437\u044c\u043a\u0456 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0456\u043d\u0442\u0435\u0440\u0432\u0430\u043b\u0443 \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f, \u0456\u043d\u0430\u043a\u0448\u0435 \u0432\u0438\u043a\u043b\u0438\u043a\u0438 \u043d\u0435 \u0431\u0443\u0434\u0443\u0442\u044c \u0433\u0435\u043d\u0435\u0440\u0443\u0432\u0430\u0442\u0438 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u043e \u043f\u043e\u043c\u0438\u043b\u043a\u0443 \u0432 \u0436\u0443\u0440\u043d\u0430\u043b\u0456.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/translations/de.json b/homeassistant/components/twentemilieu/translations/de.json index 27ba9bb29c7..38cabb6c22e 100644 --- a/homeassistant/components/twentemilieu/translations/de.json +++ b/homeassistant/components/twentemilieu/translations/de.json @@ -1,7 +1,10 @@ { "config": { + "abort": { + "already_configured": "Standort ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_address": "Adresse nicht im Einzugsgebiet von Twente Milieu gefunden." }, "step": { diff --git a/homeassistant/components/twentemilieu/translations/tr.json b/homeassistant/components/twentemilieu/translations/tr.json new file mode 100644 index 00000000000..590aec1894c --- /dev/null +++ b/homeassistant/components/twentemilieu/translations/tr.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/translations/uk.json b/homeassistant/components/twentemilieu/translations/uk.json new file mode 100644 index 00000000000..435bd79fb85 --- /dev/null +++ b/homeassistant/components/twentemilieu/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_address": "\u0410\u0434\u0440\u0435\u0441\u0443 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0432 \u0437\u043e\u043d\u0456 \u043e\u0431\u0441\u043b\u0443\u0433\u043e\u0432\u0443\u0432\u0430\u043d\u043d\u044f Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "\u0414\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f \u0434\u043e \u043d\u043e\u043c\u0435\u0440\u0443 \u0434\u043e\u043c\u0443", + "house_number": "\u041d\u043e\u043c\u0435\u0440 \u0431\u0443\u0434\u0438\u043d\u043a\u0443", + "post_code": "\u041f\u043e\u0448\u0442\u043e\u0432\u0438\u0439 \u0456\u043d\u0434\u0435\u043a\u0441" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Twente Milieu \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0432\u0438\u0432\u0435\u0437\u0435\u043d\u043d\u044f \u0441\u043c\u0456\u0442\u0442\u044f \u0437\u0430 \u0412\u0430\u0448\u043e\u044e \u0430\u0434\u0440\u0435\u0441\u043e\u044e.", + "title": "Twente Milieu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/translations/de.json b/homeassistant/components/twilio/translations/de.json index 864fee4c238..61df22c10f8 100644 --- a/homeassistant/components/twilio/translations/de.json +++ b/homeassistant/components/twilio/translations/de.json @@ -1,11 +1,15 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", + "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." + }, "create_entry": { - "default": "Um Ereignisse an den Home Assistant zu senden, musst du [Webhooks mit Twilio]({twilio_url}) einrichten. \n\n F\u00fclle die folgenden Informationen aus: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhaltstyp: application / x-www-form-urlencoded \n\nLies in der [Dokumentation]({docs_url}) wie du Automationen f\u00fcr die Verarbeitung eingehender Daten konfigurierst." + "default": "Um Ereignisse an Home Assistant zu senden, musst du [Webhooks mit Twilio]({twilio_url}) einrichten. \n\n F\u00fclle die folgenden Informationen aus: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhaltstyp: application / x-www-form-urlencoded \n\nLies in der [Dokumentation]({docs_url}), wie du Automationen f\u00fcr die Verarbeitung eingehender Daten konfigurierst." }, "step": { "user": { - "description": "M\u00f6chtest du Twilio wirklich einrichten?", + "description": "M\u00f6chtest du mit der Einrichtung beginnen?", "title": "Twilio-Webhook einrichten" } } diff --git a/homeassistant/components/twilio/translations/lb.json b/homeassistant/components/twilio/translations/lb.json index 2721402c1f3..7889f244c6e 100644 --- a/homeassistant/components/twilio/translations/lb.json +++ b/homeassistant/components/twilio/translations/lb.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech." + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech.", + "webhook_not_internet_accessible": "Deng Home Assistant Instanz muss iwwert Internet accessibel si fir Webhook Noriichten z'empf\u00e4nken." }, "create_entry": { "default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, mussen [Webhooks mat Twilio]({twilio_url}) ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nLiest [Dokumentatioun]({docs_url}) w\u00e9i een Automatiounen ariicht welch eingehend Donn\u00e9\u00eb trait\u00e9ieren." diff --git a/homeassistant/components/twilio/translations/tr.json b/homeassistant/components/twilio/translations/tr.json new file mode 100644 index 00000000000..84adcdf8225 --- /dev/null +++ b/homeassistant/components/twilio/translations/tr.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/translations/uk.json b/homeassistant/components/twilio/translations/uk.json new file mode 100644 index 00000000000..8ea0ce86a37 --- /dev/null +++ b/homeassistant/components/twilio/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f [Twilio]({twilio_url}). \n\n\u0417\u0430\u043f\u043e\u0432\u043d\u0456\u0442\u044c \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0456\u0439 \u043f\u043e \u043e\u0431\u0440\u043e\u0431\u0446\u0456 \u0434\u0430\u043d\u0438\u0445, \u0449\u043e \u043d\u0430\u0434\u0445\u043e\u0434\u044f\u0442\u044c." + }, + "step": { + "user": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?", + "title": "Twilio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/de.json b/homeassistant/components/twinkly/translations/de.json index 2b4c70a0bad..c196f53262d 100644 --- a/homeassistant/components/twinkly/translations/de.json +++ b/homeassistant/components/twinkly/translations/de.json @@ -1,7 +1,10 @@ { "config": { + "abort": { + "device_exists": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/twinkly/translations/fr.json b/homeassistant/components/twinkly/translations/fr.json new file mode 100644 index 00000000000..5071b7e302a --- /dev/null +++ b/homeassistant/components/twinkly/translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "device_exists": "D\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Connexion impossible" + }, + "step": { + "user": { + "data": { + "host": "Nom r\u00e9seau (ou adresse IP) de votre Twinkly" + }, + "description": "Configurer votre Twinkly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/lb.json b/homeassistant/components/twinkly/translations/lb.json new file mode 100644 index 00000000000..2e00a8ae4db --- /dev/null +++ b/homeassistant/components/twinkly/translations/lb.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "device_exists": "Apparat ass scho konfigur\u00e9iert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/tr.json b/homeassistant/components/twinkly/translations/tr.json index 14365f988bd..d2e7173dad3 100644 --- a/homeassistant/components/twinkly/translations/tr.json +++ b/homeassistant/components/twinkly/translations/tr.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "device_exists": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/twinkly/translations/uk.json b/homeassistant/components/twinkly/translations/uk.json new file mode 100644 index 00000000000..bd256d31b03 --- /dev/null +++ b/homeassistant/components/twinkly/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "device_exists": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "host": "\u0406\u043c'\u044f \u0445\u043e\u0441\u0442\u0430 (\u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430) \u0412\u0430\u0448\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Twinkly" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0441\u0432\u0456\u0442\u043b\u043e\u0434\u0456\u043e\u0434\u043d\u043e\u0457 \u0441\u0442\u0440\u0456\u0447\u043a\u0438 Twinkly", + "title": "Twinkly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/ca.json b/homeassistant/components/unifi/translations/ca.json index a07c034fe12..f1cf4a6349b 100644 --- a/homeassistant/components/unifi/translations/ca.json +++ b/homeassistant/components/unifi/translations/ca.json @@ -1,13 +1,16 @@ { "config": { "abort": { - "already_configured": "El lloc del controlador ja est\u00e0 configurat" + "already_configured": "El lloc del controlador ja est\u00e0 configurat", + "configuration_updated": "S'ha actualitzat la configuraci\u00f3.", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "faulty_credentials": "[%key::common::config_flow::error::invalid_auth%]", "service_unavailable": "[%key::common::config_flow::error::cannot_connect%]", "unknown_client_mac": "No hi ha cap client disponible en aquesta adre\u00e7a MAC" }, + "flow_title": "Xarxa UniFi {site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/cs.json b/homeassistant/components/unifi/translations/cs.json index 1247a97de9d..0281dfbb750 100644 --- a/homeassistant/components/unifi/translations/cs.json +++ b/homeassistant/components/unifi/translations/cs.json @@ -1,13 +1,15 @@ { "config": { "abort": { - "already_configured": "Ovlada\u010d je ji\u017e nastaven" + "already_configured": "Ovlada\u010d je ji\u017e nastaven", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "faulty_credentials": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "service_unavailable": "Nepoda\u0159ilo se p\u0159ipojit", "unknown_client_mac": "Na t\u00e9to MAC adrese nen\u00ed dostupn\u00fd \u017e\u00e1dn\u00fd klient" }, + "flow_title": "UniFi s\u00ed\u0165 {site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/da.json b/homeassistant/components/unifi/translations/da.json index 15ec878f1ce..84dafd36e1a 100644 --- a/homeassistant/components/unifi/translations/da.json +++ b/homeassistant/components/unifi/translations/da.json @@ -7,6 +7,7 @@ "faulty_credentials": "Ugyldige legitimationsoplysninger", "service_unavailable": "Service utilg\u00e6ngelig" }, + "flow_title": "UniFi-netv\u00e6rket {site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/de.json b/homeassistant/components/unifi/translations/de.json index 626236792ea..be38ddf1a4d 100644 --- a/homeassistant/components/unifi/translations/de.json +++ b/homeassistant/components/unifi/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Controller-Site ist bereits konfiguriert" }, "error": { - "faulty_credentials": "Ung\u00fcltige Anmeldeinformationen", + "faulty_credentials": "Ung\u00fcltige Authentifizierung", "service_unavailable": "Verbindung fehlgeschlagen", "unknown_client_mac": "Unter dieser MAC-Adresse ist kein Client verf\u00fcgbar." }, @@ -16,7 +16,7 @@ "port": "Port", "site": "Site-ID", "username": "Benutzername", - "verify_ssl": "Controller mit ordnungsgem\u00e4ssem Zertifikat" + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, "title": "UniFi-Controller einrichten" } @@ -51,7 +51,9 @@ }, "simple_options": { "data": { - "track_clients": "Netzwerk Ger\u00e4te \u00fcberwachen" + "block_client": "Clients mit Netzwerkzugriffskontrolle", + "track_clients": "Netzwerger\u00e4te \u00fcberwachen", + "track_devices": "Verfolgen von Netzwerkger\u00e4ten (Ubiquiti-Ger\u00e4te)" }, "description": "Konfigurieren Sie die UniFi-Integration" }, diff --git a/homeassistant/components/unifi/translations/en.json b/homeassistant/components/unifi/translations/en.json index 06e8ae1eb60..41507faa430 100644 --- a/homeassistant/components/unifi/translations/en.json +++ b/homeassistant/components/unifi/translations/en.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Controller site is already configured", + "configuration_updated": "Configuration updated.", "reauth_successful": "Re-authentication was successful" }, "error": { diff --git a/homeassistant/components/unifi/translations/es.json b/homeassistant/components/unifi/translations/es.json index 0fa4aaf2eb7..a676d70e88c 100644 --- a/homeassistant/components/unifi/translations/es.json +++ b/homeassistant/components/unifi/translations/es.json @@ -1,13 +1,15 @@ { "config": { "abort": { - "already_configured": "El sitio del controlador ya est\u00e1 configurado" + "already_configured": "El sitio del controlador ya est\u00e1 configurado", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "faulty_credentials": "Autenticaci\u00f3n no v\u00e1lida", "service_unavailable": "Error al conectar", "unknown_client_mac": "Ning\u00fan cliente disponible en esa direcci\u00f3n MAC" }, + "flow_title": "Red UniFi {site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/et.json b/homeassistant/components/unifi/translations/et.json index 8e95da9aa5b..e9d76520435 100644 --- a/homeassistant/components/unifi/translations/et.json +++ b/homeassistant/components/unifi/translations/et.json @@ -1,13 +1,16 @@ { "config": { "abort": { - "already_configured": "Kontroller on juba seadistatud" + "already_configured": "Kontroller on juba seadistatud", + "configuration_updated": "Seaded on v\u00e4rskendatud.", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "faulty_credentials": "Tuvastamine nurjus", "service_unavailable": "\u00dchendamine nurjus", "unknown_client_mac": "Sellel MAC-aadressil pole \u00fchtegi klienti saadaval" }, + "flow_title": "UniFi Network {site} ( {host} )", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/it.json b/homeassistant/components/unifi/translations/it.json index 79a7206923e..d50018227c5 100644 --- a/homeassistant/components/unifi/translations/it.json +++ b/homeassistant/components/unifi/translations/it.json @@ -1,13 +1,16 @@ { "config": { "abort": { - "already_configured": "Il sito del Controller \u00e8 gi\u00e0 configurato" + "already_configured": "Il sito del Controller \u00e8 gi\u00e0 configurato", + "configuration_updated": "Configurazione aggiornata.", + "reauth_successful": "La riautenticazione ha avuto successo" }, "error": { "faulty_credentials": "Autenticazione non valida", "service_unavailable": "Impossibile connettersi", "unknown_client_mac": "Nessun client disponibile su quell'indirizzo MAC" }, + "flow_title": "Rete UniFi {site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/no.json b/homeassistant/components/unifi/translations/no.json index 5cda9ad7ab5..72944a9d540 100644 --- a/homeassistant/components/unifi/translations/no.json +++ b/homeassistant/components/unifi/translations/no.json @@ -1,13 +1,16 @@ { "config": { "abort": { - "already_configured": "Kontroller nettstedet er allerede konfigurert" + "already_configured": "Kontroller nettstedet er allerede konfigurert", + "configuration_updated": "Konfigurasjonen er oppdatert.", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "faulty_credentials": "Ugyldig godkjenning", "service_unavailable": "Tilkobling mislyktes", "unknown_client_mac": "Ingen klient tilgjengelig p\u00e5 den MAC-adressen" }, + "flow_title": "UniFi-nettverk {site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/pl.json b/homeassistant/components/unifi/translations/pl.json index 8ff5f1e4793..6c8c74e726a 100644 --- a/homeassistant/components/unifi/translations/pl.json +++ b/homeassistant/components/unifi/translations/pl.json @@ -1,13 +1,16 @@ { "config": { "abort": { - "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "configuration_updated": "Konfiguracja zaktualizowana", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "faulty_credentials": "Niepoprawne uwierzytelnienie", "service_unavailable": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "unknown_client_mac": "Brak klienta z tym adresem MAC" }, + "flow_title": "Sie\u0107 UniFi {site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/ru.json b/homeassistant/components/unifi/translations/ru.json index 789212dca17..3b69bf0ee33 100644 --- a/homeassistant/components/unifi/translations/ru.json +++ b/homeassistant/components/unifi/translations/ru.json @@ -1,13 +1,15 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "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": { "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", "service_unavailable": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "unknown_client_mac": "\u041d\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043d\u0430 \u044d\u0442\u043e\u043c MAC-\u0430\u0434\u0440\u0435\u0441\u0435." }, + "flow_title": "UniFi Network {site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/tr.json b/homeassistant/components/unifi/translations/tr.json index 903a7aaa21f..c39fa08217a 100644 --- a/homeassistant/components/unifi/translations/tr.json +++ b/homeassistant/components/unifi/translations/tr.json @@ -1,9 +1,22 @@ { "config": { + "abort": { + "already_configured": "Denetleyici sitesi zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "configuration_updated": "Yap\u0131land\u0131rma g\u00fcncellendi.", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "faulty_credentials": "Ge\u00e7ersiz kimlik do\u011frulama", + "service_unavailable": "Ba\u011flanma hatas\u0131", + "unknown_client_mac": "Bu MAC adresinde kullan\u0131labilir istemci yok" + }, + "flow_title": "UniFi A\u011f\u0131 {site} ( {host} )", "step": { "user": { "data": { + "host": "Ana Bilgisayar", "password": "Parola", + "port": "Port", "username": "Kullan\u0131c\u0131 ad\u0131" } } diff --git a/homeassistant/components/unifi/translations/uk.json b/homeassistant/components/unifi/translations/uk.json new file mode 100644 index 00000000000..0f83c35840a --- /dev/null +++ b/homeassistant/components/unifi/translations/uk.json @@ -0,0 +1,66 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + }, + "error": { + "faulty_credentials": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "service_unavailable": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown_client_mac": "\u041d\u0435\u043c\u0430\u0454 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432 \u043d\u0430 \u0446\u0456\u0439 MAC-\u0430\u0434\u0440\u0435\u0441\u0456." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "site": "ID \u0441\u0430\u0439\u0442\u0443", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" + }, + "title": "UniFi Controller" + } + } + }, + "options": { + "step": { + "client_control": { + "data": { + "block_client": "\u041a\u043b\u0456\u0454\u043d\u0442\u0438 \u0437 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u043c \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0443", + "dpi_restrictions": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f \u0433\u0440\u0443\u043f\u0430\u043c\u0438 \u043e\u0431\u043c\u0435\u0436\u0435\u043d\u044c DPI", + "poe_clients": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 POE \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044c \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0435\u043b\u0435\u043c\u0435\u043d\u0442\u0456\u0432 \u0443\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f. \n\n\u0421\u0442\u0432\u043e\u0440\u0456\u0442\u044c \u043f\u0435\u0440\u0435\u043c\u0438\u043a\u0430\u0447\u0456 \u0434\u043b\u044f \u0441\u0435\u0440\u0456\u0439\u043d\u0438\u0445 \u043d\u043e\u043c\u0435\u0440\u0456\u0432, \u0434\u043b\u044f \u044f\u043a\u0438\u0445 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044e\u0432\u0430\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u043c\u0435\u0440\u0435\u0436\u0456.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f UniFi. \u041a\u0440\u043e\u043a 2." + }, + "device_tracker": { + "data": { + "detection_time": "\u0427\u0430\u0441 \u0432\u0456\u0434 \u043e\u0441\u0442\u0430\u043d\u043d\u044c\u043e\u0433\u043e \u0441\u0435\u0430\u043d\u0441\u0443 \u0437\u0432'\u044f\u0437\u043a\u0443 \u0437 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c (\u0441\u0435\u043a.), \u043f\u043e \u0437\u0430\u043a\u0456\u043d\u0447\u0435\u043d\u043d\u044e \u044f\u043a\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043e\u0442\u0440\u0438\u043c\u0430\u0454 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u041d\u0435 \u0432\u0434\u043e\u043c\u0430\".", + "ignore_wired_bug": "\u0412\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043b\u043e\u0433\u0456\u043a\u0443 \u043f\u043e\u043c\u0438\u043b\u043a\u0438 \u0434\u043b\u044f \u0434\u0440\u043e\u0442\u043e\u0432\u0438\u0445 \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432 UniFi", + "ssid_filter": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c SSID \u0434\u043b\u044f \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f \u0431\u0435\u0437\u0434\u0440\u043e\u0442\u043e\u0432\u0438\u0445 \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432", + "track_clients": "\u0412\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432 \u043c\u0435\u0440\u0435\u0436\u0456", + "track_devices": "\u0412\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 (\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 Ubiquiti)", + "track_wired_clients": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043f\u0440\u043e\u0432\u0456\u0434\u043d\u0438\u0445 \u043c\u0435\u0440\u0435\u0436\u043d\u0438\u0445 \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f UniFi. \u041a\u0440\u043e\u043a 1" + }, + "simple_options": { + "data": { + "block_client": "\u041a\u043b\u0456\u0454\u043d\u0442\u0438 \u0437 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u043c \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0443", + "track_clients": "\u0412\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432 \u043c\u0435\u0440\u0435\u0436\u0456", + "track_devices": "\u0412\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 (\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 Ubiquiti)" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 UniFi." + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "\u0414\u0430\u0442\u0447\u0438\u043a\u0438 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u043d\u043e\u0457 \u0437\u0434\u0430\u0442\u043d\u043e\u0441\u0442\u0456 \u0434\u043b\u044f \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u0438\u0445 \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432", + "allow_uptime_sensors": "\u0421\u0435\u043d\u0441\u043e\u0440\u0438 \u0447\u0430\u0441\u0443 \u0440\u043e\u0431\u043e\u0442\u0438 \u0434\u043b\u044f \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u0438\u0445 \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0441\u0435\u043d\u0441\u043e\u0440\u0456\u0432 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0438", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f UniFi. \u043a\u0440\u043e\u043a 3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/zh-Hant.json b/homeassistant/components/unifi/translations/zh-Hant.json index d87f8cf51e0..add0a387309 100644 --- a/homeassistant/components/unifi/translations/zh-Hant.json +++ b/homeassistant/components/unifi/translations/zh-Hant.json @@ -1,13 +1,16 @@ { "config": { "abort": { - "already_configured": "\u63a7\u5236\u5668\u4f4d\u5740\u5df2\u7d93\u8a2d\u5b9a" + "already_configured": "\u63a7\u5236\u5668\u4f4d\u5740\u5df2\u7d93\u8a2d\u5b9a", + "configuration_updated": "\u8a2d\u5b9a\u5df2\u66f4\u65b0\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "faulty_credentials": "\u9a57\u8b49\u78bc\u7121\u6548", "service_unavailable": "\u9023\u7dda\u5931\u6557", "unknown_client_mac": "\u8a72 Mac \u4f4d\u5740\u7121\u53ef\u7528\u5ba2\u6236\u7aef" }, + "flow_title": "UniFi Network {site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/upb/translations/de.json b/homeassistant/components/upb/translations/de.json index ea6f1d37150..908db20f22b 100644 --- a/homeassistant/components/upb/translations/de.json +++ b/homeassistant/components/upb/translations/de.json @@ -1,9 +1,12 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Fehler beim Herstellen einer Verbindung zu UPB PIM. Versuchen Sie es erneut.", - "invalid_upb_file": "Fehlende oder ung\u00fcltige UPB UPStart-Exportdatei, \u00fcberpr\u00fcfen Sie den Namen und den Pfad der Datei.", - "unknown": "Unerwarteter Fehler." + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_upb_file": "Fehlende oder ung\u00fcltige UPB UPStart-Exportdatei, \u00fcberpr\u00fcfe den Namen und den Pfad der Datei.", + "unknown": "Unerwarteter Fehler" }, "step": { "user": { @@ -12,7 +15,7 @@ "file_path": "Pfad und Name der UPStart UPB-Exportdatei.", "protocol": "Protokoll" }, - "title": "Stellen Sie eine Verbindung zu UPB PIM her" + "title": "Stelle eine Verbindung zu UPB PIM her" } } } diff --git a/homeassistant/components/upb/translations/tr.json b/homeassistant/components/upb/translations/tr.json new file mode 100644 index 00000000000..818531fcaa0 --- /dev/null +++ b/homeassistant/components/upb/translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/uk.json b/homeassistant/components/upb/translations/uk.json new file mode 100644 index 00000000000..062503848a8 --- /dev/null +++ b/homeassistant/components/upb/translations/uk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_upb_file": "\u0412\u0456\u0434\u0441\u0443\u0442\u043d\u0456\u0439 \u0430\u0431\u043e \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439 \u0444\u0430\u0439\u043b \u0435\u043a\u0441\u043f\u043e\u0440\u0442\u0443 UPB UPStart, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0456\u043c'\u044f \u0456 \u0448\u043b\u044f\u0445 \u0434\u043e \u0444\u0430\u0439\u043b\u0443.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "address": "\u0410\u0434\u0440\u0435\u0441\u0430 (\u0434\u0438\u0432. \u043e\u043f\u0438\u0441 \u0432\u0438\u0449\u0435)", + "file_path": "\u0428\u043b\u044f\u0445 \u0456 \u0456\u043c'\u044f \u0444\u0430\u0439\u043b\u0443 \u0435\u043a\u0441\u043f\u043e\u0440\u0442\u0443 UPStart UPB.", + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b" + }, + "description": "\u0420\u044f\u0434\u043e\u043a \u0430\u0434\u0440\u0435\u0441\u0438 \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'address[:port]' \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 'tcp' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: '192.168.1.42'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'port' \u0432\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e, \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0456\u043d \u0434\u043e\u0440\u0456\u0432\u043d\u044e\u0454 2101. \u0414\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 'serial' \u0430\u0434\u0440\u0435\u0441\u0430 \u043f\u043e\u0432\u0438\u043d\u043d\u0430 \u0431\u0443\u0442\u0438 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'tty[:baud]' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: '/dev/ttyS1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'baud' \u0432\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e, \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0456\u043d \u0434\u043e\u0440\u0456\u0432\u043d\u044e\u0454 4800.", + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e UPB PIM" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upcloud/translations/de.json b/homeassistant/components/upcloud/translations/de.json index 76bbc705690..ee1802f1d38 100644 --- a/homeassistant/components/upcloud/translations/de.json +++ b/homeassistant/components/upcloud/translations/de.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { "user": { diff --git a/homeassistant/components/upcloud/translations/tr.json b/homeassistant/components/upcloud/translations/tr.json new file mode 100644 index 00000000000..f1840698493 --- /dev/null +++ b/homeassistant/components/upcloud/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upcloud/translations/uk.json b/homeassistant/components/upcloud/translations/uk.json new file mode 100644 index 00000000000..bf8781c1eb2 --- /dev/null +++ b/homeassistant/components/upcloud/translations/uk.json @@ -0,0 +1,25 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445, \u043c\u0456\u043d\u0456\u043c\u0443\u043c 30)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/ro.json b/homeassistant/components/upnp/translations/ro.json index ceb1c19131a..2fd83a0b371 100644 --- a/homeassistant/components/upnp/translations/ro.json +++ b/homeassistant/components/upnp/translations/ro.json @@ -7,6 +7,13 @@ "few": "", "one": "Unul", "other": "" + }, + "step": { + "init": { + "few": "Pu\u021bine", + "one": "Unul", + "other": "Altele" + } } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/tr.json b/homeassistant/components/upnp/translations/tr.json new file mode 100644 index 00000000000..2715f66e090 --- /dev/null +++ b/homeassistant/components/upnp/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "flow_title": "UPnP / IGD: {name}", + "step": { + "ssdp_confirm": { + "description": "Bu UPnP / IGD cihaz\u0131n\u0131 kurmak istiyor musunuz?" + }, + "user": { + "data": { + "scan_interval": "G\u00fcncelleme aral\u0131\u011f\u0131 (saniye, minimum 30)", + "usn": "Cihaz" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/uk.json b/homeassistant/components/upnp/translations/uk.json index 0b8747f902e..905958eeca9 100644 --- a/homeassistant/components/upnp/translations/uk.json +++ b/homeassistant/components/upnp/translations/uk.json @@ -1,7 +1,21 @@ { "config": { "abort": { - "already_configured": "UPnP/IGD \u0432\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0454\u043d\u043e" + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "incomplete_discovery": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043f\u0440\u043e\u0446\u0435\u0441.", + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456." + }, + "flow_title": "UPnP/IGD: {name}", + "step": { + "ssdp_confirm": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0446\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 UPnP / IGD?" + }, + "user": { + "data": { + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445, \u043c\u0456\u043d\u0456\u043c\u0443\u043c 30)", + "usn": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/vacuum/translations/de.json b/homeassistant/components/vacuum/translations/de.json index be137a5566b..8de386b3506 100644 --- a/homeassistant/components/vacuum/translations/de.json +++ b/homeassistant/components/vacuum/translations/de.json @@ -18,7 +18,7 @@ "cleaning": "Reinigen", "docked": "Angedockt", "error": "Fehler", - "idle": "Standby", + "idle": "Unt\u00e4tig", "off": "Aus", "on": "An", "paused": "Pausiert", diff --git a/homeassistant/components/vacuum/translations/uk.json b/homeassistant/components/vacuum/translations/uk.json index 9febc8aff1f..64223a85f74 100644 --- a/homeassistant/components/vacuum/translations/uk.json +++ b/homeassistant/components/vacuum/translations/uk.json @@ -1,4 +1,18 @@ { + "device_automation": { + "action_type": { + "clean": "\u0412\u0456\u0434\u043f\u0440\u0430\u0432\u0438\u0442\u0438 {entity_name} \u0440\u043e\u0431\u0438\u0442\u0438 \u043f\u0440\u0438\u0431\u0438\u0440\u0430\u043d\u043d\u044f", + "dock": "{entity_name}: \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u0442\u0438 \u043d\u0430 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0456\u044e" + }, + "condition_type": { + "is_cleaning": "{entity_name} \u0432\u0438\u043a\u043e\u043d\u0443\u0454 \u043f\u0440\u0438\u0431\u0438\u0440\u0430\u043d\u043d\u044f", + "is_docked": "{entity_name} \u043d\u0430 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0456\u0457" + }, + "trigger_type": { + "cleaning": "{entity_name} \u043f\u043e\u0447\u0438\u043d\u0430\u0454 \u043f\u0440\u0438\u0431\u0438\u0440\u0430\u043d\u043d\u044f", + "docked": "{entity_name} \u0441\u0442\u0438\u043a\u0443\u0454\u0442\u044c\u0441\u044f \u0437 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0456\u0454\u044e" + } + }, "state": { "_": { "cleaning": "\u041f\u0440\u0438\u0431\u0438\u0440\u0430\u043d\u043d\u044f", @@ -8,7 +22,7 @@ "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e", "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e", "paused": "\u041f\u0440\u0438\u0437\u0443\u043f\u0438\u043d\u0435\u043d\u043e", - "returning": "\u041f\u043e\u0432\u0435\u0440\u043d\u0435\u043d\u043d\u044f \u0434\u043e \u0434\u043e\u043a\u0430" + "returning": "\u041f\u043e\u0432\u0435\u0440\u043d\u0435\u043d\u043d\u044f \u043d\u0430 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0456\u044e" } }, "title": "\u041f\u0438\u043b\u043e\u0441\u043e\u0441" diff --git a/homeassistant/components/velbus/translations/de.json b/homeassistant/components/velbus/translations/de.json index c6c872c85e6..9bbb23b1bcd 100644 --- a/homeassistant/components/velbus/translations/de.json +++ b/homeassistant/components/velbus/translations/de.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/velbus/translations/tr.json b/homeassistant/components/velbus/translations/tr.json new file mode 100644 index 00000000000..e7ee4ea7157 --- /dev/null +++ b/homeassistant/components/velbus/translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/translations/uk.json b/homeassistant/components/velbus/translations/uk.json new file mode 100644 index 00000000000..6e8b97cc457 --- /dev/null +++ b/homeassistant/components/velbus/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430", + "port": "\u0420\u044f\u0434\u043e\u043a \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + }, + "title": "Velbus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vera/translations/tr.json b/homeassistant/components/vera/translations/tr.json new file mode 100644 index 00000000000..35e81599bb1 --- /dev/null +++ b/homeassistant/components/vera/translations/tr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "cannot_connect": "{base_url} url'si ile denetleyiciye ba\u011flan\u0131lamad\u0131" + } + }, + "options": { + "step": { + "init": { + "title": "Vera denetleyici se\u00e7enekleri" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vera/translations/uk.json b/homeassistant/components/vera/translations/uk.json new file mode 100644 index 00000000000..8c591a1cc10 --- /dev/null +++ b/homeassistant/components/vera/translations/uk.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u043e\u043c \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e {base_url}." + }, + "step": { + "user": { + "data": { + "exclude": "ID \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 Vera, \u0434\u043b\u044f \u0432\u0438\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0437 Home Assistant", + "lights": "ID \u0432\u0438\u043c\u0438\u043a\u0430\u0447\u0456\u0432 Vera, \u0434\u043b\u044f \u0456\u043c\u043f\u043e\u0440\u0442\u0443 \u0432 \u043e\u0441\u0432\u0456\u0442\u043b\u0435\u043d\u043d\u044f", + "vera_controller_url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430" + }, + "description": "\u0412\u043a\u0430\u0436\u0456\u0442\u044c URL-\u0430\u0434\u0440\u0435\u0441\u0443 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'address[:port]' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: 'http://192.168.1.161:3480').", + "title": "Vera" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "exclude": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 Vera \u0434\u043b\u044f \u0432\u0438\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0437 Home Assistant.", + "lights": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 Vera \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u043f\u0440\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0437 \u0432\u0438\u043c\u0438\u043a\u0430\u0447\u0430 \u0432 \u043e\u0441\u0432\u0456\u0442\u043b\u0435\u043d\u043d\u044f \u0432 Home Assistant." + }, + "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438: https://www.home-assistant.io/integrations/vera/.\n\u0414\u043b\u044f \u0432\u043d\u0435\u0441\u0435\u043d\u043d\u044f \u0431\u0443\u0434\u044c-\u044f\u043a\u0438\u0445 \u0437\u043c\u0456\u043d \u043f\u043e\u0442\u0440\u0456\u0431\u0435\u043d \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Home Assistant. \u0429\u043e\u0431 \u043e\u0447\u0438\u0441\u0442\u0438\u0442\u0438 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f, \u043f\u043e\u0441\u0442\u0430\u0432\u0442\u0435 \u043f\u0440\u043e\u0431\u0456\u043b.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430 Vera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/translations/de.json b/homeassistant/components/vesync/translations/de.json index c52b10c3293..ea05a60ff82 100644 --- a/homeassistant/components/vesync/translations/de.json +++ b/homeassistant/components/vesync/translations/de.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/vesync/translations/tr.json b/homeassistant/components/vesync/translations/tr.json new file mode 100644 index 00000000000..8b4f8b60630 --- /dev/null +++ b/homeassistant/components/vesync/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "E-posta" + }, + "title": "Kullan\u0131c\u0131 Ad\u0131 ve \u015eifre Girin" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/translations/uk.json b/homeassistant/components/vesync/translations/uk.json new file mode 100644 index 00000000000..7f6b3a46b15 --- /dev/null +++ b/homeassistant/components/vesync/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + }, + "title": "VeSync" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/translations/de.json b/homeassistant/components/vilfo/translations/de.json index 4880154b58e..8f20c074ff4 100644 --- a/homeassistant/components/vilfo/translations/de.json +++ b/homeassistant/components/vilfo/translations/de.json @@ -4,9 +4,9 @@ "already_configured": "Dieser Vilfo Router ist bereits konfiguriert." }, "error": { - "cannot_connect": "Verbindung nicht m\u00f6glich. Bitte \u00fcberpr\u00fcfen Sie die von Ihnen angegebenen Informationen und versuchen Sie es erneut.", - "invalid_auth": "Ung\u00fcltige Authentifizierung. Bitte \u00fcberpr\u00fcfen Sie den Zugriffstoken und versuchen Sie es erneut.", - "unknown": "Beim Einrichten der Integration ist ein unerwarteter Fehler aufgetreten." + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung. Bitte \u00fcberpr\u00fcfe den Zugriffstoken und versuche es erneut.", + "unknown": "Unerwarteter Fehler" }, "step": { "user": { diff --git a/homeassistant/components/vilfo/translations/tr.json b/homeassistant/components/vilfo/translations/tr.json new file mode 100644 index 00000000000..dc66041e35a --- /dev/null +++ b/homeassistant/components/vilfo/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "access_token": "Eri\u015fim Belirteci", + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/translations/uk.json b/homeassistant/components/vilfo/translations/uk.json new file mode 100644 index 00000000000..1a93176f290 --- /dev/null +++ b/homeassistant/components/vilfo/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443", + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 Vilfo. \u0412\u043a\u0430\u0436\u0456\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u0440\u043e\u0443\u0442\u0435\u0440\u0430 \u0456 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 API. \u0414\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0446\u0456\u0454\u0457 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457, \u0432\u0456\u0434\u0432\u0456\u0434\u0430\u0439\u0442\u0435 \u0432\u0435\u0431-\u0441\u0430\u0439\u0442: https://www.home-assistant.io/integrations/vilfo.", + "title": "Vilfo Router" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/de.json b/homeassistant/components/vizio/translations/de.json index ddb68ec09fa..ad0cc604d13 100644 --- a/homeassistant/components/vizio/translations/de.json +++ b/homeassistant/components/vizio/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "cannot_connect": "Verbindungsfehler", + "already_configured_device": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", "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": { @@ -10,17 +11,17 @@ "step": { "pair_tv": { "data": { - "pin": "PIN" + "pin": "PIN-Code" }, "description": "Ihr Fernseher sollte einen Code anzeigen. Geben Sie diesen Code in das Formular ein und fahren Sie mit dem n\u00e4chsten Schritt fort, um die Kopplung abzuschlie\u00dfen.", "title": "Schlie\u00dfen Sie den Pairing-Prozess ab" }, "pairing_complete": { - "description": "Ihr VIZIO SmartCast-Ger\u00e4t ist jetzt mit Home Assistant verbunden.", + "description": "Dein Richten Sie das VIZIO SmartCast-Ger\u00e4t ein ist jetzt mit Home Assistant verbunden.", "title": "Kopplung abgeschlossen" }, "pairing_complete_import": { - "description": "Ihr VIZIO SmartCast-Fernseher ist jetzt mit Home Assistant verbunden. \n\n Ihr Zugriffstoken ist '**{access_token}**'.", + "description": "Dein Richten Sie das VIZIO SmartCast-Ger\u00e4t ein ist jetzt mit Home Assistant verbunden.\n\nDein Zugangstoken ist '**{access_token}**'.", "title": "Kopplung abgeschlossen" }, "user": { @@ -30,7 +31,7 @@ "host": "Host", "name": "Name" }, - "description": "Ein Zugriffstoken wird nur f\u00fcr Fernsehger\u00e4te ben\u00f6tigt. Wenn Sie ein Fernsehger\u00e4t konfigurieren und noch kein Zugriffstoken haben, lassen Sie es leer, um einen Pairing-Vorgang durchzuf\u00fchren.", + "description": "Ein Zugangstoken wird nur f\u00fcr Fernsehger\u00e4te ben\u00f6tigt. Wenn du ein Fernsehger\u00e4t konfigurierst und noch kein Zugangstoken hast, lass es leer, um einen Pairing-Vorgang durchzuf\u00fchren.", "title": "Richten Sie das VIZIO SmartCast-Ger\u00e4t ein" } } @@ -44,7 +45,7 @@ "volume_step": "Lautst\u00e4rken-Schrittgr\u00f6\u00dfe" }, "description": "Wenn Sie \u00fcber ein Smart-TV-Ger\u00e4t verf\u00fcgen, k\u00f6nnen Sie Ihre Quellliste optional filtern, indem Sie ausw\u00e4hlen, welche Apps in Ihre Quellliste aufgenommen oder ausgeschlossen werden sollen.", - "title": "Aktualisieren Sie die VIZIO SmartCast-Optionen" + "title": "Aktualisiere die Richten Sie das VIZIO SmartCast-Ger\u00e4t ein-Optionen" } } } diff --git a/homeassistant/components/vizio/translations/tr.json b/homeassistant/components/vizio/translations/tr.json new file mode 100644 index 00000000000..4b923cfb4b3 --- /dev/null +++ b/homeassistant/components/vizio/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "access_token": "Eri\u015fim Belirteci", + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/uk.json b/homeassistant/components/vizio/translations/uk.json new file mode 100644 index 00000000000..958307d543f --- /dev/null +++ b/homeassistant/components/vizio/translations/uk.json @@ -0,0 +1,54 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "updated_entry": "\u0426\u044f \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439, \u0430\u043b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438, \u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u0456 \u0432 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457, \u043d\u0435 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u0430\u044e\u0442\u044c \u0440\u0430\u043d\u0456\u0448\u0435 \u0456\u043c\u043f\u043e\u0440\u0442\u043e\u0432\u0430\u043d\u0438\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f\u043c, \u0442\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457 \u0431\u0443\u043b\u0430 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u043d\u0438\u043c \u0447\u0438\u043d\u043e\u043c \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0439." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "complete_pairing_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438. \u041f\u0435\u0440\u0448 \u043d\u0456\u0436 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0438 \u0441\u043f\u0440\u043e\u0431\u0443, \u043f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0432\u0432\u0435\u0434\u0435\u043d\u0438\u0439 \u0412\u0430\u043c\u0438 PIN-\u043a\u043e\u0434 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439, \u0430 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 \u0456 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 \u0434\u043e \u043c\u0435\u0440\u0435\u0436\u0456.", + "existing_config_entry_found": "\u0406\u0441\u043d\u0443\u044e\u0447\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 VIZIO SmartCast \u0437 \u0442\u0430\u043a\u0438\u043c \u0441\u0435\u0440\u0456\u0439\u043d\u0438\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e. \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u0456\u0441\u043d\u0443\u044e\u0447\u0438\u0439 \u0437\u0430\u043f\u0438\u0441, \u0449\u043e\u0431 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u043e\u0442\u043e\u0447\u043d\u0438\u0439." + }, + "step": { + "pair_tv": { + "data": { + "pin": "PIN-\u043a\u043e\u0434" + }, + "description": "\u0412\u0430\u0448 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0437\u0430\u0440\u0430\u0437 \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u0438 \u043a\u043e\u0434. \u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0446\u0435\u0439 \u043a\u043e\u0434 \u0443 \u0444\u043e\u0440\u043c\u0443, \u0430 \u043f\u043e\u0442\u0456\u043c \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0434\u043e \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u043e\u0433\u043e \u043a\u0440\u043e\u043a\u0443, \u0449\u043e\u0431 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438.", + "title": "\u0417\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044f \u043f\u0440\u043e\u0446\u0435\u0441\u0443 \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "pairing_complete": { + "description": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 VIZIO SmartCast \u0442\u0435\u043f\u0435\u0440 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e Home Assistant.", + "title": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u043e" + }, + "pairing_complete_import": { + "description": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 VIZIO SmartCast \u0442\u0435\u043f\u0435\u0440 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e Home Assistant. \n\n \u0412\u0430\u0448 \u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 - '** {access_token} **'.", + "title": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u043e" + }, + "user": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443", + "device_class": "\u0422\u0438\u043f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0438\u0439 \u0442\u0456\u043b\u044c\u043a\u0438 \u0434\u043b\u044f \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0456\u0432. \u042f\u043a\u0449\u043e \u0412\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0443\u0454\u0442\u0435 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0456 \u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u0443 \u0412\u0430\u0441 \u0449\u0435 \u043d\u0435 \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043e, \u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u0446\u0435 \u043f\u043e\u043b\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c, \u0449\u043e\u0431 \u0432\u0438\u043a\u043e\u043d\u0430\u0442\u0438 \u043f\u0440\u043e\u0446\u0435\u0441 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438.", + "title": "VIZIO SmartCast" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "apps_to_include_or_exclude": "\u0421\u043f\u0438\u0441\u043e\u043a \u0434\u0436\u0435\u0440\u0435\u043b", + "include_or_exclude": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0430\u0431\u043e \u0432\u0438\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0434\u0436\u0435\u0440\u0435\u043b\u0430?", + "volume_step": "\u041a\u0440\u043e\u043a \u0433\u0443\u0447\u043d\u043e\u0441\u0442\u0456" + }, + "description": "\u042f\u043a\u0449\u043e \u0443 \u0432\u0430\u0441 \u0454 Smart TV, \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u0440\u0438 \u0431\u0430\u0436\u0430\u043d\u043d\u0456 \u0432\u0456\u0434\u0444\u0456\u043b\u044c\u0442\u0440\u0443\u0432\u0430\u0442\u0438 \u0441\u043f\u0438\u0441\u043e\u043a \u0434\u0436\u0435\u0440\u0435\u043b, \u0432\u043a\u043b\u044e\u0447\u0438\u0432\u0448\u0438 \u0430\u0431\u043e \u0432\u0438\u043a\u043b\u044e\u0447\u0438\u0432\u0448\u0438 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0438 \u0437\u0456 \u0441\u043f\u0438\u0441\u043a\u0443.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f VIZIO SmartCast" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volumio/translations/de.json b/homeassistant/components/volumio/translations/de.json index ef455299de6..45727d85ee0 100644 --- a/homeassistant/components/volumio/translations/de.json +++ b/homeassistant/components/volumio/translations/de.json @@ -1,7 +1,19 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/volumio/translations/tr.json b/homeassistant/components/volumio/translations/tr.json new file mode 100644 index 00000000000..249bb17d64e --- /dev/null +++ b/homeassistant/components/volumio/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ke\u015ffedilen Volumio'ya ba\u011flan\u0131lam\u0131yor" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volumio/translations/uk.json b/homeassistant/components/volumio/translations/uk.json index 58947e14e4f..c517eafa2bd 100644 --- a/homeassistant/components/volumio/translations/uk.json +++ b/homeassistant/components/volumio/translations/uk.json @@ -1,14 +1,16 @@ { "config": { "abort": { - "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u0438\u043c Volumio." }, "error": { - "cannot_connect": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "step": { "discovery_confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 Volumio `{name}`?", "title": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e Volumio" }, "user": { diff --git a/homeassistant/components/water_heater/translations/uk.json b/homeassistant/components/water_heater/translations/uk.json new file mode 100644 index 00000000000..d6558828a8e --- /dev/null +++ b/homeassistant/components/water_heater/translations/uk.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "{entity_name}: \u0432\u0438\u043c\u043a\u043d\u0443\u0442\u0438", + "turn_on": "{entity_name}: \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/de.json b/homeassistant/components/wemo/translations/de.json index f20ad5598ab..81694f65ea2 100644 --- a/homeassistant/components/wemo/translations/de.json +++ b/homeassistant/components/wemo/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Es wurden keine Wemo-Ger\u00e4te im Netzwerk gefunden.", - "single_instance_allowed": "Nur eine einzige Konfiguration von Wemo ist zul\u00e4ssig." + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { "confirm": { diff --git a/homeassistant/components/wemo/translations/tr.json b/homeassistant/components/wemo/translations/tr.json index 411a536ceed..a87d832eece 100644 --- a/homeassistant/components/wemo/translations/tr.json +++ b/homeassistant/components/wemo/translations/tr.json @@ -1,7 +1,13 @@ { "config": { "abort": { - "no_devices_found": "A\u011fda Wemo cihaz\u0131 bulunamad\u0131." + "no_devices_found": "A\u011fda Wemo cihaz\u0131 bulunamad\u0131.", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "Wemo'yu kurmak istiyor musunuz?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/uk.json b/homeassistant/components/wemo/translations/uk.json new file mode 100644 index 00000000000..1217d664234 --- /dev/null +++ b/homeassistant/components/wemo/translations/uk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "step": { + "confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Wemo?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/tr.json b/homeassistant/components/wiffi/translations/tr.json new file mode 100644 index 00000000000..26ec2e61e00 --- /dev/null +++ b/homeassistant/components/wiffi/translations/tr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "addr_in_use": "Sunucu ba\u011flant\u0131 noktas\u0131 zaten kullan\u0131l\u0131yor.", + "start_server_failed": "Ba\u015flatma sunucusu ba\u015far\u0131s\u0131z oldu." + }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Zaman a\u015f\u0131m\u0131 (dakika)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/uk.json b/homeassistant/components/wiffi/translations/uk.json new file mode 100644 index 00000000000..dc8dac9cd56 --- /dev/null +++ b/homeassistant/components/wiffi/translations/uk.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "addr_in_use": "\u041f\u043e\u0440\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f.", + "start_server_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 \u0441\u0435\u0440\u0432\u0435\u0440." + }, + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f TCP-\u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0434\u043b\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 WIFFI" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 (\u0432 \u0445\u0432\u0438\u043b\u0438\u043d\u0430\u0445)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/de.json b/homeassistant/components/wilight/translations/de.json index 07d00495af7..d56e782279a 100644 --- a/homeassistant/components/wilight/translations/de.json +++ b/homeassistant/components/wilight/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "flow_title": "WiLight: {name}", "step": { "confirm": { diff --git a/homeassistant/components/wilight/translations/tr.json b/homeassistant/components/wilight/translations/tr.json new file mode 100644 index 00000000000..5307276a71d --- /dev/null +++ b/homeassistant/components/wilight/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/wilight/translations/uk.json b/homeassistant/components/wilight/translations/uk.json new file mode 100644 index 00000000000..7517538499e --- /dev/null +++ b/homeassistant/components/wilight/translations/uk.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "not_supported_device": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0430\u0440\u0430\u0437\u0456 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f.", + "not_wilight_device": "\u0426\u0435 \u043d\u0435 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 WiLight." + }, + "flow_title": "WiLight: {name}", + "step": { + "confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 WiLight {name}? \n\n \u0426\u0435 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454: {components}", + "title": "WiLight" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/de.json b/homeassistant/components/withings/translations/de.json index d217640e44b..05d3795a0b0 100644 --- a/homeassistant/components/withings/translations/de.json +++ b/homeassistant/components/withings/translations/de.json @@ -1,22 +1,30 @@ { "config": { "abort": { - "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Autorisierungs-URL.", - "missing_configuration": "Die Withings-Integration ist nicht konfiguriert. Bitte folgen Sie der Dokumentation." + "already_configured": "Konfiguration des Profils aktualisiert.", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url})." }, "create_entry": { "default": "Erfolgreiche Authentifizierung mit Withings." }, + "error": { + "already_configured": "Konto wurde bereits konfiguriert" + }, "step": { "pick_implementation": { - "title": "Authentifizierungsmethode ausw\u00e4hlen" + "title": "W\u00e4hle die Authentifizierungsmethode" }, "profile": { "data": { - "profile": "Profil" + "profile": "Profilname" }, "description": "Welches Profil hast du auf der Withings-Website ausgew\u00e4hlt? Es ist wichtig, dass die Profile \u00fcbereinstimmen, da sonst die Daten falsch beschriftet werden.", "title": "Benutzerprofil" + }, + "reauth": { + "title": "Integration erneut authentifizieren" } } } diff --git a/homeassistant/components/withings/translations/fr.json b/homeassistant/components/withings/translations/fr.json index 017a9e63078..b5f524698f5 100644 --- a/homeassistant/components/withings/translations/fr.json +++ b/homeassistant/components/withings/translations/fr.json @@ -10,7 +10,7 @@ "default": "Authentifi\u00e9 avec succ\u00e8s \u00e0 Withings pour le profil s\u00e9lectionn\u00e9." }, "error": { - "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9" + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" }, "flow_title": "Withings: {profile}", "step": { diff --git a/homeassistant/components/withings/translations/tr.json b/homeassistant/components/withings/translations/tr.json new file mode 100644 index 00000000000..4e0228708ea --- /dev/null +++ b/homeassistant/components/withings/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Profil i\u00e7in yap\u0131land\u0131rma g\u00fcncellendi." + }, + "error": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "profile": { + "data": { + "profile": "Profil Ad\u0131" + }, + "title": "Kullan\u0131c\u0131 profili." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/uk.json b/homeassistant/components/withings/translations/uk.json new file mode 100644 index 00000000000..5efc27042b1 --- /dev/null +++ b/homeassistant/components/withings/translations/uk.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "\u041e\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u043f\u0440\u043e\u0444\u0456\u043b\u044e.", + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443." + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "error": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "flow_title": "Withings: {profile}", + "step": { + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "profile": { + "data": { + "profile": "\u041d\u0430\u0437\u0432\u0430 \u043f\u0440\u043e\u0444\u0456\u043b\u044e" + }, + "description": "\u0412\u043a\u0430\u0436\u0456\u0442\u044c \u0443\u043d\u0456\u043a\u0430\u043b\u044c\u043d\u0435 \u0456\u043c'\u044f \u043f\u0440\u043e\u0444\u0456\u043b\u044e \u0434\u043b\u044f \u0446\u0438\u0445 \u0434\u0430\u043d\u0438\u0445. \u042f\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u043e, \u0446\u0435 \u043d\u0430\u0437\u0432\u0430, \u043e\u0431\u0440\u0430\u043d\u0430 \u043d\u0430 \u043f\u043e\u043f\u0435\u0440\u0435\u0434\u043d\u044c\u043e\u043c\u0443 \u043a\u0440\u043e\u0446\u0456.", + "title": "Withings" + }, + "reauth": { + "description": "\u041f\u0440\u043e\u0444\u0456\u043b\u044c \"{profile}\" \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u043e\u0432\u0430\u043d\u0438\u0439 \u0434\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u0432\u0436\u0435\u043d\u043d\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0434\u0430\u043d\u0438\u0445 Withings.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/de.json b/homeassistant/components/wled/translations/de.json index ff12e429bd6..0dd13f763d6 100644 --- a/homeassistant/components/wled/translations/de.json +++ b/homeassistant/components/wled/translations/de.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Dieses WLED-Ger\u00e4t ist bereits konfiguriert." + "already_configured": "Dieses WLED-Ger\u00e4t ist bereits konfiguriert.", + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" }, "flow_title": "WLED: {name}", "step": { diff --git a/homeassistant/components/wled/translations/tr.json b/homeassistant/components/wled/translations/tr.json new file mode 100644 index 00000000000..f02764c8aba --- /dev/null +++ b/homeassistant/components/wled/translations/tr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar" + }, + "description": "WLED'inizi Home Assistant ile t\u00fcmle\u015ftirmek i\u00e7in ayarlay\u0131n." + }, + "zeroconf_confirm": { + "description": "Home Assistant'a '{name}' adl\u0131 WLED'i eklemek istiyor musunuz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/uk.json b/homeassistant/components/wled/translations/uk.json new file mode 100644 index 00000000000..c0280d33993 --- /dev/null +++ b/homeassistant/components/wled/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 WLED." + }, + "zeroconf_confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 WLED `{name}`?", + "title": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 WLED" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/de.json b/homeassistant/components/wolflink/translations/de.json index cb7e571d1e6..71f48a6413d 100644 --- a/homeassistant/components/wolflink/translations/de.json +++ b/homeassistant/components/wolflink/translations/de.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, "step": { "device": { "data": { diff --git a/homeassistant/components/wolflink/translations/sensor.de.json b/homeassistant/components/wolflink/translations/sensor.de.json index 373e1989578..9680716cd19 100644 --- a/homeassistant/components/wolflink/translations/sensor.de.json +++ b/homeassistant/components/wolflink/translations/sensor.de.json @@ -1,6 +1,7 @@ { "state": { "wolflink__state": { + "permanent": "Permanent", "solarbetrieb": "Solarmodus", "sparbetrieb": "Sparmodus", "sparen": "Sparen", diff --git a/homeassistant/components/wolflink/translations/sensor.tr.json b/homeassistant/components/wolflink/translations/sensor.tr.json index 8b2eb0a8c53..4b1e2778af1 100644 --- a/homeassistant/components/wolflink/translations/sensor.tr.json +++ b/homeassistant/components/wolflink/translations/sensor.tr.json @@ -1,10 +1,19 @@ { "state": { "wolflink__state": { + "glt_betrieb": "BMS modu", + "heizbetrieb": "Is\u0131tma modu", + "kalibration_heizbetrieb": "Is\u0131tma modu kalibrasyonu", + "kalibration_kombibetrieb": "Kombi modu kalibrasyonu", + "reduzierter_betrieb": "S\u0131n\u0131rl\u0131 mod", + "solarbetrieb": "G\u00fcne\u015f modu", + "sparbetrieb": "Ekonomi modu", "standby": "Bekleme", "start": "Ba\u015flat", "storung": "Hata", - "test": "Test" + "test": "Test", + "urlaubsmodus": "Tatil modu", + "warmwasserbetrieb": "DHW modu" } } } \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.uk.json b/homeassistant/components/wolflink/translations/sensor.uk.json index 665ff99992c..c8a69f2c007 100644 --- a/homeassistant/components/wolflink/translations/sensor.uk.json +++ b/homeassistant/components/wolflink/translations/sensor.uk.json @@ -1,15 +1,87 @@ { "state": { "wolflink__state": { + "1_x_warmwasser": "1 \u0445 \u0413\u0412\u041f", + "abgasklappe": "\u0417\u0430\u0441\u043b\u0456\u043d\u043a\u0430 \u0434\u0438\u043c\u043e\u0432\u0438\u0445 \u0433\u0430\u0437\u0456\u0432", + "absenkbetrieb": "\u0420\u0435\u0436\u0438\u043c \u0430\u0432\u0430\u0440\u0456\u0457", + "absenkstop": "\u0410\u0432\u0430\u0440\u0456\u0439\u043d\u0430 \u0437\u0443\u043f\u0438\u043d\u043a\u0430", + "aktiviert": "\u0410\u043a\u0442\u0438\u0432\u043e\u0432\u0430\u043d\u043e", + "antilegionellenfunktion": "\u0424\u0443\u043d\u043a\u0446\u0456\u044f \u0430\u043d\u0442\u0438-\u043b\u0435\u0433\u0438\u043e\u043d\u0435\u043b\u043b\u0438", + "at_abschaltung": "\u041e\u0422 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "at_frostschutz": "\u041e\u0422 \u0437\u0430\u0445\u0438\u0441\u0442 \u0432\u0456\u0434 \u0437\u0430\u043c\u0435\u0440\u0437\u0430\u043d\u043d\u044f", + "aus": "\u0412\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "auto": "\u0410\u0432\u0442\u043e", + "auto_off_cool": "AutoOffCool", + "auto_on_cool": "AutoOnCool", + "automatik_aus": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0435 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043d\u044f", + "automatik_ein": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0435 \u0432\u043c\u0438\u043a\u0430\u043d\u043d\u044f", + "bereit_keine_ladung": "\u0413\u043e\u0442\u043e\u0432\u0438\u0439, \u043d\u0435 \u0437\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0443\u0454\u0442\u044c\u0441\u044f", + "betrieb_ohne_brenner": "\u0420\u043e\u0431\u043e\u0442\u0430 \u0431\u0435\u0437 \u043f\u0430\u043b\u044c\u043d\u0438\u043a\u0430", + "cooling": "\u041e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f", + "deaktiviert": "\u041d\u0435 \u0430\u043a\u0442\u0438\u0432\u043d\u043e", + "dhw_prior": "DHWPrior", + "eco": "\u0415\u043a\u043e", + "ein": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e", + "estrichtrocknung": "\u0421\u0443\u0448\u0456\u043d\u043d\u044f", + "externe_deaktivierung": "\u0417\u043e\u0432\u043d\u0456\u0448\u043d\u044f \u0434\u0435\u0430\u043a\u0442\u0438\u0432\u0430\u0446\u0456\u044f", + "fernschalter_ein": "\u0414\u0438\u0441\u0442\u0430\u043d\u0446\u0456\u0439\u043d\u0435 \u043a\u0435\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e", + "frost_heizkreis": "\u0417\u0430\u043c\u0435\u0440\u0437\u0430\u043d\u043d\u044f \u043a\u043e\u043d\u0442\u0443\u0440\u0443 \u043e\u043f\u0430\u043b\u0435\u043d\u043d\u044f", + "frost_warmwasser": "\u0417\u0430\u043c\u0435\u0440\u0437\u0430\u043d\u043d\u044f \u0413\u0412\u041f", + "frostschutz": "\u0417\u0430\u0445\u0438\u0441\u0442 \u0432\u0456\u0434 \u0437\u0430\u043c\u0435\u0440\u0437\u0430\u043d\u043d\u044f", + "gasdruck": "\u0422\u0438\u0441\u043a \u0433\u0430\u0437\u0443", + "glt_betrieb": "\u0420\u0435\u0436\u0438\u043c BMS", + "gradienten_uberwachung": "\u0413\u0440\u0430\u0434\u0456\u0454\u043d\u0442\u043d\u0438\u0439 \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433", + "heizbetrieb": "\u0420\u0435\u0436\u0438\u043c \u043e\u043f\u0430\u043b\u0435\u043d\u043d\u044f", + "heizgerat_mit_speicher": "\u041a\u043e\u0442\u0435\u043b \u0437 \u0446\u0438\u043b\u0456\u043d\u0434\u0440\u043e\u043c", + "heizung": "\u041e\u0431\u0456\u0433\u0440\u0456\u0432", + "initialisierung": "\u0406\u043d\u0456\u0446\u0456\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u044f", + "kalibration": "\u041a\u0430\u043b\u0456\u0431\u0440\u0443\u0432\u0430\u043d\u043d\u044f", + "kalibration_heizbetrieb": "\u041a\u0430\u043b\u0456\u0431\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u0440\u0435\u0436\u0438\u043c\u0443 \u043e\u043f\u0430\u043b\u0435\u043d\u043d\u044f", + "kalibration_kombibetrieb": "\u041a\u0430\u043b\u0456\u0431\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u0432 \u043a\u043e\u043c\u0431\u0456\u043d\u043e\u0432\u0430\u043d\u043e\u043c\u0443 \u0440\u0435\u0436\u0438\u043c\u0456", + "kalibration_warmwasserbetrieb": "\u041a\u0430\u043b\u0456\u0431\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u0413\u0412\u041f", + "kaskadenbetrieb": "\u041a\u0430\u0441\u043a\u0430\u0434\u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u044f", + "kombibetrieb": "\u041a\u043e\u043c\u0431\u0456\u043d\u043e\u0432\u0430\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c", + "kombigerat": "\u0414\u0432\u043e\u043a\u043e\u043d\u0442\u0443\u0440\u043d\u0438\u0439 \u043a\u043e\u0442\u0435\u043b", + "kombigerat_mit_solareinbindung": "\u0414\u0432\u043e\u043a\u043e\u043d\u0442\u0443\u0440\u043d\u0438\u0439 \u043a\u043e\u0442\u0435\u043b \u0437 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0454\u044e \u0441\u043e\u043d\u044f\u0447\u043d\u043e\u0457 \u0441\u0438\u0441\u0442\u0435\u043c\u0438", + "mindest_kombizeit": "\u041c\u0456\u043d\u0456\u043c\u0430\u043b\u044c\u043d\u0438\u0439 \u043a\u043e\u043c\u0431\u0456\u043d\u043e\u0432\u0430\u043d\u0438\u0439 \u0447\u0430\u0441", + "nachlauf_heizkreispumpe": "\u0420\u043e\u0431\u043e\u0442\u0430 \u043d\u0430\u0441\u043e\u0441\u0430 \u043a\u043e\u043d\u0442\u0443\u0440\u0443 \u043e\u043f\u0430\u043b\u0435\u043d\u043d\u044f", + "nachspulen": "\u041f\u043e\u0441\u0442-\u043f\u0440\u043e\u043c\u0438\u0432\u043a\u0430", + "nur_heizgerat": "\u0422\u0456\u043b\u044c\u043a\u0438 \u0431\u043e\u0439\u043b\u0435\u0440", + "parallelbetrieb": "\u041f\u0430\u0440\u0430\u043b\u0435\u043b\u044c\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c", + "partymodus": "\u0420\u0435\u0436\u0438\u043c \u0432\u0435\u0447\u0456\u0440\u043a\u0438", + "perm_cooling": "\u041f\u043e\u0441\u0442\u0456\u0439\u043d\u0435 \u043e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f", "permanent": "\u041f\u043e\u0441\u0442\u0456\u0439\u043d\u043e", + "permanentbetrieb": "\u041f\u043e\u0441\u0442\u0456\u0439\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c", + "reduzierter_betrieb": "\u041e\u0431\u043c\u0435\u0436\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c", + "rt_abschaltung": "RT \u0432\u0438\u043c\u0438\u043a\u0430\u043d\u043d\u044f", + "rt_frostschutz": "RT \u0437\u0430\u0445\u0438\u0441\u0442 \u0432\u0456\u0434 \u0437\u0430\u043c\u0435\u0440\u0437\u0430\u043d\u043d\u044f", + "ruhekontakt": "\u0420\u0435\u0448\u0442\u0430 \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u0456\u0432", + "schornsteinfeger": "\u0422\u0435\u0441\u0442 \u043d\u0430 \u0432\u0438\u043a\u0438\u0434\u0438", + "smart_grid": "\u0420\u043e\u0437\u0443\u043c\u043d\u0430 \u043c\u0435\u0440\u0435\u0436\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043f\u043e\u0441\u0442\u0430\u0447\u0430\u043d\u043d\u044f", "smart_home": "\u0420\u043e\u0437\u0443\u043c\u043d\u0438\u0439 \u0434\u0456\u043c", + "softstart": "\u041c'\u044f\u043a\u0438\u0439 \u0441\u0442\u0430\u0440\u0442", + "solarbetrieb": "\u0421\u043e\u043d\u044f\u0447\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c", + "sparbetrieb": "\u0420\u0435\u0436\u0438\u043c \u0435\u043a\u043e\u043d\u043e\u043c\u0456\u0457", "sparen": "\u0415\u043a\u043e\u043d\u043e\u043c\u0456\u044f", + "spreizung_hoch": "dT \u0437\u0430\u043d\u0430\u0434\u0442\u043e \u0448\u0438\u0440\u043e\u043a\u0438\u0439", + "spreizung_kf": "\u0421\u043f\u0440\u0435\u0434 KF", "stabilisierung": "\u0421\u0442\u0430\u0431\u0456\u043b\u0456\u0437\u0430\u0446\u0456\u044f", "standby": "\u041e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f", - "start": "\u041f\u043e\u0447\u0430\u0442\u043e\u043a", + "start": "\u0417\u0430\u043f\u0443\u0441\u043a", "storung": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430", - "taktsperre": "\u0410\u043d\u0442\u0438\u0446\u0438\u043a\u043b", - "test": "\u0422\u0435\u0441\u0442" + "taktsperre": "\u0410\u043d\u0442\u0438-\u0446\u0438\u043a\u043b", + "telefonfernschalter": "\u0414\u0438\u0441\u0442\u0430\u043d\u0446\u0456\u0439\u043d\u0435 \u043a\u0435\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u0437 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0443", + "test": "\u0422\u0435\u0441\u0442", + "tpw": "TPW", + "urlaubsmodus": "\u0420\u0435\u0436\u0438\u043c \"\u0432\u0438\u0445\u0456\u0434\u043d\u0456\"", + "ventilprufung": "\u0422\u0435\u0441\u0442 \u043a\u043b\u0430\u043f\u0430\u043d\u0430", + "vorspulen": "\u041f\u0440\u043e\u043c\u0438\u0432\u0430\u043d\u043d\u044f \u0432\u0445\u043e\u0434\u0443", + "warmwasser": "\u0413\u0412\u041f", + "warmwasser_schnellstart": "\u0428\u0432\u0438\u0434\u043a\u0438\u0439 \u0437\u0430\u043f\u0443\u0441\u043a \u0413\u0412\u041f", + "warmwasserbetrieb": "\u0420\u0435\u0436\u0438\u043c \u0413\u0412\u041f", + "warmwassernachlauf": "\u0417\u0430\u043f\u0443\u0441\u043a \u0413\u0412\u041f", + "warmwasservorrang": "\u041f\u0440\u0456\u043e\u0440\u0438\u0442\u0435\u0442 \u0413\u0412\u041f", + "zunden": "\u0417\u0430\u043f\u0430\u043b\u044e\u0432\u0430\u043d\u043d\u044f" } } } \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/tr.json b/homeassistant/components/wolflink/translations/tr.json new file mode 100644 index 00000000000..6ed28a58c79 --- /dev/null +++ b/homeassistant/components/wolflink/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/uk.json b/homeassistant/components/wolflink/translations/uk.json index a7fbdfff913..3fdf20a6ace 100644 --- a/homeassistant/components/wolflink/translations/uk.json +++ b/homeassistant/components/wolflink/translations/uk.json @@ -1,17 +1,26 @@ { "config": { "abort": { - "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "step": { "device": { "data": { "device_name": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" - } + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 WOLF" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "WOLF SmartSet" } } } diff --git a/homeassistant/components/xbox/translations/de.json b/homeassistant/components/xbox/translations/de.json index c67f3a49ea4..04f32e05f8b 100644 --- a/homeassistant/components/xbox/translations/de.json +++ b/homeassistant/components/xbox/translations/de.json @@ -1,5 +1,10 @@ { "config": { + "abort": { + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, "create_entry": { "default": "Erfolgreich authentifiziert" }, diff --git a/homeassistant/components/xbox/translations/lb.json b/homeassistant/components/xbox/translations/lb.json index d305909389f..b83b6d0a499 100644 --- a/homeassistant/components/xbox/translations/lb.json +++ b/homeassistant/components/xbox/translations/lb.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "authorize_url_timeout": "Z\u00e4itiwwerschreidung beim erstellen vun der Authorisatiouns URL.", "missing_configuration": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun.", "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." + }, + "create_entry": { + "default": "Erfollegr\u00e4ich authentifiz\u00e9iert" } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/tr.json b/homeassistant/components/xbox/translations/tr.json new file mode 100644 index 00000000000..a152eb19468 --- /dev/null +++ b/homeassistant/components/xbox/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/uk.json b/homeassistant/components/xbox/translations/uk.json new file mode 100644 index 00000000000..a1b3f8340fc --- /dev/null +++ b/homeassistant/components/xbox/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "step": { + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/de.json b/homeassistant/components/xiaomi_aqara/translations/de.json index f86868987a0..6b0e25dfcd5 100644 --- a/homeassistant/components/xiaomi_aqara/translations/de.json +++ b/homeassistant/components/xiaomi_aqara/translations/de.json @@ -1,5 +1,14 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt" + }, + "error": { + "discovery_error": "Es konnte kein Xiaomi Aqara Gateway gefunden werden, versuche die IP von Home Assistant als Interface zu nutzen", + "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse, schau unter https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", + "invalid_mac": "Ung\u00fcltige MAC-Adresse" + }, "flow_title": "Xiaomi Aqara Gateway: {name}", "step": { "select": { @@ -8,7 +17,11 @@ } }, "user": { - "description": "Stellen Sie eine Verbindung zu Ihrem Xiaomi Aqara Gateway her. Wenn die IP- und Mac-Adressen leer bleiben, wird die automatische Erkennung verwendet", + "data": { + "host": "IP-Adresse", + "mac": "MAC-Adresse" + }, + "description": "Stelle eine Verbindung zu deinem Xiaomi Aqara Gateway her. Wenn die IP- und MAC-Adressen leer bleiben, wird die automatische Erkennung verwendet", "title": "Xiaomi Aqara Gateway" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/tr.json b/homeassistant/components/xiaomi_aqara/translations/tr.json index 10d1374187e..24da29417d1 100644 --- a/homeassistant/components/xiaomi_aqara/translations/tr.json +++ b/homeassistant/components/xiaomi_aqara/translations/tr.json @@ -1,7 +1,38 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "not_xiaomi_aqara": "Xiaomi Aqara A\u011f Ge\u00e7idi de\u011fil, ke\u015ffedilen cihaz bilinen a\u011f ge\u00e7itleriyle e\u015fle\u015fmedi" + }, "error": { + "discovery_error": "Bir Xiaomi Aqara A\u011f Ge\u00e7idi ke\u015ffedilemedi, HomeAssistant'\u0131 aray\u00fcz olarak \u00e7al\u0131\u015ft\u0131ran cihaz\u0131n IP'sini kullanmay\u0131 deneyin", + "invalid_interface": "Ge\u00e7ersiz a\u011f aray\u00fcz\u00fc", + "invalid_key": "Ge\u00e7ersiz a\u011f ge\u00e7idi anahtar\u0131", "invalid_mac": "Ge\u00e7ersiz Mac Adresi" + }, + "flow_title": "Xiaomi Aqara A\u011f Ge\u00e7idi: {name}", + "step": { + "select": { + "data": { + "select_ip": "\u0130p Adresi" + }, + "description": "Ek a\u011f ge\u00e7itlerini ba\u011flamak istiyorsan\u0131z kurulumu tekrar \u00e7al\u0131\u015ft\u0131r\u0131n.", + "title": "Ba\u011flamak istedi\u011finiz Xiaomi Aqara A\u011f Ge\u00e7idini se\u00e7in" + }, + "settings": { + "data": { + "key": "A\u011f ge\u00e7idinizin anahtar\u0131", + "name": "A\u011f Ge\u00e7idinin Ad\u0131" + }, + "description": "Anahtar (parola) bu \u00f6\u011fretici kullan\u0131larak al\u0131nabilir: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Anahtar sa\u011flanmazsa, yaln\u0131zca sens\u00f6rlere eri\u015filebilir" + }, + "user": { + "data": { + "host": "\u0130p Adresi (iste\u011fe ba\u011fl\u0131)", + "mac": "Mac Adresi (iste\u011fe ba\u011fl\u0131)" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/uk.json b/homeassistant/components/xiaomi_aqara/translations/uk.json new file mode 100644 index 00000000000..1598e96b38e --- /dev/null +++ b/homeassistant/components/xiaomi_aqara/translations/uk.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "not_xiaomi_aqara": "\u0426\u0435 \u043d\u0435 \u0448\u043b\u044e\u0437 Xiaomi Aqara. \u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u0430\u0454 \u0432\u0456\u0434\u043e\u043c\u0438\u043c \u0448\u043b\u044e\u0437\u0456\u0432." + }, + "error": { + "discovery_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0438\u044f\u0432\u0438\u0442\u0438 \u0448\u043b\u044e\u0437 Xiaomi Aqara, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u0442\u0438 IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0437 HomeAssistant \u0432 \u044f\u043a\u043e\u0441\u0442\u0456 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443.", + "invalid_host": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430. . \u0421\u043f\u043e\u0441\u043e\u0431\u0438 \u0432\u0438\u0440\u0456\u0448\u0435\u043d\u043d\u044f \u0446\u0456\u0454\u0457 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u043e\u043f\u0438\u0441\u0430\u043d\u0456 \u0442\u0443\u0442: https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem.", + "invalid_interface": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u0438\u0439 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.", + "invalid_key": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 \u0448\u043b\u044e\u0437\u0443.", + "invalid_mac": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0430 MAC-\u0430\u0434\u0440\u0435\u0441\u0430." + }, + "flow_title": "\u0428\u043b\u044e\u0437 Xiaomi Aqara: {name}", + "step": { + "select": { + "data": { + "select_ip": "IP-\u0430\u0434\u0440\u0435\u0441\u0430" + }, + "description": "\u041f\u043e\u0447\u043d\u0456\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u044e\u0432\u0430\u043d\u043d\u044f \u0437\u043d\u043e\u0432\u0443, \u044f\u043a\u0449\u043e \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0434\u043e\u0434\u0430\u0442\u0438 \u0456\u043d\u0448\u0438\u0439 \u0448\u043b\u044e\u0437", + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0448\u043b\u044e\u0437 Xiaomi Aqara" + }, + "settings": { + "data": { + "key": "\u041a\u043b\u044e\u0447", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u041a\u043b\u044e\u0447 (\u043f\u0430\u0440\u043e\u043b\u044c) \u043c\u043e\u0436\u043d\u0430 \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0446\u0456\u0454\u0457 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0457: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. \u042f\u043a\u0449\u043e \u043a\u043b\u044e\u0447 \u043d\u0435 \u0432\u043a\u0430\u0437\u0430\u043d\u043e, \u0431\u0443\u0434\u0443\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0456 \u0442\u0456\u043b\u044c\u043a\u0438 \u0434\u0430\u0442\u0447\u0438\u043a\u0438.", + "title": "\u0428\u043b\u044e\u0437 Xiaomi Aqara" + }, + "user": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "interface": "\u041c\u0435\u0440\u0435\u0436\u0435\u0432\u0438\u0439 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441", + "mac": "MAC-\u0430\u0434\u0440\u0435\u0441\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437\u0456 \u0448\u043b\u044e\u0437\u043e\u043c Xiaomi Aqara. \u0414\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f \u0448\u043b\u044e\u0437\u0443, \u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u043b\u044f IP \u0456 MAC-\u0430\u0434\u0440\u0435\u0441\u0438 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c\u0438.", + "title": "\u0428\u043b\u044e\u0437 Xiaomi Aqara" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json index 0b5f593ffcd..d56a81e14d4 100644 --- a/homeassistant/components/xiaomi_miio/translations/de.json +++ b/homeassistant/components/xiaomi_miio/translations/de.json @@ -2,27 +2,28 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "already_in_progress": "Der Konfigurationsablauf f\u00fcr dieses Xiaomi Miio-Ger\u00e4t wird bereits ausgef\u00fchrt." + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt" }, "error": { - "no_device_selected": "Kein Ger\u00e4t ausgew\u00e4hlt, bitte w\u00e4hlen Sie ein Ger\u00e4t aus." + "cannot_connect": "Verbindung fehlgeschlagen", + "no_device_selected": "Kein Ger\u00e4t ausgew\u00e4hlt, bitte w\u00e4hle ein Ger\u00e4t aus." }, "flow_title": "Xiaomi Miio: {name}", "step": { "gateway": { "data": { - "host": "IP Adresse", + "host": "IP-Adresse", "name": "Name des Gateways", "token": "API-Token" }, - "description": "Sie ben\u00f6tigen den 32 Zeichen langen API-Token. Anweisungen finden Sie unter https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.", - "title": "Stellen Sie eine Verbindung zu einem Xiaomi Gateway her" + "description": "Du ben\u00f6tigst den 32 Zeichen langen API-Token. Anweisungen findest du unter https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.", + "title": "Stelle eine Verbindung zu einem Xiaomi Gateway her" }, "user": { "data": { - "gateway": "Stellen Sie eine Verbindung zu einem Xiaomi Gateway her" + "gateway": "Stelle eine Verbindung zu einem Xiaomi Gateway her" }, - "description": "W\u00e4hlen Sie aus, mit welchem Ger\u00e4t Sie eine Verbindung herstellen m\u00f6chten.", + "description": "W\u00e4hle aus, mit welchem Ger\u00e4t du eine Verbindung herstellen m\u00f6chtest.", "title": "Xiaomi Miio" } } diff --git a/homeassistant/components/xiaomi_miio/translations/tr.json b/homeassistant/components/xiaomi_miio/translations/tr.json new file mode 100644 index 00000000000..46a6493ab3a --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/tr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "no_device_selected": "Cihaz se\u00e7ilmedi, l\u00fctfen bir cihaz se\u00e7in." + }, + "step": { + "gateway": { + "data": { + "host": "\u0130p Adresi", + "name": "A\u011f Ge\u00e7idinin Ad\u0131", + "token": "API Belirteci" + }, + "title": "Bir Xiaomi A\u011f Ge\u00e7idine ba\u011flan\u0131n" + }, + "user": { + "data": { + "gateway": "Bir Xiaomi A\u011f Ge\u00e7idine ba\u011flan\u0131n" + }, + "description": "Hangi cihaza ba\u011flanmak istedi\u011finizi se\u00e7in.", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/uk.json b/homeassistant/components/xiaomi_miio/translations/uk.json new file mode 100644 index 00000000000..f32105589f6 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/uk.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "no_device_selected": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u0434\u0438\u043d \u0437 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432." + }, + "flow_title": "Xiaomi Miio: {name}", + "step": { + "gateway": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430", + "token": "\u0422\u043e\u043a\u0435\u043d API" + }, + "description": "\u0414\u043b\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e 32-\u0445 \u0437\u043d\u0430\u0447\u043d\u0438\u0439 \u0422\u043e\u043a\u0435\u043d API . \u041f\u0440\u043e \u0442\u0435, \u044f\u043a \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0442\u043e\u043a\u0435\u043d, \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0456\u0437\u043d\u0430\u0442\u0438\u0441\u044f \u0442\u0443\u0442:\nhttps://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.\n\u0417\u0432\u0435\u0440\u043d\u0456\u0442\u044c \u0443\u0432\u0430\u0433\u0443, \u0449\u043e \u0446\u0435\u0439 \u0442\u043e\u043a\u0435\u043d \u0432\u0456\u0434\u0440\u0456\u0437\u043d\u044f\u0454\u0442\u044c\u0441\u044f \u0432\u0456\u0434 \u043a\u043b\u044e\u0447\u0430, \u044f\u043a\u0438\u0439 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u043f\u0440\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 Xiaomi Aqara.", + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0448\u043b\u044e\u0437\u0443 Xiaomi" + }, + "user": { + "data": { + "gateway": "\u0428\u043b\u044e\u0437 Xiaomi" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u044f\u043a\u0438\u0439 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438.", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/de.json b/homeassistant/components/yeelight/translations/de.json index 90c154f882b..6eaff2e87a3 100644 --- a/homeassistant/components/yeelight/translations/de.json +++ b/homeassistant/components/yeelight/translations/de.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, "step": { "pick_device": { "data": { @@ -9,7 +16,8 @@ "user": { "data": { "host": "Host" - } + }, + "description": "Wenn du den Host leer l\u00e4sst, wird die Erkennung verwendet, um Ger\u00e4te zu finden." } } }, diff --git a/homeassistant/components/yeelight/translations/tr.json b/homeassistant/components/yeelight/translations/tr.json new file mode 100644 index 00000000000..322f13f47b0 --- /dev/null +++ b/homeassistant/components/yeelight/translations/tr.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "pick_device": { + "data": { + "device": "Cihaz" + } + }, + "user": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "Model (Opsiyonel)", + "save_on_change": "De\u011fi\u015fiklikte Durumu Kaydet", + "transition": "Ge\u00e7i\u015f S\u00fcresi (ms)", + "use_music_mode": "M\u00fczik Modunu Etkinle\u015ftir" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/uk.json b/homeassistant/components/yeelight/translations/uk.json new file mode 100644 index 00000000000..0a173ccb6e4 --- /dev/null +++ b/homeassistant/components/yeelight/translations/uk.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "pick_device": { + "data": { + "device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u042f\u043a\u0449\u043e \u043d\u0435 \u0432\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u0430\u0434\u0440\u0435\u0441\u0443 \u0445\u043e\u0441\u0442\u0430, \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u0431\u0443\u0434\u0443\u0442\u044c \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u0456 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "\u041c\u043e\u0434\u0435\u043b\u044c (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "nightlight_switch": "\u041f\u0435\u0440\u0435\u043c\u0438\u043a\u0430\u0447 \u0434\u043b\u044f \u043d\u0456\u0447\u043d\u0438\u043a\u0430", + "save_on_change": "\u0417\u0431\u0435\u0440\u0456\u0433\u0430\u0442\u0438 \u0441\u0442\u0430\u0442\u0443\u0441 \u043f\u0440\u0438 \u0437\u043c\u0456\u043d\u0456", + "transition": "\u0427\u0430\u0441 \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0443 (\u0432 \u043c\u0456\u043b\u0456\u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", + "use_music_mode": "\u041c\u0443\u0437\u0438\u0447\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c" + }, + "description": "\u042f\u043a\u0449\u043e \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u0432\u0438\u0431\u0440\u0430\u043d\u043e, \u0432\u043e\u043d\u0430 \u0431\u0443\u0434\u0435 \u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/tr.json b/homeassistant/components/zerproc/translations/tr.json index 49fa9545e94..3df15466f03 100644 --- a/homeassistant/components/zerproc/translations/tr.json +++ b/homeassistant/components/zerproc/translations/tr.json @@ -1,7 +1,13 @@ { "config": { "abort": { - "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/uk.json b/homeassistant/components/zerproc/translations/uk.json new file mode 100644 index 00000000000..292861e9129 --- /dev/null +++ b/homeassistant/components/zerproc/translations/uk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "step": { + "confirm": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/cs.json b/homeassistant/components/zha/translations/cs.json index 1ac4c7c2d61..cedf56d73c4 100644 --- a/homeassistant/components/zha/translations/cs.json +++ b/homeassistant/components/zha/translations/cs.json @@ -51,20 +51,20 @@ "device_rotated": "Za\u0159\u00edzen\u00ed oto\u010deno \"{subtype}\"", "device_shaken": "Za\u0159\u00edzen\u00ed se zat\u0159\u00e1slo", "device_tilted": "Za\u0159\u00edzen\u00ed naklon\u011bno", - "remote_button_alt_double_press": "Dvakr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\" (alternativn\u00ed re\u017eim)", + "remote_button_alt_double_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto dvakr\u00e1t (alternativn\u00ed re\u017eim)", "remote_button_alt_long_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\" po dlouh\u00e9m stisku (alternativn\u00ed re\u017eim)", - "remote_button_alt_quadruple_press": "\u010cty\u0159ikr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\" (alternativn\u00ed re\u017eim)", - "remote_button_alt_quintuple_press": "P\u011btkr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\" (alternativn\u00ed re\u017eim)", - "remote_button_alt_short_press": "Stiknuto tla\u010d\u00edtko \"{subtype}\" (alternativn\u00ed re\u017eim)", + "remote_button_alt_quadruple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto \u010dty\u0159ikr\u00e1t (alternativn\u00ed re\u017eim)", + "remote_button_alt_quintuple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto p\u011btkr\u00e1t (alternativn\u00ed re\u017eim)", + "remote_button_alt_short_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto (alternativn\u00ed re\u017eim)", "remote_button_alt_short_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\" (alternativn\u00ed re\u017eim)", - "remote_button_alt_triple_press": "T\u0159ikr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\" (alternativn\u00ed re\u017eim)", - "remote_button_double_press": "Dvakr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\"", + "remote_button_alt_triple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto t\u0159ikr\u00e1t (alternativn\u00ed re\u017eim)", + "remote_button_double_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto dvakr\u00e1t", "remote_button_long_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\" po dlouh\u00e9m stisku", - "remote_button_quadruple_press": "\u010cty\u0159ikr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\"", - "remote_button_quintuple_press": "P\u011btkr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\"", - "remote_button_short_press": "Stiknuto tla\u010d\u00edtko \"{subtype}\"", + "remote_button_quadruple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto \u010dty\u0159ikr\u00e1t", + "remote_button_quintuple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto p\u011btkr\u00e1t", + "remote_button_short_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto", "remote_button_short_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\"", - "remote_button_triple_press": "T\u0159ikr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\"" + "remote_button_triple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto t\u0159ikr\u00e1t" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index 592450fcfbc..61e9b8e37ba 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "single_instance_allowed": "Es ist nur eine einzige Konfiguration von ZHA zul\u00e4ssig." + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { - "cannot_connect": "Kein Verbindung zu ZHA-Ger\u00e4t m\u00f6glich" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "pick_radio": { diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index ab4402558ba..4cdada49f50 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -70,22 +70,22 @@ "device_shaken": "nast\u0105pi potrz\u0105\u015bni\u0119cie urz\u0105dzeniem", "device_slid": "nast\u0105pi przesuni\u0119cie urz\u0105dzenia \"{subtype}\"", "device_tilted": "nast\u0105pi przechylenie urz\u0105dzenia", - "remote_button_alt_double_press": "\"{subtype}\" dwukrotnie naci\u015bni\u0119ty (tryb alternatywny)", - "remote_button_alt_long_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y (tryb alternatywny)", - "remote_button_alt_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu (tryb alternatywny)", - "remote_button_alt_quadruple_press": "\"{subtype}\" czterokrotnie naci\u015bni\u0119ty (tryb alternatywny)", - "remote_button_alt_quintuple_press": "\"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty (tryb alternatywny)", - "remote_button_alt_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty (tryb alternatywny)", - "remote_button_alt_short_release": "\"{subtype}\" zostanie zwolniony (tryb alternatywny)", - "remote_button_alt_triple_press": "\"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty (tryb alternatywny)", - "remote_button_double_press": "\"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", - "remote_button_long_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", - "remote_button_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", - "remote_button_quadruple_press": "\"{subtype}\" czterokrotnie naci\u015bni\u0119ty", - "remote_button_quintuple_press": "\"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", - "remote_button_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty", - "remote_button_short_release": "\"{subtype}\" zostanie zwolniony", - "remote_button_triple_press": "\"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" + "remote_button_alt_double_press": "przycisk \"{subtype}\" zostanie dwukrotnie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_alt_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y (tryb alternatywny)", + "remote_button_alt_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu (tryb alternatywny)", + "remote_button_alt_quadruple_press": "przycisk \"{subtype}\" zostanie czterokrotnie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_alt_quintuple_press": "przycisk \"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_alt_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_alt_short_release": "przycisk \"{subtype}\" zostanie zwolniony (tryb alternatywny)", + "remote_button_alt_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_double_press": "przycisk \"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_quadruple_press": "przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", + "remote_button_quintuple_press": "przycisk \"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", + "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", + "remote_button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/tr.json b/homeassistant/components/zha/translations/tr.json new file mode 100644 index 00000000000..a74f56a2f4e --- /dev/null +++ b/homeassistant/components/zha/translations/tr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "pick_radio": { + "title": "Radyo Tipi" + }, + "port_config": { + "data": { + "path": "Seri cihaz yolu" + }, + "title": "Ayarlar" + } + } + }, + "device_automation": { + "trigger_type": { + "device_offline": "Cihaz \u00e7evrimd\u0131\u015f\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/uk.json b/homeassistant/components/zha/translations/uk.json new file mode 100644 index 00000000000..7bd62cf26e1 --- /dev/null +++ b/homeassistant/components/zha/translations/uk.json @@ -0,0 +1,91 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "pick_radio": { + "data": { + "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0456\u043e\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0438\u043f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Zigbee", + "title": "\u0422\u0438\u043f \u0440\u0430\u0434\u0456\u043e\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "port_config": { + "data": { + "baudrate": "\u0448\u0432\u0438\u0434\u043a\u0456\u0441\u0442\u044c \u043f\u043e\u0440\u0442\u0443", + "flow_control": "\u043a\u0435\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0442\u043e\u043a\u043e\u043c \u0434\u0430\u043d\u0438\u0445", + "path": "\u0428\u043b\u044f\u0445 \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "description": "\u0412\u043a\u0430\u0436\u0456\u0442\u044c \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0440\u0442\u0443", + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438" + }, + "user": { + "data": { + "path": "\u0428\u043b\u044f\u0445 \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u043e\u0441\u043b\u0456\u0434\u043e\u0432\u043d\u0438\u0439 \u043f\u043e\u0440\u0442 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u043e\u0440\u0430 \u043c\u0435\u0440\u0435\u0436\u0456 Zigbee", + "title": "Zigbee Home Automation" + } + } + }, + "device_automation": { + "action_type": { + "squawk": "\u0422\u0440\u0430\u043d\u0441\u043f\u043e\u043d\u0434\u0435\u0440", + "warn": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u044f \u043e\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u043d\u044f" + }, + "trigger_subtype": { + "both_buttons": "\u041e\u0431\u0438\u0434\u0432\u0456 \u043a\u043d\u043e\u043f\u043a\u0438", + "button_1": "\u041f\u0435\u0440\u0448\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_2": "\u0414\u0440\u0443\u0433\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_3": "\u0422\u0440\u0435\u0442\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_5": "\u041f'\u044f\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_6": "\u0428\u043e\u0441\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "close": "\u0417\u0430\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f", + "dim_down": "\u0417\u043c\u0435\u043d\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c", + "dim_up": "\u0417\u0431\u0456\u043b\u044c\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c", + "face_1": "\u041d\u0430 \u043f\u0435\u0440\u0448\u0456\u0439 \u0433\u0440\u0430\u043d\u0456", + "face_2": "\u041d\u0430 \u0434\u0440\u0443\u0433\u0438\u0439 \u0433\u0440\u0430\u043d\u0456", + "face_3": "\u041d\u0430 \u0442\u0440\u0435\u0442\u0456\u0439 \u0433\u0440\u0430\u043d\u0456", + "face_4": "\u041d\u0430 \u0447\u0435\u0442\u0432\u0435\u0440\u0442\u0456\u0439 \u0433\u0440\u0430\u043d\u0456", + "face_5": "\u041d\u0430 \u043f'\u044f\u0442\u0456\u0439 \u0433\u0440\u0430\u043d\u0456", + "face_6": "\u041d\u0430 \u0448\u043e\u0441\u0442\u0438\u0439 \u0433\u0440\u0430\u043d\u0456", + "face_any": "\u041d\u0430 \u0431\u0443\u0434\u044c-\u044f\u043a\u0456\u0439 \u0433\u0440\u0430\u043d\u0456", + "left": "\u041b\u0456\u0432\u043e\u0440\u0443\u0447", + "open": "\u0412\u0456\u0434\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f", + "right": "\u041f\u0440\u0430\u0432\u043e\u0440\u0443\u0447", + "turn_off": "\u0412\u0438\u043c\u043a\u043d\u0443\u0442\u0438", + "turn_on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438" + }, + "trigger_type": { + "device_dropped": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0441\u043a\u0438\u043d\u0443\u043b\u0438", + "device_flipped": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 {subtype}", + "device_knocked": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c \u043f\u043e\u0441\u0442\u0443\u043a\u0430\u043b\u0438 {subtype}", + "device_offline": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456", + "device_rotated": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u043b\u0438 {subtype}", + "device_shaken": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u043e\u0442\u0440\u044f\u0441\u043b\u0438", + "device_slid": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0437\u0440\u0443\u0448\u0438\u043b\u0438 {subtype}", + "device_tilted": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0430\u0445\u0438\u043b\u0438\u043b\u0438", + "remote_button_alt_double_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0438 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_long_press": "{subtype} \u0434\u043e\u0432\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_long_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u0434\u043e\u0432\u0433\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_quadruple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0447\u043e\u0442\u0438\u0440\u0438 \u0440\u0430\u0437\u0438 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_quintuple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u043f'\u044f\u0442\u044c \u0440\u0430\u0437\u0456\u0432 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_short_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_short_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u0434\u043e\u0432\u0433\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_triple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0438 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_double_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0438", + "remote_button_long_press": "{subtype} \u0434\u043e\u0432\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430", + "remote_button_long_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u0434\u043e\u0432\u0433\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f", + "remote_button_quadruple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0447\u043e\u0442\u0438\u0440\u0438 \u0440\u0430\u0437\u0438", + "remote_button_quintuple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u043f'\u044f\u0442\u044c \u0440\u0430\u0437\u0456\u0432", + "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430", + "remote_button_short_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f", + "remote_button_triple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zodiac/translations/sensor.tr.json b/homeassistant/components/zodiac/translations/sensor.tr.json new file mode 100644 index 00000000000..f9e0357799d --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.tr.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "Kova", + "aries": "Ko\u00e7", + "cancer": "Yenge\u00e7", + "capricorn": "O\u011flak", + "gemini": "Ikizler", + "leo": "Aslan", + "libra": "Terazi", + "pisces": "Bal\u0131k", + "sagittarius": "Yay", + "scorpio": "Akrep", + "taurus": "Bo\u011fa", + "virgo": "Ba\u015fak" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zodiac/translations/sensor.uk.json b/homeassistant/components/zodiac/translations/sensor.uk.json new file mode 100644 index 00000000000..e0c891a8b23 --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.uk.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "\u0412\u043e\u0434\u043e\u043b\u0456\u0439", + "aries": "\u041e\u0432\u0435\u043d", + "cancer": "\u0420\u0430\u043a", + "capricorn": "\u041a\u043e\u0437\u0435\u0440\u0456\u0433", + "gemini": "\u0411\u043b\u0438\u0437\u043d\u044e\u043a\u0438", + "leo": "\u041b\u0435\u0432", + "libra": "\u0422\u0435\u0440\u0435\u0437\u0438", + "pisces": "\u0420\u0438\u0431\u0438", + "sagittarius": "\u0421\u0442\u0440\u0456\u043b\u0435\u0446\u044c", + "scorpio": "\u0421\u043a\u043e\u0440\u043f\u0456\u043e\u043d", + "taurus": "\u0422\u0435\u043b\u0435\u0446\u044c", + "virgo": "\u0414\u0456\u0432\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/translations/tr.json b/homeassistant/components/zone/translations/tr.json new file mode 100644 index 00000000000..dad65ac92a7 --- /dev/null +++ b/homeassistant/components/zone/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "init": { + "data": { + "latitude": "Enlem", + "longitude": "Boylam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/de.json b/homeassistant/components/zoneminder/translations/de.json index 1362dcbd62d..5fa5d0a5234 100644 --- a/homeassistant/components/zoneminder/translations/de.json +++ b/homeassistant/components/zoneminder/translations/de.json @@ -1,10 +1,19 @@ { "config": { + "abort": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, "flow_title": "ZoneMinder", "step": { "user": { "data": { "password": "Passwort", + "ssl": "Nutzt ein SSL-Zertifikat", "username": "Benutzername", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" } diff --git a/homeassistant/components/zoneminder/translations/tr.json b/homeassistant/components/zoneminder/translations/tr.json new file mode 100644 index 00000000000..971f8cc9bd7 --- /dev/null +++ b/homeassistant/components/zoneminder/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "auth_fail": "Kullan\u0131c\u0131 ad\u0131 veya \u015fifre yanl\u0131\u015f.", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "error": { + "auth_fail": "Kullan\u0131c\u0131 ad\u0131 veya \u015fifre yanl\u0131\u015f.", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/uk.json b/homeassistant/components/zoneminder/translations/uk.json new file mode 100644 index 00000000000..e5b04ae124f --- /dev/null +++ b/homeassistant/components/zoneminder/translations/uk.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "auth_fail": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u043b\u043e\u0433\u0456\u043d \u0430\u0431\u043e \u043f\u0430\u0440\u043e\u043b\u044c.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "connection_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 ZoneMinder.", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "create_entry": { + "default": "\u0414\u043e\u0434\u0430\u043d\u043e \u0441\u0435\u0440\u0432\u0435\u0440 ZoneMinder." + }, + "error": { + "auth_fail": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u043b\u043e\u0433\u0456\u043d \u0430\u0431\u043e \u043f\u0430\u0440\u043e\u043b\u044c.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "connection_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 ZoneMinder.", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "flow_title": "ZoneMinder", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442 \u0456 \u043f\u043e\u0440\u0442 (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: 10.10.0.4:8010)", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "path": "\u0428\u043b\u044f\u0445 \u0434\u043e ZM", + "path_zms": "\u0428\u043b\u044f\u0445 \u0434\u043e ZMS", + "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" + }, + "title": "ZoneMinder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/de.json b/homeassistant/components/zwave/translations/de.json index 60b5aa88024..f592c2243ac 100644 --- a/homeassistant/components/zwave/translations/de.json +++ b/homeassistant/components/zwave/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Z-Wave ist bereits konfiguriert" + "already_configured": "Z-Wave ist bereits konfiguriert", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { "option_error": "Z-Wave-Validierung fehlgeschlagen. Ist der Pfad zum USB-Stick korrekt?" diff --git a/homeassistant/components/zwave/translations/tr.json b/homeassistant/components/zwave/translations/tr.json index 3938868d280..383ccc6cc4f 100644 --- a/homeassistant/components/zwave/translations/tr.json +++ b/homeassistant/components/zwave/translations/tr.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/zwave/translations/uk.json b/homeassistant/components/zwave/translations/uk.json index d00986cae58..5cdd6060cc4 100644 --- a/homeassistant/components/zwave/translations/uk.json +++ b/homeassistant/components/zwave/translations/uk.json @@ -1,14 +1,33 @@ { + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "error": { + "option_error": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0438 Z-Wave. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0448\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e." + }, + "step": { + "user": { + "data": { + "network_key": "\u041a\u043b\u044e\u0447 \u043c\u0435\u0440\u0435\u0436\u0456 (\u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438)", + "usb_path": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", + "title": "Z-Wave" + } + } + }, "state": { "_": { - "dead": "\u041d\u0435\u0440\u043e\u0431\u043e\u0447\u0430", + "dead": "\u041d\u0435\u0441\u043f\u0440\u0430\u0432\u043d\u0438\u0439", "initializing": "\u0406\u043d\u0456\u0446\u0456\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u044f", "ready": "\u0413\u043e\u0442\u043e\u0432\u0438\u0439", - "sleeping": "\u0421\u043f\u043b\u044f\u0447\u043a\u0430" + "sleeping": "\u0420\u0435\u0436\u0438\u043c \u0441\u043d\u0443" }, "query_stage": { - "dead": "\u041d\u0435\u0440\u043e\u0431\u043e\u0447\u0430 ({query_stage})", - "initializing": "\u0406\u043d\u0456\u0446\u0456\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u044f ( {query_stage} )" + "dead": "\u041d\u0435\u0441\u043f\u0440\u0430\u0432\u043d\u0438\u0439", + "initializing": "\u0406\u043d\u0456\u0446\u0456\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u044f" } } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/ca.json b/homeassistant/components/zwave_js/translations/ca.json new file mode 100644 index 00000000000..93ec53a644e --- /dev/null +++ b/homeassistant/components/zwave_js/translations/ca.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "addon_get_discovery_info_failed": "No s'ha pogut obtenir la informaci\u00f3 de descobriment del complement Z-Wave JS.", + "addon_info_failed": "No s'ha pogut obtenir la informaci\u00f3 del complement Z-Wave JS.", + "addon_install_failed": "No s'ha pogut instal\u00b7lar el complement Z-Wave JS.", + "addon_missing_discovery_info": "Falta la informaci\u00f3 de descobriment del complement Z-Wave JS.", + "addon_set_config_failed": "No s'ha pogut establir la configuraci\u00f3 de Z-Wave JS.", + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "error": { + "addon_start_failed": "No s'ha pogut iniciar el complement Z-Wave JS. Comprova la configuraci\u00f3.", + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_ws_url": "URL del websocket inv\u00e0lid", + "unknown": "Error inesperat" + }, + "progress": { + "install_addon": "Espera mentre finalitza la instal\u00b7laci\u00f3 del complement Z-Wave JS. Pot tardar uns quants minuts." + }, + "step": { + "hassio_confirm": { + "title": "Configura la integraci\u00f3 Z-Wave JS mitjan\u00e7ant el complement Z-Wave JS" + }, + "install_addon": { + "title": "Ha comen\u00e7at la instal\u00b7laci\u00f3 del complement Z-Wave JS" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Utilitza el complement Z-Wave JS Supervisor" + }, + "description": "Vols utilitzar el complement Supervisor de Z-Wave JS?", + "title": "Selecciona el m\u00e8tode de connexi\u00f3" + }, + "start_addon": { + "data": { + "network_key": "Clau de xarxa", + "usb_path": "Ruta del port USB del dispositiu" + }, + "title": "Introdueix la configuraci\u00f3 del complement Z-Wave JS" + }, + "user": { + "data": { + "url": "URL" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/cs.json b/homeassistant/components/zwave_js/translations/cs.json new file mode 100644 index 00000000000..96073b579ed --- /dev/null +++ b/homeassistant/components/zwave_js/translations/cs.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "manual": { + "data": { + "url": "URL" + } + }, + "start_addon": { + "data": { + "usb_path": "Cesta k USB za\u0159\u00edzen\u00ed" + } + }, + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/de.json b/homeassistant/components/zwave_js/translations/de.json new file mode 100644 index 00000000000..d4903bc8c6d --- /dev/null +++ b/homeassistant/components/zwave_js/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json index 4aa510df6be..977651a576b 100644 --- a/homeassistant/components/zwave_js/translations/en.json +++ b/homeassistant/components/zwave_js/translations/en.json @@ -44,6 +44,11 @@ "usb_path": "USB Device Path" }, "title": "Enter the Z-Wave JS add-on configuration" + }, + "user": { + "data": { + "url": "URL" + } } } }, diff --git a/homeassistant/components/zwave_js/translations/es.json b/homeassistant/components/zwave_js/translations/es.json new file mode 100644 index 00000000000..e5ee009c0d1 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/es.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "cannot_connect": "No se pudo conectar" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_ws_url": "URL de websocket no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "title": "Selecciona el m\u00e9todo de conexi\u00f3n" + }, + "start_addon": { + "data": { + "network_key": "Clave de red", + "usb_path": "Ruta del dispositivo USB" + } + }, + "user": { + "data": { + "url": "URL" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/et.json b/homeassistant/components/zwave_js/translations/et.json new file mode 100644 index 00000000000..7a7aadfb841 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/et.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "addon_get_discovery_info_failed": "Z-Wave JS lisandmooduli tuvastusteabe hankimine nurjus.", + "addon_info_failed": "Z-Wave JS lisandmooduli teabe hankimine nurjus.", + "addon_install_failed": "Z-Wave JS lisandmooduli paigaldamine nurjus.", + "addon_missing_discovery_info": "Z-Wave JS lisandmooduli tuvastusteave puudub.", + "addon_set_config_failed": "Z-Wave JS konfiguratsiooni m\u00e4\u00e4ramine nurjus.", + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "cannot_connect": "\u00dchendamine nurjus" + }, + "error": { + "addon_start_failed": "Z-Wave JS lisandmooduli k\u00e4ivitamine nurjus. Kontrolli seadistusi.", + "cannot_connect": "\u00dchendamine nurjus", + "invalid_ws_url": "Vale sihtkoha aadress", + "unknown": "Ootamatu t\u00f5rge" + }, + "progress": { + "install_addon": "Palun oota kuni Z-Wave JS lisandmoodul on paigaldatud. See v\u00f5ib v\u00f5tta mitu minutit." + }, + "step": { + "hassio_confirm": { + "title": "Seadista Z-Wave JS-i sidumine Z-Wave JS-i lisandmooduliga" + }, + "install_addon": { + "title": "Z-Wave JS lisandmooduli paigaldamine on alanud" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Kasuta lisandmoodulit Z-Wave JS Supervisor" + }, + "description": "Kas soovid kasutada Z-Wave JSi halduri lisandmoodulit?", + "title": "Vali \u00fchendusviis" + }, + "start_addon": { + "data": { + "network_key": "V\u00f5rgu v\u00f5ti", + "usb_path": "USB-seadme asukoha rada" + }, + "title": "Sisesta Z-Wave JS lisandmooduli seaded" + }, + "user": { + "data": { + "url": "" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/fr.json b/homeassistant/components/zwave_js/translations/fr.json new file mode 100644 index 00000000000..f3a9aff1a29 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Le p\u00e9riph\u00e9rique est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Erreur de connection", + "invalid_ws_url": "URL websocket invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/it.json b/homeassistant/components/zwave_js/translations/it.json new file mode 100644 index 00000000000..fc76b309a34 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/it.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "addon_get_discovery_info_failed": "Impossibile ottenere le informazioni sul rilevamento del componente aggiuntivo Z-Wave JS.", + "addon_info_failed": "Impossibile ottenere le informazioni sul componente aggiuntivo Z-Wave JS.", + "addon_install_failed": "Impossibile installare il componente aggiuntivo Z-Wave JS.", + "addon_missing_discovery_info": "Informazioni sul rilevamento del componente aggiuntivo Z-Wave JS mancanti.", + "addon_set_config_failed": "Impossibile impostare la configurazione di Z-Wave JS.", + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "cannot_connect": "Impossibile connettersi" + }, + "error": { + "addon_start_failed": "Impossibile avviare il componente aggiuntivo Z-Wave JS. Controlla la configurazione.", + "cannot_connect": "Impossibile connettersi", + "invalid_ws_url": "URL websocket non valido", + "unknown": "Errore imprevisto" + }, + "progress": { + "install_addon": "Attendi il termine dell'installazione del componente aggiuntivo Z-Wave JS. Questa operazione pu\u00f2 richiedere diversi minuti." + }, + "step": { + "hassio_confirm": { + "title": "Configura l'integrazione di Z-Wave JS con il componente aggiuntivo Z-Wave JS" + }, + "install_addon": { + "title": "L'installazione del componente aggiuntivo Z-Wave JS \u00e8 iniziata" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Usa il componente aggiuntivo Z-Wave JS Supervisor" + }, + "description": "Desideri utilizzare il componente aggiuntivo Z-Wave JS Supervisor?", + "title": "Seleziona il metodo di connessione" + }, + "start_addon": { + "data": { + "network_key": "Chiave di rete", + "usb_path": "Percorso del dispositivo USB" + }, + "title": "Accedi alla configurazione del componente aggiuntivo Z-Wave JS" + }, + "user": { + "data": { + "url": "URL" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/lb.json b/homeassistant/components/zwave_js/translations/lb.json new file mode 100644 index 00000000000..302addbd7cf --- /dev/null +++ b/homeassistant/components/zwave_js/translations/lb.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_ws_url": "Ong\u00eblteg Websocket URL", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/no.json b/homeassistant/components/zwave_js/translations/no.json new file mode 100644 index 00000000000..e16425b59ec --- /dev/null +++ b/homeassistant/components/zwave_js/translations/no.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "addon_get_discovery_info_failed": "Kunne ikke hente oppdagelsesinformasjon om Z-Wave JS-tillegg", + "addon_info_failed": "Kunne ikke hente informasjon om Z-Wave JS-tillegg", + "addon_install_failed": "Kunne ikke installere Z-Wave JS-tillegg", + "addon_missing_discovery_info": "Manglende oppdagelsesinformasjon for Z-Wave JS-tillegg", + "addon_set_config_failed": "Kunne ikke angi Z-Wave JS-konfigurasjon", + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "cannot_connect": "Tilkobling mislyktes" + }, + "error": { + "addon_start_failed": "Kunne ikke starte Z-Wave JS-tillegg. Sjekk konfigurasjonen.", + "cannot_connect": "Tilkobling mislyktes", + "invalid_ws_url": "Ugyldig websocket URL", + "unknown": "Uventet feil" + }, + "progress": { + "install_addon": "Vent mens installasjonen av Z-Wave JS-tillegg er ferdig. Dette kan ta flere minutter." + }, + "step": { + "hassio_confirm": { + "title": "Sett opp Z-Wave JS-integrasjon med Z-Wave JS-tillegg" + }, + "install_addon": { + "title": "Installasjon av Z-Wave JS-tillegg har startet" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Bruk Z-Wave JS Supervisor-tillegg" + }, + "description": "Vil du bruke Z-Wave JS Supervisor-tillegg?", + "title": "Velg tilkoblingsmetode" + }, + "start_addon": { + "data": { + "network_key": "Nettverksn\u00f8kkel", + "usb_path": "USB enhetsbane" + }, + "title": "Angi konfigurasjon for Z-Wave JS-tillegg" + }, + "user": { + "data": { + "url": "URL" + } + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/pl.json b/homeassistant/components/zwave_js/translations/pl.json new file mode 100644 index 00000000000..47e263c6101 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/pl.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "addon_get_discovery_info_failed": "Nie uda\u0142o si\u0119 uzyska\u0107 informacji wykrywania dodatku Z-Wave JS", + "addon_info_failed": "Nie uda\u0142o si\u0119 uzyska\u0107 informacji o dodatku Z-Wave JS", + "addon_install_failed": "Nie uda\u0142o si\u0119 zainstalowa\u0107 dodatku Z-Wave JS", + "addon_missing_discovery_info": "Brak informacji wykrywania dodatku Z-Wave JS", + "addon_set_config_failed": "Nie uda\u0142o si\u0119 skonfigurowa\u0107 Z-Wave JS", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "error": { + "addon_start_failed": "Nie uda\u0142o si\u0119 uruchomi\u0107 dodatku Z-Wave JS. Sprawd\u017a konfiguracj\u0119", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_ws_url": "Nieprawid\u0142owy URL websocket", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "progress": { + "install_addon": "Poczekaj, a\u017c zako\u0144czy si\u0119 instalacja dodatku Z-Wave JS. Mo\u017ce to zaj\u0105\u0107 kilka minut." + }, + "step": { + "hassio_confirm": { + "title": "Skonfiguruj integracj\u0119 Z-Wave JS z dodatkiem Z-Wave JS" + }, + "install_addon": { + "title": "Rozpocz\u0119\u0142a si\u0119 instalacja dodatku Z-Wave JS" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "U\u017cyj dodatku Z-Wave JS Supervisor" + }, + "description": "Czy chcesz skorzysta\u0107 z dodatku Z-Wave JS Supervisor?", + "title": "Wybierz metod\u0119 po\u0142\u0105czenia" + }, + "start_addon": { + "data": { + "network_key": "Klucz sieci", + "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" + }, + "title": "Wprowad\u017a konfiguracj\u0119 dodatku Z-Wave JS" + }, + "user": { + "data": { + "url": "URL" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/pt-BR.json b/homeassistant/components/zwave_js/translations/pt-BR.json new file mode 100644 index 00000000000..e29d809ebff --- /dev/null +++ b/homeassistant/components/zwave_js/translations/pt-BR.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/ru.json b/homeassistant/components/zwave_js/translations/ru.json new file mode 100644 index 00000000000..2d9609e9d00 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/ru.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "addon_get_discovery_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\u0431 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0438 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS.", + "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 Z-Wave JS.", + "addon_install_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS.", + "addon_missing_discovery_info": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0438 Z-Wave JS.", + "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 Z-Wave JS.", + "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.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "error": { + "addon_start_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_ws_url": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "progress": { + "install_addon": "\u041f\u043e\u0434\u043e\u0436\u0434\u0438\u0442\u0435, \u043f\u043e\u043a\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS. \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 Z-Wave JS (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant Z-Wave JS)" + }, + "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 Z-Wave JS" + }, + "manual": { + "data": { + "url": "URL-\u0430\u0434\u0440\u0435\u0441" + } + }, + "on_supervisor": { + "data": { + "use_addon": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Supervisor Z-Wave JS" + }, + "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Supervisor Z-Wave JS?", + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" + }, + "start_addon": { + "data": { + "network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438", + "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, + "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS" + }, + "user": { + "data": { + "url": "URL-\u0430\u0434\u0440\u0435\u0441" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/tr.json b/homeassistant/components/zwave_js/translations/tr.json new file mode 100644 index 00000000000..2faa8ba4307 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/tr.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "addon_get_discovery_info_failed": "Z-Wave JS eklenti ke\u015fif bilgileri al\u0131namad\u0131.", + "addon_info_failed": "Z-Wave JS eklenti bilgileri al\u0131namad\u0131.", + "addon_install_failed": "Z-Wave JS eklentisi y\u00fcklenemedi.", + "addon_missing_discovery_info": "Eksik Z-Wave JS eklenti bulma bilgileri.", + "addon_set_config_failed": "Z-Wave JS yap\u0131land\u0131rmas\u0131 ayarlanamad\u0131.", + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "error": { + "addon_start_failed": "Z-Wave JS eklentisi ba\u015flat\u0131lamad\u0131. Yap\u0131land\u0131rmay\u0131 kontrol edin.", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_ws_url": "Ge\u00e7ersiz websocket URL'si", + "unknown": "Beklenmeyen hata" + }, + "progress": { + "install_addon": "L\u00fctfen Z-Wave JS eklenti kurulumu bitene kadar bekleyin. Bu birka\u00e7 dakika s\u00fcrebilir." + }, + "step": { + "hassio_confirm": { + "title": "Z-Wave JS eklentisiyle Z-Wave JS entegrasyonunu ayarlay\u0131n" + }, + "install_addon": { + "title": "Z-Wave JS eklenti kurulumu ba\u015flad\u0131" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Z-Wave JS Supervisor eklentisini kullan\u0131n" + }, + "description": "Z-Wave JS Supervisor eklentisini kullanmak istiyor musunuz?", + "title": "Ba\u011flant\u0131 y\u00f6ntemini se\u00e7in" + }, + "start_addon": { + "data": { + "network_key": "A\u011f Anahtar\u0131", + "usb_path": "USB Ayg\u0131t Yolu" + }, + "title": "Z-Wave JS eklenti yap\u0131land\u0131rmas\u0131na girin" + }, + "user": { + "data": { + "url": "URL" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/uk.json b/homeassistant/components/zwave_js/translations/uk.json new file mode 100644 index 00000000000..f5ff5224347 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_ws_url": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0432\u0435\u0431-\u0441\u043e\u043a\u0435\u0442\u0430", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/zh-Hant.json b/homeassistant/components/zwave_js/translations/zh-Hant.json new file mode 100644 index 00000000000..1cbde8f886b --- /dev/null +++ b/homeassistant/components/zwave_js/translations/zh-Hant.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "addon_get_discovery_info_failed": "\u53d6\u5f97 Z-Wave JS add-on \u63a2\u7d22\u8cc7\u8a0a\u5931\u6557\u3002", + "addon_info_failed": "\u53d6\u5f97 Z-Wave JS add-on \u8cc7\u8a0a\u5931\u6557\u3002", + "addon_install_failed": "Z-Wave JS add-on \u5b89\u88dd\u5931\u6557\u3002", + "addon_missing_discovery_info": "\u7f3a\u5c11 Z-Wave JS add-on \u63a2\u7d22\u8cc7\u8a0a\u3002", + "addon_set_config_failed": "Z-Wave JS 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", + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "error": { + "addon_start_failed": "Z-Wave JS add-on \u555f\u52d5\u5931\u6557\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5b9a\u3002", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_ws_url": "Websocket URL \u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "progress": { + "install_addon": "\u8acb\u7a0d\u7b49 Z-Wave JS add-on \u5b89\u88dd\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002" + }, + "step": { + "hassio_confirm": { + "title": "\u4ee5 Z-Wave JS add-on \u8a2d\u5b9a Z-Wave JS \u6574\u5408" + }, + "install_addon": { + "title": "Z-Wave JS add-on \u5b89\u88dd\u5df2\u555f\u52d5" + }, + "manual": { + "data": { + "url": "\u7db2\u5740" + } + }, + "on_supervisor": { + "data": { + "use_addon": "\u4f7f\u7528 Z-Wave JS Supervisor add-on" + }, + "description": "\u662f\u5426\u8981\u4f7f\u7528 Z-Wave JS Supervisor add-on\uff1f", + "title": "\u9078\u64c7\u9023\u7dda\u985e\u578b" + }, + "start_addon": { + "data": { + "network_key": "\u7db2\u8def\u5bc6\u9470", + "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" + }, + "title": "\u8f38\u5165 Z-Wave JS \u9644\u52a0\u8a2d\u5b9a" + }, + "user": { + "data": { + "url": "\u7db2\u5740" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file From a2ec1a47d550c01087ba4daa7cec58c4c5b0cab4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 3 Feb 2021 11:54:00 +0100 Subject: [PATCH 154/796] Mark Z-Wave as deprecated (#45896) --- homeassistant/components/zwave/manifest.json | 2 +- homeassistant/components/zwave/strings.json | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave/manifest.json b/homeassistant/components/zwave/manifest.json index a3a2b5e0d83..6623036d2fe 100644 --- a/homeassistant/components/zwave/manifest.json +++ b/homeassistant/components/zwave/manifest.json @@ -1,6 +1,6 @@ { "domain": "zwave", - "name": "Z-Wave", + "name": "Z-Wave (deprecated)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave", "requirements": ["homeassistant-pyozw==0.1.10", "pydispatcher==2.0.5"], diff --git a/homeassistant/components/zwave/strings.json b/homeassistant/components/zwave/strings.json index 852b8ca22fa..69401b171e2 100644 --- a/homeassistant/components/zwave/strings.json +++ b/homeassistant/components/zwave/strings.json @@ -2,8 +2,7 @@ "config": { "step": { "user": { - "title": "Set up Z-Wave", - "description": "See https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables", + "description": "This integration is no longer maintained. For new installations, use Z-Wave JS instead.\n\nSee https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables", "data": { "usb_path": "[%key:common::config_flow::data::usb_path%]", "network_key": "Network Key (leave blank to auto-generate)" From 40ba182144254f172875e3a6f4004cda9f8e0614 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 3 Feb 2021 11:58:46 +0100 Subject: [PATCH 155/796] Upgrade Z-Wave JS Python to 0.17.0 (#45895) --- homeassistant/components/zwave_js/__init__.py | 23 ++++++++++++------- homeassistant/components/zwave_js/api.py | 2 +- .../components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/conftest.py | 4 ++-- 6 files changed, 21 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 1a2cdfa7017..01b8f4785c5 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -45,7 +45,7 @@ from .const import ( from .discovery import async_discover_values from .entity import get_device_id -LOGGER = logging.getLogger(__name__) +LOGGER = logging.getLogger(__package__) CONNECT_TIMEOUT = 10 DATA_CLIENT_LISTEN_TASK = "client_listen_task" DATA_START_PLATFORM_TASK = "start_platform_task" @@ -263,13 +263,20 @@ async def client_listen( driver_ready: asyncio.Event, ) -> None: """Listen with the client.""" + should_reload = True try: await client.listen(driver_ready) + except asyncio.CancelledError: + should_reload = False except BaseZwaveJSServerError: - # The entry needs to be reloaded since a new driver state - # will be acquired on reconnect. - # All model instances will be replaced when the new state is acquired. - hass.async_create_task(hass.config_entries.async_reload(entry.entry_id)) + pass + + # The entry needs to be reloaded since a new driver state + # will be acquired on reconnect. + # All model instances will be replaced when the new state is acquired. + if should_reload: + LOGGER.info("Disconnected from server. Reloading integration") + asyncio.create_task(hass.config_entries.async_reload(entry.entry_id)) async def disconnect_client( @@ -280,14 +287,14 @@ async def disconnect_client( platform_task: asyncio.Task, ) -> None: """Disconnect client.""" - await client.disconnect() - listen_task.cancel() platform_task.cancel() await asyncio.gather(listen_task, platform_task) - LOGGER.info("Disconnected from Zwave JS Server") + if client.connected: + await client.disconnect() + LOGGER.info("Disconnected from Zwave JS Server") async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 1a8a197571b..03a917217a9 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -51,7 +51,7 @@ def websocket_network_status( data = { "client": { "ws_server_url": client.ws_server_url, - "state": client.state, + "state": "connected" if client.connected else "disconnected", "driver_version": client.version.driver_version, "server_version": client.version.server_version, }, diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index de77ebbf5e0..7df75d7aed2 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.16.0"], + "requirements": ["zwave-js-server-python==0.17.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"] } diff --git a/requirements_all.txt b/requirements_all.txt index 01d6fe0a92a..c25ea05aa33 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2384,4 +2384,4 @@ zigpy==0.32.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.16.0 +zwave-js-server-python==0.17.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 95eab6a7440..b156395df13 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1203,4 +1203,4 @@ zigpy-znp==0.3.0 zigpy==0.32.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.16.0 +zwave-js-server-python==0.17.0 diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index b5301f4cd2f..984ec42b9f3 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -157,14 +157,14 @@ def mock_client_fixture(controller_state, version_state): async def connect(): await asyncio.sleep(0) - client.state = "connected" client.connected = True async def listen(driver_ready: asyncio.Event) -> None: driver_ready.set() + await asyncio.sleep(30) + assert False, "Listen wasn't canceled!" async def disconnect(): - client.state = "disconnected" client.connected = False client.connect = AsyncMock(side_effect=connect) From eaa9fff3ba00ee3125da295b8ae9cee4c69bd33f Mon Sep 17 00:00:00 2001 From: Jesse Campbell Date: Wed, 3 Feb 2021 06:02:49 -0500 Subject: [PATCH 156/796] Remove v4 multilevel transitional currentValue workaround in zwave_js (#45884) * Remove v4 multilevel transitional currentValue workaround This was only needed because the get-after-set was reporting a transitional currentValue instead of the final one. zwave-js v6.1.1 removes the get-after-set functionality completely, so this is no longer required (and breaks status reporting entirely) * Fix tests to check currentValue instead of targetValue as well --- homeassistant/components/zwave_js/light.py | 8 -------- tests/components/zwave_js/conftest.py | 2 +- tests/components/zwave_js/test_light.py | 4 ++-- tests/fixtures/zwave_js/eaton_rf9640_dimmer_state.json | 4 ++-- 4 files changed, 5 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index acfa22e5847..dd444fdb40d 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -105,14 +105,6 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): Z-Wave multilevel switches use a range of [0, 99] to control brightness. """ - # prefer targetValue only if CC Version >= 4 - # otherwise use currentValue (pre V4 dimmers) - if ( - self._target_value - and self._target_value.value is not None - and self._target_value.cc_version >= 4 - ): - return round((self._target_value.value / 99) * 255) if self.info.primary_value.value is not None: return round((self.info.primary_value.value / 99) * 255) return 0 diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 984ec42b9f3..903de6d3bd5 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -88,7 +88,7 @@ def bulb_6_multi_color_state_fixture(): @pytest.fixture(name="eaton_rf9640_dimmer_state", scope="session") def eaton_rf9640_dimmer_state_fixture(): - """Load the bulb 6 multi-color node state fixture data.""" + """Load the eaton rf9640 dimmer node state fixture data.""" return json.loads(load_fixture("zwave_js/eaton_rf9640_dimmer_state.json")) diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py index a1b2318022b..b60c7281874 100644 --- a/tests/components/zwave_js/test_light.py +++ b/tests/components/zwave_js/test_light.py @@ -395,5 +395,5 @@ async def test_v4_dimmer_light(hass, client, eaton_rf9640_dimmer, integration): assert state assert state.state == STATE_ON - # the light should pick targetvalue which has zwave value 20 - assert state.attributes[ATTR_BRIGHTNESS] == 52 + # the light should pick currentvalue which has zwave value 22 + assert state.attributes[ATTR_BRIGHTNESS] == 57 diff --git a/tests/fixtures/zwave_js/eaton_rf9640_dimmer_state.json b/tests/fixtures/zwave_js/eaton_rf9640_dimmer_state.json index 38cbb63b1c6..0f2f45d01e3 100644 --- a/tests/fixtures/zwave_js/eaton_rf9640_dimmer_state.json +++ b/tests/fixtures/zwave_js/eaton_rf9640_dimmer_state.json @@ -124,7 +124,7 @@ "max": 99, "label": "Current value" }, - "value": 0, + "value": 22, "ccVersion": 4 }, { @@ -779,4 +779,4 @@ "ccVersion": 3 } ] -} \ No newline at end of file +} From 9998fe368480fa762f9ceda029a6594e859036bf Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 3 Feb 2021 13:08:00 +0100 Subject: [PATCH 157/796] Update discovery scheme for Meter CC in zwave_js integration (#45897) --- homeassistant/components/zwave_js/discovery.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 88717d9fc83..8c322b8e0e3 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -155,13 +155,22 @@ DISCOVERY_SCHEMAS = [ hint="numeric_sensor", command_class={ CommandClass.SENSOR_MULTILEVEL, - CommandClass.METER, CommandClass.SENSOR_ALARM, CommandClass.INDICATOR, CommandClass.BATTERY, }, type={"number"}, ), + # numeric sensors for Meter CC + ZWaveDiscoverySchema( + platform="sensor", + hint="numeric_sensor", + command_class={ + CommandClass.METER, + }, + type={"number"}, + property={"value"}, + ), # special list sensors (Notification CC) ZWaveDiscoverySchema( platform="sensor", From 5615ab4c2574998837a48caf55e37a8877c5e8ae Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 3 Feb 2021 13:59:19 +0100 Subject: [PATCH 158/796] Add support for climate setpoint thermostats to zwave_js (#45890) --- homeassistant/components/zwave_js/climate.py | 16 +- .../components/zwave_js/discovery.py | 14 + tests/components/zwave_js/conftest.py | 28 + tests/components/zwave_js/test_climate.py | 96 ++ .../zwave_js/climate_danfoss_lc_13_state.json | 368 +++++ .../zwave_js/climate_heatit_z_trm3_state.json | 1181 +++++++++++++++++ 6 files changed, 1699 insertions(+), 4 deletions(-) create mode 100644 tests/fixtures/zwave_js/climate_danfoss_lc_13_state.json create mode 100644 tests/fixtures/zwave_js/climate_heatit_z_trm3_state.json diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 417f5aa5e5d..b125c8bcd6a 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -5,6 +5,7 @@ from typing import Any, Callable, Dict, List, Optional from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import ( THERMOSTAT_CURRENT_TEMP_PROPERTY, + THERMOSTAT_MODE_PROPERTY, THERMOSTAT_MODE_SETPOINT_MAP, THERMOSTAT_MODES, THERMOSTAT_OPERATING_STATE_PROPERTY, @@ -119,7 +120,9 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): self._hvac_presets: Dict[str, Optional[int]] = {} self._unit_value: ZwaveValue = None - self._current_mode = self.info.primary_value + self._current_mode = self.get_zwave_value( + THERMOSTAT_MODE_PROPERTY, command_class=CommandClass.THERMOSTAT_MODE + ) self._setpoint_values: Dict[ThermostatSetpointType, ZwaveValue] = {} for enum in ThermostatSetpointType: self._setpoint_values[enum] = self.get_zwave_value( @@ -165,10 +168,12 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): # Z-Wave uses one list for both modes and presets. # Iterate over all Z-Wave ThermostatModes and extract the hvac modes and presets. - current_mode = self._current_mode - if not current_mode: + if self._current_mode is None: + self._hvac_modes = { + ZW_HVAC_MODE_MAP[ThermostatMode.HEAT]: ThermostatMode.HEAT + } return - for mode_id, mode_name in current_mode.metadata.states.items(): + for mode_id, mode_name in self._current_mode.metadata.states.items(): mode_id = int(mode_id) if mode_id in THERMOSTAT_MODES: # treat value as hvac mode @@ -184,6 +189,9 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): @property def _current_mode_setpoint_enums(self) -> List[Optional[ThermostatSetpointType]]: """Return the list of enums that are relevant to the current thermostat mode.""" + if self._current_mode is None: + # Thermostat(valve) with no support for setting a mode is considered heating-only + return [ThermostatSetpointType.HEATING] return THERMOSTAT_MODE_SETPOINT_MAP.get(int(self._current_mode.value), []) # type: ignore @property diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 8c322b8e0e3..d741946a1c9 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -56,6 +56,8 @@ class ZWaveDiscoverySchema: type: Optional[Set[str]] = None +# For device class mapping see: +# https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/deviceClasses.json DISCOVERY_SCHEMAS = [ # locks ZWaveDiscoverySchema( @@ -105,6 +107,18 @@ DISCOVERY_SCHEMAS = [ property={"mode"}, type={"number"}, ), + # climate + # setpoint thermostats + ZWaveDiscoverySchema( + platform="climate", + device_class_generic={"Thermostat"}, + device_class_specific={ + "Setpoint Thermostat", + }, + command_class={CommandClass.THERMOSTAT_SETPOINT}, + property={"setpoint"}, + type={"number"}, + ), # lights # primary value is the currentValue (brightness) ZWaveDiscoverySchema( diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 903de6d3bd5..9cb950ba6e7 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -128,6 +128,18 @@ def climate_radio_thermostat_ct100_plus_different_endpoints_state_fixture(): ) +@pytest.fixture(name="climate_danfoss_lc_13_state", scope="session") +def climate_danfoss_lc_13_state_fixture(): + """Load the climate Danfoss (LC-13) electronic radiator thermostat node state fixture data.""" + return json.loads(load_fixture("zwave_js/climate_danfoss_lc_13_state.json")) + + +@pytest.fixture(name="climate_heatit_z_trm3_state", scope="session") +def climate_heatit_z_trm3_state_fixture(): + """Load the climate HEATIT Z-TRM3 thermostat node state fixture data.""" + return json.loads(load_fixture("zwave_js/climate_heatit_z_trm3_state.json")) + + @pytest.fixture(name="nortek_thermostat_state", scope="session") def nortek_thermostat_state_fixture(): """Load the nortek thermostat node state fixture data.""" @@ -254,6 +266,22 @@ def climate_radio_thermostat_ct100_plus_different_endpoints_fixture( return node +@pytest.fixture(name="climate_danfoss_lc_13") +def climate_danfoss_lc_13_fixture(client, climate_danfoss_lc_13_state): + """Mock a climate radio danfoss LC-13 node.""" + node = Node(client, climate_danfoss_lc_13_state) + client.driver.controller.nodes[node.node_id] = node + return node + + +@pytest.fixture(name="climate_heatit_z_trm3") +def climate_heatit_z_trm3_fixture(client, climate_heatit_z_trm3_state): + """Mock a climate radio HEATIT Z-TRM3 node.""" + node = Node(client, climate_heatit_z_trm3_state) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="nortek_thermostat") def nortek_thermostat_fixture(client, nortek_thermostat_state): """Mock a nortek thermostat node.""" diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index bede37e6959..b2455f3cbbd 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -26,6 +26,8 @@ from homeassistant.components.climate.const import ( from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE CLIMATE_RADIO_THERMOSTAT_ENTITY = "climate.z_wave_thermostat_thermostat_mode" +CLIMATE_DANFOSS_LC13_ENTITY = "climate.living_connect_z_thermostat_heating" +CLIMATE_FLOOR_THERMOSTAT_ENTITY = "climate.floor_thermostat_thermostat_mode" async def test_thermostat_v2( @@ -335,3 +337,97 @@ async def test_thermostat_different_endpoints( state = hass.states.get(CLIMATE_RADIO_THERMOSTAT_ENTITY) assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.5 + + +async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integration): + """Test a setpoint thermostat command class entity.""" + node = climate_danfoss_lc_13 + state = hass.states.get(CLIMATE_DANFOSS_LC13_ENTITY) + + assert state + assert state.state == HVAC_MODE_HEAT + assert state.attributes[ATTR_TEMPERATURE] == 25 + assert state.attributes[ATTR_HVAC_MODES] == [HVAC_MODE_HEAT] + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + + client.async_send_command.reset_mock() + + # Test setting temperature + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: CLIMATE_DANFOSS_LC13_ENTITY, + ATTR_TEMPERATURE: 21.5, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 5 + assert args["valueId"] == { + "endpoint": 0, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "unit": "\u00b0C", + "ccSpecific": {"setpointType": 1}, + }, + "value": 25, + } + assert args["value"] == 21.5 + + client.async_send_command.reset_mock() + + # Test setpoint mode update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 5, + "args": { + "commandClassName": "Thermostat Setpoint", + "commandClass": 67, + "endpoint": 0, + "property": "setpoint", + "propertyKey": 1, + "propertyKeyName": "Heating", + "propertyName": "setpoint", + "newValue": 23, + "prevValue": 21.5, + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(CLIMATE_DANFOSS_LC13_ENTITY) + assert state.state == HVAC_MODE_HEAT + assert state.attributes[ATTR_TEMPERATURE] == 23 + + client.async_send_command.reset_mock() + + +async def test_thermostat_heatit(hass, client, climate_heatit_z_trm3, integration): + """Test a thermostat v2 command class entity.""" + state = hass.states.get(CLIMATE_FLOOR_THERMOSTAT_ENTITY) + + assert state + assert state.state == HVAC_MODE_HEAT + assert state.attributes[ATTR_HVAC_MODES] == [ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + ] + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.9 + assert state.attributes[ATTR_TEMPERATURE] == 22.5 + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE diff --git a/tests/fixtures/zwave_js/climate_danfoss_lc_13_state.json b/tests/fixtures/zwave_js/climate_danfoss_lc_13_state.json new file mode 100644 index 00000000000..e218d3b6a0e --- /dev/null +++ b/tests/fixtures/zwave_js/climate_danfoss_lc_13_state.json @@ -0,0 +1,368 @@ +{ + "nodeId": 5, + "index": 0, + "status": 1, + "ready": true, + "deviceClass": { + "basic": "Routing Slave", + "generic": "Thermostat", + "specific": "Setpoint Thermostat", + "mandatorySupportedCCs": [ + "Manufacturer Specific", + "Multi Command", + "Thermostat Setpoint", + "Version" + ], + "mandatoryControlCCs": [] + }, + "isListening": false, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 2, + "productId": 4, + "productType": 5, + "firmwareVersion": "1.1", + "deviceConfig": { + "manufacturerId": 2, + "manufacturer": "Danfoss", + "label": "LC-13", + "description": "Living Connect Z Thermostat", + "devices": [ + { + "productType": "0x0005", + "productId": "0x0004" + }, + { + "productType": "0x8005", + "productId": "0x0001" + }, + { + "productType": "0x8005", + "productId": "0x0002" + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "associations": {}, + "compat": { + "valueIdRegex": {}, + "queryOnWakeup": [ + [ + "Battery", + "get" + ], + [ + "Thermostat Setpoint", + "get", + 1 + ] + ] + } + }, + "label": "LC-13", + "neighbors": [ + 1, + 14 + ], + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 5, + "index": 0 + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "unit": "\u00b0C", + "ccSpecific": { + "setpointType": 1 + } + }, + "value": 25 + }, + { + "endpoint": 0, + "commandClass": 70, + "commandClassName": "Climate Control Schedule", + "property": "changeCounter", + "propertyName": "changeCounter", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + }, + { + "endpoint": 0, + "commandClass": 70, + "commandClassName": "Climate Control Schedule", + "property": "overrideType", + "propertyName": "overrideType", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 70, + "commandClassName": "Climate Control Schedule", + "property": "overrideState", + "propertyName": "overrideState", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "Unused" + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 4 + }, + { + "endpoint": 0, + "commandClass": 117, + "commandClassName": "Protection", + "property": "local", + "propertyName": "local", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Local protection state", + "states": { + "0": "Unprotected", + "2": "NoOperationPossible" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 117, + "commandClassName": "Protection", + "property": "rf", + "propertyName": "rf", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "RF protection state", + "states": {} + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 117, + "commandClassName": "Protection", + "property": "exclusiveControlNodeId", + "propertyName": "exclusiveControlNodeId", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + }, + { + "endpoint": 0, + "commandClass": 117, + "commandClassName": "Protection", + "property": "timeout", + "propertyName": "timeout", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 100, + "unit": "%", + "label": "Battery level" + }, + "value": 53 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 132, + "commandClassName": "Wake Up", + "property": "wakeUpInterval", + "propertyName": "wakeUpInterval", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "min": 60, + "max": 1800, + "label": "Wake Up interval", + "steps": 60, + "default": 300 + }, + "value": 300 + }, + { + "endpoint": 0, + "commandClass": 132, + "commandClassName": "Wake Up", + "property": "controllerNodeId", + "propertyName": "controllerNodeId", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Node ID of the controller" + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 6 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "3.67" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "1.1" + ] + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/zwave_js/climate_heatit_z_trm3_state.json b/tests/fixtures/zwave_js/climate_heatit_z_trm3_state.json new file mode 100644 index 00000000000..066811c7374 --- /dev/null +++ b/tests/fixtures/zwave_js/climate_heatit_z_trm3_state.json @@ -0,0 +1,1181 @@ +{ + "nodeId": 24, + "index": 0, + "installerIcon": 4608, + "userIcon": 4609, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Routing Slave", + "generic": "Thermostat", + "specific": "Thermostat General V2", + "mandatorySupportedCCs": [ + "Basic", + "Manufacturer Specific", + "Thermostat Mode", + "Thermostat Setpoint", + "Version" + ], + "mandatoryControlCCs": [] + }, + "isListening": true, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 411, + "productId": 515, + "productType": 3, + "firmwareVersion": "4.0", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 5, + "deviceConfig": { + "manufacturerId": 411, + "manufacturer": "ThermoFloor", + "label": "Heatit Z-TRM3", + "description": "Floor thermostat", + "devices": [ + { + "productType": "0x0003", + "productId": "0x0203" + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + }, + "compat": { + "valueIdRegex": {}, + "overrideFloatEncoding": { + "size": 2 + }, + "addCCs": {} + } + }, + "label": "Heatit Z-TRM3", + "neighbors": [ + 1, + 2, + 3, + 4, + 6, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 17, + 18, + 19, + 25, + 26, + 28 + ], + "endpointCountIsDynamic": false, + "endpointsHaveIdenticalCapabilities": false, + "individualEndpointCount": 4, + "aggregatedEndpointCount": 0, + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 24, + "index": 0, + "installerIcon": 4608, + "userIcon": 4609 + }, + { + "nodeId": 24, + "index": 1, + "installerIcon": 4608, + "userIcon": 4609 + }, + { + "nodeId": 24, + "index": 2, + "installerIcon": 3328, + "userIcon": 3329 + }, + { + "nodeId": 24, + "index": 3, + "installerIcon": 3328, + "userIcon": 3329 + }, + { + "nodeId": 24, + "index": 4, + "installerIcon": 3328, + "userIcon": 3329 + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "param001", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "Sensor mode", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 4, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "F-mode, floor sensor mode", + "1": "A-mode, internal room sensor mode", + "2": "AF-mode, internal sensor and floor sensor mode", + "3": "A2-mode, external room sensor mode", + "4": "A2F-mode, external sensor with floor limitation" + }, + "label": "Sensor mode", + "description": "Sensor mode", + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Floor sensor type", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 5, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "10K-NTC", + "1": "12K-NTC", + "2": "15K-NTC", + "3": "22K-NTC", + "4": "33K-NTC", + "5": "47K-NTC" + }, + "label": "Floor sensor type", + "description": "Floor sensor type", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Temperature control hysteresis (DIFF I)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 3, + "max": 30, + "default": 5, + "format": 0, + "allowManualEntry": true, + "label": "Temperature control hysteresis (DIFF I)", + "description": "Temperature control hysteresis (DIFF I), 1 equals 0.1 \u00b0C", + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "Floor minimum temperature limit (FLo)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 50, + "max": 400, + "default": 50, + "format": 0, + "allowManualEntry": true, + "label": "Floor minimum temperature limit (FLo)", + "description": "Floor minimum temperature limit (FLo), 1 equals 0.1 \u00b0C", + "isFromConfig": true + }, + "value": 50 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyName": "Floor maximum temperature (FHi)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 50, + "max": 400, + "default": 400, + "format": 0, + "allowManualEntry": true, + "label": "Floor maximum temperature (FHi)", + "description": "Floor maximum temperature (FHi), 1 equals 0.1 \u00b0C", + "isFromConfig": true + }, + "value": 400 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "Air minimum temperature limit (ALo)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 50, + "max": 400, + "default": 50, + "format": 0, + "allowManualEntry": true, + "label": "Air minimum temperature limit (ALo)", + "description": "Air minimum temperature limit (ALo), 1 equals 0.1 \u00b0C", + "isFromConfig": true + }, + "value": 50 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyName": "Air maximum temperature limit (AHi)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 50, + "max": 400, + "default": 400, + "format": 0, + "allowManualEntry": true, + "label": "Air maximum temperature limit (AHi)", + "description": "Air maximum temperature limit (AHi), 1 equals 0.1 \u00b0C", + "isFromConfig": true + }, + "value": 400 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 9, + "propertyName": "param009", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 225 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "Room sensor calibration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": -60, + "max": 60, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Room sensor calibration", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 11, + "propertyName": "Floor sensor calibration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": -60, + "max": 60, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Floor sensor calibration", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 12, + "propertyName": "External sensor calibration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": -60, + "max": 60, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "External sensor calibration", + "isFromConfig": true + }, + "value": -42 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 13, + "propertyName": "Temperature display", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Display setpoint temperature", + "1": "Display calculated temperature" + }, + "label": "Temperature display", + "description": "Selects which temperature is shown on the display.", + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 14, + "propertyName": "Button brightness - dimmed state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 50, + "format": 0, + "allowManualEntry": true, + "label": "Button brightness - dimmed state", + "description": "Button brightness - dimmed state", + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 15, + "propertyName": "Button brightness - active state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 100, + "format": 0, + "allowManualEntry": true, + "label": "Button brightness - active state", + "description": "Button brightness - active state", + "isFromConfig": true + }, + "value": 50 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 16, + "propertyName": "Display brightness - dimmed state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 50, + "format": 0, + "allowManualEntry": true, + "label": "Display brightness - dimmed state", + "description": "Display brightness - dimmed state", + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 17, + "propertyName": "Display brightness - active state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 100, + "format": 0, + "allowManualEntry": true, + "label": "Display brightness - active state", + "description": "Display brightness - active state", + "isFromConfig": true + }, + "value": 50 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 18, + "propertyName": "Temperature report interval", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 32767, + "default": 60, + "format": 0, + "allowManualEntry": true, + "label": "Temperature report interval", + "description": "Temperature report interval", + "isFromConfig": true + }, + "value": 360 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 19, + "propertyName": "Temperature report hysteresis", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 100, + "default": 10, + "format": 0, + "allowManualEntry": true, + "label": "Temperature report hysteresis", + "description": "Temperature report hysteresis", + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 20, + "propertyName": "Meter report interval", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 32767, + "default": 90, + "format": 0, + "allowManualEntry": true, + "label": "Meter report interval", + "description": "Meter report interval", + "isFromConfig": true + }, + "value": 3600 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 21, + "propertyName": "Meter report delta value", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 10, + "format": 1, + "allowManualEntry": true, + "label": "Meter report delta value", + "description": "Meter report delta value", + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 411 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 515 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "6.7" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "4.0" + ] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "sdkVersion", + "propertyName": "sdkVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "6.81.6" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkAPIVersion", + "propertyName": "applicationFrameworkAPIVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "4.3.0" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkBuildNumber", + "propertyName": "applicationFrameworkBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 52445 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceVersion", + "propertyName": "hostInterfaceVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "unused" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceBuildNumber", + "propertyName": "hostInterfaceBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolVersion", + "propertyName": "zWaveProtocolVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "6.7.0" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolBuildNumber", + "propertyName": "zWaveProtocolBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 97 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationVersion", + "propertyName": "applicationVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "4.0.33" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationBuildNumber", + "propertyName": "applicationBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 52445 + }, + { + "endpoint": 1, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 5, + "max": 35, + "unit": "\u00b0C", + "ccSpecific": { + "setpointType": 1 + } + }, + "value": 22.5 + }, + { + "endpoint": 1, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "mode", + "propertyName": "mode", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 31, + "label": "Thermostat mode", + "states": { + "0": "Off", + "1": "Heat" + } + }, + "value": 1 + }, + { + "endpoint": 1, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "manufacturerData", + "propertyName": "manufacturerData", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + }, + { + "endpoint": 1, + "commandClass": 66, + "commandClassName": "Thermostat Operating State", + "property": "state", + "propertyName": "state", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Operating state", + "states": { + "0": "Idle", + "1": "Heating", + "2": "Cooling", + "3": "Fan Only", + "4": "Pending Heat", + "5": "Pending Cool", + "6": "Vent/Economizer", + "7": "Aux Heating", + "8": "2nd Stage Heating", + "9": "2nd Stage Cooling", + "10": "2nd Stage Aux Heat", + "11": "3rd Stage Aux Heat" + } + }, + "value": 0 + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyName": "value", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Value (Electric, Consumed)", + "unit": "kWh", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 0 + } + }, + "value": 369.2 + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "deltaTime", + "propertyName": "deltaTime", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Time since the previous reading", + "unit": "s", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 0 + } + }, + "value": 0 + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyName": "value", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Value (Electric, Consumed)", + "unit": "W", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 2 + } + }, + "value": 0.09 + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "deltaTime", + "propertyName": "deltaTime", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Time since the previous reading", + "unit": "s", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 2 + } + }, + "value": 0 + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyName": "value", + "propertyKeyName": "Electric_V_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Value (Electric, Consumed)", + "unit": "V", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 4 + } + }, + "value": 238 + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "deltaTime", + "propertyName": "deltaTime", + "propertyKeyName": "Electric_V_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Time since the previous reading", + "unit": "s", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 4 + } + }, + "value": 0 + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "previousValue", + "propertyName": "previousValue", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Previous value (Electric, Consumed)", + "unit": "kWh", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 0 + } + } + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "previousValue", + "propertyName": "previousValue", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Previous value (Electric, Consumed)", + "unit": "W", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 2 + } + } + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "previousValue", + "propertyName": "previousValue", + "propertyKeyName": "Electric_V_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Previous value (Electric, Consumed)", + "unit": "V", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 4 + } + } + }, + { + "endpoint": 2, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "\u00b0C", + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + } + }, + "value": 22.9 + }, + { + "endpoint": 3, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "\u00b0C", + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + } + }, + "value": 0 + }, + { + "endpoint": 4, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "\u00b0C", + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + } + }, + "value": 25.5 + } + ] +} \ No newline at end of file From 90973f471f991de9c270c39c540abd4d6770e2d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 3 Feb 2021 14:40:11 +0100 Subject: [PATCH 159/796] Add integration name to the deprecation warnings (#45901) --- homeassistant/helpers/deprecation.py | 30 +++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 0022f888829..7478a7fede9 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -4,6 +4,8 @@ import inspect import logging from typing import Any, Callable, Dict, Optional +from ..helpers.frame import MissingIntegrationFrame, get_integration_frame + def deprecated_substitute(substitute_name: str) -> Callable[..., Callable]: """Help migrate properties to new names. @@ -86,11 +88,29 @@ def deprecated_function(replacement: str) -> Callable[..., Callable]: def deprecated_func(*args: tuple, **kwargs: Dict[str, Any]) -> Any: """Wrap for the original function.""" logger = logging.getLogger(func.__module__) - logger.warning( - "%s is a deprecated function. Use %s instead", - func.__name__, - replacement, - ) + try: + _, integration, path = get_integration_frame() + if path == "custom_components/": + logger.warning( + "%s was called from %s, this is a deprecated function. Use %s instead, please report this to the maintainer of %s", + func.__name__, + integration, + replacement, + integration, + ) + else: + logger.warning( + "%s was called from %s, this is a deprecated function. Use %s instead", + func.__name__, + integration, + replacement, + ) + except MissingIntegrationFrame: + logger.warning( + "%s is a deprecated function. Use %s instead", + func.__name__, + replacement, + ) return func(*args, **kwargs) return deprecated_func From fcc14933d0526c3da5cb59053224368ebefcd4f1 Mon Sep 17 00:00:00 2001 From: Keith Lamprecht <1894492+Nixon506E@users.noreply.github.com> Date: Wed, 3 Feb 2021 09:42:52 -0500 Subject: [PATCH 160/796] Add transitiontime to hue scene service (#45785) --- homeassistant/components/hue/bridge.py | 15 +++++++++-- homeassistant/components/hue/const.py | 2 ++ tests/components/hue/test_bridge.py | 35 ++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 880d9abcc35..201e9f3a546 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -19,6 +19,7 @@ from .const import ( CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_HUE_GROUPS, DEFAULT_ALLOW_UNREACHABLE, + DEFAULT_SCENE_TRANSITION, LOGGER, ) from .errors import AuthenticationRequired, CannotConnect @@ -28,8 +29,15 @@ from .sensor_base import SensorManager SERVICE_HUE_SCENE = "hue_activate_scene" ATTR_GROUP_NAME = "group_name" ATTR_SCENE_NAME = "scene_name" +ATTR_TRANSITION = "transition" SCENE_SCHEMA = vol.Schema( - {vol.Required(ATTR_GROUP_NAME): cv.string, vol.Required(ATTR_SCENE_NAME): cv.string} + { + vol.Required(ATTR_GROUP_NAME): cv.string, + vol.Required(ATTR_SCENE_NAME): cv.string, + vol.Optional( + ATTR_TRANSITION, default=DEFAULT_SCENE_TRANSITION + ): cv.positive_int, + } ) # How long should we sleep if the hub is busy HUB_BUSY_SLEEP = 0.5 @@ -201,6 +209,7 @@ class HueBridge: """Service to call directly into bridge to set scenes.""" group_name = call.data[ATTR_GROUP_NAME] scene_name = call.data[ATTR_SCENE_NAME] + transition = call.data.get(ATTR_TRANSITION, DEFAULT_SCENE_TRANSITION) group = next( (group for group in self.api.groups.values() if group.name == group_name), @@ -236,7 +245,9 @@ class HueBridge: LOGGER.warning("Unable to find scene %s", scene_name) return False - return await self.async_request_call(partial(group.set_action, scene=scene.id)) + return await self.async_request_call( + partial(group.set_action, scene=scene.id, transitiontime=transition) + ) async def handle_unauthorized_error(self): """Create a new config flow when the authorization is no longer valid.""" diff --git a/homeassistant/components/hue/const.py b/homeassistant/components/hue/const.py index e2189515482..4fa11f2ad58 100644 --- a/homeassistant/components/hue/const.py +++ b/homeassistant/components/hue/const.py @@ -13,3 +13,5 @@ DEFAULT_ALLOW_UNREACHABLE = False CONF_ALLOW_HUE_GROUPS = "allow_hue_groups" DEFAULT_ALLOW_HUE_GROUPS = True + +DEFAULT_SCENE_TRANSITION = 4 diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 3e6465d6bc8..29bc2acf03a 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -192,6 +192,41 @@ async def test_hue_activate_scene(hass, mock_api): assert mock_api.mock_requests[2]["path"] == "groups/group_1/action" +async def test_hue_activate_scene_transition(hass, mock_api): + """Test successful hue_activate_scene with transition.""" + config_entry = config_entries.ConfigEntry( + 1, + hue.DOMAIN, + "Mock Title", + {"host": "mock-host", "username": "mock-username"}, + "test", + config_entries.CONN_CLASS_LOCAL_POLL, + system_options={}, + options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, + ) + hue_bridge = bridge.HueBridge(hass, config_entry) + + mock_api.mock_group_responses.append(GROUP_RESPONSE) + mock_api.mock_scene_responses.append(SCENE_RESPONSE) + + with patch("aiohue.Bridge", return_value=mock_api), patch.object( + hass.config_entries, "async_forward_entry_setup" + ): + assert await hue_bridge.async_setup() is True + + assert hue_bridge.api is mock_api + + call = Mock() + call.data = {"group_name": "Group 1", "scene_name": "Cozy dinner", "transition": 30} + with patch("aiohue.Bridge", return_value=mock_api): + assert await hue_bridge.hue_activate_scene(call) is None + + assert len(mock_api.mock_requests) == 3 + assert mock_api.mock_requests[2]["json"]["scene"] == "scene_1" + assert mock_api.mock_requests[2]["json"]["transitiontime"] == 30 + assert mock_api.mock_requests[2]["path"] == "groups/group_1/action" + + async def test_hue_activate_scene_group_not_found(hass, mock_api): """Test failed hue_activate_scene due to missing group.""" config_entry = config_entries.ConfigEntry( From 0875f654c800492fd093c7be6b006a8ffd972c2b Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 3 Feb 2021 18:03:22 +0200 Subject: [PATCH 161/796] Add support for Shelly battery operated devices (#45406) Co-authored-by: Paulus Schoutsen --- homeassistant/components/shelly/__init__.py | 178 +++++++++++------- .../components/shelly/binary_sensor.py | 42 ++++- .../components/shelly/config_flow.py | 23 ++- homeassistant/components/shelly/const.py | 5 +- homeassistant/components/shelly/entity.py | 135 ++++++++++++- homeassistant/components/shelly/manifest.json | 2 +- homeassistant/components/shelly/sensor.py | 30 ++- homeassistant/components/shelly/utils.py | 32 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/shelly/conftest.py | 7 +- tests/components/shelly/test_config_flow.py | 13 +- 12 files changed, 366 insertions(+), 105 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index d2df03b44a5..84bc73f3c0f 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -17,12 +17,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import ( - aiohttp_client, - device_registry, - singleton, - update_coordinator, -) +from homeassistant.helpers import aiohttp_client, device_registry, update_coordinator from .const import ( AIOSHELLY_DEVICE_TIMEOUT_SEC, @@ -32,36 +27,23 @@ from .const import ( BATTERY_DEVICES_WITH_PERMANENT_CONNECTION, COAP, DATA_CONFIG_ENTRY, + DEVICE, DOMAIN, EVENT_SHELLY_CLICK, INPUTS_EVENTS_DICT, - POLLING_TIMEOUT_MULTIPLIER, + POLLING_TIMEOUT_SEC, REST, REST_SENSORS_UPDATE_INTERVAL, SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, ) -from .utils import get_device_name +from .utils import get_coap_context, get_device_name, get_device_sleep_period PLATFORMS = ["binary_sensor", "cover", "light", "sensor", "switch"] +SLEEPING_PLATFORMS = ["binary_sensor", "sensor"] _LOGGER = logging.getLogger(__name__) -@singleton.singleton("shelly_coap") -async def get_coap_context(hass): - """Get CoAP context to be used in all Shelly devices.""" - context = aioshelly.COAP() - await context.initialize() - - @callback - def shutdown_listener(ev): - context.close() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_listener) - - return context - - async def async_setup(hass: HomeAssistant, config: dict): """Set up the Shelly component.""" hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}} @@ -70,6 +52,9 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Shelly from a config entry.""" + hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = {} + hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = None + temperature_unit = "C" if hass.config.units.is_metric else "F" ip_address = await hass.async_add_executor_job(gethostbyname, entry.data[CONF_HOST]) @@ -83,33 +68,79 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): coap_context = await get_coap_context(hass) - try: - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - device = await aioshelly.Device.create( - aiohttp_client.async_get_clientsession(hass), - coap_context, - options, - ) - except (asyncio.TimeoutError, OSError) as err: - raise ConfigEntryNotReady from err + device = await aioshelly.Device.create( + aiohttp_client.async_get_clientsession(hass), + coap_context, + options, + False, + ) - hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = {} - coap_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][ + dev_reg = await device_registry.async_get_registry(hass) + identifier = (DOMAIN, entry.unique_id) + device_entry = dev_reg.async_get_device(identifiers={identifier}, connections=set()) + + sleep_period = entry.data.get("sleep_period") + + @callback + def _async_device_online(_): + _LOGGER.debug("Device %s is online, resuming setup", entry.title) + hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = None + + if sleep_period is None: + data = {**entry.data} + data["sleep_period"] = get_device_sleep_period(device.settings) + data["model"] = device.settings["device"]["type"] + hass.config_entries.async_update_entry(entry, data=data) + + hass.async_create_task(async_device_setup(hass, entry, device)) + + if sleep_period == 0: + # Not a sleeping device, finish setup + _LOGGER.debug("Setting up online device %s", entry.title) + try: + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): + await device.initialize(True) + except (asyncio.TimeoutError, OSError) as err: + raise ConfigEntryNotReady from err + + await async_device_setup(hass, entry, device) + elif sleep_period is None or device_entry is None: + # Need to get sleep info or first time sleeping device setup, wait for device + hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = device + _LOGGER.debug( + "Setup for device %s will resume when device is online", entry.title + ) + device.subscribe_updates(_async_device_online) + else: + # Restore sensors for sleeping device + _LOGGER.debug("Setting up offline device %s", entry.title) + await async_device_setup(hass, entry, device) + + return True + + +async def async_device_setup( + hass: HomeAssistant, entry: ConfigEntry, device: aioshelly.Device +): + """Set up a device that is online.""" + device_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][ COAP ] = ShellyDeviceWrapper(hass, entry, device) - await coap_wrapper.async_setup() + await device_wrapper.async_setup() - hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][ - REST - ] = ShellyDeviceRestWrapper(hass, device) + platforms = SLEEPING_PLATFORMS - for component in PLATFORMS: + if not entry.data.get("sleep_period"): + hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][ + REST + ] = ShellyDeviceRestWrapper(hass, device) + platforms = PLATFORMS + + for component in platforms: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) ) - return True - class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): """Wrapper for a Shelly device with Home Assistant specific functions.""" @@ -117,43 +148,40 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): def __init__(self, hass, entry, device: aioshelly.Device): """Initialize the Shelly device wrapper.""" self.device_id = None - sleep_mode = device.settings.get("sleep_mode") + sleep_period = entry.data["sleep_period"] - if sleep_mode: - sleep_period = sleep_mode["period"] - if sleep_mode["unit"] == "h": - sleep_period *= 60 # hours to minutes - - update_interval = ( - SLEEP_PERIOD_MULTIPLIER * sleep_period * 60 - ) # minutes to seconds + if sleep_period: + update_interval = SLEEP_PERIOD_MULTIPLIER * sleep_period else: update_interval = ( UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] ) + device_name = get_device_name(device) if device.initialized else entry.title super().__init__( hass, _LOGGER, - name=get_device_name(device), + name=device_name, update_interval=timedelta(seconds=update_interval), ) self.hass = hass self.entry = entry self.device = device - self.device.subscribe_updates(self.async_set_updated_data) - - self._async_remove_input_events_handler = self.async_add_listener( - self._async_input_events_handler + self._async_remove_device_updates_handler = self.async_add_listener( + self._async_device_updates_handler ) - self._last_input_events_count = dict() + self._last_input_events_count = {} hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) @callback - def _async_input_events_handler(self): - """Handle device input events.""" + def _async_device_updates_handler(self): + """Handle device updates.""" + if not self.device.initialized: + return + + # Check for input events for block in self.device.blocks: if ( "inputEvent" not in block.sensor_ids @@ -192,13 +220,9 @@ 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( - POLLING_TIMEOUT_MULTIPLIER - * self.device.settings["coiot"]["update_period"] - ): + async with async_timeout.timeout(POLLING_TIMEOUT_SEC): return await self.device.update() except OSError as err: raise update_coordinator.UpdateFailed("Error fetching data") from err @@ -206,18 +230,17 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): @property def model(self): """Model of the device.""" - return self.device.settings["device"]["type"] + return self.entry.data["model"] @property def mac(self): """Mac address of the device.""" - return self.device.settings["device"]["mac"] + return self.entry.unique_id 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"] + sw_version = self.device.settings["fw"] if self.device.initialized else "" entry = dev_reg.async_get_or_create( config_entry_id=self.entry.entry_id, name=self.name, @@ -225,15 +248,16 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): # This is duplicate but otherwise via_device can't work identifiers={(DOMAIN, self.mac)}, manufacturer="Shelly", - model=aioshelly.MODEL_NAMES.get(model_type, model_type), - sw_version=self.device.settings["fw"], + model=aioshelly.MODEL_NAMES.get(self.model, self.model), + sw_version=sw_version, ) self.device_id = entry.id + self.device.subscribe_updates(self.async_set_updated_data) def shutdown(self): """Shutdown the wrapper.""" self.device.shutdown() - self._async_remove_input_events_handler() + self._async_remove_device_updates_handler() @callback def _handle_ha_stop(self, _): @@ -282,11 +306,23 @@ class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" + device = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id].get(DEVICE) + if device is not None: + # If device is present, device wrapper is not setup yet + device.shutdown() + return True + + platforms = SLEEPING_PLATFORMS + + if not entry.data.get("sleep_period"): + hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][REST] = None + platforms = PLATFORMS + unload_ok = all( await asyncio.gather( *[ hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + for component in platforms ] ) ) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index d53f089054a..8f99e6a7a6e 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_PROBLEM, DEVICE_CLASS_SMOKE, DEVICE_CLASS_VIBRATION, + STATE_ON, BinarySensorEntity, ) @@ -17,6 +18,7 @@ from .entity import ( RestAttributeDescription, ShellyBlockAttributeEntity, ShellyRestAttributeEntity, + ShellySleepingBlockAttributeEntity, async_setup_entry_attribute_entities, async_setup_entry_rest, ) @@ -98,13 +100,25 @@ REST_SENSORS = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up sensors for device.""" - await async_setup_entry_attribute_entities( - hass, config_entry, async_add_entities, SENSORS, ShellyBinarySensor - ) - - await async_setup_entry_rest( - hass, config_entry, async_add_entities, REST_SENSORS, ShellyRestBinarySensor - ) + if config_entry.data["sleep_period"]: + await async_setup_entry_attribute_entities( + hass, + config_entry, + async_add_entities, + SENSORS, + ShellySleepingBinarySensor, + ) + else: + await async_setup_entry_attribute_entities( + hass, config_entry, async_add_entities, SENSORS, ShellyBinarySensor + ) + await async_setup_entry_rest( + hass, + config_entry, + async_add_entities, + REST_SENSORS, + ShellyRestBinarySensor, + ) class ShellyBinarySensor(ShellyBlockAttributeEntity, BinarySensorEntity): @@ -123,3 +137,17 @@ class ShellyRestBinarySensor(ShellyRestAttributeEntity, BinarySensorEntity): def is_on(self): """Return true if REST sensor state is on.""" return bool(self.attribute_value) + + +class ShellySleepingBinarySensor( + ShellySleepingBlockAttributeEntity, BinarySensorEntity +): + """Represent a shelly sleeping binary sensor.""" + + @property + def is_on(self): + """Return true if sensor state is on.""" + if self.block is not None: + return bool(self.attribute_value) + + return self.last_state == STATE_ON diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index b47c76cbb7a..09fc477e512 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -17,9 +17,9 @@ from homeassistant.const import ( ) from homeassistant.helpers import aiohttp_client -from . import get_coap_context from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC from .const import DOMAIN # pylint:disable=unused-import +from .utils import get_coap_context, get_device_sleep_period _LOGGER = logging.getLogger(__name__) @@ -53,6 +53,8 @@ async def validate_input(hass: core.HomeAssistant, host, data): return { "title": device.settings["name"], "hostname": device.settings["device"]["hostname"], + "sleep_period": get_device_sleep_period(device.settings), + "model": device.settings["device"]["type"], } @@ -95,7 +97,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): else: return self.async_create_entry( title=device_info["title"] or device_info["hostname"], - data=user_input, + data={ + **user_input, + "sleep_period": device_info["sleep_period"], + "model": device_info["model"], + }, ) return self.async_show_form( @@ -121,7 +127,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): else: return self.async_create_entry( title=device_info["title"] or device_info["hostname"], - data={**user_input, CONF_HOST: self.host}, + data={ + **user_input, + CONF_HOST: self.host, + "sleep_period": device_info["sleep_period"], + "model": device_info["model"], + }, ) else: user_input = {} @@ -172,7 +183,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): else: return self.async_create_entry( title=device_info["title"] or device_info["hostname"], - data={"host": self.host}, + data={ + "host": self.host, + "sleep_period": device_info["sleep_period"], + "model": device_info["model"], + }, ) return self.async_show_form( diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index a5922d0b9c0..9d1c333b201 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -2,11 +2,12 @@ COAP = "coap" DATA_CONFIG_ENTRY = "config_entry" +DEVICE = "device" DOMAIN = "shelly" REST = "rest" -# Used to calculate the timeout in "_async_update_data" used for polling data from devices. -POLLING_TIMEOUT_MULTIPLIER = 1.2 +# Used in "_async_update_data" as timeout for polling data from devices. +POLLING_TIMEOUT_SEC = 18 # Refresh interval for REST sensors REST_SENSORS_UPDATE_INTERVAL = 60 diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index b4df2d486f8..b934a41728f 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -1,24 +1,49 @@ """Shelly entity helper.""" from dataclasses import dataclass +import logging from typing import Any, Callable, Optional, Union import aioshelly +from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback -from homeassistant.helpers import device_registry, entity, update_coordinator +from homeassistant.helpers import ( + device_registry, + entity, + entity_registry, + update_coordinator, +) +from homeassistant.helpers.restore_state import RestoreEntity from . import ShellyDeviceRestWrapper, ShellyDeviceWrapper from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN, REST from .utils import async_remove_shelly_entity, get_entity_name +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry_attribute_entities( hass, config_entry, async_add_entities, sensors, sensor_class ): - """Set up entities for block attributes.""" + """Set up entities for attributes.""" wrapper: ShellyDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ config_entry.entry_id ][COAP] + + if wrapper.device.initialized: + await async_setup_block_attribute_entities( + hass, async_add_entities, wrapper, sensors, sensor_class + ) + else: + await async_restore_block_attribute_entities( + hass, config_entry, async_add_entities, wrapper, sensor_class + ) + + +async def async_setup_block_attribute_entities( + hass, async_add_entities, wrapper, sensors, sensor_class +): + """Set up entities for block attributes.""" blocks = [] for block in wrapper.device.blocks: @@ -36,9 +61,7 @@ async def async_setup_entry_attribute_entities( wrapper.device.settings, block ): domain = sensor_class.__module__.split(".")[-1] - unique_id = sensor_class( - wrapper, block, sensor_id, description - ).unique_id + unique_id = f"{wrapper.mac}-{block.description}-{sensor_id}" await async_remove_shelly_entity(hass, domain, unique_id) else: blocks.append((block, sensor_id, description)) @@ -54,6 +77,39 @@ async def async_setup_entry_attribute_entities( ) +async def async_restore_block_attribute_entities( + hass, config_entry, async_add_entities, wrapper, sensor_class +): + """Restore block attributes entities.""" + entities = [] + + ent_reg = await entity_registry.async_get_registry(hass) + entries = entity_registry.async_entries_for_config_entry( + ent_reg, config_entry.entry_id + ) + + domain = sensor_class.__module__.split(".")[-1] + + for entry in entries: + if entry.domain != domain: + continue + + attribute = entry.unique_id.split("-")[-1] + description = BlockAttributeDescription( + name="", + icon=entry.original_icon, + unit=entry.unit_of_measurement, + device_class=entry.device_class, + ) + + entities.append(sensor_class(wrapper, None, attribute, description, entry)) + + if not entities: + return + + async_add_entities(entities) + + async def async_setup_entry_rest( hass, config_entry, async_add_entities, sensors, sensor_class ): @@ -163,7 +219,7 @@ class ShellyBlockEntity(entity.Entity): class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): - """Switch that controls a relay block on Shelly devices.""" + """Helper class to represent a block attribute.""" def __init__( self, @@ -176,12 +232,11 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): super().__init__(wrapper, block) self.attribute = attribute self.description = description - self.info = block.info(attribute) unit = self.description.unit if callable(unit): - unit = unit(self.info) + unit = unit(block.info(attribute)) self._unit = unit self._unique_id = f"{super().unique_id}-{self.attribute}" @@ -320,3 +375,67 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): return None return self.description.device_state_attributes(self.wrapper.device.status) + + +class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEntity): + """Represent a shelly sleeping block attribute entity.""" + + # pylint: disable=super-init-not-called + def __init__( + self, + wrapper: ShellyDeviceWrapper, + block: aioshelly.Block, + attribute: str, + description: BlockAttributeDescription, + entry: Optional[ConfigEntry] = None, + ) -> None: + """Initialize the sleeping sensor.""" + self.last_state = None + self.wrapper = wrapper + self.attribute = attribute + self.block = block + self.description = description + self._unit = self.description.unit + + if block is not None: + if callable(self._unit): + self._unit = self._unit(block.info(attribute)) + + self._unique_id = f"{self.wrapper.mac}-{block.description}-{attribute}" + self._name = get_entity_name( + self.wrapper.device, block, self.description.name + ) + else: + self._unique_id = entry.unique_id + self._name = entry.original_name + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + await super().async_added_to_hass() + + last_state = await self.async_get_last_state() + + if last_state is not None: + self.last_state = last_state.state + + @callback + def _update_callback(self): + """Handle device update.""" + if self.block is not None: + super()._update_callback() + return + + _, entity_block, entity_sensor = self.unique_id.split("-") + + for block in self.wrapper.device.blocks: + if block.description != entity_block: + continue + + for sensor_id in block.sensor_ids: + if sensor_id != entity_sensor: + continue + + self.block = block + _LOGGER.debug("Entity %s attached to block", self.name) + super()._update_callback() + return diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 923bcdced34..fffa98b6870 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==0.5.3"], + "requirements": ["aioshelly==0.5.4"], "zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }], "codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"] } diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index b92b90c1b46..36656740b92 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -18,6 +18,7 @@ from .entity import ( RestAttributeDescription, ShellyBlockAttributeEntity, ShellyRestAttributeEntity, + ShellySleepingBlockAttributeEntity, async_setup_entry_attribute_entities, async_setup_entry_rest, ) @@ -185,12 +186,17 @@ REST_SENSORS = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up sensors for device.""" - await async_setup_entry_attribute_entities( - hass, config_entry, async_add_entities, SENSORS, ShellySensor - ) - await async_setup_entry_rest( - hass, config_entry, async_add_entities, REST_SENSORS, ShellyRestSensor - ) + if config_entry.data["sleep_period"]: + await async_setup_entry_attribute_entities( + hass, config_entry, async_add_entities, SENSORS, ShellySleepingSensor + ) + else: + await async_setup_entry_attribute_entities( + hass, config_entry, async_add_entities, SENSORS, ShellySensor + ) + await async_setup_entry_rest( + hass, config_entry, async_add_entities, REST_SENSORS, ShellyRestSensor + ) class ShellySensor(ShellyBlockAttributeEntity): @@ -209,3 +215,15 @@ class ShellyRestSensor(ShellyRestAttributeEntity): def state(self): """Return value of sensor.""" return self.attribute_value + + +class ShellySleepingSensor(ShellySleepingBlockAttributeEntity): + """Represent a shelly sleeping sensor.""" + + @property + def state(self): + """Return value of sensor.""" + if self.block is not None: + return self.attribute_value + + return self.last_state diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 97d8bda609b..b4148801b35 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -6,8 +6,9 @@ from typing import List, Optional, Tuple import aioshelly -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.core import HomeAssistant +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import singleton from homeassistant.util.dt import parse_datetime, utcnow from .const import ( @@ -182,3 +183,30 @@ def get_device_wrapper(hass: HomeAssistant, device_id: str): return wrapper return None + + +@singleton.singleton("shelly_coap") +async def get_coap_context(hass): + """Get CoAP context to be used in all Shelly devices.""" + context = aioshelly.COAP() + await context.initialize() + + @callback + def shutdown_listener(ev): + context.close() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_listener) + + return context + + +def get_device_sleep_period(settings: dict) -> int: + """Return the device sleep period in seconds or 0 for non sleeping devices.""" + sleep_period = 0 + + if settings.get("sleep_mode", False): + sleep_period = settings["sleep_mode"]["period"] + if settings["sleep_mode"]["unit"] == "h": + sleep_period *= 60 # hours to minutes + + return sleep_period * 60 # minutes to seconds diff --git a/requirements_all.txt b/requirements_all.txt index c25ea05aa33..4315c577283 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -218,7 +218,7 @@ aiopylgtv==0.3.3 aiorecollect==1.0.1 # homeassistant.components.shelly -aioshelly==0.5.3 +aioshelly==0.5.4 # homeassistant.components.switcher_kis aioswitcher==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b156395df13..d8c7d112fba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -137,7 +137,7 @@ aiopylgtv==0.3.3 aiorecollect==1.0.1 # homeassistant.components.shelly -aioshelly==0.5.3 +aioshelly==0.5.4 # homeassistant.components.switcher_kis aioswitcher==1.2.1 diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 804d5a75952..7e7bd068842 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -91,7 +91,11 @@ async def coap_wrapper(hass): """Setups a coap wrapper with mocked device.""" await async_setup_component(hass, "shelly", {}) - config_entry = MockConfigEntry(domain=DOMAIN, data={}) + config_entry = MockConfigEntry( + domain=DOMAIN, + data={"sleep_period": 0, "model": "SHSW-25"}, + unique_id="12345678", + ) config_entry.add_to_hass(hass) device = Mock( @@ -99,6 +103,7 @@ async def coap_wrapper(hass): settings=MOCK_SETTINGS, shelly=MOCK_SHELLY, update=AsyncMock(), + initialized=True, ) hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}} diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 60f899296f6..1d5099cec1c 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -13,7 +13,8 @@ from tests.common import MockConfigEntry MOCK_SETTINGS = { "name": "Test name", - "device": {"mac": "test-mac", "hostname": "test-host"}, + "device": {"mac": "test-mac", "hostname": "test-host", "type": "SHSW-1"}, + "sleep_period": 0, } DISCOVERY_INFO = { "host": "1.1.1.1", @@ -57,6 +58,8 @@ async def test_form(hass): assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", + "model": "SHSW-1", + "sleep_period": 0, } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -101,6 +104,8 @@ async def test_title_without_name(hass): assert result2["title"] == "shelly1pm-12345" assert result2["data"] == { "host": "1.1.1.1", + "model": "SHSW-1", + "sleep_period": 0, } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -149,6 +154,8 @@ async def test_form_auth(hass): assert result3["title"] == "Test name" assert result3["data"] == { "host": "1.1.1.1", + "model": "SHSW-1", + "sleep_period": 0, "username": "test username", "password": "test password", } @@ -369,6 +376,8 @@ async def test_zeroconf(hass): assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", + "model": "SHSW-1", + "sleep_period": 0, } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -502,6 +511,8 @@ async def test_zeroconf_require_auth(hass): assert result3["title"] == "Test name" assert result3["data"] == { "host": "1.1.1.1", + "model": "SHSW-1", + "sleep_period": 0, "username": "test username", "password": "test password", } From 6458ff774f22ade7904227d1b8d93bd4cd3fa09f Mon Sep 17 00:00:00 2001 From: badguy99 <61918526+badguy99@users.noreply.github.com> Date: Wed, 3 Feb 2021 16:05:20 +0000 Subject: [PATCH 162/796] Homeconnect remote states (#45610) Co-authored-by: Paulus Schoutsen --- homeassistant/components/home_connect/api.py | 143 +++++++++++++++--- .../components/home_connect/binary_sensor.py | 40 +++-- .../components/home_connect/const.py | 2 + .../components/home_connect/sensor.py | 9 +- 4 files changed, 157 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/home_connect/api.py b/homeassistant/components/home_connect/api.py index 8db8afa3a6b..44669ed200f 100644 --- a/homeassistant/components/home_connect/api.py +++ b/homeassistant/components/home_connect/api.py @@ -13,6 +13,7 @@ from homeassistant.helpers.dispatcher import dispatcher_send from .const import ( BSH_ACTIVE_PROGRAM, + BSH_OPERATION_STATE, BSH_POWER_OFF, BSH_POWER_STANDBY, SIGNAL_UPDATE_ENTITIES, @@ -156,6 +157,25 @@ class DeviceWithPrograms(HomeConnectDevice): ] +class DeviceWithOpState(HomeConnectDevice): + """Device that has an operation state sensor.""" + + def get_opstate_sensor(self): + """Get a list with info about operation state sensors.""" + + return [ + { + "device": self, + "desc": "Operation State", + "unit": None, + "key": BSH_OPERATION_STATE, + "icon": "mdi:state-machine", + "device_class": None, + "sign": 1, + } + ] + + class DeviceWithDoor(HomeConnectDevice): """Device that has a door sensor.""" @@ -164,6 +184,7 @@ class DeviceWithDoor(HomeConnectDevice): return { "device": self, "desc": "Door", + "sensor_type": "door", "device_class": "door", } @@ -173,11 +194,7 @@ class DeviceWithLight(HomeConnectDevice): def get_light_entity(self): """Get a dictionary with info about the lighting.""" - return { - "device": self, - "desc": "Light", - "ambient": None, - } + return {"device": self, "desc": "Light", "ambient": None} class DeviceWithAmbientLight(HomeConnectDevice): @@ -185,14 +202,36 @@ class DeviceWithAmbientLight(HomeConnectDevice): def get_ambientlight_entity(self): """Get a dictionary with info about the ambient lighting.""" + return {"device": self, "desc": "AmbientLight", "ambient": True} + + +class DeviceWithRemoteControl(HomeConnectDevice): + """Device that has Remote Control binary sensor.""" + + def get_remote_control(self): + """Get a dictionary with info about the remote control sensor.""" return { "device": self, - "desc": "AmbientLight", - "ambient": True, + "desc": "Remote Control", + "sensor_type": "remote_control", } -class Dryer(DeviceWithDoor, DeviceWithPrograms): +class DeviceWithRemoteStart(HomeConnectDevice): + """Device that has a Remote Start binary sensor.""" + + def get_remote_start(self): + """Get a dictionary with info about the remote start sensor.""" + return {"device": self, "desc": "Remote Start", "sensor_type": "remote_start"} + + +class Dryer( + DeviceWithDoor, + DeviceWithOpState, + DeviceWithPrograms, + DeviceWithRemoteControl, + DeviceWithRemoteStart, +): """Dryer class.""" PROGRAMS = [ @@ -217,16 +256,26 @@ class Dryer(DeviceWithDoor, DeviceWithPrograms): def get_entity_info(self): """Get a dictionary with infos about the associated entities.""" door_entity = self.get_door_entity() + remote_control = self.get_remote_control() + remote_start = self.get_remote_start() + op_state_sensor = self.get_opstate_sensor() program_sensors = self.get_program_sensors() program_switches = self.get_program_switches() return { - "binary_sensor": [door_entity], + "binary_sensor": [door_entity, remote_control, remote_start], "switch": program_switches, - "sensor": program_sensors, + "sensor": program_sensors + op_state_sensor, } -class Dishwasher(DeviceWithDoor, DeviceWithAmbientLight, DeviceWithPrograms): +class Dishwasher( + DeviceWithDoor, + DeviceWithAmbientLight, + DeviceWithOpState, + DeviceWithPrograms, + DeviceWithRemoteControl, + DeviceWithRemoteStart, +): """Dishwasher class.""" PROGRAMS = [ @@ -257,16 +306,25 @@ class Dishwasher(DeviceWithDoor, DeviceWithAmbientLight, DeviceWithPrograms): def get_entity_info(self): """Get a dictionary with infos about the associated entities.""" door_entity = self.get_door_entity() + remote_control = self.get_remote_control() + remote_start = self.get_remote_start() + op_state_sensor = self.get_opstate_sensor() program_sensors = self.get_program_sensors() program_switches = self.get_program_switches() return { - "binary_sensor": [door_entity], + "binary_sensor": [door_entity, remote_control, remote_start], "switch": program_switches, - "sensor": program_sensors, + "sensor": program_sensors + op_state_sensor, } -class Oven(DeviceWithDoor, DeviceWithPrograms): +class Oven( + DeviceWithDoor, + DeviceWithOpState, + DeviceWithPrograms, + DeviceWithRemoteControl, + DeviceWithRemoteStart, +): """Oven class.""" PROGRAMS = [ @@ -282,16 +340,25 @@ class Oven(DeviceWithDoor, DeviceWithPrograms): def get_entity_info(self): """Get a dictionary with infos about the associated entities.""" door_entity = self.get_door_entity() + remote_control = self.get_remote_control() + remote_start = self.get_remote_start() + op_state_sensor = self.get_opstate_sensor() program_sensors = self.get_program_sensors() program_switches = self.get_program_switches() return { - "binary_sensor": [door_entity], + "binary_sensor": [door_entity, remote_control, remote_start], "switch": program_switches, - "sensor": program_sensors, + "sensor": program_sensors + op_state_sensor, } -class Washer(DeviceWithDoor, DeviceWithPrograms): +class Washer( + DeviceWithDoor, + DeviceWithOpState, + DeviceWithPrograms, + DeviceWithRemoteControl, + DeviceWithRemoteStart, +): """Washer class.""" PROGRAMS = [ @@ -321,16 +388,19 @@ class Washer(DeviceWithDoor, DeviceWithPrograms): def get_entity_info(self): """Get a dictionary with infos about the associated entities.""" door_entity = self.get_door_entity() + remote_control = self.get_remote_control() + remote_start = self.get_remote_start() + op_state_sensor = self.get_opstate_sensor() program_sensors = self.get_program_sensors() program_switches = self.get_program_switches() return { - "binary_sensor": [door_entity], + "binary_sensor": [door_entity, remote_control, remote_start], "switch": program_switches, - "sensor": program_sensors, + "sensor": program_sensors + op_state_sensor, } -class CoffeeMaker(DeviceWithPrograms): +class CoffeeMaker(DeviceWithOpState, DeviceWithPrograms, DeviceWithRemoteStart): """Coffee maker class.""" PROGRAMS = [ @@ -354,12 +424,25 @@ class CoffeeMaker(DeviceWithPrograms): def get_entity_info(self): """Get a dictionary with infos about the associated entities.""" + remote_start = self.get_remote_start() + op_state_sensor = self.get_opstate_sensor() program_sensors = self.get_program_sensors() program_switches = self.get_program_switches() - return {"switch": program_switches, "sensor": program_sensors} + return { + "binary_sensor": [remote_start], + "switch": program_switches, + "sensor": program_sensors + op_state_sensor, + } -class Hood(DeviceWithLight, DeviceWithAmbientLight, DeviceWithPrograms): +class Hood( + DeviceWithLight, + DeviceWithAmbientLight, + DeviceWithOpState, + DeviceWithPrograms, + DeviceWithRemoteControl, + DeviceWithRemoteStart, +): """Hood class.""" PROGRAMS = [ @@ -370,13 +453,17 @@ class Hood(DeviceWithLight, DeviceWithAmbientLight, DeviceWithPrograms): def get_entity_info(self): """Get a dictionary with infos about the associated entities.""" + remote_control = self.get_remote_control() + remote_start = self.get_remote_start() light_entity = self.get_light_entity() ambientlight_entity = self.get_ambientlight_entity() + op_state_sensor = self.get_opstate_sensor() program_sensors = self.get_program_sensors() program_switches = self.get_program_switches() return { + "binary_sensor": [remote_control, remote_start], "switch": program_switches, - "sensor": program_sensors, + "sensor": program_sensors + op_state_sensor, "light": [light_entity, ambientlight_entity], } @@ -390,13 +477,19 @@ class FridgeFreezer(DeviceWithDoor): return {"binary_sensor": [door_entity]} -class Hob(DeviceWithPrograms): +class Hob(DeviceWithOpState, DeviceWithPrograms, DeviceWithRemoteControl): """Hob class.""" PROGRAMS = [{"name": "Cooking.Hob.Program.PowerLevelMode"}] def get_entity_info(self): """Get a dictionary with infos about the associated entities.""" + remote_control = self.get_remote_control() + op_state_sensor = self.get_opstate_sensor() program_sensors = self.get_program_sensors() program_switches = self.get_program_switches() - return {"switch": program_switches, "sensor": program_sensors} + return { + "binary_sensor": [remote_control], + "switch": program_switches, + "sensor": program_sensors + op_state_sensor, + } diff --git a/homeassistant/components/home_connect/binary_sensor.py b/homeassistant/components/home_connect/binary_sensor.py index 4810231b432..1713d34809a 100644 --- a/homeassistant/components/home_connect/binary_sensor.py +++ b/homeassistant/components/home_connect/binary_sensor.py @@ -3,7 +3,12 @@ import logging from homeassistant.components.binary_sensor import BinarySensorEntity -from .const import BSH_DOOR_STATE, DOMAIN +from .const import ( + BSH_DOOR_STATE, + BSH_REMOTE_CONTROL_ACTIVATION_STATE, + BSH_REMOTE_START_ALLOWANCE_STATE, + DOMAIN, +) from .entity import HomeConnectEntity _LOGGER = logging.getLogger(__name__) @@ -26,11 +31,27 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class HomeConnectBinarySensor(HomeConnectEntity, BinarySensorEntity): """Binary sensor for Home Connect.""" - def __init__(self, device, desc, device_class): + def __init__(self, device, desc, sensor_type, device_class=None): """Initialize the entity.""" super().__init__(device, desc) - self._device_class = device_class self._state = None + self._device_class = device_class + self._type = sensor_type + if self._type == "door": + self._update_key = BSH_DOOR_STATE + self._false_value_list = ( + "BSH.Common.EnumType.DoorState.Closed", + "BSH.Common.EnumType.DoorState.Locked", + ) + self._true_value_list = ["BSH.Common.EnumType.DoorState.Open"] + elif self._type == "remote_control": + self._update_key = BSH_REMOTE_CONTROL_ACTIVATION_STATE + self._false_value_list = [False] + self._true_value_list = [True] + elif self._type == "remote_start": + self._update_key = BSH_REMOTE_START_ALLOWANCE_STATE + self._false_value_list = [False] + self._true_value_list = [True] @property def is_on(self): @@ -44,18 +65,17 @@ class HomeConnectBinarySensor(HomeConnectEntity, BinarySensorEntity): async def async_update(self): """Update the binary sensor's status.""" - state = self.device.appliance.status.get(BSH_DOOR_STATE, {}) + state = self.device.appliance.status.get(self._update_key, {}) if not state: self._state = None - elif state.get("value") in [ - "BSH.Common.EnumType.DoorState.Closed", - "BSH.Common.EnumType.DoorState.Locked", - ]: + elif state.get("value") in self._false_value_list: self._state = False - elif state.get("value") == "BSH.Common.EnumType.DoorState.Open": + elif state.get("value") in self._true_value_list: self._state = True else: - _LOGGER.warning("Unexpected value for HomeConnect door state: %s", state) + _LOGGER.warning( + "Unexpected value for HomeConnect %s state: %s", self._type, state + ) self._state = None _LOGGER.debug("Updated, new state: %s", self._state) diff --git a/homeassistant/components/home_connect/const.py b/homeassistant/components/home_connect/const.py index 22ce4dba676..98dd8d383bd 100644 --- a/homeassistant/components/home_connect/const.py +++ b/homeassistant/components/home_connect/const.py @@ -11,6 +11,8 @@ 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" +BSH_REMOTE_CONTROL_ACTIVATION_STATE = "BSH.Common.Status.RemoteControlActive" +BSH_REMOTE_START_ALLOWANCE_STATE = "BSH.Common.Status.RemoteControlStartAllowed" COOKING_LIGHTING = "Cooking.Common.Setting.Lighting" COOKING_LIGHTING_BRIGHTNESS = "Cooking.Common.Setting.LightingBrightness" diff --git a/homeassistant/components/home_connect/sensor.py b/homeassistant/components/home_connect/sensor.py index 0ae5a9fcd36..e51efe06057 100644 --- a/homeassistant/components/home_connect/sensor.py +++ b/homeassistant/components/home_connect/sensor.py @@ -6,7 +6,7 @@ import logging from homeassistant.const import DEVICE_CLASS_TIMESTAMP import homeassistant.util.dt as dt_util -from .const import DOMAIN +from .const import BSH_OPERATION_STATE, DOMAIN from .entity import HomeConnectEntity _LOGGER = logging.getLogger(__name__) @@ -51,7 +51,7 @@ class HomeConnectSensor(HomeConnectEntity): return self._state is not None async def async_update(self): - """Update the sensos status.""" + """Update the sensor's status.""" status = self.device.appliance.status if self._key not in status: self._state = None @@ -74,6 +74,11 @@ class HomeConnectSensor(HomeConnectEntity): ).isoformat() else: self._state = status[self._key].get("value") + if self._key == BSH_OPERATION_STATE: + # Value comes back as an enum, we only really care about the + # last part, so split it off + # https://developer.home-connect.com/docs/status/operation_state + self._state = self._state.split(".")[-1] _LOGGER.debug("Updated, new state: %s", self._state) @property From a584ad5ac3a143e6526105ee06adc6b2120ef04d Mon Sep 17 00:00:00 2001 From: Berni Moses Date: Wed, 3 Feb 2021 17:06:02 +0100 Subject: [PATCH 163/796] Fix duplicate lg_soundbar entities and disable polling (#42044) Co-authored-by: Paulus Schoutsen --- .../components/lg_soundbar/media_player.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index ee396a7a9ee..c2d196196f9 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -29,12 +29,11 @@ class LGDevice(MediaPlayerEntity): def __init__(self, discovery_info): """Initialize the LG speakers.""" - self._host = discovery_info.get("host") - self._port = discovery_info.get("port") - properties = discovery_info.get("properties") - self._uuid = properties.get("UUID") + self._host = discovery_info["host"] + self._port = discovery_info["port"] + self._hostname = discovery_info["hostname"] - self._name = "" + self._name = self._hostname.split(".")[0] self._volume = 0 self._volume_min = 0 self._volume_max = 0 @@ -122,9 +121,9 @@ class LGDevice(MediaPlayerEntity): self._device.get_product_info() @property - def unique_id(self): - """Return the device's unique ID.""" - return self._uuid + def should_poll(self): + """No polling needed.""" + return False @property def name(self): From 4b208746e5a91997aed59a77714cf799e96287a9 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Wed, 3 Feb 2021 11:38:12 -0500 Subject: [PATCH 164/796] Add Mazda Connected Services integration (#45768) --- CODEOWNERS | 1 + homeassistant/components/mazda/__init__.py | 173 ++++++++++ homeassistant/components/mazda/config_flow.py | 117 +++++++ homeassistant/components/mazda/const.py | 8 + homeassistant/components/mazda/manifest.json | 9 + homeassistant/components/mazda/sensor.py | 263 +++++++++++++++ homeassistant/components/mazda/strings.json | 35 ++ .../components/mazda/translations/en.json | 35 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/mazda/__init__.py | 53 +++ tests/components/mazda/test_config_flow.py | 310 ++++++++++++++++++ tests/components/mazda/test_init.py | 100 ++++++ tests/components/mazda/test_sensor.py | 160 +++++++++ tests/fixtures/mazda/get_vehicle_status.json | 37 +++ tests/fixtures/mazda/get_vehicles.json | 17 + 17 files changed, 1325 insertions(+) create mode 100644 homeassistant/components/mazda/__init__.py create mode 100644 homeassistant/components/mazda/config_flow.py create mode 100644 homeassistant/components/mazda/const.py create mode 100644 homeassistant/components/mazda/manifest.json create mode 100644 homeassistant/components/mazda/sensor.py create mode 100644 homeassistant/components/mazda/strings.json create mode 100644 homeassistant/components/mazda/translations/en.json create mode 100644 tests/components/mazda/__init__.py create mode 100644 tests/components/mazda/test_config_flow.py create mode 100644 tests/components/mazda/test_init.py create mode 100644 tests/components/mazda/test_sensor.py create mode 100644 tests/fixtures/mazda/get_vehicle_status.json create mode 100644 tests/fixtures/mazda/get_vehicles.json diff --git a/CODEOWNERS b/CODEOWNERS index 499b7e131f7..cb69b86ed8c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -263,6 +263,7 @@ homeassistant/components/lutron_caseta/* @swails @bdraco homeassistant/components/lyric/* @timmo001 homeassistant/components/mastodon/* @fabaff homeassistant/components/matrix/* @tinloaf +homeassistant/components/mazda/* @bdr99 homeassistant/components/mcp23017/* @jardiamj homeassistant/components/media_source/* @hunterjm homeassistant/components/mediaroom/* @dgomes diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py new file mode 100644 index 00000000000..14b33df66c0 --- /dev/null +++ b/homeassistant/components/mazda/__init__.py @@ -0,0 +1,173 @@ +"""The Mazda Connected Services integration.""" +import asyncio +from datetime import timedelta +import logging + +import async_timeout +from pymazda import ( + Client as MazdaAPI, + MazdaAccountLockedException, + MazdaAPIEncryptionException, + MazdaAuthenticationException, + MazdaException, + MazdaTokenExpiredException, +) + +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) +from homeassistant.util.async_ import gather_with_concurrency + +from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["sensor"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Mazda Connected Services component.""" + hass.data[DOMAIN] = {} + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Mazda Connected Services from a config entry.""" + email = entry.data[CONF_EMAIL] + password = entry.data[CONF_PASSWORD] + region = entry.data[CONF_REGION] + + websession = aiohttp_client.async_get_clientsession(hass) + mazda_client = MazdaAPI(email, password, region, websession) + + try: + await mazda_client.validate_credentials() + except MazdaAuthenticationException: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data=entry.data, + ) + ) + return False + except ( + MazdaException, + MazdaAccountLockedException, + MazdaTokenExpiredException, + MazdaAPIEncryptionException, + ) as ex: + _LOGGER.error("Error occurred during Mazda login request: %s", ex) + raise ConfigEntryNotReady from ex + + async def async_update_data(): + """Fetch data from Mazda API.""" + + async def with_timeout(task): + async with async_timeout.timeout(10): + return await task + + try: + vehicles = await with_timeout(mazda_client.get_vehicles()) + + vehicle_status_tasks = [ + with_timeout(mazda_client.get_vehicle_status(vehicle["id"])) + for vehicle in vehicles + ] + statuses = await gather_with_concurrency(5, *vehicle_status_tasks) + + for vehicle, status in zip(vehicles, statuses): + vehicle["status"] = status + + return vehicles + except MazdaAuthenticationException as ex: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data=entry.data, + ) + ) + raise UpdateFailed("Not authenticated with Mazda API") from ex + except Exception as ex: + _LOGGER.exception( + "Unknown error occurred during Mazda update request: %s", ex + ) + raise UpdateFailed(ex) from ex + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=DOMAIN, + update_method=async_update_data, + update_interval=timedelta(seconds=60), + ) + + hass.data[DOMAIN][entry.entry_id] = { + DATA_CLIENT: mazda_client, + DATA_COORDINATOR: coordinator, + } + + # Fetch initial data so we have data when entities subscribe + await coordinator.async_refresh() + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + # Setup components + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class MazdaEntity(CoordinatorEntity): + """Defines a base Mazda entity.""" + + def __init__(self, coordinator, index): + """Initialize the Mazda entity.""" + super().__init__(coordinator) + self.index = index + self.vin = self.coordinator.data[self.index]["vin"] + + @property + def device_info(self): + """Return device info for the Mazda entity.""" + data = self.coordinator.data[self.index] + return { + "identifiers": {(DOMAIN, self.vin)}, + "name": self.get_vehicle_name(), + "manufacturer": "Mazda", + "model": f"{data['modelYear']} {data['carlineName']}", + } + + def get_vehicle_name(self): + """Return the vehicle name, to be used as a prefix for names of other entities.""" + data = self.coordinator.data[self.index] + if "nickname" in data and len(data["nickname"]) > 0: + return data["nickname"] + return f"{data['modelYear']} {data['carlineName']}" diff --git a/homeassistant/components/mazda/config_flow.py b/homeassistant/components/mazda/config_flow.py new file mode 100644 index 00000000000..53c08b9bd69 --- /dev/null +++ b/homeassistant/components/mazda/config_flow.py @@ -0,0 +1,117 @@ +"""Config flow for Mazda Connected Services integration.""" +import logging + +import aiohttp +from pymazda import ( + Client as MazdaAPI, + MazdaAccountLockedException, + MazdaAuthenticationException, +) +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION +from homeassistant.helpers import aiohttp_client + +# https://github.com/PyCQA/pylint/issues/3202 +from .const import DOMAIN # pylint: disable=unused-import +from .const import MAZDA_REGIONS + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_EMAIL): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_REGION): vol.In(MAZDA_REGIONS), + } +) + + +class MazdaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Mazda Connected Services.""" + + 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: + await self.async_set_unique_id(user_input[CONF_EMAIL].lower()) + + try: + websession = aiohttp_client.async_get_clientsession(self.hass) + mazda_client = MazdaAPI( + user_input[CONF_EMAIL], + user_input[CONF_PASSWORD], + user_input[CONF_REGION], + websession, + ) + await mazda_client.validate_credentials() + except MazdaAuthenticationException: + errors["base"] = "invalid_auth" + except MazdaAccountLockedException: + errors["base"] = "account_locked" + except aiohttp.ClientError: + errors["base"] = "cannot_connect" + except Exception as ex: # pylint: disable=broad-except + errors["base"] = "unknown" + _LOGGER.exception( + "Unknown error occurred during Mazda login request: %s", ex + ) + else: + return self.async_create_entry( + title=user_input[CONF_EMAIL], data=user_input + ) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_reauth(self, user_input=None): + """Perform reauth if the user credentials have changed.""" + errors = {} + + if user_input is not None: + try: + websession = aiohttp_client.async_get_clientsession(self.hass) + mazda_client = MazdaAPI( + user_input[CONF_EMAIL], + user_input[CONF_PASSWORD], + user_input[CONF_REGION], + websession, + ) + await mazda_client.validate_credentials() + except MazdaAuthenticationException: + errors["base"] = "invalid_auth" + except MazdaAccountLockedException: + errors["base"] = "account_locked" + except aiohttp.ClientError: + errors["base"] = "cannot_connect" + except Exception as ex: # pylint: disable=broad-except + errors["base"] = "unknown" + _LOGGER.exception( + "Unknown error occurred during Mazda login request: %s", ex + ) + else: + await self.async_set_unique_id(user_input[CONF_EMAIL].lower()) + + for entry in self._async_current_entries(): + if entry.unique_id == self.unique_id: + self.hass.config_entries.async_update_entry( + entry, data=user_input + ) + + # Reload the config entry otherwise devices will remain unavailable + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + + return self.async_abort(reason="reauth_successful") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="reauth", data_schema=DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/mazda/const.py b/homeassistant/components/mazda/const.py new file mode 100644 index 00000000000..c75f6bf3b77 --- /dev/null +++ b/homeassistant/components/mazda/const.py @@ -0,0 +1,8 @@ +"""Constants for the Mazda Connected Services integration.""" + +DOMAIN = "mazda" + +DATA_CLIENT = "mazda_client" +DATA_COORDINATOR = "coordinator" + +MAZDA_REGIONS = {"MNAO": "North America", "MME": "Europe", "MJO": "Japan"} diff --git a/homeassistant/components/mazda/manifest.json b/homeassistant/components/mazda/manifest.json new file mode 100644 index 00000000000..b3826d42318 --- /dev/null +++ b/homeassistant/components/mazda/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "mazda", + "name": "Mazda Connected Services", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/mazda", + "requirements": ["pymazda==0.0.8"], + "codeowners": ["@bdr99"], + "quality_scale": "platinum" +} \ No newline at end of file diff --git a/homeassistant/components/mazda/sensor.py b/homeassistant/components/mazda/sensor.py new file mode 100644 index 00000000000..fa03eb7f410 --- /dev/null +++ b/homeassistant/components/mazda/sensor.py @@ -0,0 +1,263 @@ +"""Platform for Mazda sensor integration.""" +from homeassistant.const import ( + CONF_UNIT_SYSTEM_IMPERIAL, + LENGTH_KILOMETERS, + LENGTH_MILES, + PERCENTAGE, + PRESSURE_PSI, +) + +from . import MazdaEntity +from .const import DATA_COORDINATOR, DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the sensor platform.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + + entities = [] + + for index, _ in enumerate(coordinator.data): + entities.append(MazdaFuelRemainingSensor(coordinator, index)) + entities.append(MazdaFuelDistanceSensor(coordinator, index)) + entities.append(MazdaOdometerSensor(coordinator, index)) + entities.append(MazdaFrontLeftTirePressureSensor(coordinator, index)) + entities.append(MazdaFrontRightTirePressureSensor(coordinator, index)) + entities.append(MazdaRearLeftTirePressureSensor(coordinator, index)) + entities.append(MazdaRearRightTirePressureSensor(coordinator, index)) + + async_add_entities(entities) + + +class MazdaFuelRemainingSensor(MazdaEntity): + """Class for the fuel remaining sensor.""" + + @property + def name(self): + """Return the name of the sensor.""" + vehicle_name = self.get_vehicle_name() + return f"{vehicle_name} Fuel Remaining Percentage" + + @property + def unique_id(self): + """Return a unique identifier for this entity.""" + return f"{self.vin}_fuel_remaining_percentage" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return PERCENTAGE + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return "mdi:gas-station" + + @property + def state(self): + """Return the state of the sensor.""" + return self.coordinator.data[self.index]["status"]["fuelRemainingPercent"] + + +class MazdaFuelDistanceSensor(MazdaEntity): + """Class for the fuel distance sensor.""" + + @property + def name(self): + """Return the name of the sensor.""" + vehicle_name = self.get_vehicle_name() + return f"{vehicle_name} Fuel Distance Remaining" + + @property + def unique_id(self): + """Return a unique identifier for this entity.""" + return f"{self.vin}_fuel_distance_remaining" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: + return LENGTH_MILES + return LENGTH_KILOMETERS + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return "mdi:gas-station" + + @property + def state(self): + """Return the state of the sensor.""" + fuel_distance_km = self.coordinator.data[self.index]["status"][ + "fuelDistanceRemainingKm" + ] + return round(self.hass.config.units.length(fuel_distance_km, LENGTH_KILOMETERS)) + + +class MazdaOdometerSensor(MazdaEntity): + """Class for the odometer sensor.""" + + @property + def name(self): + """Return the name of the sensor.""" + vehicle_name = self.get_vehicle_name() + return f"{vehicle_name} Odometer" + + @property + def unique_id(self): + """Return a unique identifier for this entity.""" + return f"{self.vin}_odometer" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: + return LENGTH_MILES + return LENGTH_KILOMETERS + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return "mdi:speedometer" + + @property + def state(self): + """Return the state of the sensor.""" + odometer_km = self.coordinator.data[self.index]["status"]["odometerKm"] + return round(self.hass.config.units.length(odometer_km, LENGTH_KILOMETERS)) + + +class MazdaFrontLeftTirePressureSensor(MazdaEntity): + """Class for the front left tire pressure sensor.""" + + @property + def name(self): + """Return the name of the sensor.""" + vehicle_name = self.get_vehicle_name() + return f"{vehicle_name} Front Left Tire Pressure" + + @property + def unique_id(self): + """Return a unique identifier for this entity.""" + return f"{self.vin}_front_left_tire_pressure" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return PRESSURE_PSI + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return "mdi:car-tire-alert" + + @property + def state(self): + """Return the state of the sensor.""" + return round( + self.coordinator.data[self.index]["status"]["tirePressure"][ + "frontLeftTirePressurePsi" + ] + ) + + +class MazdaFrontRightTirePressureSensor(MazdaEntity): + """Class for the front right tire pressure sensor.""" + + @property + def name(self): + """Return the name of the sensor.""" + vehicle_name = self.get_vehicle_name() + return f"{vehicle_name} Front Right Tire Pressure" + + @property + def unique_id(self): + """Return a unique identifier for this entity.""" + return f"{self.vin}_front_right_tire_pressure" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return PRESSURE_PSI + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return "mdi:car-tire-alert" + + @property + def state(self): + """Return the state of the sensor.""" + return round( + self.coordinator.data[self.index]["status"]["tirePressure"][ + "frontRightTirePressurePsi" + ] + ) + + +class MazdaRearLeftTirePressureSensor(MazdaEntity): + """Class for the rear left tire pressure sensor.""" + + @property + def name(self): + """Return the name of the sensor.""" + vehicle_name = self.get_vehicle_name() + return f"{vehicle_name} Rear Left Tire Pressure" + + @property + def unique_id(self): + """Return a unique identifier for this entity.""" + return f"{self.vin}_rear_left_tire_pressure" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return PRESSURE_PSI + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return "mdi:car-tire-alert" + + @property + def state(self): + """Return the state of the sensor.""" + return round( + self.coordinator.data[self.index]["status"]["tirePressure"][ + "rearLeftTirePressurePsi" + ] + ) + + +class MazdaRearRightTirePressureSensor(MazdaEntity): + """Class for the rear right tire pressure sensor.""" + + @property + def name(self): + """Return the name of the sensor.""" + vehicle_name = self.get_vehicle_name() + return f"{vehicle_name} Rear Right Tire Pressure" + + @property + def unique_id(self): + """Return a unique identifier for this entity.""" + return f"{self.vin}_rear_right_tire_pressure" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return PRESSURE_PSI + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return "mdi:car-tire-alert" + + @property + def state(self): + """Return the state of the sensor.""" + return round( + self.coordinator.data[self.index]["status"]["tirePressure"][ + "rearRightTirePressurePsi" + ] + ) diff --git a/homeassistant/components/mazda/strings.json b/homeassistant/components/mazda/strings.json new file mode 100644 index 00000000000..1950260bfcb --- /dev/null +++ b/homeassistant/components/mazda/strings.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + }, + "error": { + "account_locked": "Account locked. Please try again later.", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "step": { + "reauth": { + "data": { + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]", + "region": "Region" + }, + "description": "Authentication failed for Mazda Connected Services. Please enter your current credentials.", + "title": "Mazda Connected Services - Authentication Failed" + }, + "user": { + "data": { + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]", + "region": "Region" + }, + "description": "Please enter the email address and password you use to log into the MyMazda mobile app.", + "title": "Mazda Connected Services - Add Account" + } + } + }, + "title": "Mazda Connected Services" +} \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/en.json b/homeassistant/components/mazda/translations/en.json new file mode 100644 index 00000000000..b9e02fb3a41 --- /dev/null +++ b/homeassistant/components/mazda/translations/en.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "account_locked": "Account locked. Please try again later.", + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "reauth": { + "data": { + "email": "Email", + "password": "Password", + "region": "Region" + }, + "description": "Authentication failed for Mazda Connected Services. Please enter your current credentials.", + "title": "Mazda Connected Services - Authentication Failed" + }, + "user": { + "data": { + "email": "Email", + "password": "Password", + "region": "Region" + }, + "description": "Please enter the email address and password you use to log into the MyMazda mobile app.", + "title": "Mazda Connected Services - Add Account" + } + } + }, + "title": "Mazda Connected Services" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 282d039128d..c2e36d9f846 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -123,6 +123,7 @@ FLOWS = [ "lutron_caseta", "lyric", "mailgun", + "mazda", "melcloud", "met", "meteo_france", diff --git a/requirements_all.txt b/requirements_all.txt index 4315c577283..93922e9f934 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1517,6 +1517,9 @@ pymailgunner==1.4 # homeassistant.components.firmata pymata-express==1.19 +# homeassistant.components.mazda +pymazda==0.0.8 + # homeassistant.components.mediaroom pymediaroom==0.6.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d8c7d112fba..9e379612060 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -789,6 +789,9 @@ pymailgunner==1.4 # homeassistant.components.firmata pymata-express==1.19 +# homeassistant.components.mazda +pymazda==0.0.8 + # homeassistant.components.melcloud pymelcloud==2.5.2 diff --git a/tests/components/mazda/__init__.py b/tests/components/mazda/__init__.py new file mode 100644 index 00000000000..f7a267a5110 --- /dev/null +++ b/tests/components/mazda/__init__.py @@ -0,0 +1,53 @@ +"""Tests for the Mazda Connected Services integration.""" + +import json +from unittest.mock import AsyncMock, MagicMock, patch + +from pymazda import Client as MazdaAPI + +from homeassistant.components.mazda.const import DOMAIN +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION +from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client + +from tests.common import MockConfigEntry, load_fixture + +FIXTURE_USER_INPUT = { + CONF_EMAIL: "example@example.com", + CONF_PASSWORD: "password", + CONF_REGION: "MNAO", +} + + +async def init_integration(hass: HomeAssistant, use_nickname=True) -> MockConfigEntry: + """Set up the Mazda Connected Services integration in Home Assistant.""" + get_vehicles_fixture = json.loads(load_fixture("mazda/get_vehicles.json")) + if not use_nickname: + get_vehicles_fixture[0].pop("nickname") + + get_vehicle_status_fixture = json.loads( + load_fixture("mazda/get_vehicle_status.json") + ) + + config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT) + config_entry.add_to_hass(hass) + + client_mock = MagicMock( + MazdaAPI( + FIXTURE_USER_INPUT[CONF_EMAIL], + FIXTURE_USER_INPUT[CONF_PASSWORD], + FIXTURE_USER_INPUT[CONF_REGION], + aiohttp_client.async_get_clientsession(hass), + ) + ) + client_mock.get_vehicles = AsyncMock(return_value=get_vehicles_fixture) + client_mock.get_vehicle_status = AsyncMock(return_value=get_vehicle_status_fixture) + + with patch( + "homeassistant.components.mazda.config_flow.MazdaAPI", + return_value=client_mock, + ), patch("homeassistant.components.mazda.MazdaAPI", return_value=client_mock): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry diff --git a/tests/components/mazda/test_config_flow.py b/tests/components/mazda/test_config_flow.py new file mode 100644 index 00000000000..fbdd74bfdfa --- /dev/null +++ b/tests/components/mazda/test_config_flow.py @@ -0,0 +1,310 @@ +"""Test the Mazda Connected Services config flow.""" +from unittest.mock import patch + +import aiohttp + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.mazda.config_flow import ( + MazdaAccountLockedException, + MazdaAuthenticationException, +) +from homeassistant.components.mazda.const import DOMAIN +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +FIXTURE_USER_INPUT = { + CONF_EMAIL: "example@example.com", + CONF_PASSWORD: "password", + CONF_REGION: "MNAO", +} +FIXTURE_USER_INPUT_REAUTH = { + CONF_EMAIL: "example@example.com", + CONF_PASSWORD: "password_fixed", + CONF_REGION: "MNAO", +} + + +async def test_form(hass): + """Test the entire flow.""" + 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" + assert result["errors"] == {} + + with patch( + "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", + return_value=True, + ), patch( + "homeassistant.components.mazda.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.mazda.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == FIXTURE_USER_INPUT[CONF_EMAIL] + assert result2["data"] == FIXTURE_USER_INPUT + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + 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" + assert result["errors"] == {} + + with patch( + "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", + side_effect=MazdaAuthenticationException("Failed to authenticate"), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_account_locked(hass: HomeAssistant) -> None: + """Test we handle account locked error.""" + 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" + assert result["errors"] == {} + + with patch( + "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", + side_effect=MazdaAccountLockedException("Account locked"), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "account_locked"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", + side_effect=aiohttp.ClientError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_unknown_error(hass): + """Test we handle unknown error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +async def test_reauth_flow(hass: HomeAssistant) -> None: + """Test reauth works.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", + side_effect=MazdaAuthenticationException("Failed to authenticate"), + ): + mock_config = MockConfigEntry( + domain=DOMAIN, + unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], + data=FIXTURE_USER_INPUT, + ) + mock_config.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth" + assert result["errors"] == {"base": "invalid_auth"} + + with patch( + "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "reauth", "unique_id": FIXTURE_USER_INPUT[CONF_EMAIL]}, + data=FIXTURE_USER_INPUT_REAUTH, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_reauth_authorization_error(hass: HomeAssistant) -> None: + """Test we show user form on authorization error.""" + with patch( + "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", + side_effect=MazdaAuthenticationException("Failed to authenticate"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT_REAUTH, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "reauth" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_reauth_account_locked(hass: HomeAssistant) -> None: + """Test we show user form on account_locked error.""" + with patch( + "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", + side_effect=MazdaAccountLockedException("Account locked"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT_REAUTH, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "reauth" + assert result2["errors"] == {"base": "account_locked"} + + +async def test_reauth_connection_error(hass: HomeAssistant) -> None: + """Test we show user form on connection error.""" + with patch( + "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", + side_effect=aiohttp.ClientError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT_REAUTH, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "reauth" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_reauth_unknown_error(hass: HomeAssistant) -> None: + """Test we show user form on unknown error.""" + with patch( + "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", + side_effect=Exception, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT_REAUTH, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "reauth" + assert result2["errors"] == {"base": "unknown"} + + +async def test_reauth_unique_id_not_found(hass: HomeAssistant) -> None: + """Test we show user form when unique id not found during reauth.""" + with patch( + "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth" + + # Change the unique_id of the flow in order to cause a mismatch + flows = hass.config_entries.flow.async_progress() + flows[0]["context"]["unique_id"] = "example2@example.com" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT_REAUTH, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "reauth" + assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/mazda/test_init.py b/tests/components/mazda/test_init.py new file mode 100644 index 00000000000..d0352682f53 --- /dev/null +++ b/tests/components/mazda/test_init.py @@ -0,0 +1,100 @@ +"""Tests for the Mazda Connected Services integration.""" +from unittest.mock import patch + +from pymazda import MazdaAuthenticationException, MazdaException + +from homeassistant.components.mazda.const import DATA_COORDINATOR, DOMAIN +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_SETUP_ERROR, + ENTRY_STATE_SETUP_RETRY, +) +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.mazda import init_integration + +FIXTURE_USER_INPUT = { + CONF_EMAIL: "example@example.com", + CONF_PASSWORD: "password", + CONF_REGION: "MNAO", +} + + +async def test_config_entry_not_ready(hass: HomeAssistant) -> None: + """Test the Mazda configuration entry not ready.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.mazda.MazdaAPI.validate_credentials", + side_effect=MazdaException("Unknown error"), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_init_auth_failure(hass: HomeAssistant): + """Test auth failure during setup.""" + with patch( + "homeassistant.components.mazda.MazdaAPI.validate_credentials", + side_effect=MazdaAuthenticationException("Login failed"), + ): + config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ENTRY_STATE_SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth" + + +async def test_update_auth_failure(hass: HomeAssistant): + """Test auth failure during data update.""" + with patch( + "homeassistant.components.mazda.MazdaAPI.validate_credentials", + return_value=True, + ), patch("homeassistant.components.mazda.MazdaAPI.get_vehicles", return_value={}): + config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ENTRY_STATE_LOADED + + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + with patch( + "homeassistant.components.mazda.MazdaAPI.validate_credentials", + side_effect=MazdaAuthenticationException("Login failed"), + ), patch( + "homeassistant.components.mazda.MazdaAPI.get_vehicles", + side_effect=MazdaAuthenticationException("Login failed"), + ): + await coordinator.async_refresh() + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth" + + +async def test_unload_config_entry(hass: HomeAssistant) -> None: + """Test the Mazda configuration entry unloading.""" + entry = await init_integration(hass) + assert hass.data[DOMAIN] + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert not hass.data.get(DOMAIN) diff --git a/tests/components/mazda/test_sensor.py b/tests/components/mazda/test_sensor.py new file mode 100644 index 00000000000..1cb9f7ac4b7 --- /dev/null +++ b/tests/components/mazda/test_sensor.py @@ -0,0 +1,160 @@ +"""The sensor tests for the Mazda Connected Services integration.""" + +from homeassistant.components.mazda.const import DOMAIN +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + LENGTH_KILOMETERS, + LENGTH_MILES, + PERCENTAGE, + PRESSURE_PSI, +) +from homeassistant.util.unit_system import IMPERIAL_SYSTEM + +from tests.components.mazda import init_integration + + +async def test_device_nickname(hass): + """Test creation of the device when vehicle has a nickname.""" + await init_integration(hass, use_nickname=True) + + device_registry = await hass.helpers.device_registry.async_get_registry() + reg_device = device_registry.async_get_device( + identifiers={(DOMAIN, "JM000000000000000")}, + ) + + assert reg_device.model == "2021 MAZDA3 2.5 S SE AWD" + assert reg_device.manufacturer == "Mazda" + assert reg_device.name == "My Mazda3" + + +async def test_device_no_nickname(hass): + """Test creation of the device when vehicle has no nickname.""" + await init_integration(hass, use_nickname=False) + + device_registry = await hass.helpers.device_registry.async_get_registry() + reg_device = device_registry.async_get_device( + identifiers={(DOMAIN, "JM000000000000000")}, + ) + + assert reg_device.model == "2021 MAZDA3 2.5 S SE AWD" + assert reg_device.manufacturer == "Mazda" + assert reg_device.name == "2021 MAZDA3 2.5 S SE AWD" + + +async def test_sensors(hass): + """Test creation of the sensors.""" + await init_integration(hass) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + # Fuel Remaining Percentage + state = hass.states.get("sensor.my_mazda3_fuel_remaining_percentage") + assert state + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "My Mazda3 Fuel Remaining Percentage" + ) + assert state.attributes.get(ATTR_ICON) == "mdi:gas-station" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + assert state.state == "87.0" + entry = entity_registry.async_get("sensor.my_mazda3_fuel_remaining_percentage") + assert entry + assert entry.unique_id == "JM000000000000000_fuel_remaining_percentage" + + # Fuel Distance Remaining + state = hass.states.get("sensor.my_mazda3_fuel_distance_remaining") + assert state + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Fuel Distance Remaining" + ) + assert state.attributes.get(ATTR_ICON) == "mdi:gas-station" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS + assert state.state == "381" + entry = entity_registry.async_get("sensor.my_mazda3_fuel_distance_remaining") + assert entry + assert entry.unique_id == "JM000000000000000_fuel_distance_remaining" + + # Odometer + state = hass.states.get("sensor.my_mazda3_odometer") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Odometer" + assert state.attributes.get(ATTR_ICON) == "mdi:speedometer" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS + assert state.state == "2796" + entry = entity_registry.async_get("sensor.my_mazda3_odometer") + assert entry + assert entry.unique_id == "JM000000000000000_odometer" + + # Front Left Tire Pressure + state = hass.states.get("sensor.my_mazda3_front_left_tire_pressure") + assert state + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Front Left Tire Pressure" + ) + assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PSI + assert state.state == "35" + entry = entity_registry.async_get("sensor.my_mazda3_front_left_tire_pressure") + assert entry + assert entry.unique_id == "JM000000000000000_front_left_tire_pressure" + + # Front Right Tire Pressure + state = hass.states.get("sensor.my_mazda3_front_right_tire_pressure") + assert state + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "My Mazda3 Front Right Tire Pressure" + ) + assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PSI + assert state.state == "35" + entry = entity_registry.async_get("sensor.my_mazda3_front_right_tire_pressure") + assert entry + assert entry.unique_id == "JM000000000000000_front_right_tire_pressure" + + # Rear Left Tire Pressure + state = hass.states.get("sensor.my_mazda3_rear_left_tire_pressure") + assert state + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Left Tire Pressure" + ) + assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PSI + assert state.state == "33" + entry = entity_registry.async_get("sensor.my_mazda3_rear_left_tire_pressure") + assert entry + assert entry.unique_id == "JM000000000000000_rear_left_tire_pressure" + + # Rear Right Tire Pressure + state = hass.states.get("sensor.my_mazda3_rear_right_tire_pressure") + assert state + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Right Tire Pressure" + ) + assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PSI + assert state.state == "33" + entry = entity_registry.async_get("sensor.my_mazda3_rear_right_tire_pressure") + assert entry + assert entry.unique_id == "JM000000000000000_rear_right_tire_pressure" + + +async def test_sensors_imperial_units(hass): + """Test that the sensors work properly with imperial units.""" + hass.config.units = IMPERIAL_SYSTEM + + await init_integration(hass) + + # Fuel Distance Remaining + state = hass.states.get("sensor.my_mazda3_fuel_distance_remaining") + assert state + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_MILES + assert state.state == "237" + + # Odometer + state = hass.states.get("sensor.my_mazda3_odometer") + assert state + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_MILES + assert state.state == "1737" diff --git a/tests/fixtures/mazda/get_vehicle_status.json b/tests/fixtures/mazda/get_vehicle_status.json new file mode 100644 index 00000000000..f170b222b31 --- /dev/null +++ b/tests/fixtures/mazda/get_vehicle_status.json @@ -0,0 +1,37 @@ +{ + "lastUpdatedTimestamp": "20210123143809", + "latitude": 1.234567, + "longitude": -2.345678, + "positionTimestamp": "20210123143808", + "fuelRemainingPercent": 87.0, + "fuelDistanceRemainingKm": 380.8, + "odometerKm": 2795.8, + "doors": { + "driverDoorOpen": false, + "passengerDoorOpen": false, + "rearLeftDoorOpen": false, + "rearRightDoorOpen": false, + "trunkOpen": false, + "hoodOpen": false, + "fuelLidOpen": false + }, + "doorLocks": { + "driverDoorUnlocked": false, + "passengerDoorUnlocked": false, + "rearLeftDoorUnlocked": false, + "rearRightDoorUnlocked": false + }, + "windows": { + "driverWindowOpen": false, + "passengerWindowOpen": false, + "rearLeftWindowOpen": false, + "rearRightWindowOpen": false + }, + "hazardLightsOn": false, + "tirePressure": { + "frontLeftTirePressurePsi": 35.0, + "frontRightTirePressurePsi": 35.0, + "rearLeftTirePressurePsi": 33.0, + "rearRightTirePressurePsi": 33.0 + } +} \ No newline at end of file diff --git a/tests/fixtures/mazda/get_vehicles.json b/tests/fixtures/mazda/get_vehicles.json new file mode 100644 index 00000000000..871eeb9d2ec --- /dev/null +++ b/tests/fixtures/mazda/get_vehicles.json @@ -0,0 +1,17 @@ +[ + { + "vin": "JM000000000000000", + "id": 12345, + "nickname": "My Mazda3", + "carlineCode": "M3S", + "carlineName": "MAZDA3 2.5 S SE AWD", + "modelYear": "2021", + "modelCode": "M3S SE XA", + "modelName": "W/ SELECT PKG AWD SDN", + "automaticTransmission": true, + "interiorColorCode": "BY3", + "interiorColorName": "BLACK", + "exteriorColorCode": "42M", + "exteriorColorName": "DEEP CRYSTAL BLUE MICA" + } +] \ No newline at end of file From a775b79d4b6651de73a8bdd611050af2ec27e8ad Mon Sep 17 00:00:00 2001 From: Niccolo Zapponi Date: Wed, 3 Feb 2021 18:18:31 +0000 Subject: [PATCH 165/796] Add support for iCloud 2FA (#45818) * Add support for iCloud 2FA * Updated dependency for iCloud * Updated dependency and logic fix * Added logic for handling incorrect 2FA code * Bug fix on failing test * Added myself to codeowners * Added check for 2FA on setup * Updated error message --- CODEOWNERS | 2 +- homeassistant/components/icloud/account.py | 34 ++++--- .../components/icloud/config_flow.py | 54 ++++++++--- homeassistant/components/icloud/const.py | 2 +- homeassistant/components/icloud/manifest.json | 4 +- homeassistant/components/icloud/strings.json | 2 +- .../components/icloud/translations/en.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/icloud/test_config_flow.py | 90 +++++++++++++++++++ 10 files changed, 164 insertions(+), 30 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index cb69b86ed8c..efb338dd4b4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -211,7 +211,7 @@ homeassistant/components/hydrawise/* @ptcryan homeassistant/components/hyperion/* @dermotduffy homeassistant/components/iammeter/* @lewei50 homeassistant/components/iaqualink/* @flz -homeassistant/components/icloud/* @Quentame +homeassistant/components/icloud/* @Quentame @nzapponi homeassistant/components/ign_sismologia/* @exxamalte homeassistant/components/image/* @home-assistant/core homeassistant/components/incomfort/* @zxdavb diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index e6337085e04..4221cf635ba 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -113,6 +113,12 @@ class IcloudAccount: self._icloud_dir.path, with_family=self._with_family, ) + + if not self.api.is_trusted_session or self.api.requires_2fa: + # Session is no longer trusted + # Trigger a new log in to ensure the user enters the 2FA code again. + raise PyiCloudFailedLoginException + except PyiCloudFailedLoginException: self.api = None # Login failed which means credentials need to be updated. @@ -125,16 +131,7 @@ class IcloudAccount: self._config_entry.data[CONF_USERNAME], ) - self.hass.add_job( - self.hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data={ - **self._config_entry.data, - "unique_id": self._config_entry.unique_id, - }, - ) - ) + self._require_reauth() return try: @@ -165,6 +162,10 @@ class IcloudAccount: if self.api is None: return + if not self.api.is_trusted_session or self.api.requires_2fa: + self._require_reauth() + return + api_devices = {} try: api_devices = self.api.devices @@ -228,6 +229,19 @@ class IcloudAccount: utcnow() + timedelta(minutes=self._fetch_interval), ) + def _require_reauth(self): + """Require the user to log in again.""" + self.hass.add_job( + self.hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data={ + **self._config_entry.data, + "unique_id": self._config_entry.unique_id, + }, + ) + ) + def _determine_interval(self) -> int: """Calculate new interval between two API fetch (in minutes).""" intervals = {"default": self._max_interval} diff --git a/homeassistant/components/icloud/config_flow.py b/homeassistant/components/icloud/config_flow.py index d447790e432..c79024c4f64 100644 --- a/homeassistant/components/icloud/config_flow.py +++ b/homeassistant/components/icloud/config_flow.py @@ -125,6 +125,9 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {CONF_PASSWORD: "invalid_auth"} return self._show_setup_form(user_input, errors, step_id) + if self.api.requires_2fa: + return await self.async_step_verification_code() + if self.api.requires_2sa: return await self.async_step_trusted_device() @@ -243,22 +246,29 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - async def async_step_verification_code(self, user_input=None): + async def async_step_verification_code(self, user_input=None, errors=None): """Ask the verification code to the user.""" - errors = {} + if errors is None: + errors = {} if user_input is None: - return await self._show_verification_code_form(user_input) + return await self._show_verification_code_form(user_input, errors) self._verification_code = user_input[CONF_VERIFICATION_CODE] try: - if not await self.hass.async_add_executor_job( - self.api.validate_verification_code, - self._trusted_device, - self._verification_code, - ): - raise PyiCloudException("The code you entered is not valid.") + if self.api.requires_2fa: + if not await self.hass.async_add_executor_job( + self.api.validate_2fa_code, self._verification_code + ): + raise PyiCloudException("The code you entered is not valid.") + else: + if not await self.hass.async_add_executor_job( + self.api.validate_verification_code, + self._trusted_device, + self._verification_code, + ): + raise PyiCloudException("The code you entered is not valid.") except PyiCloudException as error: # Reset to the initial 2FA state to allow the user to retry _LOGGER.error("Failed to verify verification code: %s", error) @@ -266,7 +276,27 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._verification_code = None errors["base"] = "validate_verification_code" - return await self.async_step_trusted_device(None, errors) + if self.api.requires_2fa: + try: + self.api = await self.hass.async_add_executor_job( + PyiCloudService, + self._username, + self._password, + self.hass.helpers.storage.Store( + STORAGE_VERSION, STORAGE_KEY + ).path, + True, + None, + self._with_family, + ) + return await self.async_step_verification_code(None, errors) + except PyiCloudFailedLoginException as error: + _LOGGER.error("Error logging into iCloud service: %s", error) + self.api = None + errors = {CONF_PASSWORD: "invalid_auth"} + return self._show_setup_form(user_input, errors, "user") + else: + return await self.async_step_trusted_device(None, errors) return await self.async_step_user( { @@ -278,11 +308,11 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } ) - async def _show_verification_code_form(self, user_input=None): + async def _show_verification_code_form(self, user_input=None, errors=None): """Show the verification_code form to the user.""" return self.async_show_form( step_id=CONF_VERIFICATION_CODE, data_schema=vol.Schema({vol.Required(CONF_VERIFICATION_CODE): str}), - errors=None, + errors=errors or {}, ) diff --git a/homeassistant/components/icloud/const.py b/homeassistant/components/icloud/const.py index d62bacf1212..58c62f8a868 100644 --- a/homeassistant/components/icloud/const.py +++ b/homeassistant/components/icloud/const.py @@ -12,7 +12,7 @@ DEFAULT_GPS_ACCURACY_THRESHOLD = 500 # meters # to store the cookie STORAGE_KEY = DOMAIN -STORAGE_VERSION = 1 +STORAGE_VERSION = 2 PLATFORMS = ["device_tracker", "sensor"] diff --git a/homeassistant/components/icloud/manifest.json b/homeassistant/components/icloud/manifest.json index 40b58cbf2d0..4d96f42b8cb 100644 --- a/homeassistant/components/icloud/manifest.json +++ b/homeassistant/components/icloud/manifest.json @@ -3,6 +3,6 @@ "name": "Apple iCloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/icloud", - "requirements": ["pyicloud==0.9.7"], - "codeowners": ["@Quentame"] + "requirements": ["pyicloud==0.10.2"], + "codeowners": ["@Quentame", "@nzapponi"] } diff --git a/homeassistant/components/icloud/strings.json b/homeassistant/components/icloud/strings.json index d07b3c3b870..70ab11157d3 100644 --- a/homeassistant/components/icloud/strings.json +++ b/homeassistant/components/icloud/strings.json @@ -35,7 +35,7 @@ "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "send_verification_code": "Failed to send verification code", - "validate_verification_code": "Failed to verify your verification code, choose a trust device and start the verification again" + "validate_verification_code": "Failed to verify your verification code, try again" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", diff --git a/homeassistant/components/icloud/translations/en.json b/homeassistant/components/icloud/translations/en.json index 3097302ded2..36e657011e3 100644 --- a/homeassistant/components/icloud/translations/en.json +++ b/homeassistant/components/icloud/translations/en.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "Invalid authentication", "send_verification_code": "Failed to send verification code", - "validate_verification_code": "Failed to verify your verification code, choose a trust device and start the verification again" + "validate_verification_code": "Failed to verify your verification code, try again" }, "step": { "reauth": { diff --git a/requirements_all.txt b/requirements_all.txt index 93922e9f934..37422a65972 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1443,7 +1443,7 @@ pyhomematic==0.1.71 pyhomeworks==0.0.6 # homeassistant.components.icloud -pyicloud==0.9.7 +pyicloud==0.10.2 # homeassistant.components.insteon pyinsteon==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e379612060..2cd33ee3fc9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -745,7 +745,7 @@ pyheos==0.7.2 pyhomematic==0.1.71 # homeassistant.components.icloud -pyicloud==0.9.7 +pyicloud==0.10.2 # homeassistant.components.insteon pyinsteon==1.0.9 diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py index a774e61f3ec..998a69c575a 100644 --- a/tests/components/icloud/test_config_flow.py +++ b/tests/components/icloud/test_config_flow.py @@ -51,6 +51,7 @@ def mock_controller_service(): with patch( "homeassistant.components.icloud.config_flow.PyiCloudService" ) as service_mock: + service_mock.return_value.requires_2fa = False service_mock.return_value.requires_2sa = True service_mock.return_value.trusted_devices = TRUSTED_DEVICES service_mock.return_value.send_verification_code = Mock(return_value=True) @@ -58,15 +59,31 @@ def mock_controller_service(): yield service_mock +@pytest.fixture(name="service_2fa") +def mock_controller_2fa_service(): + """Mock a successful 2fa service.""" + with patch( + "homeassistant.components.icloud.config_flow.PyiCloudService" + ) as service_mock: + service_mock.return_value.requires_2fa = True + service_mock.return_value.requires_2sa = True + service_mock.return_value.validate_2fa_code = Mock(return_value=True) + service_mock.return_value.is_trusted_session = False + yield service_mock + + @pytest.fixture(name="service_authenticated") def mock_controller_service_authenticated(): """Mock a successful service while already authenticate.""" with patch( "homeassistant.components.icloud.config_flow.PyiCloudService" ) as service_mock: + service_mock.return_value.requires_2fa = False service_mock.return_value.requires_2sa = False + service_mock.return_value.is_trusted_session = True service_mock.return_value.trusted_devices = TRUSTED_DEVICES service_mock.return_value.send_verification_code = Mock(return_value=True) + service_mock.return_value.validate_2fa_code = Mock(return_value=True) service_mock.return_value.validate_verification_code = Mock(return_value=True) yield service_mock @@ -77,6 +94,7 @@ def mock_controller_service_authenticated_no_device(): with patch( "homeassistant.components.icloud.config_flow.PyiCloudService" ) as service_mock: + service_mock.return_value.requires_2fa = False service_mock.return_value.requires_2sa = False service_mock.return_value.trusted_devices = TRUSTED_DEVICES service_mock.return_value.send_verification_code = Mock(return_value=True) @@ -85,24 +103,53 @@ def mock_controller_service_authenticated_no_device(): yield service_mock +@pytest.fixture(name="service_authenticated_not_trusted") +def mock_controller_service_authenticated_not_trusted(): + """Mock a successful service while already authenticated, but the session is not trusted.""" + with patch( + "homeassistant.components.icloud.config_flow.PyiCloudService" + ) as service_mock: + service_mock.return_value.requires_2fa = False + service_mock.return_value.requires_2sa = False + service_mock.return_value.is_trusted_session = False + service_mock.return_value.trusted_devices = TRUSTED_DEVICES + service_mock.return_value.send_verification_code = Mock(return_value=True) + service_mock.return_value.validate_2fa_code = Mock(return_value=True) + service_mock.return_value.validate_verification_code = Mock(return_value=True) + yield service_mock + + @pytest.fixture(name="service_send_verification_code_failed") def mock_controller_service_send_verification_code_failed(): """Mock a failed service during sending verification code step.""" with patch( "homeassistant.components.icloud.config_flow.PyiCloudService" ) as service_mock: + service_mock.return_value.requires_2fa = False service_mock.return_value.requires_2sa = True service_mock.return_value.trusted_devices = TRUSTED_DEVICES service_mock.return_value.send_verification_code = Mock(return_value=False) yield service_mock +@pytest.fixture(name="service_validate_2fa_code_failed") +def mock_controller_service_validate_2fa_code_failed(): + """Mock a failed service during validation of 2FA verification code step.""" + with patch( + "homeassistant.components.icloud.config_flow.PyiCloudService" + ) as service_mock: + service_mock.return_value.requires_2fa = True + service_mock.return_value.validate_2fa_code = Mock(return_value=False) + yield service_mock + + @pytest.fixture(name="service_validate_verification_code_failed") def mock_controller_service_validate_verification_code_failed(): """Mock a failed service during validation of verification code step.""" with patch( "homeassistant.components.icloud.config_flow.PyiCloudService" ) as service_mock: + service_mock.return_value.requires_2fa = False service_mock.return_value.requires_2sa = True service_mock.return_value.trusted_devices = TRUSTED_DEVICES service_mock.return_value.send_verification_code = Mock(return_value=True) @@ -409,6 +456,49 @@ async def test_validate_verification_code_failed( assert result["errors"] == {"base": "validate_verification_code"} +async def test_2fa_code_success(hass: HomeAssistantType, service_2fa: MagicMock): + """Test 2fa step success.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + ) + service_2fa.return_value.requires_2fa = False + service_2fa.return_value.requires_2sa = False + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_VERIFICATION_CODE: "0"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == USERNAME + assert result["title"] == USERNAME + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_WITH_FAMILY] == DEFAULT_WITH_FAMILY + assert result["data"][CONF_MAX_INTERVAL] == DEFAULT_MAX_INTERVAL + assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == DEFAULT_GPS_ACCURACY_THRESHOLD + + +async def test_validate_2fa_code_failed( + hass: HomeAssistantType, service_validate_2fa_code_failed: MagicMock +): + """Test when we have errors during validate_verification_code.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_VERIFICATION_CODE: "0"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == CONF_VERIFICATION_CODE + assert result["errors"] == {"base": "validate_verification_code"} + + async def test_password_update( hass: HomeAssistantType, service_authenticated: MagicMock ): From 51e695fd45d5250be8e32b0c92923ec6c538df67 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 3 Feb 2021 15:35:05 -0500 Subject: [PATCH 166/796] add api to refresh topology (#44840) --- homeassistant/components/zha/api.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 91fe51f7793..2bd712ff681 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -469,6 +469,19 @@ async def websocket_reconfigure_node(hass, connection, msg): hass.async_create_task(device.async_configure()) +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zha/topology/update", + } +) +async def websocket_update_topology(hass, connection, msg): + """Update the ZHA network topology.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + hass.async_create_task(zha_gateway.application_controller.topology.scan()) + + @websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( @@ -1143,6 +1156,7 @@ def async_load_api(hass): websocket_api.async_register_command(hass, websocket_get_bindable_devices) websocket_api.async_register_command(hass, websocket_bind_devices) websocket_api.async_register_command(hass, websocket_unbind_devices) + websocket_api.async_register_command(hass, websocket_update_topology) @callback From adf38f7074764174076ce5a90e93304f87d7e700 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 4 Feb 2021 00:06:54 +0000 Subject: [PATCH 167/796] [ci skip] Translation update --- .../components/icloud/translations/ca.json | 2 +- .../components/mazda/translations/ca.json | 35 +++++++++++++++++++ .../components/mazda/translations/et.json | 35 +++++++++++++++++++ .../components/zwave/translations/ca.json | 2 +- .../components/zwave/translations/en.json | 2 +- .../components/zwave/translations/et.json | 2 +- 6 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/mazda/translations/ca.json create mode 100644 homeassistant/components/mazda/translations/et.json diff --git a/homeassistant/components/icloud/translations/ca.json b/homeassistant/components/icloud/translations/ca.json index a0d74aba98c..6e92897161a 100644 --- a/homeassistant/components/icloud/translations/ca.json +++ b/homeassistant/components/icloud/translations/ca.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "send_verification_code": "No s'ha pogut enviar el codi de verificaci\u00f3", - "validate_verification_code": "No s'ha pogut verificar el codi de verificaci\u00f3, tria un dispositiu de confian\u00e7a i torna a iniciar el proc\u00e9s" + "validate_verification_code": "No s'ha pogut verificar el codi de verificaci\u00f3, torna-ho a provar" }, "step": { "reauth": { diff --git a/homeassistant/components/mazda/translations/ca.json b/homeassistant/components/mazda/translations/ca.json new file mode 100644 index 00000000000..d45b9177c3f --- /dev/null +++ b/homeassistant/components/mazda/translations/ca.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja ha estat configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "account_locked": "Compte bloquejat. Intenta-ho m\u00e9s tard.", + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "reauth": { + "data": { + "email": "Correu electr\u00f2nic", + "password": "Contrasenya", + "region": "Regi\u00f3" + }, + "description": "Ha fallat l'autenticaci\u00f3 dels Serveis connectats de Mazda. Introdueix les teves credencials actuals.", + "title": "Serveis connectats de Mazda - Ha fallat l'autenticaci\u00f3" + }, + "user": { + "data": { + "email": "Correu electr\u00f2nic", + "password": "Contrasenya", + "region": "Regi\u00f3" + }, + "description": "Introdueix el correu electr\u00f2nic i la contrasenya que utilitzes per iniciar sessi\u00f3 a l'aplicaci\u00f3 de m\u00f2bil MyMazda.", + "title": "Serveis connectats de Mazda - Afegeix un compte" + } + } + }, + "title": "Serveis connectats de Mazda" +} \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/et.json b/homeassistant/components/mazda/translations/et.json new file mode 100644 index 00000000000..4ce2e2fa5f3 --- /dev/null +++ b/homeassistant/components/mazda/translations/et.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "account_locked": "Konto on lukus. Proovi hiljem uuesti.", + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "reauth": { + "data": { + "email": "E-posti aadress", + "password": "Salas\u00f5na", + "region": "Piirkond" + }, + "description": "Mazda Connected Services tuvastamine nurjus. Sisesta oma kehtivad andmed.", + "title": "Mazda Connected Services - tuvastamine nurjus" + }, + "user": { + "data": { + "email": "E-posti aadress", + "password": "Salas\u00f5na", + "region": "Piirkond" + }, + "description": "Sisesta e-posti aadress ja salas\u00f5na mida kasutad MyMazda mobiilirakendusse sisselogimiseks.", + "title": "Mazda Connected Services - lisa konto" + } + } + }, + "title": "Mazda Connected Services" +} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/ca.json b/homeassistant/components/zwave/translations/ca.json index 3c97d8c212f..13805a2d1ed 100644 --- a/homeassistant/components/zwave/translations/ca.json +++ b/homeassistant/components/zwave/translations/ca.json @@ -13,7 +13,7 @@ "network_key": "Clau de xarxa (deixa-ho en blanc per generar-la autom\u00e0ticament)", "usb_path": "Ruta del port USB del dispositiu" }, - "description": "Consulta https://www.home-assistant.io/docs/z-wave/installation/ per obtenir informaci\u00f3 sobre les variables de configuraci\u00f3", + "description": "Aquesta integraci\u00f3 ja no s'actualitzar\u00e0. Utilitza Z-Wave JS per a instal\u00b7lacions noves.\n\nConsulta https://www.home-assistant.io/docs/z-wave/installation/ per a m\u00e9s informaci\u00f3 sobre les variables de configuraci\u00f3", "title": "Configuraci\u00f3 de Z-Wave" } } diff --git a/homeassistant/components/zwave/translations/en.json b/homeassistant/components/zwave/translations/en.json index d13e5575e61..2fe3e15646a 100644 --- a/homeassistant/components/zwave/translations/en.json +++ b/homeassistant/components/zwave/translations/en.json @@ -13,7 +13,7 @@ "network_key": "Network Key (leave blank to auto-generate)", "usb_path": "USB Device Path" }, - "description": "See https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables", + "description": "This integration is no longer maintained. For new installations, use Z-Wave JS instead.\n\nSee https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables", "title": "Set up Z-Wave" } } diff --git a/homeassistant/components/zwave/translations/et.json b/homeassistant/components/zwave/translations/et.json index ef36101b3ad..b1fa6127076 100644 --- a/homeassistant/components/zwave/translations/et.json +++ b/homeassistant/components/zwave/translations/et.json @@ -13,7 +13,7 @@ "network_key": "V\u00f5rguv\u00f5ti (j\u00e4ta automaatse genereerimise jaoks t\u00fchjaks)", "usb_path": "USB seadme rada" }, - "description": "Konfiguratsioonimuutujate kohta leiad teavet https://www.home-assistant.io/docs/z-wave/installation/", + "description": "Seda sidumist enam ei hallata. Uueks sidumiseks kasuta Z-Wave JS.\n\nKonfiguratsioonimuutujate kohta leiad teavet https://www.home-assistant.io/docs/z-wave/installation/", "title": "Seadista Z-Wave" } } From 04f39d7dd4fcfae02783ef0cf4d7bcc772f8bec1 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Wed, 3 Feb 2021 19:19:22 -0500 Subject: [PATCH 168/796] Use core constants for command_line auth provider (#45907) --- homeassistant/auth/providers/command_line.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/auth/providers/command_line.py b/homeassistant/auth/providers/command_line.py index d194d8119d1..b2b19221979 100644 --- a/homeassistant/auth/providers/command_line.py +++ b/homeassistant/auth/providers/command_line.py @@ -8,12 +8,12 @@ from typing import Any, Dict, Optional, cast import voluptuous as vol +from homeassistant.const import CONF_COMMAND from homeassistant.exceptions import HomeAssistantError from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from ..models import Credentials, UserMeta -CONF_COMMAND = "command" CONF_ARGS = "args" CONF_META = "meta" From 44914c01ac9ecfedac9a268bc8b7fe690557b8be Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Thu, 4 Feb 2021 01:26:32 +0100 Subject: [PATCH 169/796] Fix typo in Roomba strings (#45928) --- homeassistant/components/roomba/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/roomba/strings.json b/homeassistant/components/roomba/strings.json index 512e27a758e..48e130df4f5 100644 --- a/homeassistant/components/roomba/strings.json +++ b/homeassistant/components/roomba/strings.json @@ -23,7 +23,7 @@ }, "link_manual": { "title": "Enter Password", - "description": "The password could not be retrivied from the device automatically. Please follow the steps outlined in the documentation at: {auth_help_url}", + "description": "The password could not be retrieved from the device automatically. Please follow the steps outlined in the documentation at: {auth_help_url}", "data": { "password": "[%key:common::config_flow::data::password%]" } From 5a0715d388487f7a47659722876b9f4c280905cd Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Thu, 4 Feb 2021 01:43:07 +0100 Subject: [PATCH 170/796] Consistent spelling of IT abbreviations / protocol / format names (#45913) --- homeassistant/components/ambiclimate/strings.json | 2 +- homeassistant/components/cast/services.yaml | 2 +- homeassistant/components/color_extractor/services.yaml | 2 +- homeassistant/components/deconz/services.yaml | 2 +- homeassistant/components/denonavr/services.yaml | 2 +- homeassistant/components/frontend/services.yaml | 2 +- homeassistant/components/goalzero/strings.json | 2 +- homeassistant/components/habitica/services.yaml | 4 ++-- homeassistant/components/homekit/services.yaml | 2 +- homeassistant/components/konnected/strings.json | 2 +- homeassistant/components/lovelace/services.yaml | 2 +- homeassistant/components/lutron_caseta/strings.json | 2 +- homeassistant/components/media_extractor/services.yaml | 2 +- homeassistant/components/mqtt/services.yaml | 2 +- homeassistant/components/soma/strings.json | 2 +- homeassistant/components/spotify/strings.json | 2 +- homeassistant/components/traccar/strings.json | 2 +- homeassistant/components/vera/strings.json | 4 ++-- homeassistant/components/xiaomi_aqara/strings.json | 4 ++-- homeassistant/strings.json | 2 +- 20 files changed, 23 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/ambiclimate/strings.json b/homeassistant/components/ambiclimate/strings.json index b08b8919da2..c51c25a2f61 100644 --- a/homeassistant/components/ambiclimate/strings.json +++ b/homeassistant/components/ambiclimate/strings.json @@ -3,7 +3,7 @@ "step": { "auth": { "title": "Authenticate Ambiclimate", - "description": "Please follow this [link]({authorization_url}) and **Allow** access to your Ambiclimate account, then come back and press **Submit** below.\n(Make sure the specified callback url is {cb_url})" + "description": "Please follow this [link]({authorization_url}) and **Allow** access to your Ambiclimate account, then come back and press **Submit** below.\n(Make sure the specified callback URL is {cb_url})" } }, "create_entry": { diff --git a/homeassistant/components/cast/services.yaml b/homeassistant/components/cast/services.yaml index d1c29281aad..8e4466c349c 100644 --- a/homeassistant/components/cast/services.yaml +++ b/homeassistant/components/cast/services.yaml @@ -5,7 +5,7 @@ show_lovelace_view: description: Media Player entity to show the Lovelace view on. example: "media_player.kitchen" dashboard_path: - description: The url path of the Lovelace dashboard to show. + description: The URL path of the Lovelace dashboard to show. example: lovelace-cast view_path: description: The path of the Lovelace view to show. diff --git a/homeassistant/components/color_extractor/services.yaml b/homeassistant/components/color_extractor/services.yaml index fa97dacf3d1..33055fd41b9 100644 --- a/homeassistant/components/color_extractor/services.yaml +++ b/homeassistant/components/color_extractor/services.yaml @@ -1,5 +1,5 @@ turn_on: - description: Set the light RGB to the predominant color found in the image provided by url or file path. + description: Set the light RGB to the predominant color found in the image provided by URL or file path. fields: color_extract_url: description: The URL of the image we want to extract RGB values from. Must be allowed in allowlist_external_urls. diff --git a/homeassistant/components/deconz/services.yaml b/homeassistant/components/deconz/services.yaml index 9d85e76d8d3..1703037fc64 100644 --- a/homeassistant/components/deconz/services.yaml +++ b/homeassistant/components/deconz/services.yaml @@ -11,7 +11,7 @@ configure: entity (when entity is specified). example: '"/lights/1/state" or "/state"' data: - description: Data is a json object with what data you want to alter. + description: Data is a JSON object with what data you want to alter. example: '{"on": true}' bridgeid: description: (Optional) Bridgeid is a string unique for each deCONZ hardware. It can be found as part of the integration name. diff --git a/homeassistant/components/denonavr/services.yaml b/homeassistant/components/denonavr/services.yaml index c9831a68aa5..35dedd8fb7f 100644 --- a/homeassistant/components/denonavr/services.yaml +++ b/homeassistant/components/denonavr/services.yaml @@ -1,7 +1,7 @@ # Describes the format for available webostv services get_command: - description: "Send a generic http get command." + description: "Send a generic HTTP get command." fields: entity_id: description: Name(s) of the denonavr entities where to run the API method. diff --git a/homeassistant/components/frontend/services.yaml b/homeassistant/components/frontend/services.yaml index cc0d6bde216..075b73986ff 100644 --- a/homeassistant/components/frontend/services.yaml +++ b/homeassistant/components/frontend/services.yaml @@ -11,4 +11,4 @@ set_theme: example: "dark" reload_themes: - description: Reload themes from yaml configuration. + description: Reload themes from YAML configuration. diff --git a/homeassistant/components/goalzero/strings.json b/homeassistant/components/goalzero/strings.json index 0a1b8909f54..bd59cd5e7f5 100644 --- a/homeassistant/components/goalzero/strings.json +++ b/homeassistant/components/goalzero/strings.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "Goal Zero Yeti", - "description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wifi network. Then get the host ip from your router. DHCP must be set up in your router settings for the device to ensure the host ip does not change. Refer to your router's user manual.", + "description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wifi network. Then get the host IP from your router. DHCP must be set up in your router settings for the device to ensure the host IP does not change. Refer to your router's user manual.", "data": { "host": "[%key:common::config_flow::data::host%]", "name": "[%key:common::config_flow::data::name%]" diff --git a/homeassistant/components/habitica/services.yaml b/homeassistant/components/habitica/services.yaml index 20794b4c47b..6fa8589ba4c 100644 --- a/homeassistant/components/habitica/services.yaml +++ b/homeassistant/components/habitica/services.yaml @@ -1,6 +1,6 @@ # Describes the format for Habitica service api_call: - description: Call Habitica api + description: Call Habitica API fields: name: description: Habitica's username to call for @@ -9,5 +9,5 @@ api_call: description: "Items from API URL in form of an array with method attached at the end. Consult https://habitica.com/apidoc/. Example uses https://habitica.com/apidoc/#api-Task-CreateUserTasks" example: '["tasks", "user", "post"]' args: - description: Any additional json or url parameter arguments. See apidoc mentioned for path. Example uses same api endpoint + description: Any additional JSON or URL parameter arguments. See apidoc mentioned for path. Example uses same API endpoint example: '{"text": "Use API from Home Assistant", "type": "todo"}' diff --git a/homeassistant/components/homekit/services.yaml b/homeassistant/components/homekit/services.yaml index c2dde2cac6c..96971f70300 100644 --- a/homeassistant/components/homekit/services.yaml +++ b/homeassistant/components/homekit/services.yaml @@ -4,7 +4,7 @@ start: description: Starts the HomeKit driver. reload: - description: Reload homekit and re-process yaml configuration. + description: Reload homekit and re-process YAML configuration. reset_accessory: description: Reset a HomeKit accessory. This can be useful when changing a media_player’s device class to tv, linking a battery, or whenever Home Assistant adds support for new HomeKit features to existing entities. diff --git a/homeassistant/components/konnected/strings.json b/homeassistant/components/konnected/strings.json index 4fd62dee8e7..e53838ad0d7 100644 --- a/homeassistant/components/konnected/strings.json +++ b/homeassistant/components/konnected/strings.json @@ -99,7 +99,7 @@ } }, "error": { - "bad_host": "Invalid Override API host url" + "bad_host": "Invalid Override API host URL" }, "abort": { "not_konn_panel": "Not a recognized Konnected.io device" diff --git a/homeassistant/components/lovelace/services.yaml b/homeassistant/components/lovelace/services.yaml index 1147f287e59..20450942dce 100644 --- a/homeassistant/components/lovelace/services.yaml +++ b/homeassistant/components/lovelace/services.yaml @@ -1,4 +1,4 @@ # Describes the format for available lovelace services reload_resources: - description: Reload Lovelace resources from yaml configuration. + description: Reload Lovelace resources from YAML configuration. diff --git a/homeassistant/components/lutron_caseta/strings.json b/homeassistant/components/lutron_caseta/strings.json index bdaec22e776..604a3c24ab2 100644 --- a/homeassistant/components/lutron_caseta/strings.json +++ b/homeassistant/components/lutron_caseta/strings.json @@ -8,7 +8,7 @@ }, "user": { "title": "Automaticlly connect to the bridge", - "description": "Enter the ip address of the device.", + "description": "Enter the IP address of the device.", "data": { "host": "[%key:common::config_flow::data::host%]" } diff --git a/homeassistant/components/media_extractor/services.yaml b/homeassistant/components/media_extractor/services.yaml index 17abffee89d..1e58c19baf1 100644 --- a/homeassistant/components/media_extractor/services.yaml +++ b/homeassistant/components/media_extractor/services.yaml @@ -1,5 +1,5 @@ play_media: - description: Downloads file from given url. + description: Downloads file from given URL. fields: entity_id: description: Name(s) of entities to play media on. diff --git a/homeassistant/components/mqtt/services.yaml b/homeassistant/components/mqtt/services.yaml index 04dce23f5de..992dd1b3545 100644 --- a/homeassistant/components/mqtt/services.yaml +++ b/homeassistant/components/mqtt/services.yaml @@ -37,4 +37,4 @@ dump: default: 5 reload: - description: Reload all mqtt entities from yaml. + description: Reload all MQTT entities from YAML. diff --git a/homeassistant/components/soma/strings.json b/homeassistant/components/soma/strings.json index 8ab7335dd5c..a31b404dad7 100644 --- a/homeassistant/components/soma/strings.json +++ b/homeassistant/components/soma/strings.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "You can only configure one Soma account.", - "authorize_url_timeout": "Timeout generating authorize url.", + "authorize_url_timeout": "Timeout generating authorize URL.", "missing_configuration": "The Soma component is not configured. Please follow the documentation.", "result_error": "SOMA Connect responded with error status.", "connection_error": "Failed to connect to SOMA Connect." diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json index 74df79c4d78..f775e5df85d 100644 --- a/homeassistant/components/spotify/strings.json +++ b/homeassistant/components/spotify/strings.json @@ -10,7 +10,7 @@ } }, "abort": { - "authorize_url_timeout": "Timeout generating authorize url.", + "authorize_url_timeout": "Timeout generating authorize URL.", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", "missing_configuration": "The Spotify integration is not configured. Please follow the documentation.", "reauth_account_mismatch": "The Spotify account authenticated with, does not match the account needed re-authentication." diff --git a/homeassistant/components/traccar/strings.json b/homeassistant/components/traccar/strings.json index d9d9fff4bd3..89689ee43df 100644 --- a/homeassistant/components/traccar/strings.json +++ b/homeassistant/components/traccar/strings.json @@ -11,7 +11,7 @@ "webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]" }, "create_entry": { - "default": "To send events to Home Assistant, you will need to setup the webhook feature in Traccar.\n\nUse the following url: `{webhook_url}`\n\nSee [the documentation]({docs_url}) for further details." + "default": "To send events to Home Assistant, you will need to setup the webhook feature in Traccar.\n\nUse the following URL: `{webhook_url}`\n\nSee [the documentation]({docs_url}) for further details." } } } diff --git a/homeassistant/components/vera/strings.json b/homeassistant/components/vera/strings.json index 844d1777f5d..66958f44a62 100644 --- a/homeassistant/components/vera/strings.json +++ b/homeassistant/components/vera/strings.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "cannot_connect": "Could not connect to controller with url {base_url}" + "cannot_connect": "Could not connect to controller with URL {base_url}" }, "step": { "user": { "title": "Setup Vera controller", - "description": "Provide a Vera controller url below. It should look like this: http://192.168.1.161:3480.", + "description": "Provide a Vera controller URL below. It should look like this: http://192.168.1.161:3480.", "data": { "vera_controller_url": "Controller URL", "lights": "Vera switch device ids to treat as lights in Home Assistant.", diff --git a/homeassistant/components/xiaomi_aqara/strings.json b/homeassistant/components/xiaomi_aqara/strings.json index d5fb25d2c3f..a2c8a226c95 100644 --- a/homeassistant/components/xiaomi_aqara/strings.json +++ b/homeassistant/components/xiaomi_aqara/strings.json @@ -4,7 +4,7 @@ "step": { "user": { "title": "Xiaomi Aqara Gateway", - "description": "Connect to your Xiaomi Aqara Gateway, if the IP and mac addresses are left empty, auto-discovery is used", + "description": "Connect to your Xiaomi Aqara Gateway, if the IP and MAC addresses are left empty, auto-discovery is used", "data": { "interface": "The network interface to use", "host": "[%key:common::config_flow::data::ip%] (optional)", @@ -21,7 +21,7 @@ }, "select": { "title": "Select the Xiaomi Aqara Gateway that you wish to connect", - "description": "Run the setup again if you want to connect aditional gateways", + "description": "Run the setup again if you want to connect additional gateways", "data": { "select_ip": "[%key:common::config_flow::data::ip%]" } diff --git a/homeassistant/strings.json b/homeassistant/strings.json index e2a85637fbb..b8e7dee2996 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -71,7 +71,7 @@ "oauth2_authorize_url_timeout": "Timeout generating authorize URL.", "oauth2_no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", "reauth_successful": "Re-authentication was successful", - "unknown_authorize_url_generation": "Unknown error generating an authorize url." + "unknown_authorize_url_generation": "Unknown error generating an authorize URL." } } } From 14d914e300ff5e8f2e3b607d4f1e7d108cd1f690 Mon Sep 17 00:00:00 2001 From: denes44 <60078357+denes44@users.noreply.github.com> Date: Thu, 4 Feb 2021 02:35:27 +0100 Subject: [PATCH 171/796] Enable emulated_hue setting XY color and transition time by client (#45844) * Enable setting XY color and transition time by client * New test for setting XY color value * Correct block outdent * New test for setting transition time value * Fixed commented out code --- .../components/emulated_hue/hue_api.py | 30 ++++++++++ tests/components/emulated_hue/test_hue_api.py | 58 ++++++++++++++++++- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 51580a28adf..1630405a73e 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -43,9 +43,12 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + ATTR_TRANSITION, + ATTR_XY_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, + SUPPORT_TRANSITION, ) from homeassistant.components.media_player.const import ( ATTR_MEDIA_VOLUME_LEVEL, @@ -82,6 +85,8 @@ STATE_COLORMODE = "colormode" STATE_HUE = "hue" STATE_SATURATION = "sat" STATE_COLOR_TEMP = "ct" +STATE_TRANSITON = "tt" +STATE_XY = "xy" # Hue API states, defined separately in case they change HUE_API_STATE_ON = "on" @@ -90,7 +95,9 @@ HUE_API_STATE_COLORMODE = "colormode" HUE_API_STATE_HUE = "hue" HUE_API_STATE_SAT = "sat" HUE_API_STATE_CT = "ct" +HUE_API_STATE_XY = "xy" HUE_API_STATE_EFFECT = "effect" +HUE_API_STATE_TRANSITION = "transitiontime" # Hue API min/max values - https://developers.meethue.com/develop/hue-api/lights-api/ HUE_API_STATE_BRI_MIN = 1 # Brightness @@ -357,6 +364,8 @@ class HueOneLightChangeView(HomeAssistantView): STATE_HUE: None, STATE_SATURATION: None, STATE_COLOR_TEMP: None, + STATE_XY: None, + STATE_TRANSITON: None, } if HUE_API_STATE_ON in request_json: @@ -372,6 +381,7 @@ class HueOneLightChangeView(HomeAssistantView): (HUE_API_STATE_HUE, STATE_HUE), (HUE_API_STATE_SAT, STATE_SATURATION), (HUE_API_STATE_CT, STATE_COLOR_TEMP), + (HUE_API_STATE_TRANSITION, STATE_TRANSITON), ): if key in request_json: try: @@ -379,6 +389,17 @@ class HueOneLightChangeView(HomeAssistantView): except ValueError: _LOGGER.error("Unable to parse data (2): %s", request_json) return self.json_message("Bad request", HTTP_BAD_REQUEST) + if HUE_API_STATE_XY in request_json: + try: + parsed[STATE_XY] = tuple( + ( + float(request_json[HUE_API_STATE_XY][0]), + float(request_json[HUE_API_STATE_XY][1]), + ) + ) + except ValueError: + _LOGGER.error("Unable to parse data (2): %s", request_json) + return self.json_message("Bad request", HTTP_BAD_REQUEST) if HUE_API_STATE_BRI in request_json: if entity.domain == light.DOMAIN: @@ -444,10 +465,17 @@ class HueOneLightChangeView(HomeAssistantView): data[ATTR_HS_COLOR] = (hue, sat) + if parsed[STATE_XY] is not None: + data[ATTR_XY_COLOR] = parsed[STATE_XY] + if entity_features & SUPPORT_COLOR_TEMP: if parsed[STATE_COLOR_TEMP] is not None: data[ATTR_COLOR_TEMP] = parsed[STATE_COLOR_TEMP] + if entity_features & SUPPORT_TRANSITION: + if parsed[STATE_TRANSITON] is not None: + data[ATTR_TRANSITION] = parsed[STATE_TRANSITON] / 10 + # If the requested entity is a script, add some variables elif entity.domain == script.DOMAIN: data["variables"] = { @@ -557,6 +585,8 @@ class HueOneLightChangeView(HomeAssistantView): (STATE_HUE, HUE_API_STATE_HUE), (STATE_SATURATION, HUE_API_STATE_SAT), (STATE_COLOR_TEMP, HUE_API_STATE_CT), + (STATE_XY, HUE_API_STATE_XY), + (STATE_TRANSITON, HUE_API_STATE_TRANSITION), ): if parsed[key] is not None: json_response.append( diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 04832f4adc7..e3f965616f9 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -28,6 +28,8 @@ from homeassistant.components.emulated_hue.hue_api import ( HUE_API_STATE_HUE, HUE_API_STATE_ON, HUE_API_STATE_SAT, + HUE_API_STATE_TRANSITION, + HUE_API_STATE_XY, HUE_API_USERNAME, HueAllGroupsStateView, HueAllLightsStateView, @@ -39,6 +41,7 @@ from homeassistant.components.emulated_hue.hue_api import ( ) from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, CONTENT_TYPE_JSON, HTTP_NOT_FOUND, HTTP_OK, @@ -51,7 +54,11 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed, get_test_instance_port +from tests.common import ( + async_fire_time_changed, + async_mock_service, + get_test_instance_port, +) HTTP_SERVER_PORT = get_test_instance_port() BRIDGE_SERVER_PORT = get_test_instance_port() @@ -663,6 +670,25 @@ async def test_put_light_state(hass, hass_hue, hue_client): assert ceiling_json["state"][HUE_API_STATE_HUE] == 4369 assert ceiling_json["state"][HUE_API_STATE_SAT] == 127 + # update light state through api + await perform_put_light_state( + hass_hue, + hue_client, + "light.ceiling_lights", + True, + brightness=100, + xy=((0.488, 0.48)), + ) + + # go through api to get the state back + ceiling_json = await perform_get_light_state( + hue_client, "light.ceiling_lights", HTTP_OK + ) + assert ceiling_json["state"][HUE_API_STATE_BRI] == 100 + assert hass.states.get("light.ceiling_lights").attributes[light.ATTR_XY_COLOR] == ( + (0.488, 0.48) + ) + # Go through the API to turn it off ceiling_result = await perform_put_light_state( hass_hue, hue_client, "light.ceiling_lights", False @@ -714,6 +740,30 @@ async def test_put_light_state(hass, hass_hue, hue_client): == 50 ) + # mock light.turn_on call + hass.states.async_set( + "light.ceiling_lights", STATE_ON, {ATTR_SUPPORTED_FEATURES: 55} + ) + call_turn_on = async_mock_service(hass, "light", "turn_on") + + # update light state through api + await perform_put_light_state( + hass_hue, + hue_client, + "light.ceiling_lights", + True, + brightness=99, + xy=((0.488, 0.48)), + transitiontime=60, + ) + + await hass.async_block_till_done() + assert call_turn_on[0] + assert call_turn_on[0].data[ATTR_ENTITY_ID] == ["light.ceiling_lights"] + assert call_turn_on[0].data[light.ATTR_BRIGHTNESS] == 99 + assert call_turn_on[0].data[light.ATTR_XY_COLOR] == ((0.488, 0.48)) + assert call_turn_on[0].data[light.ATTR_TRANSITION] == 6 + async def test_put_light_state_script(hass, hass_hue, hue_client): """Test the setting of script variables.""" @@ -1173,6 +1223,8 @@ async def perform_put_light_state( saturation=None, color_temp=None, with_state=True, + xy=None, + transitiontime=None, ): """Test the setting of a light state.""" req_headers = {"Content-Type": content_type} @@ -1188,8 +1240,12 @@ async def perform_put_light_state( data[HUE_API_STATE_HUE] = hue if saturation is not None: data[HUE_API_STATE_SAT] = saturation + if xy is not None: + data[HUE_API_STATE_XY] = xy if color_temp is not None: data[HUE_API_STATE_CT] = color_temp + if transitiontime is not None: + data[HUE_API_STATE_TRANSITION] = transitiontime entity_number = ENTITY_NUMBERS_BY_ID[entity_id] result = await client.put( From 1b6ee8301a5c076f93d0799b9f7fcb82cc6eb902 Mon Sep 17 00:00:00 2001 From: Christopher Gozdziewski Date: Thu, 4 Feb 2021 01:32:43 -0600 Subject: [PATCH 172/796] Convert ozw climate values to correct units (#45369) * Convert ozw climate values to correct units * Remove debugger logging * Fix black code formatting * Remove extra spaces * Add method descriptions and change to use setpoint * Fix build and respond to comments * Remove self from convert_units call * Move method to top * Move method outside class * Add blank lines * Fix test to use farenheit * Update another value to farenheit * Change to celsius * Another test fix * test fix * Fix a value * missed one * Add unit test for convert_units * fix unit test import * Add new line to end of test file * fix convert units import * Reorder imports * Grab const from different import Co-authored-by: Trevor --- homeassistant/components/ozw/climate.py | 51 +++++++++++++++++++++---- tests/components/ozw/test_climate.py | 20 ++++++---- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/ozw/climate.py b/homeassistant/components/ozw/climate.py index a74fd869f0f..67bbe5cdc4d 100644 --- a/homeassistant/components/ozw/climate.py +++ b/homeassistant/components/ozw/climate.py @@ -28,6 +28,7 @@ from homeassistant.components.climate.const import ( from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.temperature import convert as convert_temperature from .const import DATA_UNSUBSCRIBE, DOMAIN from .entity import ZWaveDeviceEntity @@ -154,6 +155,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) +def convert_units(units): + """Return units as a farenheit or celsius constant.""" + if units == "F": + return TEMP_FAHRENHEIT + return TEMP_CELSIUS + + class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): """Representation of a Z-Wave Climate device.""" @@ -199,16 +207,18 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): @property def temperature_unit(self): """Return the unit of measurement.""" - if self.values.temperature is not None and self.values.temperature.units == "F": - return TEMP_FAHRENHEIT - return TEMP_CELSIUS + return convert_units(self._current_mode_setpoint_values[0].units) @property def current_temperature(self): """Return the current temperature.""" if not self.values.temperature: return None - return self.values.temperature.value + return convert_temperature( + self.values.temperature.value, + convert_units(self._current_mode_setpoint_values[0].units), + self.temperature_unit, + ) @property def hvac_action(self): @@ -236,17 +246,29 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): @property def target_temperature(self): """Return the temperature we try to reach.""" - return self._current_mode_setpoint_values[0].value + return convert_temperature( + self._current_mode_setpoint_values[0].value, + convert_units(self._current_mode_setpoint_values[0].units), + self.temperature_unit, + ) @property def target_temperature_low(self) -> Optional[float]: """Return the lowbound target temperature we try to reach.""" - return self._current_mode_setpoint_values[0].value + return convert_temperature( + self._current_mode_setpoint_values[0].value, + convert_units(self._current_mode_setpoint_values[0].units), + self.temperature_unit, + ) @property def target_temperature_high(self) -> Optional[float]: """Return the highbound target temperature we try to reach.""" - return self._current_mode_setpoint_values[1].value + return convert_temperature( + self._current_mode_setpoint_values[1].value, + convert_units(self._current_mode_setpoint_values[1].units), + self.temperature_unit, + ) async def async_set_temperature(self, **kwargs): """Set new target temperature. @@ -262,14 +284,29 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): setpoint = self._current_mode_setpoint_values[0] target_temp = kwargs.get(ATTR_TEMPERATURE) if setpoint is not None and target_temp is not None: + target_temp = convert_temperature( + target_temp, + self.temperature_unit, + convert_units(setpoint.units), + ) setpoint.send_value(target_temp) elif len(self._current_mode_setpoint_values) == 2: (setpoint_low, setpoint_high) = self._current_mode_setpoint_values target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) if setpoint_low is not None and target_temp_low is not None: + target_temp_low = convert_temperature( + target_temp_low, + self.temperature_unit, + convert_units(setpoint_low.units), + ) setpoint_low.send_value(target_temp_low) if setpoint_high is not None and target_temp_high is not None: + target_temp_high = convert_temperature( + target_temp_high, + self.temperature_unit, + convert_units(setpoint_high.units), + ) setpoint_high.send_value(target_temp_high) async def async_set_fan_mode(self, fan_mode): diff --git a/tests/components/ozw/test_climate.py b/tests/components/ozw/test_climate.py index 3414e6c4832..e251a93c115 100644 --- a/tests/components/ozw/test_climate.py +++ b/tests/components/ozw/test_climate.py @@ -16,6 +16,8 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, ) +from homeassistant.components.ozw.climate import convert_units +from homeassistant.const import TEMP_FAHRENHEIT from .common import setup_ozw @@ -36,8 +38,8 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): HVAC_MODE_HEAT_COOL, ] assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE - assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 23.1 - assert state.attributes[ATTR_TEMPERATURE] == 21.1 + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 73.5 + assert state.attributes[ATTR_TEMPERATURE] == 70.0 assert state.attributes.get(ATTR_TARGET_TEMP_LOW) is None assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) is None assert state.attributes[ATTR_FAN_MODE] == "Auto Low" @@ -54,7 +56,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): msg = sent_messages[-1] assert msg["topic"] == "OpenZWave/1/command/setvalue/" # Celsius is converted to Fahrenheit here! - assert round(msg["payload"]["Value"], 2) == 78.98 + assert round(msg["payload"]["Value"], 2) == 26.1 assert msg["payload"]["ValueIDKey"] == 281475099443218 # Test hvac_mode with set_temperature @@ -72,7 +74,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): msg = sent_messages[-1] assert msg["topic"] == "OpenZWave/1/command/setvalue/" # Celsius is converted to Fahrenheit here! - assert round(msg["payload"]["Value"], 2) == 75.38 + assert round(msg["payload"]["Value"], 2) == 24.1 assert msg["payload"]["ValueIDKey"] == 281475099443218 # Test set mode @@ -127,8 +129,8 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): assert state is not None assert state.state == HVAC_MODE_HEAT_COOL assert state.attributes.get(ATTR_TEMPERATURE) is None - assert state.attributes[ATTR_TARGET_TEMP_LOW] == 21.1 - assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 25.6 + assert state.attributes[ATTR_TARGET_TEMP_LOW] == 70.0 + assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 78.0 # Test setting high/low temp on multiple setpoints await hass.services.async_call( @@ -144,11 +146,11 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): assert len(sent_messages) == 7 # 2 messages ! msg = sent_messages[-2] # low setpoint assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert round(msg["payload"]["Value"], 2) == 68.0 + assert round(msg["payload"]["Value"], 2) == 20.0 assert msg["payload"]["ValueIDKey"] == 281475099443218 msg = sent_messages[-1] # high setpoint assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert round(msg["payload"]["Value"], 2) == 77.0 + assert round(msg["payload"]["Value"], 2) == 25.0 assert msg["payload"]["ValueIDKey"] == 562950076153874 # Test basic/single-setpoint thermostat (node 16 in dump) @@ -325,3 +327,5 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): ) assert len(sent_messages) == 12 assert "does not support setting a mode" in caplog.text + + assert convert_units("F") == TEMP_FAHRENHEIT From 8256acb8efb3f32ff302f4766a85a6830f306cd5 Mon Sep 17 00:00:00 2001 From: olijouve <17448560+olijouve@users.noreply.github.com> Date: Thu, 4 Feb 2021 09:25:35 +0100 Subject: [PATCH 173/796] Fix onvif ConnectionResetError (#45899) Fix "ConnectionResetError: Cannot write to closing transport" error we can have on lots of chinese cams(like Goke GK7102 based IP cameras) Those non full onvif compliant cams can "crash" when calling non implemented functions like events or ptz and they are likely react by closing transport, leaving the request in a uncatched error state. My camera used to fail on setup, and now it run nicely with that simple fix. --- homeassistant/components/onvif/device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index 84761a4777f..c0851cbe32f 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -250,14 +250,14 @@ class ONVIFDevice: pullpoint = False try: pullpoint = await self.events.async_start() - except (ONVIFError, Fault): + except (ONVIFError, Fault, RequestError): pass ptz = False try: self.device.get_definition("ptz") ptz = True - except ONVIFError: + except (ONVIFError, Fault, RequestError): pass return Capabilities(snapshot, pullpoint, ptz) From 06e6005fbba2420f708bcf4046e21dbcb6dcd8fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 4 Feb 2021 09:59:41 +0100 Subject: [PATCH 174/796] Add warning to custom integrations without version (#45919) Co-authored-by: Paulus Schoutsen --- homeassistant/loader.py | 63 ++++++++++++++++++++++++--- homeassistant/package_constraints.txt | 1 + requirements.txt | 1 + requirements_test.txt | 1 - script/bootstrap | 2 +- script/hassfest/manifest.py | 15 ++----- setup.py | 1 + tests/hassfest/test_version.py | 47 ++++++++++++++++++++ tests/test_loader.py | 62 +++++++++++++++++++++++++- 9 files changed, 173 insertions(+), 20 deletions(-) create mode 100644 tests/hassfest/test_version.py diff --git a/homeassistant/loader.py b/homeassistant/loader.py index bedc04928af..f1a0ccc0730 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -26,6 +26,9 @@ from typing import ( cast, ) +from awesomeversion import AwesomeVersion +from awesomeversion.strategy import AwesomeVersionStrategy + from homeassistant.generated.dhcp import DHCP from homeassistant.generated.mqtt import MQTT from homeassistant.generated.ssdp import SSDP @@ -52,7 +55,19 @@ CUSTOM_WARNING = ( "You are using a custom integration %s which has not " "been tested by Home Assistant. This component might " "cause stability problems, be sure to disable it if you " - "experience issues with Home Assistant." + "experience issues with Home Assistant" +) +CUSTOM_WARNING_VERSION_MISSING = ( + "No 'version' key in the manifest file for " + "custom integration '%s'. This will not be " + "allowed in a future version of Home " + "Assistant. Please report this to the " + "maintainer of '%s'" +) +CUSTOM_WARNING_VERSION_TYPE = ( + "'%s' is not a valid version for " + "custom integration '%s'. " + "Please report this to the maintainer of '%s'" ) _UNDEF = object() # Internal; not helpers.typing.UNDEFINED due to circular dependency @@ -83,6 +98,7 @@ class Manifest(TypedDict, total=False): dhcp: List[Dict[str, str]] homekit: Dict[str, List[str]] is_built_in: bool + version: str codeowners: List[str] @@ -417,6 +433,13 @@ class Integration: """Test if package is a built-in integration.""" return self.pkg_path.startswith(PACKAGE_BUILTIN) + @property + def version(self) -> Optional[AwesomeVersion]: + """Return the version of the integration.""" + if "version" not in self.manifest: + return None + return AwesomeVersion(self.manifest["version"]) + @property def all_dependencies(self) -> Set[str]: """Return all dependencies including sub-dependencies.""" @@ -513,7 +536,7 @@ async def async_get_integration(hass: "HomeAssistant", domain: str) -> Integrati # components to find the integration. integration = (await async_get_custom_components(hass)).get(domain) if integration is not None: - _LOGGER.warning(CUSTOM_WARNING, domain) + custom_integration_warning(integration) cache[domain] = integration event.set() return integration @@ -531,6 +554,7 @@ async def async_get_integration(hass: "HomeAssistant", domain: str) -> Integrati integration = Integration.resolve_legacy(hass, domain) if integration is not None: + custom_integration_warning(integration) cache[domain] = integration else: # Remove event from cache. @@ -605,9 +629,6 @@ def _load_file( cache[comp_or_platform] = module - if module.__name__.startswith(PACKAGE_CUSTOM_COMPONENTS): - _LOGGER.warning(CUSTOM_WARNING, comp_or_platform) - return module except ImportError as err: @@ -756,3 +777,35 @@ def _lookup_path(hass: "HomeAssistant") -> List[str]: if hass.config.safe_mode: return [PACKAGE_BUILTIN] return [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN] + + +def validate_custom_integration_version(version: str) -> bool: + """Validate the version of custom integrations.""" + return AwesomeVersion(version).strategy in ( + AwesomeVersionStrategy.CALVER, + AwesomeVersionStrategy.SEMVER, + AwesomeVersionStrategy.SIMPLEVER, + AwesomeVersionStrategy.BUILDVER, + AwesomeVersionStrategy.PEP440, + ) + + +def custom_integration_warning(integration: Integration) -> None: + """Create logs for custom integrations.""" + if not integration.pkg_path.startswith(PACKAGE_CUSTOM_COMPONENTS): + return None + + _LOGGER.warning(CUSTOM_WARNING, integration.domain) + + if integration.manifest.get("version") is None: + _LOGGER.warning( + CUSTOM_WARNING_VERSION_MISSING, integration.domain, integration.domain + ) + else: + if not validate_custom_integration_version(integration.manifest["version"]): + _LOGGER.warning( + CUSTOM_WARNING_VERSION_TYPE, + integration.domain, + integration.manifest["version"], + integration.domain, + ) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b280c23982d..69f75af396b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,6 +5,7 @@ aiohttp_cors==0.7.0 astral==1.10.1 async_timeout==3.0.1 attrs==19.3.0 +awesomeversion==21.2.0 bcrypt==3.1.7 certifi>=2020.12.5 ciso8601==2.1.3 diff --git a/requirements.txt b/requirements.txt index c094efe3e46..17bb82d472f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ aiohttp==3.7.3 astral==1.10.1 async_timeout==3.0.1 attrs==19.3.0 +awesomeversion==21.2.0 bcrypt==3.1.7 certifi>=2020.12.5 ciso8601==2.1.3 diff --git a/requirements_test.txt b/requirements_test.txt index 2c10083ecfd..077c895293f 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,7 +13,6 @@ pre-commit==2.10.0 pylint==2.6.0 astroid==2.4.2 pipdeptree==1.0.0 -awesomeversion==21.2.0 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 pytest-cov==2.10.1 diff --git a/script/bootstrap b/script/bootstrap index 32e9d11bc4d..f27fef8a07d 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -16,4 +16,4 @@ fi echo "Installing development dependencies..." python3 -m pip install wheel --constraint homeassistant/package_constraints.txt -python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) $(grep stdlib-list requirements_test.txt) $(grep tqdm requirements_test.txt) $(grep pipdeptree requirements_test.txt) $(grep awesomeversion requirements_test.txt) --constraint homeassistant/package_constraints.txt +python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) $(grep stdlib-list requirements_test.txt) $(grep tqdm requirements_test.txt) $(grep pipdeptree requirements_test.txt) $(grep awesomeversion requirements.txt) --constraint homeassistant/package_constraints.txt diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 3beb6aadfc5..583cafc8161 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -2,11 +2,11 @@ from typing import Dict from urllib.parse import urlparse -from awesomeversion import AwesomeVersion -from awesomeversion.strategy import AwesomeVersionStrategy import voluptuous as vol from voluptuous.humanize import humanize_error +from homeassistant.loader import validate_custom_integration_version + from .model import Integration DOCUMENTATION_URL_SCHEMA = "https" @@ -53,16 +53,9 @@ def verify_uppercase(value: str): def verify_version(value: str): """Verify the version.""" - version = AwesomeVersion(value) - if version.strategy not in [ - AwesomeVersionStrategy.CALVER, - AwesomeVersionStrategy.SEMVER, - AwesomeVersionStrategy.SIMPLEVER, - AwesomeVersionStrategy.BUILDVER, - AwesomeVersionStrategy.PEP440, - ]: + if not validate_custom_integration_version(value): raise vol.Invalid( - f"'{version}' is not a valid version. This will cause a future version of Home Assistant to block this integration.", + f"'{value}' is not a valid version. This will cause a future version of Home Assistant to block this integration.", ) return value diff --git a/setup.py b/setup.py index 7e0df7f95c9..fc2c250ec0f 100755 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ REQUIRES = [ "astral==1.10.1", "async_timeout==3.0.1", "attrs==19.3.0", + "awesomeversion==21.2.0", "bcrypt==3.1.7", "certifi>=2020.12.5", "ciso8601==2.1.3", diff --git a/tests/hassfest/test_version.py b/tests/hassfest/test_version.py new file mode 100644 index 00000000000..203042e79be --- /dev/null +++ b/tests/hassfest/test_version.py @@ -0,0 +1,47 @@ +"""Tests for hassfest version.""" +import pytest +import voluptuous as vol + +from script.hassfest.manifest import ( + CUSTOM_INTEGRATION_MANIFEST_SCHEMA, + validate_version, +) +from script.hassfest.model import Integration + + +@pytest.fixture +def integration(): + """Fixture for hassfest integration model.""" + integration = Integration("") + integration.manifest = { + "domain": "test", + "documentation": "https://example.com", + "name": "test", + "codeowners": ["@awesome"], + } + return integration + + +def test_validate_version_no_key(integration: Integration): + """Test validate version with no key.""" + validate_version(integration) + assert ( + "No 'version' key in the manifest file. This will cause a future version of Home Assistant to block this integration." + in [x.error for x in integration.warnings] + ) + + +def test_validate_custom_integration_manifest(integration: Integration): + """Test validate custom integration manifest.""" + + with pytest.raises(vol.Invalid): + integration.manifest["version"] = "lorem_ipsum" + CUSTOM_INTEGRATION_MANIFEST_SCHEMA(integration.manifest) + + with pytest.raises(vol.Invalid): + integration.manifest["version"] = None + CUSTOM_INTEGRATION_MANIFEST_SCHEMA(integration.manifest) + + integration.manifest["version"] = "1" + schema = CUSTOM_INTEGRATION_MANIFEST_SCHEMA(integration.manifest) + assert schema["version"] == "1" diff --git a/tests/test_loader.py b/tests/test_loader.py index 22f61c0a397..8acc8a7de4f 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -130,13 +130,69 @@ async def test_custom_component_name(hass): async def test_log_warning_custom_component(hass, caplog): """Test that we log a warning when loading a custom component.""" - hass.components.test_standalone + await loader.async_get_integration(hass, "test_standalone") assert "You are using a custom integration test_standalone" in caplog.text await loader.async_get_integration(hass, "test") assert "You are using a custom integration test " in caplog.text +async def test_custom_integration_missing_version(hass, caplog): + """Test that we log a warning when custom integrations are missing a version.""" + test_integration_1 = loader.Integration( + hass, "custom_components.test1", None, {"domain": "test1"} + ) + test_integration_2 = loader.Integration( + hass, + "custom_components.test2", + None, + loader.manifest_from_legacy_module("test2", "custom_components.test2"), + ) + + with patch("homeassistant.loader.async_get_custom_components") as mock_get: + mock_get.return_value = { + "test1": test_integration_1, + "test2": test_integration_2, + } + + await loader.async_get_integration(hass, "test1") + assert ( + "No 'version' key in the manifest file for custom integration 'test1'." + in caplog.text + ) + + await loader.async_get_integration(hass, "test2") + assert ( + "No 'version' key in the manifest file for custom integration 'test2'." + in caplog.text + ) + + +async def test_no_version_warning_for_none_custom_integrations(hass, caplog): + """Test that we do not log a warning when core integrations are missing a version.""" + await loader.async_get_integration(hass, "hue") + assert ( + "No 'version' key in the manifest file for custom integration 'hue'." + not in caplog.text + ) + + +async def test_custom_integration_version_not_valid(hass, caplog): + """Test that we log a warning when custom integrations have a invalid version.""" + test_integration = loader.Integration( + hass, "custom_components.test", None, {"domain": "test", "version": "test"} + ) + + with patch("homeassistant.loader.async_get_custom_components") as mock_get: + mock_get.return_value = {"test": test_integration} + + await loader.async_get_integration(hass, "test") + assert ( + "'test' is not a valid version for custom integration 'test'." + in caplog.text + ) + + async def test_get_integration(hass): """Test resolving integration.""" integration = await loader.async_get_integration(hass, "hue") @@ -154,7 +210,6 @@ async def test_get_integration_legacy(hass): async def test_get_integration_custom_component(hass, enable_custom_integrations): """Test resolving integration.""" integration = await loader.async_get_integration(hass, "test_package") - print(integration) assert integration.get_component().DOMAIN == "test_package" assert integration.name == "Test Package" @@ -189,6 +244,7 @@ def test_integration_properties(hass): {"manufacturer": "Signify", "modelName": "Philips hue bridge 2015"}, ], "mqtt": ["hue/discovery"], + "version": "1.0.0", }, ) assert integration.name == "Philips Hue" @@ -215,6 +271,7 @@ def test_integration_properties(hass): assert integration.dependencies == ["test-dep"] assert integration.requirements == ["test-req==1.0.0"] assert integration.is_built_in is True + assert integration.version == "1.0.0" integration = loader.Integration( hass, @@ -233,6 +290,7 @@ def test_integration_properties(hass): assert integration.dhcp is None assert integration.ssdp is None assert integration.mqtt is None + assert integration.version is None integration = loader.Integration( hass, From 7e9500e46585b326235ffe4518f3d6f81e6763b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 4 Feb 2021 10:41:28 +0100 Subject: [PATCH 175/796] Use bootstrap in devcontainer (#45968) --- .devcontainer/devcontainer.json | 3 ++- .pre-commit-config.yaml | 4 ++-- script/bootstrap | 9 +++++++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 26e4b2e78ad..94d3c284f1a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,7 +2,8 @@ "name": "Home Assistant Dev", "context": "..", "dockerFile": "../Dockerfile.dev", - "postCreateCommand": "mkdir -p config && pip3 install -e .", + "postCreateCommand": "script/bootstrap", + "containerEnv": { "DEVCONTAINER": "1" }, "appPort": 8123, "runArgs": ["-e", "GIT_EDITOR=code --wait"], "extensions": [ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8944a69d9ed..efd4b86e8ac 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -59,8 +59,8 @@ repos: rev: v1.24.2 hooks: - id: yamllint - - repo: https://github.com/prettier/prettier - rev: 2.0.4 + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.2.1 hooks: - id: prettier stages: [manual] diff --git a/script/bootstrap b/script/bootstrap index f27fef8a07d..12a2e5da707 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -17,3 +17,12 @@ fi echo "Installing development dependencies..." python3 -m pip install wheel --constraint homeassistant/package_constraints.txt python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) $(grep stdlib-list requirements_test.txt) $(grep tqdm requirements_test.txt) $(grep pipdeptree requirements_test.txt) $(grep awesomeversion requirements.txt) --constraint homeassistant/package_constraints.txt + +if [ -n "$DEVCONTAINER" ];then + pre-commit install + pre-commit install-hooks + python3 -m pip install -e . --constraint homeassistant/package_constraints.txt + + mkdir -p config + hass --script ensure_config -c config +fi \ No newline at end of file From fefe4a20217f1d7847e6cc2ce7a6804d65e5dd2e Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 4 Feb 2021 12:07:30 +0200 Subject: [PATCH 176/796] Fix exception in Shelly sleeping device that switches to polling (#45930) --- homeassistant/components/shelly/__init__.py | 4 ++++ homeassistant/components/shelly/entity.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 84bc73f3c0f..537caf9707f 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -220,6 +220,10 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): async def _async_update_data(self): """Fetch data.""" + if self.entry.data.get("sleep_period"): + # Sleeping device, no point polling it, just mark it unavailable + raise update_coordinator.UpdateFailed("Sleeping device did not update") + _LOGGER.debug("Polling Shelly Device - %s", self.name) try: async with async_timeout.timeout(POLLING_TIMEOUT_SEC): diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index b934a41728f..71ab4703c79 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -421,7 +421,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti @callback def _update_callback(self): """Handle device update.""" - if self.block is not None: + if self.block is not None or not self.wrapper.device.initialized: super()._update_callback() return From dd150bb79795bcf7323276e268358b0243f887c3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 4 Feb 2021 11:08:10 +0100 Subject: [PATCH 177/796] Allow manual configuration of ignored singleton config entries (#45161) Co-authored-by: Paulus Schoutsen --- homeassistant/config_entries.py | 18 ++++++++++----- tests/test_config_entries.py | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index abc6b2f46af..122b6f15e41 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -905,7 +905,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): if self.unique_id is None: return - for entry in self._async_current_entries(): + for entry in self._async_current_entries(include_ignore=True): if entry.unique_id == self.unique_id: if updates is not None: changed = self.hass.config_entries.async_update_entry( @@ -949,17 +949,25 @@ class ConfigFlow(data_entry_flow.FlowHandler): if progress["context"].get("unique_id") == DEFAULT_DISCOVERY_UNIQUE_ID: self.hass.config_entries.flow.async_abort(progress["flow_id"]) - for entry in self._async_current_entries(): + for entry in self._async_current_entries(include_ignore=True): if entry.unique_id == unique_id: return entry return None @callback - def _async_current_entries(self) -> List[ConfigEntry]: - """Return current entries.""" + def _async_current_entries(self, include_ignore: bool = False) -> List[ConfigEntry]: + """Return current entries. + + If the flow is user initiated, filter out ignored entries unless include_ignore is True. + """ assert self.hass is not None - return self.hass.config_entries.async_entries(self.handler) + config_entries = self.hass.config_entries.async_entries(self.handler) + + if include_ignore or self.source != SOURCE_USER: + return config_entries + + return [entry for entry in config_entries if entry.source != SOURCE_IGNORE] @callback def _async_current_ids(self, include_ignore: bool = True) -> Set[Optional[str]]: diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 24444f6a6c0..435f2a11cc2 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1585,6 +1585,45 @@ async def test_manual_add_overrides_ignored_entry(hass, manager): assert len(async_reload.mock_calls) == 0 +async def test_manual_add_overrides_ignored_entry_singleton(hass, manager): + """Test that we can ignore manually add entry, overriding ignored entry.""" + hass.config.components.add("comp") + entry = MockConfigEntry( + domain="comp", + state=config_entries.ENTRY_STATE_LOADED, + source=config_entries.SOURCE_IGNORE, + ) + entry.add_to_hass(hass) + + mock_setup_entry = AsyncMock(return_value=True) + + mock_integration(hass, MockModule("comp", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + """Test flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Test user step.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + return self.async_create_entry(title="title", data={"token": "supersecret"}) + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow, "beer": 5}): + await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert len(mock_setup_entry.mock_calls) == 1 + p_hass, p_entry = mock_setup_entry.mock_calls[0][1] + + assert p_hass is hass + assert p_entry.data == {"token": "supersecret"} + + 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 afa7fd923a41c68e30bf01f98d4a9d7c50ce09c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 4 Feb 2021 12:51:38 +0100 Subject: [PATCH 178/796] Update yarnpkg GPG key (#45973) --- Dockerfile.dev | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Dockerfile.dev b/Dockerfile.dev index d72ebcaed01..0d86c1a7eec 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,7 +1,11 @@ FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.8 +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + RUN \ - apt-get update && apt-get install -y --no-install-recommends \ + curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ libudev-dev \ libavformat-dev \ libavcodec-dev \ From 8625b772e3bba923c7b14064667d2b48ea583ddc Mon Sep 17 00:00:00 2001 From: tkdrob Date: Thu, 4 Feb 2021 07:05:46 -0500 Subject: [PATCH 179/796] Use core constants for alert (#45935) --- homeassistant/components/alert/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 53b1a1248dc..73bea193394 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -14,6 +14,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ENTITY_ID, CONF_NAME, + CONF_REPEAT, CONF_STATE, SERVICE_TOGGLE, SERVICE_TURN_OFF, @@ -33,7 +34,6 @@ DOMAIN = "alert" CONF_CAN_ACK = "can_acknowledge" CONF_NOTIFIERS = "notifiers" -CONF_REPEAT = "repeat" CONF_SKIP_FIRST = "skip_first" CONF_ALERT_MESSAGE = "message" CONF_DONE_MESSAGE = "done_message" From d44c941efe9e9aa8f88bf7742476b060b9dfe086 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Thu, 4 Feb 2021 07:05:56 -0500 Subject: [PATCH 180/796] Use core constants for alexa (#45937) --- homeassistant/components/alexa/__init__.py | 8 ++++++-- homeassistant/components/alexa/const.py | 1 - homeassistant/components/alexa/flash_briefings.py | 3 +-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py index 5180a8d55b6..70d426905e9 100644 --- a/homeassistant/components/alexa/__init__.py +++ b/homeassistant/components/alexa/__init__.py @@ -1,7 +1,12 @@ """Support for Alexa skill service end point.""" import voluptuous as vol -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_NAME +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_NAME, + CONF_PASSWORD, +) from homeassistant.helpers import config_validation as cv, entityfilter from . import flash_briefings, intent, smart_home_http @@ -14,7 +19,6 @@ from .const import ( CONF_ENTITY_CONFIG, CONF_FILTER, CONF_LOCALE, - CONF_PASSWORD, CONF_SUPPORTED_LOCALES, CONF_TEXT, CONF_TITLE, diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index 402cb9e1fb2..ca0d8435e02 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -19,7 +19,6 @@ CONF_FILTER = "filter" CONF_ENTITY_CONFIG = "entity_config" CONF_ENDPOINT = "endpoint" CONF_LOCALE = "locale" -CONF_PASSWORD = "password" ATTR_UID = "uid" ATTR_UPDATE_DATE = "updateDate" diff --git a/homeassistant/components/alexa/flash_briefings.py b/homeassistant/components/alexa/flash_briefings.py index b8f78705e10..50463810bbf 100644 --- a/homeassistant/components/alexa/flash_briefings.py +++ b/homeassistant/components/alexa/flash_briefings.py @@ -5,7 +5,7 @@ import logging import uuid from homeassistant.components import http -from homeassistant.const import HTTP_NOT_FOUND, HTTP_UNAUTHORIZED +from homeassistant.const import CONF_PASSWORD, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED from homeassistant.core import callback from homeassistant.helpers import template import homeassistant.util.dt as dt_util @@ -20,7 +20,6 @@ from .const import ( ATTR_UPDATE_DATE, CONF_AUDIO, CONF_DISPLAY_URL, - CONF_PASSWORD, CONF_TEXT, CONF_TITLE, CONF_UID, From 9c6c2a77abaca10150a71b4e8f050fc4e36d8d28 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Thu, 4 Feb 2021 07:06:09 -0500 Subject: [PATCH 181/796] Use core constants for amazon polly (#45938) --- homeassistant/components/amazon_polly/tts.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py index fb9560832ca..bdb46abda9a 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -6,6 +6,7 @@ import botocore import voluptuous as vol from homeassistant.components.tts import PLATFORM_SCHEMA, Provider +from homeassistant.const import ATTR_CREDENTIALS, CONF_PROFILE_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -13,8 +14,6 @@ _LOGGER = logging.getLogger(__name__) CONF_REGION = "region_name" CONF_ACCESS_KEY_ID = "aws_access_key_id" CONF_SECRET_ACCESS_KEY = "aws_secret_access_key" -CONF_PROFILE_NAME = "profile_name" -ATTR_CREDENTIALS = "credentials" DEFAULT_REGION = "us-east-1" SUPPORTED_REGIONS = [ From 1a74709757959ee7708d38a57a7b7061e16d3bb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 4 Feb 2021 13:31:17 +0100 Subject: [PATCH 182/796] Throw error in hassfest when integration is missing version (#45976) --- script/hassfest/manifest.py | 2 +- tests/hassfest/test_version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 583cafc8161..c6f8f71f2d9 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -119,7 +119,7 @@ def validate_version(integration: Integration): Will be removed when the version key is no longer optional for custom integrations. """ if not integration.manifest.get("version"): - integration.add_warning( + integration.add_error( "manifest", "No 'version' key in the manifest file. This will cause a future version of Home Assistant to block this integration.", ) diff --git a/tests/hassfest/test_version.py b/tests/hassfest/test_version.py index 203042e79be..f99ee911a69 100644 --- a/tests/hassfest/test_version.py +++ b/tests/hassfest/test_version.py @@ -27,7 +27,7 @@ def test_validate_version_no_key(integration: Integration): validate_version(integration) assert ( "No 'version' key in the manifest file. This will cause a future version of Home Assistant to block this integration." - in [x.error for x in integration.warnings] + in [x.error for x in integration.errors] ) From 134b1d3f63f33d52dd361babdb49caf716170723 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 4 Feb 2021 14:16:09 +0100 Subject: [PATCH 183/796] Fix entities device_info property in Harmony integration (#45964) --- homeassistant/components/harmony/remote.py | 2 +- homeassistant/components/harmony/switch.py | 5 +++++ tests/components/harmony/conftest.py | 1 + tests/components/harmony/test_config_flow.py | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 8409983789b..9b3d53c21fa 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -161,7 +161,7 @@ class HarmonyRemote(ConnectionStateMixin, remote.RemoteEntity, RestoreEntity): @property def device_info(self): """Return device info.""" - self._data.device_info(DOMAIN) + return self._data.device_info(DOMAIN) @property def unique_id(self): diff --git a/homeassistant/components/harmony/switch.py b/homeassistant/components/harmony/switch.py index 5fae07c431b..2832872c2ef 100644 --- a/homeassistant/components/harmony/switch.py +++ b/homeassistant/components/harmony/switch.py @@ -46,6 +46,11 @@ class HarmonyActivitySwitch(ConnectionStateMixin, SwitchEntity): """Return the unique id.""" return f"{self._data.unique_id}-{self._activity}" + @property + def device_info(self): + """Return device info.""" + return self._data.device_info(DOMAIN) + @property def is_on(self): """Return if the current activity is the one for this switch.""" diff --git a/tests/components/harmony/conftest.py b/tests/components/harmony/conftest.py index e758a2795a9..cde8c43fe89 100644 --- a/tests/components/harmony/conftest.py +++ b/tests/components/harmony/conftest.py @@ -49,6 +49,7 @@ class FakeHarmonyClient: self.change_channel = AsyncMock() self.sync = AsyncMock() self._callbacks = callbacks + self.fw_version = "123.456" async def connect(self): """Connect and call the appropriate callbacks.""" diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index 52ef71fc8bc..2a7f80d5c2f 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -153,7 +153,7 @@ async def test_form_cannot_connect(hass): assert result2["errors"] == {"base": "cannot_connect"} -async def test_options_flow(hass, mock_hc): +async def test_options_flow(hass, mock_hc, mock_write_config): """Test config flow options.""" config_entry = MockConfigEntry( domain=DOMAIN, From 61a987061e9da549ccdf466a91869af0c3ab2a5f Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 4 Feb 2021 14:18:51 +0100 Subject: [PATCH 184/796] Don't log missing mpd artwork inappropriately (#45908) This can get unnecessarily spammy and doesn't represent an actual actionable issue. Fixes: #45235 --- homeassistant/components/mpd/manifest.json | 2 +- homeassistant/components/mpd/media_player.py | 18 ++++++++++-------- requirements_all.txt | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/mpd/manifest.json b/homeassistant/components/mpd/manifest.json index 12ce5b61b74..a11b9fedd80 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==3.0.3"], + "requirements": ["python-mpd2==3.0.4"], "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 6685347b3e3..371d2060680 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -281,20 +281,22 @@ class MpdDevice(MediaPlayerEntity): try: response = await self._client.readpicture(file) except mpd.CommandError as error: - _LOGGER.warning( - "Retrieving artwork through `readpicture` command failed: %s", - error, - ) + if error.errno is not mpd.FailureResponseCode.NO_EXIST: + _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 error.errno is not mpd.FailureResponseCode.NO_EXIST: + _LOGGER.warning( + "Retrieving artwork through `albumart` command failed: %s", + error, + ) if not response: return None, None diff --git a/requirements_all.txt b/requirements_all.txt index 37422a65972..f0758ae9646 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1795,7 +1795,7 @@ python-juicenet==1.0.1 python-miio==0.5.4 # homeassistant.components.mpd -python-mpd2==3.0.3 +python-mpd2==3.0.4 # homeassistant.components.mystrom python-mystrom==1.1.2 From b80c1688ad2898fbed13e3fc6ce81476564aa1be Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 4 Feb 2021 16:29:41 +0100 Subject: [PATCH 185/796] Bump zwave-js-server-python to 0.17.1 (#45988) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 7df75d7aed2..7083d6c372b 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.17.0"], + "requirements": ["zwave-js-server-python==0.17.1"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"] } diff --git a/requirements_all.txt b/requirements_all.txt index f0758ae9646..c1f914f6011 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2387,4 +2387,4 @@ zigpy==0.32.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.17.0 +zwave-js-server-python==0.17.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2cd33ee3fc9..f356ab5ada5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1206,4 +1206,4 @@ zigpy-znp==0.3.0 zigpy==0.32.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.17.0 +zwave-js-server-python==0.17.1 From 56b8e82a6901a227b9ba2ccd2f65a4cac24c447e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 4 Feb 2021 16:45:59 +0100 Subject: [PATCH 186/796] Bump awesomeversion from 21.2.0 to 21.2.2 (#45993) --- homeassistant/loader.py | 3 +-- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index f1a0ccc0730..de02db524a7 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -26,8 +26,7 @@ from typing import ( cast, ) -from awesomeversion import AwesomeVersion -from awesomeversion.strategy import AwesomeVersionStrategy +from awesomeversion import AwesomeVersion, AwesomeVersionStrategy from homeassistant.generated.dhcp import DHCP from homeassistant.generated.mqtt import MQTT diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 69f75af396b..45a871081d1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ aiohttp_cors==0.7.0 astral==1.10.1 async_timeout==3.0.1 attrs==19.3.0 -awesomeversion==21.2.0 +awesomeversion==21.2.2 bcrypt==3.1.7 certifi>=2020.12.5 ciso8601==2.1.3 diff --git a/requirements.txt b/requirements.txt index 17bb82d472f..42be3cfdf49 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ aiohttp==3.7.3 astral==1.10.1 async_timeout==3.0.1 attrs==19.3.0 -awesomeversion==21.2.0 +awesomeversion==21.2.2 bcrypt==3.1.7 certifi>=2020.12.5 ciso8601==2.1.3 diff --git a/setup.py b/setup.py index fc2c250ec0f..1b4b52ff26d 100755 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ REQUIRES = [ "astral==1.10.1", "async_timeout==3.0.1", "attrs==19.3.0", - "awesomeversion==21.2.0", + "awesomeversion==21.2.2", "bcrypt==3.1.7", "certifi>=2020.12.5", "ciso8601==2.1.3", From 7b280bdbe72d5615bb83126fb28b34fcb5ce8cd1 Mon Sep 17 00:00:00 2001 From: DeadEnd <45110141+DeadEnded@users.noreply.github.com> Date: Thu, 4 Feb 2021 11:02:56 -0500 Subject: [PATCH 187/796] Fix Local Media in Media Browser (#45987) Co-authored-by: Paulus Schoutsen --- homeassistant/components/media_source/local_source.py | 6 +++--- tests/components/media_source/test_local_source.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index d7a2bdfd938..fa62ba48c5f 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -10,7 +10,7 @@ from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source.error import Unresolvable from homeassistant.core import HomeAssistant, callback -from homeassistant.util import raise_if_invalid_filename +from homeassistant.util import raise_if_invalid_path from .const import DOMAIN, MEDIA_CLASS_MAP, MEDIA_MIME_TYPES from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia @@ -51,7 +51,7 @@ class LocalSource(MediaSource): raise Unresolvable("Unknown source directory.") try: - raise_if_invalid_filename(location) + raise_if_invalid_path(location) except ValueError as err: raise Unresolvable("Invalid path.") from err @@ -192,7 +192,7 @@ class LocalMediaView(HomeAssistantView): ) -> web.FileResponse: """Start a GET request.""" try: - raise_if_invalid_filename(location) + raise_if_invalid_path(location) except ValueError as err: raise web.HTTPBadRequest() from err diff --git a/tests/components/media_source/test_local_source.py b/tests/components/media_source/test_local_source.py index ad10df7cfd3..e3e2a3f1617 100644 --- a/tests/components/media_source/test_local_source.py +++ b/tests/components/media_source/test_local_source.py @@ -23,7 +23,7 @@ async def test_async_browse_media(hass): await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{const.DOMAIN}/local/test/not/exist" ) - assert str(excinfo.value) == "Invalid path." + assert str(excinfo.value) == "Path does not exist." # Test browse file with pytest.raises(media_source.BrowseError) as excinfo: From 912b816117c9fea85587d9c3812279a32aa8c771 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 4 Feb 2021 20:44:40 +0100 Subject: [PATCH 188/796] Bump zwave-js-server-python to 0.17.2 (#46010) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 7083d6c372b..4bd12baa685 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.17.1"], + "requirements": ["zwave-js-server-python==0.17.2"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"] } diff --git a/requirements_all.txt b/requirements_all.txt index c1f914f6011..82ace0f4438 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2387,4 +2387,4 @@ zigpy==0.32.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.17.1 +zwave-js-server-python==0.17.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f356ab5ada5..cf6c291b519 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1206,4 +1206,4 @@ zigpy-znp==0.3.0 zigpy==0.32.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.17.1 +zwave-js-server-python==0.17.2 From c7febacd9f55c44253fa20d7bf6cee0dac2c599d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 4 Feb 2021 23:32:56 +0100 Subject: [PATCH 189/796] Upgrade holidays to 0.10.5.2 (#46013) --- 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 4fb25c766cc..3351d796e93 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.4"], + "requirements": ["holidays==0.10.5.2"], "codeowners": ["@fabaff"], "quality_scale": "internal" } diff --git a/requirements_all.txt b/requirements_all.txt index 82ace0f4438..ffa20b1c028 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.4 +holidays==0.10.5.2 # homeassistant.components.frontend home-assistant-frontend==20210127.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf6c291b519..be9f18717e5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -402,7 +402,7 @@ hlk-sw16==0.0.9 hole==0.5.1 # homeassistant.components.workday -holidays==0.10.4 +holidays==0.10.5.2 # homeassistant.components.frontend home-assistant-frontend==20210127.7 From 5d3dcff7c960e9cc2d87c523ab23b60cc3527a89 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Thu, 4 Feb 2021 17:34:39 -0500 Subject: [PATCH 190/796] Use core constants for asuswrt (#46015) --- homeassistant/components/asuswrt/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index 1829d00a353..9cd47d803de 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -10,6 +10,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, + CONF_SENSORS, CONF_USERNAME, ) from homeassistant.helpers import config_validation as cv @@ -22,7 +23,6 @@ CONF_DNSMASQ = "dnsmasq" CONF_INTERFACE = "interface" CONF_PUB_KEY = "pub_key" CONF_REQUIRE_IP = "require_ip" -CONF_SENSORS = "sensors" CONF_SSH_KEY = "ssh_key" DOMAIN = "asuswrt" @@ -65,7 +65,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config, retry_delay=FIRST_RETRY_TIME): """Set up the asuswrt component.""" - conf = config[DOMAIN] api = AsusWrt( From 60268e63d947fd540831f4726d34912bc092e7b0 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Thu, 4 Feb 2021 17:36:35 -0500 Subject: [PATCH 191/796] Use core constants for aws (#46017) --- homeassistant/components/aws/__init__.py | 9 ++++++--- homeassistant/components/aws/const.py | 2 -- homeassistant/components/aws/notify.py | 17 +++++++---------- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/aws/__init__.py b/homeassistant/components/aws/__init__.py index 600874b0d25..19563efa144 100644 --- a/homeassistant/components/aws/__init__.py +++ b/homeassistant/components/aws/__init__.py @@ -7,7 +7,12 @@ import aiobotocore import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import ATTR_CREDENTIALS, CONF_NAME, CONF_PROFILE_NAME +from homeassistant.const import ( + ATTR_CREDENTIALS, + CONF_NAME, + CONF_PROFILE_NAME, + CONF_SERVICE, +) from homeassistant.helpers import config_validation as cv, discovery # Loading the config flow file will register the flow @@ -20,7 +25,6 @@ from .const import ( CONF_NOTIFY, CONF_REGION, CONF_SECRET_ACCESS_KEY, - CONF_SERVICE, CONF_VALIDATE, DATA_CONFIG, DATA_HASS_CONFIG, @@ -152,7 +156,6 @@ async def async_setup_entry(hass, entry): async def _validate_aws_credentials(hass, credential): """Validate AWS credential config.""" - aws_config = credential.copy() del aws_config[CONF_NAME] del aws_config[CONF_VALIDATE] diff --git a/homeassistant/components/aws/const.py b/homeassistant/components/aws/const.py index 499f4413596..8be6afec7ff 100644 --- a/homeassistant/components/aws/const.py +++ b/homeassistant/components/aws/const.py @@ -10,8 +10,6 @@ CONF_CONTEXT = "context" CONF_CREDENTIAL_NAME = "credential_name" CONF_CREDENTIALS = "credentials" CONF_NOTIFY = "notify" -CONF_PROFILE_NAME = "profile_name" CONF_REGION = "region_name" CONF_SECRET_ACCESS_KEY = "aws_secret_access_key" -CONF_SERVICE = "service" CONF_VALIDATE = "validate" diff --git a/homeassistant/components/aws/notify.py b/homeassistant/components/aws/notify.py index 13fa189a318..f487bc7aab3 100644 --- a/homeassistant/components/aws/notify.py +++ b/homeassistant/components/aws/notify.py @@ -12,24 +12,21 @@ from homeassistant.components.notify import ( ATTR_TITLE_DEFAULT, BaseNotificationService, ) -from homeassistant.const import CONF_NAME, CONF_PLATFORM +from homeassistant.const import ( + CONF_NAME, + CONF_PLATFORM, + CONF_PROFILE_NAME, + CONF_SERVICE, +) from homeassistant.helpers.json import JSONEncoder -from .const import ( - CONF_CONTEXT, - CONF_CREDENTIAL_NAME, - CONF_PROFILE_NAME, - CONF_REGION, - CONF_SERVICE, - DATA_SESSIONS, -) +from .const import CONF_CONTEXT, CONF_CREDENTIAL_NAME, CONF_REGION, DATA_SESSIONS _LOGGER = logging.getLogger(__name__) async def get_available_regions(hass, service): """Get available regions for a service.""" - session = aiobotocore.get_session() # get_available_regions is not a coroutine since it does not perform # network I/O. But it still perform file I/O heavily, so put it into From b9f9de0c1d199c3a601d658414f1f3fd324a964e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Feb 2021 12:39:44 -1000 Subject: [PATCH 192/796] dhcp does not need promisc mode. Disable it in scapy (#46018) --- homeassistant/components/dhcp/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index a71db430da4..f0de561c76a 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -282,4 +282,6 @@ def _verify_l2socket_creation_permission(): thread so we will not be able to capture any permission or bind errors. """ + # disable scapy promiscuous mode as we do not need it + conf.sniff_promisc = 0 conf.L2socket() From 62de921422aa3decd3afa827626c658b19a885da Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 4 Feb 2021 23:41:24 +0100 Subject: [PATCH 193/796] Upgrade slixmpp to 1.7.0 (#46019) --- homeassistant/components/xmpp/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json index b56d43b9c9c..ced8bd19e40 100644 --- a/homeassistant/components/xmpp/manifest.json +++ b/homeassistant/components/xmpp/manifest.json @@ -2,6 +2,6 @@ "domain": "xmpp", "name": "Jabber (XMPP)", "documentation": "https://www.home-assistant.io/integrations/xmpp", - "requirements": ["slixmpp==1.6.0"], + "requirements": ["slixmpp==1.7.0"], "codeowners": ["@fabaff", "@flowolf"] } diff --git a/requirements_all.txt b/requirements_all.txt index ffa20b1c028..223ebe32e76 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2049,7 +2049,7 @@ slackclient==2.5.0 sleepyq==0.8.1 # homeassistant.components.xmpp -slixmpp==1.6.0 +slixmpp==1.7.0 # homeassistant.components.smart_meter_texas smart-meter-texas==0.4.0 From 097a4e6b593b8fd09de1795019228427f28d40ed Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 4 Feb 2021 23:42:10 +0100 Subject: [PATCH 194/796] Upgrade praw to 7.1.2 (#46012) --- homeassistant/components/reddit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reddit/manifest.json b/homeassistant/components/reddit/manifest.json index fc3356b310c..e19ae570d0f 100644 --- a/homeassistant/components/reddit/manifest.json +++ b/homeassistant/components/reddit/manifest.json @@ -2,6 +2,6 @@ "domain": "reddit", "name": "Reddit", "documentation": "https://www.home-assistant.io/integrations/reddit", - "requirements": ["praw==7.1.0"], + "requirements": ["praw==7.1.2"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 223ebe32e76..2a78b25a162 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1159,7 +1159,7 @@ pmsensor==0.4 poolsense==0.0.8 # homeassistant.components.reddit -praw==7.1.0 +praw==7.1.2 # homeassistant.components.islamic_prayer_times prayer_times_calculator==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index be9f18717e5..024de6596b6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -596,7 +596,7 @@ pmsensor==0.4 poolsense==0.0.8 # homeassistant.components.reddit -praw==7.1.0 +praw==7.1.2 # homeassistant.components.islamic_prayer_times prayer_times_calculator==0.0.3 From 01e73911d629700199eda8fac36aeebb5b6eeb51 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Feb 2021 13:36:55 -1000 Subject: [PATCH 195/796] Do not listen for dhcp packets if the filter cannot be setup (#46006) --- homeassistant/components/dhcp/__init__.py | 19 ++++++++++ tests/components/dhcp/test_init.py | 44 +++++++++++++++++++---- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index f0de561c76a..d33c6159888 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -7,6 +7,7 @@ import logging import os import threading +from scapy.arch.common import compile_filter from scapy.config import conf from scapy.error import Scapy_Exception from scapy.layers.dhcp import DHCP @@ -217,6 +218,15 @@ class DHCPWatcher(WatcherBase): ) return + try: + await _async_verify_working_pcap(self.hass, FILTER) + except (Scapy_Exception, ImportError) as ex: + _LOGGER.error( + "Cannot watch for dhcp packets without a functional packet filter: %s", + ex, + ) + return + self._sniffer = AsyncSniffer( filter=FILTER, started_callback=self._started.set, @@ -285,3 +295,12 @@ def _verify_l2socket_creation_permission(): # disable scapy promiscuous mode as we do not need it conf.sniff_promisc = 0 conf.L2socket() + + +async def _async_verify_working_pcap(hass, cap_filter): + """Verify we can create a packet filter. + + If we cannot create a filter we will be listening for + all traffic which is too intensive. + """ + await hass.async_add_executor_job(compile_filter, cap_filter) diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index 049128248a7..fc24c8201e2 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -280,7 +280,11 @@ async def test_setup_and_stop(hass): ) await hass.async_block_till_done() - with patch("homeassistant.components.dhcp.AsyncSniffer.start") as start_call: + with patch("homeassistant.components.dhcp.AsyncSniffer.start") as start_call, patch( + "homeassistant.components.dhcp._verify_l2socket_creation_permission", + ), patch( + "homeassistant.components.dhcp.compile_filter", + ): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -325,21 +329,49 @@ async def test_setup_fails_non_root(hass, caplog): ) await hass.async_block_till_done() - wait_event = threading.Event() - with patch("os.geteuid", return_value=10), patch( "homeassistant.components.dhcp._verify_l2socket_creation_permission", side_effect=Scapy_Exception, ): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - wait_event.set() assert "Cannot watch for dhcp packets without root or CAP_NET_RAW" in caplog.text +async def test_setup_fails_with_broken_libpcap(hass, caplog): + """Test we abort if libpcap is missing or broken.""" + + assert await async_setup_component( + hass, + dhcp.DOMAIN, + {}, + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.dhcp._verify_l2socket_creation_permission", + ), patch( + "homeassistant.components.dhcp.compile_filter", + side_effect=ImportError, + ) as compile_filter, patch( + "homeassistant.components.dhcp.AsyncSniffer", + ) as async_sniffer: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + assert compile_filter.called + assert not async_sniffer.called + assert ( + "Cannot watch for dhcp packets without a functional packet filter" + in caplog.text + ) + + async def test_device_tracker_hostname_and_macaddress_exists_before_start(hass): """Test matching based on hostname and macaddress before start.""" hass.states.async_set( From c6bd5b1b7162265d65345c7041b33c9bfd4a92b6 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 5 Feb 2021 00:03:54 +0000 Subject: [PATCH 196/796] [ci skip] Translation update --- .../components/airvisual/translations/et.json | 2 +- .../components/airvisual/translations/it.json | 22 +++++++++- .../components/airvisual/translations/ru.json | 22 +++++++++- .../ambiclimate/translations/ca.json | 2 +- .../ambiclimate/translations/en.json | 2 +- .../ambiclimate/translations/et.json | 2 +- .../ambiclimate/translations/zh-Hant.json | 2 +- .../components/atag/translations/et.json | 2 +- .../components/auth/translations/et.json | 4 +- .../binary_sensor/translations/pl.json | 10 ++--- .../binary_sensor/translations/tr.json | 4 +- .../components/braviatv/translations/et.json | 2 +- .../components/brother/translations/et.json | 2 +- .../components/deconz/translations/et.json | 2 +- .../dialogflow/translations/et.json | 2 +- .../components/goalzero/translations/ca.json | 2 +- .../components/goalzero/translations/en.json | 2 +- .../components/goalzero/translations/et.json | 2 +- .../components/goalzero/translations/it.json | 2 +- .../components/group/translations/tr.json | 4 +- .../components/hangouts/translations/et.json | 6 +-- .../components/homekit/translations/ru.json | 14 ++++++- .../homekit_controller/translations/et.json | 2 +- .../components/hue/translations/et.json | 4 +- .../components/icloud/translations/et.json | 2 +- .../components/icloud/translations/it.json | 2 +- .../components/icloud/translations/pl.json | 2 +- .../icloud/translations/zh-Hant.json | 2 +- .../components/insteon/translations/et.json | 2 +- .../components/ipp/translations/et.json | 6 +-- .../components/konnected/translations/en.json | 2 +- .../components/konnected/translations/it.json | 2 +- .../lutron_caseta/translations/en.json | 2 +- .../components/lyric/translations/ru.json | 16 ++++++++ .../components/mazda/translations/it.json | 35 ++++++++++++++++ .../components/mazda/translations/pl.json | 35 ++++++++++++++++ .../components/mazda/translations/ru.json | 35 ++++++++++++++++ .../mazda/translations/zh-Hant.json | 35 ++++++++++++++++ .../minecraft_server/translations/et.json | 2 +- .../mobile_app/translations/et.json | 2 +- .../motion_blinds/translations/pl.json | 4 +- .../components/mqtt/translations/et.json | 2 +- .../components/nest/translations/en.json | 2 +- .../nightscout/translations/et.json | 2 +- .../openweathermap/translations/et.json | 2 +- .../components/owntracks/translations/et.json | 2 +- .../components/plaato/translations/it.json | 41 ++++++++++++++++++- .../components/plaato/translations/pl.json | 2 +- .../components/plaato/translations/ru.json | 39 +++++++++++++++++- .../components/point/translations/en.json | 2 +- .../components/risco/translations/et.json | 2 +- .../components/risco/translations/pl.json | 2 +- .../components/roomba/translations/en.json | 2 +- .../components/rpi_power/translations/et.json | 2 +- .../simplisafe/translations/et.json | 2 +- .../components/soma/translations/en.json | 2 +- .../components/soma/translations/it.json | 2 +- .../components/sonarr/translations/et.json | 2 +- .../components/spotify/translations/en.json | 2 +- .../components/spotify/translations/et.json | 2 +- .../components/spotify/translations/it.json | 2 +- .../components/spotify/translations/pl.json | 2 +- .../tellduslive/translations/en.json | 2 +- .../components/toon/translations/en.json | 2 +- .../components/toon/translations/et.json | 2 +- .../components/traccar/translations/ca.json | 2 +- .../components/traccar/translations/en.json | 2 +- .../components/traccar/translations/it.json | 2 +- .../traccar/translations/zh-Hant.json | 2 +- .../components/tuya/translations/pl.json | 2 +- .../components/unifi/translations/pl.json | 2 +- .../components/unifi/translations/ru.json | 1 + .../components/vera/translations/ca.json | 4 +- .../components/vera/translations/en.json | 4 +- .../components/vera/translations/it.json | 4 +- .../xiaomi_aqara/translations/en.json | 4 +- .../xiaomi_aqara/translations/it.json | 4 +- .../xiaomi_aqara/translations/zh-Hant.json | 2 +- .../zoneminder/translations/et.json | 2 +- .../components/zwave/translations/it.json | 2 +- .../components/zwave/translations/pl.json | 2 +- .../zwave/translations/zh-Hant.json | 2 +- 82 files changed, 377 insertions(+), 96 deletions(-) create mode 100644 homeassistant/components/lyric/translations/ru.json create mode 100644 homeassistant/components/mazda/translations/it.json create mode 100644 homeassistant/components/mazda/translations/pl.json create mode 100644 homeassistant/components/mazda/translations/ru.json create mode 100644 homeassistant/components/mazda/translations/zh-Hant.json diff --git a/homeassistant/components/airvisual/translations/et.json b/homeassistant/components/airvisual/translations/et.json index 9912dbce035..0fae2bcc57b 100644 --- a/homeassistant/components/airvisual/translations/et.json +++ b/homeassistant/components/airvisual/translations/et.json @@ -17,7 +17,7 @@ "latitude": "Laiuskraad", "longitude": "Pikkuskraad" }, - "description": "Kasutage AirVisual pilve API-t geograafilise asukoha j\u00e4lgimiseks.", + "description": "Kasuta AirVisual pilve API-t geograafilise asukoha j\u00e4lgimiseks.", "title": "Seadista Geography" }, "geography_by_coords": { diff --git a/homeassistant/components/airvisual/translations/it.json b/homeassistant/components/airvisual/translations/it.json index 7a4062fbe76..be493669a64 100644 --- a/homeassistant/components/airvisual/translations/it.json +++ b/homeassistant/components/airvisual/translations/it.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "Impossibile connettersi", "general_error": "Errore imprevisto", - "invalid_api_key": "Chiave API non valida" + "invalid_api_key": "Chiave API non valida", + "location_not_found": "Posizione non trovata" }, "step": { "geography": { @@ -19,6 +20,25 @@ "description": "Utilizzare l'API di AirVisual cloud per monitorare una posizione geografica.", "title": "Configurare una Geografia" }, + "geography_by_coords": { + "data": { + "api_key": "Chiave API", + "latitude": "Latitudine", + "longitude": "Logitudine" + }, + "description": "Usa l'API cloud di AirVisual per monitorare una latitudine/longitudine.", + "title": "Configurare un'area geografica" + }, + "geography_by_name": { + "data": { + "api_key": "Chiave API", + "city": "Citt\u00e0", + "country": "Nazione", + "state": "Stato" + }, + "description": "Usa l'API cloud di AirVisual per monitorare una citt\u00e0/stato/paese.", + "title": "Configurare un'area geografica" + }, "node_pro": { "data": { "ip_address": "Host", diff --git a/homeassistant/components/airvisual/translations/ru.json b/homeassistant/components/airvisual/translations/ru.json index de9e5a730fe..bc648c84bfe 100644 --- a/homeassistant/components/airvisual/translations/ru.json +++ b/homeassistant/components/airvisual/translations/ru.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "general_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", - "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API." + "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "location_not_found": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e." }, "step": { "geography": { @@ -19,6 +20,25 @@ "description": "\u041c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e API AirVisual.", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f" }, + "geography_by_coords": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430" + }, + "description": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0448\u0438\u0440\u043e\u0442\u044b/\u0434\u043e\u043b\u0433\u043e\u0442\u044b.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f" + }, + "geography_by_name": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "city": "\u0413\u043e\u0440\u043e\u0434", + "country": "\u0421\u0442\u0440\u0430\u043d\u0430", + "state": "\u0448\u0442\u0430\u0442" + }, + "description": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0433\u043e\u0440\u043e\u0434\u0430/\u0448\u0442\u0430\u0442\u0430/\u0441\u0442\u0440\u0430\u043d\u044b.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f" + }, "node_pro": { "data": { "ip_address": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/ambiclimate/translations/ca.json b/homeassistant/components/ambiclimate/translations/ca.json index b635d877ffe..8e54a222217 100644 --- a/homeassistant/components/ambiclimate/translations/ca.json +++ b/homeassistant/components/ambiclimate/translations/ca.json @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "V\u00e9s a l'[enlla\u00e7]({authorization_url}) i **Permet** l'acc\u00e9s al teu compte de Ambiclimate, despr\u00e9s torna i prem **Envia** (a sota).\n(Assegura't que l'enlla\u00e7 de retorn \u00e9s el seg\u00fcent {cb_url})", + "description": "V\u00e9s a l'[enlla\u00e7]({authorization_url}) i **Permet** l'acc\u00e9s al teu compte de Ambiclimate, despr\u00e9s torna i prem **Envia** a sota.\n(Assegura't que l'enlla\u00e7 de retorn \u00e9s el seg\u00fcent {cb_url})", "title": "Autenticaci\u00f3 amb Ambi Climate" } } diff --git a/homeassistant/components/ambiclimate/translations/en.json b/homeassistant/components/ambiclimate/translations/en.json index 01c52875250..8621b0e247c 100644 --- a/homeassistant/components/ambiclimate/translations/en.json +++ b/homeassistant/components/ambiclimate/translations/en.json @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "Please follow this [link]({authorization_url}) and **Allow** access to your Ambiclimate account, then come back and press **Submit** below.\n(Make sure the specified callback url is {cb_url})", + "description": "Please follow this [link]({authorization_url}) and **Allow** access to your Ambiclimate account, then come back and press **Submit** below.\n(Make sure the specified callback URL is {cb_url})", "title": "Authenticate Ambiclimate" } } diff --git a/homeassistant/components/ambiclimate/translations/et.json b/homeassistant/components/ambiclimate/translations/et.json index f9da8f8f7cd..ff2264c3e0e 100644 --- a/homeassistant/components/ambiclimate/translations/et.json +++ b/homeassistant/components/ambiclimate/translations/et.json @@ -9,7 +9,7 @@ "default": "Ambiclimate autentimine \u00f5nnestus" }, "error": { - "follow_link": "Enne Esita nupu vajutamist j\u00e4rgige linki ja autentige", + "follow_link": "Enne Esita nupu vajutamist j\u00e4rgi linki ja autendi", "no_token": "Ambiclimate ei ole autenditud" }, "step": { diff --git a/homeassistant/components/ambiclimate/translations/zh-Hant.json b/homeassistant/components/ambiclimate/translations/zh-Hant.json index f91c38dd36e..e50accd7327 100644 --- a/homeassistant/components/ambiclimate/translations/zh-Hant.json +++ b/homeassistant/components/ambiclimate/translations/zh-Hant.json @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "\u8acb\u4f7f\u7528\u6b64[\u9023\u7d50]\uff08{authorization_url}\uff09\u4e26\u9ede\u9078 **\u5141\u8a31** \u4ee5\u5b58\u53d6 Ambiclimate \u5e33\u865f\uff0c\u7136\u5f8c\u8fd4\u56de\u6b64\u9801\u9762\u4e26\u9ede\u9078\u4e0b\u65b9\u7684 **\u50b3\u9001**\u3002\n\uff08\u78ba\u5b9a Callback url \u70ba {cb_url}\uff09", + "description": "\u8acb\u4f7f\u7528\u6b64 [\u9023\u7d50]\uff08{authorization_url}\uff09\u4e26\u9ede\u9078**\u5141\u8a31**\u4ee5\u5b58\u53d6 Ambiclimate \u5e33\u865f\uff0c\u7136\u5f8c\u8fd4\u56de\u6b64\u9801\u9762\u4e26\u9ede\u9078\u4e0b\u65b9\u7684**\u50b3\u9001**\u3002\n\uff08\u78ba\u5b9a\u6307\u5b9a Callback URL \u70ba {cb_url}\uff09", "title": "\u8a8d\u8b49 Ambiclimate" } } diff --git a/homeassistant/components/atag/translations/et.json b/homeassistant/components/atag/translations/et.json index 2a4094806ed..fd0651a219c 100644 --- a/homeassistant/components/atag/translations/et.json +++ b/homeassistant/components/atag/translations/et.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00dchendamine nurjus", - "unauthorized": "Sidumine on keelatud, kontrollige seadme tuvastamistaotlust" + "unauthorized": "Sidumine on keelatud, kontrolli seadme tuvastamistaotlust" }, "step": { "user": { diff --git a/homeassistant/components/auth/translations/et.json b/homeassistant/components/auth/translations/et.json index 03fc42f38e2..9b22951e7fa 100644 --- a/homeassistant/components/auth/translations/et.json +++ b/homeassistant/components/auth/translations/et.json @@ -10,7 +10,7 @@ "step": { "init": { "description": "Vali \u00fcks teavitusteenustest:", - "title": "Seadistage Notify poolt edastatud \u00fchekordne parool" + "title": "Seadista Notify poolt edastatud \u00fchekordne parool" }, "setup": { "description": "\u00dchekordne parool on saadetud **notify. {notify_service}**. Palun sisesta see allpool:", @@ -26,7 +26,7 @@ "step": { "init": { "description": "Kahefaktorilise autentimise aktiveerimiseks ajap\u00f5histe \u00fchekordsete paroolide abil skanni QR-kood oma autentimisrakendusega. Kui seda pole, soovitame kas [Google Authenticator] (https://support.google.com/accounts/answer/1066447) v\u00f5i [Authy] (https://authy.com/).\n\n {qr_code}\n\n P\u00e4rast koodi skannimist sisesta seadistuse kinnitamiseks rakenduse kuuekohaline kood. Kui on probleeme QR-koodi skannimisega, tehke koodiga **' {code}' ** k\u00e4sitsi seadistamine.", - "title": "Seadistage TOTP-ga kaheastmeline autentimine" + "title": "Seadista TOTP-ga kaheastmeline autentimine" } }, "title": "" diff --git a/homeassistant/components/binary_sensor/translations/pl.json b/homeassistant/components/binary_sensor/translations/pl.json index 726765aea02..59fd573d5b3 100644 --- a/homeassistant/components/binary_sensor/translations/pl.json +++ b/homeassistant/components/binary_sensor/translations/pl.json @@ -95,8 +95,8 @@ "on": "w\u0142." }, "battery": { - "off": "na\u0142adowana", - "on": "roz\u0142adowana" + "off": "Normalna", + "on": "Niska" }, "battery_charging": { "off": "roz\u0142adowywanie", @@ -107,8 +107,8 @@ "on": "zimno" }, "connectivity": { - "off": "offline", - "on": "online" + "off": "Roz\u0142\u0105czony", + "on": "Po\u0142\u0105czony" }, "door": { "off": "zamkni\u0119te", @@ -135,7 +135,7 @@ "on": "otwarty" }, "moisture": { - "off": "brak wilgoci", + "off": "Sucho", "on": "wilgo\u0107" }, "motion": { diff --git a/homeassistant/components/binary_sensor/translations/tr.json b/homeassistant/components/binary_sensor/translations/tr.json index 94e1496cc30..daf44cc967b 100644 --- a/homeassistant/components/binary_sensor/translations/tr.json +++ b/homeassistant/components/binary_sensor/translations/tr.json @@ -75,8 +75,8 @@ "on": "Tak\u0131l\u0131" }, "presence": { - "off": "[%key:common::state::evde_degil%]", - "on": "[%key:common::state::evde%]" + "off": "D\u0131\u015farda", + "on": "Evde" }, "problem": { "off": "Tamam", diff --git a/homeassistant/components/braviatv/translations/et.json b/homeassistant/components/braviatv/translations/et.json index b69844ee839..6930186aeba 100644 --- a/homeassistant/components/braviatv/translations/et.json +++ b/homeassistant/components/braviatv/translations/et.json @@ -21,7 +21,7 @@ "data": { "host": "" }, - "description": "Seadista Sony Bravia TV sidumine. Kuion probleeme seadetega mine: https://www.home-assistant.io/integrations/braviatv \n\nVeenduge, et teler on sisse l\u00fclitatud.", + "description": "Seadista Sony Bravia TV sidumine. Kuion probleeme seadetega mine: https://www.home-assistant.io/integrations/braviatv \n\nVeendu, et teler on sisse l\u00fclitatud.", "title": "" } } diff --git a/homeassistant/components/brother/translations/et.json b/homeassistant/components/brother/translations/et.json index 190db6ed768..7b2b7c1b4a5 100644 --- a/homeassistant/components/brother/translations/et.json +++ b/homeassistant/components/brother/translations/et.json @@ -16,7 +16,7 @@ "host": "Host", "type": "Printeri t\u00fc\u00fcp" }, - "description": "Seadistage Brotheri printeri sidumine. Kui teil on seadistamisega probleeme minge aadressile https://www.home-assistant.io/integrations/brother" + "description": "Seadista Brotheri printeri sidumine. Kui seadistamisega on probleeme mine aadressile https://www.home-assistant.io/integrations/brother" }, "zeroconf_confirm": { "data": { diff --git a/homeassistant/components/deconz/translations/et.json b/homeassistant/components/deconz/translations/et.json index ad5c07b6607..9f6644ea186 100644 --- a/homeassistant/components/deconz/translations/et.json +++ b/homeassistant/components/deconz/translations/et.json @@ -18,7 +18,7 @@ "title": "deCONZ Zigbee v\u00e4rav Hass.io pistikprogrammi kaudu" }, "link": { - "description": "Home Assistanti registreerumiseks ava deCONZ-i l\u00fc\u00fcs.\n\n 1. Minge deCONZ Settings - > Gateway - > Advanced\n 2. Vajutage nuppu \"Authenticate app\"", + "description": "Home Assistanti registreerumiseks ava deCONZ-i l\u00fc\u00fcs.\n\n 1. Mine deCONZ Settings - > Gateway - > Advanced\n 2. Vajuta nuppu \"Authenticate app\"", "title": "\u00dchenda deCONZ-iga" }, "manual_input": { diff --git a/homeassistant/components/dialogflow/translations/et.json b/homeassistant/components/dialogflow/translations/et.json index 989db1c2564..8ffe23497ef 100644 --- a/homeassistant/components/dialogflow/translations/et.json +++ b/homeassistant/components/dialogflow/translations/et.json @@ -10,7 +10,7 @@ "step": { "user": { "description": "Kas oled kindel, et soovid seadistada Dialogflow?", - "title": "Seadistage Dialogflow veebihaak" + "title": "Seadista Dialogflow veebihaak" } } } diff --git a/homeassistant/components/goalzero/translations/ca.json b/homeassistant/components/goalzero/translations/ca.json index 2d301d5ef07..ac4c2a696e2 100644 --- a/homeassistant/components/goalzero/translations/ca.json +++ b/homeassistant/components/goalzero/translations/ca.json @@ -14,7 +14,7 @@ "host": "Amfitri\u00f3", "name": "Nom" }, - "description": "En primer lloc, has de baixar-te l'aplicaci\u00f3 Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\nSegueix les instruccions per connectar el teu Yeti a la teva xarxa Wifi. A continuaci\u00f3, has d'obtenir la IP d'amfitri\u00f3 del teu encaminador (router). Cal que aquest tingui la configuraci\u00f3 DHCP activada per al teu dispositiu per aix\u00ed garantir que la IP no canvi\u00ef. Si cal, consulta el manual del teu encaminador.", + "description": "En primer lloc, has de baixar-te l'aplicaci\u00f3 Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\nSegueix les instruccions per connectar el Yeti a la xarxa Wifi. A continuaci\u00f3, has d'obtenir la IP d'amfitri\u00f3 del teu router. Cal que aquest tingui la configuraci\u00f3 DHCP activada per al teu dispositiu, per aix\u00ed garantir que la IP no canvi\u00ef. Si cal, consulta el manual del router.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/goalzero/translations/en.json b/homeassistant/components/goalzero/translations/en.json index 08c823e2ad2..25aa32e4b75 100644 --- a/homeassistant/components/goalzero/translations/en.json +++ b/homeassistant/components/goalzero/translations/en.json @@ -14,7 +14,7 @@ "host": "Host", "name": "Name" }, - "description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wifi network. Then get the host ip from your router. DHCP must be set up in your router settings for the device to ensure the host ip does not change. Refer to your router's user manual.", + "description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wifi network. Then get the host IP from your router. DHCP must be set up in your router settings for the device to ensure the host IP does not change. Refer to your router's user manual.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/goalzero/translations/et.json b/homeassistant/components/goalzero/translations/et.json index 4479ba8669e..74f84f1d72b 100644 --- a/homeassistant/components/goalzero/translations/et.json +++ b/homeassistant/components/goalzero/translations/et.json @@ -14,7 +14,7 @@ "host": "", "name": "Nimi" }, - "description": "Alustuseks peate alla laadima rakenduse Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\n Yeti Wifi-v\u00f5rguga \u00fchendamiseks j\u00e4rgige juhiseid. Seej\u00e4rel hankige oma ruuterilt host IP. DHCP peab olema ruuteri seadetes seadistatud, et tagada, et host-IP ei muutuks. Vaadake ruuteri kasutusjuhendit.", + "description": "Alustuseks pead alla laadima rakenduse Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\n Yeti Wifi-v\u00f5rguga \u00fchendamiseks j\u00e4rgi juhiseid. Seej\u00e4rel hangi oma ruuterilt host IP. DHCP peab olema ruuteri seadetes seadistatud, et tagada, et host-IP ei muutuks. Vaata ruuteri kasutusjuhendit.", "title": "" } } diff --git a/homeassistant/components/goalzero/translations/it.json b/homeassistant/components/goalzero/translations/it.json index 10df269d59a..24f04a0bafe 100644 --- a/homeassistant/components/goalzero/translations/it.json +++ b/homeassistant/components/goalzero/translations/it.json @@ -14,7 +14,7 @@ "host": "Host", "name": "Nome" }, - "description": "Innanzitutto, devi scaricare l'app Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nSegui le istruzioni per connettere il tuo Yeti alla tua rete Wifi. Quindi ottieni l'ip host dal tuo router. Il DHCP deve essere configurato nelle impostazioni del router affinch\u00e9 il dispositivo assicuri che l'ip host non cambi. Fare riferimento al manuale utente del router.", + "description": "Innanzitutto, devi scaricare l'app Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nSegui le istruzioni per connettere il tuo Yeti alla tua rete Wifi. Quindi ottieni l'ip host dal tuo router. Il DHCP deve essere configurato nelle impostazioni del router affinch\u00e9 assicuri che l'ip host non cambi. Fare riferimento al manuale utente del router.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/group/translations/tr.json b/homeassistant/components/group/translations/tr.json index f92785a737f..5a596efdf01 100644 --- a/homeassistant/components/group/translations/tr.json +++ b/homeassistant/components/group/translations/tr.json @@ -2,9 +2,9 @@ "state": { "_": { "closed": "Kapand\u0131", - "home": "[%key:common::state::evde%]", + "home": "Evde", "locked": "Kilitli", - "not_home": "[%key:common::state::evde_degil%]", + "not_home": "D\u0131\u015far\u0131da", "off": "Kapal\u0131", "ok": "Tamam", "on": "A\u00e7\u0131k", diff --git a/homeassistant/components/hangouts/translations/et.json b/homeassistant/components/hangouts/translations/et.json index a587edcd632..6bcc19d2043 100644 --- a/homeassistant/components/hangouts/translations/et.json +++ b/homeassistant/components/hangouts/translations/et.json @@ -5,9 +5,9 @@ "unknown": "Tundmatu viga" }, "error": { - "invalid_2fa": "Vale 2-teguriline autentimine, proovige uuesti.", - "invalid_2fa_method": "Kehtetu 2FA meetod (kontrollige telefoni teel).", - "invalid_login": "Vale Kasutajanimi, palun proovige uuesti." + "invalid_2fa": "Vale 2-teguriline autentimine, proovi uuesti.", + "invalid_2fa_method": "Kehtetu 2FA meetod (kontrolli telefoni teel).", + "invalid_login": "Vale kasutajanimi, palun proovi uuesti." }, "step": { "2fa": { diff --git a/homeassistant/components/homekit/translations/ru.json b/homeassistant/components/homekit/translations/ru.json index 6cf96c2dd78..84346aed2ef 100644 --- a/homeassistant/components/homekit/translations/ru.json +++ b/homeassistant/components/homekit/translations/ru.json @@ -7,7 +7,16 @@ "accessory_mode": { "data": { "entity_id": "\u041e\u0431\u044a\u0435\u043a\u0442" - } + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u043e\u0431\u044a\u0435\u043a\u0442.", + "title": "\u0412\u044b\u0431\u043e\u0440 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit" + }, + "bridge_mode": { + "data": { + "include_domains": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u044b" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u043e\u043c\u0435\u043d\u044b. \u0411\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u0437 \u0434\u043e\u043c\u0435\u043d\u0430.", + "title": "\u0412\u044b\u0431\u043e\u0440 \u0434\u043e\u043c\u0435\u043d\u043e\u0432 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit" }, "pairing": { "description": "\u041a\u0430\u043a \u0442\u043e\u043b\u044c\u043a\u043e {name} \u0431\u0443\u0434\u0435\u0442 \u0433\u043e\u0442\u043e\u0432\u043e, \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u0432 \"\u0423\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f\u0445\" \u043a\u0430\u043a \"HomeKit Bridge Setup\".", @@ -16,7 +25,8 @@ "user": { "data": { "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 Z-Wave \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043e\u0442\u043b\u043e\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0430)", - "include_domains": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u044b" + "include_domains": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u044b", + "mode": "\u0420\u0435\u0436\u0438\u043c" }, "description": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u043e\u0431\u044a\u0435\u043a\u0442\u0430\u043c Home Assistant \u0447\u0435\u0440\u0435\u0437 HomeKit. HomeKit Bridge \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d 150 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430\u043c\u0438 \u043d\u0430 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440, \u0432\u043a\u043b\u044e\u0447\u0430\u044f \u0441\u0430\u043c \u0431\u0440\u0438\u0434\u0436. \u0415\u0441\u043b\u0438 \u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0431\u043e\u043b\u044c\u0448\u0435, \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e HomeKit Bridge \u0434\u043b\u044f \u0440\u0430\u0437\u043d\u044b\u0445 \u0434\u043e\u043c\u0435\u043d\u043e\u0432. \u0414\u0435\u0442\u0430\u043b\u044c\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 YAML. \u0414\u043b\u044f \u043b\u0443\u0447\u0448\u0435\u0439 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0438 \u043f\u0440\u0435\u0434\u043e\u0442\u0432\u0440\u0430\u0449\u0435\u043d\u0438\u044f \u043d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u043e\u0441\u0442\u0435\u0439 \u0441\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0443\u044e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440\u0430 \u0438\u043b\u0438 \u043a\u0430\u043c\u0435\u0440\u044b.", "title": "HomeKit" diff --git a/homeassistant/components/homekit_controller/translations/et.json b/homeassistant/components/homekit_controller/translations/et.json index 40537554ee2..6df49751478 100644 --- a/homeassistant/components/homekit_controller/translations/et.json +++ b/homeassistant/components/homekit_controller/translations/et.json @@ -13,7 +13,7 @@ "error": { "authentication_error": "Vale HomeKiti kood. Kontrolli seda ja proovi uuesti.", "max_peers_error": "Seade keeldus sidumist lisamast kuna puudub piisav salvestusruum.", - "pairing_failed": "Selle seadmega sidumise katsel ilmnes tundmatu t\u00f5rge. See v\u00f5ib olla ajutine t\u00f5rge v\u00f5i tseadet ei toetata praegu.", + "pairing_failed": "Selle seadmega sidumise katsel ilmnes tundmatu t\u00f5rge. See v\u00f5ib olla ajutine t\u00f5rge v\u00f5i seadet ei toetata praegu.", "unable_to_pair": "Ei saa siduda, proovi uuesti.", "unknown_error": "Seade teatas tundmatust t\u00f5rkest. Sidumine nurjus." }, diff --git a/homeassistant/components/hue/translations/et.json b/homeassistant/components/hue/translations/et.json index fcec56b9d0b..afde880690e 100644 --- a/homeassistant/components/hue/translations/et.json +++ b/homeassistant/components/hue/translations/et.json @@ -12,7 +12,7 @@ }, "error": { "linking": "Ilmnes tundmatu linkimist\u00f5rge.", - "register_failed": "Registreerimine nurjus. Proovige uuesti" + "register_failed": "Registreerimine nurjus. Proovi uuesti" }, "step": { "init": { @@ -22,7 +22,7 @@ "title": "Vali Hue sild" }, "link": { - "description": "Vajutage silla nuppu, et registreerida Philips Hue Home Assistant abil. \n\n ! [Nupu asukoht sillal] (/ static / images / config_philips_hue.jpg)", + "description": "Vajuta silla nuppu, et registreerida Philips Hue Home Assistant abil. \n\n ! [Nupu asukoht sillal] (/ static / images / config_philips_hue.jpg)", "title": "\u00dchenda jaotusseade" }, "manual": { diff --git a/homeassistant/components/icloud/translations/et.json b/homeassistant/components/icloud/translations/et.json index 29b24aabf5a..af3457bb0db 100644 --- a/homeassistant/components/icloud/translations/et.json +++ b/homeassistant/components/icloud/translations/et.json @@ -15,7 +15,7 @@ "data": { "password": "Salas\u00f5na" }, - "description": "Varem sisestatud salas\u00f5na kasutajale {username} ei t\u00f6\u00f6ta enam. Selle sidumise kasutamise j\u00e4tkamiseks v\u00e4rskendage oma salas\u00f5na.", + "description": "Varem sisestatud salas\u00f5na kasutajale {username} ei t\u00f6\u00f6ta enam. Selle sidumise kasutamise j\u00e4tkamiseks v\u00e4rskenda oma salas\u00f5na.", "title": "iCloudi tuvastusandmed" }, "trusted_device": { diff --git a/homeassistant/components/icloud/translations/it.json b/homeassistant/components/icloud/translations/it.json index 4fde8b33526..32931d96a32 100644 --- a/homeassistant/components/icloud/translations/it.json +++ b/homeassistant/components/icloud/translations/it.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "Autenticazione non valida", "send_verification_code": "Impossibile inviare il codice di verifica", - "validate_verification_code": "Impossibile verificare il codice di verifica, scegliere un dispositivo attendibile e riavviare la verifica" + "validate_verification_code": "Impossibile verificare il codice di verifica, riprovare" }, "step": { "reauth": { diff --git a/homeassistant/components/icloud/translations/pl.json b/homeassistant/components/icloud/translations/pl.json index 4ac02d1f3f0..e111518710b 100644 --- a/homeassistant/components/icloud/translations/pl.json +++ b/homeassistant/components/icloud/translations/pl.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "Niepoprawne uwierzytelnienie", "send_verification_code": "Nie uda\u0142o si\u0119 wys\u0142a\u0107 kodu weryfikacyjnego", - "validate_verification_code": "Nie uda\u0142o si\u0119 zweryfikowa\u0107 kodu weryfikacyjnego, wybierz urz\u0105dzenie zaufane i ponownie rozpocznij weryfikacj\u0119" + "validate_verification_code": "Nie uda\u0142o si\u0119 zweryfikowa\u0107 kodu weryfikacyjnego, spr\u00f3buj ponownie" }, "step": { "reauth": { diff --git a/homeassistant/components/icloud/translations/zh-Hant.json b/homeassistant/components/icloud/translations/zh-Hant.json index 1c16db77faf..fe421275e2a 100644 --- a/homeassistant/components/icloud/translations/zh-Hant.json +++ b/homeassistant/components/icloud/translations/zh-Hant.json @@ -8,7 +8,7 @@ "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\u88dd\u7f6e\u3001\u7136\u5f8c\u91cd\u65b0\u57f7\u884c\u9a57\u8b49\u3002" + "validate_verification_code": "\u9a57\u8b49\u8f38\u5165\u9a57\u8b49\u78bc\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002" }, "step": { "reauth": { diff --git a/homeassistant/components/insteon/translations/et.json b/homeassistant/components/insteon/translations/et.json index 5fee63e1219..69368300c7e 100644 --- a/homeassistant/components/insteon/translations/et.json +++ b/homeassistant/components/insteon/translations/et.json @@ -76,7 +76,7 @@ "port": "", "username": "Kasutajanimi" }, - "description": "Muutda Insteon Hubi \u00fchenduse teavet. P\u00e4rast selle muudatuse tegemist pead Home Assistanti taask\u00e4ivitama. See ei muuda jaoturi enda konfiguratsiooni. Hubis muudatuste tegemiseks kasutage rakendust Hub.", + "description": "Muutda Insteon Hubi \u00fchenduse teavet. P\u00e4rast selle muudatuse tegemist pead Home Assistanti taask\u00e4ivitama. See ei muuda jaoturi enda konfiguratsiooni. Hubis muudatuste tegemiseks kasuta rakendust Hub.", "title": "" }, "init": { diff --git a/homeassistant/components/ipp/translations/et.json b/homeassistant/components/ipp/translations/et.json index 622b71d758a..5a0a2e69cfb 100644 --- a/homeassistant/components/ipp/translations/et.json +++ b/homeassistant/components/ipp/translations/et.json @@ -11,7 +11,7 @@ }, "error": { "cannot_connect": "\u00dchendamine nurjus", - "connection_upgrade": "Printeriga \u00fchenduse loomine nurjus. Proovige uuesti kui SSL/TLS-i suvand on m\u00e4rgitud." + "connection_upgrade": "Printeriga \u00fchenduse loomine nurjus. Proovi uuesti kui SSL/TLS-i suvand on m\u00e4rgitud." }, "flow_title": "Printer: {name}", "step": { @@ -23,8 +23,8 @@ "ssl": "Printer toetab SSL/TLS \u00fchendust", "verify_ssl": "Printer kasutab \u00f5iget SSL-serti" }, - "description": "Seadistage oma printer Interneti-printimisprotokolli (IPP) kaudu, et see integreeruks Home Assistantiga.", - "title": "Linkige oma printer" + "description": "Seadista oma printer Interneti-printimisprotokolli (IPP) kaudu, et see integreeruks Home Assistantiga.", + "title": "Lingi oma printer" }, "zeroconf_confirm": { "description": "Kas soovite seadistada {name}?", diff --git a/homeassistant/components/konnected/translations/en.json b/homeassistant/components/konnected/translations/en.json index 920e1453e49..32cf120e8af 100644 --- a/homeassistant/components/konnected/translations/en.json +++ b/homeassistant/components/konnected/translations/en.json @@ -32,7 +32,7 @@ "not_konn_panel": "Not a recognized Konnected.io device" }, "error": { - "bad_host": "Invalid Override API host url" + "bad_host": "Invalid Override API host URL" }, "step": { "options_binary": { diff --git a/homeassistant/components/konnected/translations/it.json b/homeassistant/components/konnected/translations/it.json index b618ee04b48..da88fb0ac4d 100644 --- a/homeassistant/components/konnected/translations/it.json +++ b/homeassistant/components/konnected/translations/it.json @@ -32,7 +32,7 @@ "not_konn_panel": "Non \u00e8 un dispositivo Konnected.io riconosciuto" }, "error": { - "bad_host": "URL dell'host API di sostituzione non valido" + "bad_host": "URL host API di sostituzione non valido" }, "step": { "options_binary": { diff --git a/homeassistant/components/lutron_caseta/translations/en.json b/homeassistant/components/lutron_caseta/translations/en.json index 8ea0672a3f3..96c00d6cb42 100644 --- a/homeassistant/components/lutron_caseta/translations/en.json +++ b/homeassistant/components/lutron_caseta/translations/en.json @@ -22,7 +22,7 @@ "data": { "host": "Host" }, - "description": "Enter the ip address of the device.", + "description": "Enter the IP address of the device.", "title": "Automaticlly connect to the bridge" } } diff --git a/homeassistant/components/lyric/translations/ru.json b/homeassistant/components/lyric/translations/ru.json new file mode 100644 index 00000000000..8d41a95fd29 --- /dev/null +++ b/homeassistant/components/lyric/translations/ru.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "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." + }, + "create_entry": { + "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "step": { + "pick_implementation": { + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/it.json b/homeassistant/components/mazda/translations/it.json new file mode 100644 index 00000000000..5eb995a4dfb --- /dev/null +++ b/homeassistant/components/mazda/translations/it.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_successful": "La riautenticazione ha avuto successo" + }, + "error": { + "account_locked": "Account bloccato. Per favore riprova pi\u00f9 tardi.", + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "reauth": { + "data": { + "email": "E-mail", + "password": "Password", + "region": "Area geografica" + }, + "description": "Autenticazione non riuscita per Mazda Connected Services. Inserisci le tue credenziali attuali.", + "title": "Mazda Connected Services - Autenticazione non riuscita" + }, + "user": { + "data": { + "email": "E-mail", + "password": "Password", + "region": "Area geografica" + }, + "description": "Inserisci l'indirizzo e-mail e la password che utilizzi per accedere all'app mobile MyMazda.", + "title": "Mazda Connected Services - Aggiungi account" + } + } + }, + "title": "Mazda Connected Services" +} \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/pl.json b/homeassistant/components/mazda/translations/pl.json new file mode 100644 index 00000000000..12254f20662 --- /dev/null +++ b/homeassistant/components/mazda/translations/pl.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "account_locked": "Konto zablokowane. Spr\u00f3buj ponownie p\u00f3\u017aniej.", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "reauth": { + "data": { + "email": "Adres e-mail", + "password": "Has\u0142o", + "region": "Region" + }, + "description": "Uwierzytelnianie dla Mazda Connected Services nie powiod\u0142o si\u0119. Wprowad\u017a aktualne dane uwierzytelniaj\u0105ce.", + "title": "Mazda Connected Services - Uwierzytelnianie nie powiod\u0142o si\u0119" + }, + "user": { + "data": { + "email": "Adres e-mail", + "password": "Has\u0142o", + "region": "Region" + }, + "description": "Wprowad\u017a adres e-mail i has\u0142o, kt\u00f3rych u\u017cywasz do logowania si\u0119 do aplikacji mobilnej MyMazda.", + "title": "Mazda Connected Services - Dodawanie konta" + } + } + }, + "title": "Mazda Connected Services" +} \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/ru.json b/homeassistant/components/mazda/translations/ru.json new file mode 100644 index 00000000000..e41babd499d --- /dev/null +++ b/homeassistant/components/mazda/translations/ru.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "account_locked": "\u0410\u043a\u043a\u0430\u0443\u043d\u0442 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d. \u041f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "reauth": { + "data": { + "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "region": "\u0420\u0435\u0433\u0438\u043e\u043d" + }, + "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. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0442\u0435\u043a\u0443\u0449\u0438\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "user": { + "data": { + "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "region": "\u0420\u0435\u0433\u0438\u043e\u043d" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0412\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u0432 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 MyMazda.", + "title": "Mazda Connected Services" + } + } + }, + "title": "Mazda Connected Services" +} \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/zh-Hant.json b/homeassistant/components/mazda/translations/zh-Hant.json new file mode 100644 index 00000000000..48232664683 --- /dev/null +++ b/homeassistant/components/mazda/translations/zh-Hant.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "account_locked": "\u5e33\u865f\u5df2\u9396\u5b9a\uff0c\u8acb\u7a0d\u5f8c\u518d\u8a66\u3002", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "reauth": { + "data": { + "email": "\u96fb\u5b50\u90f5\u4ef6", + "password": "\u5bc6\u78bc", + "region": "\u5340\u57df" + }, + "description": "Mazda Connected \u670d\u52d9\u8a8d\u8b49\u5931\u6557\u3002\u8acb\u8f38\u5165\u76ee\u524d\u6191\u8b49\u3002", + "title": "Mazda Connected \u670d\u52d9 - \u8a8d\u8b49\u5931\u6557" + }, + "user": { + "data": { + "email": "\u96fb\u5b50\u90f5\u4ef6", + "password": "\u5bc6\u78bc", + "region": "\u5340\u57df" + }, + "description": "\u8acb\u8f38\u5165\u767b\u5165MyMazda \u884c\u52d5 App \u4e4b Email \u5730\u5740\u8207\u5bc6\u78bc\u3002", + "title": "Mazda Connected \u670d\u52d9 - \u65b0\u589e\u5e33\u865f" + } + } + }, + "title": "Mazda Connected \u670d\u52d9" +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/translations/et.json b/homeassistant/components/minecraft_server/translations/et.json index a92449b0512..f8de21662aa 100644 --- a/homeassistant/components/minecraft_server/translations/et.json +++ b/homeassistant/components/minecraft_server/translations/et.json @@ -4,7 +4,7 @@ "already_configured": "Teenus on juba seadistatud" }, "error": { - "cannot_connect": "Serveriga \u00fchenduse loomine nurjus. Kontrollige hosti ja porti ning proovige uuesti. Samuti veenduge, et kasutate oma serveris v\u00e4hemalt Minecrafti versiooni 1.7.", + "cannot_connect": "Serveriga \u00fchenduse loomine nurjus. Kontrolli hosti ja porti ning proovi uuesti. Samuti veendu, et kasutad oma serveris v\u00e4hemalt Minecrafti versiooni 1.7.", "invalid_ip": "IP-aadress on vale (MAC-aadressi ei \u00f5nnestunud tuvastada). Paranda ja proovi uuesti.", "invalid_port": "Lubatud pordivahemik on 1024\u201365535. Paranda ja proovi uuesti." }, diff --git a/homeassistant/components/mobile_app/translations/et.json b/homeassistant/components/mobile_app/translations/et.json index 41d2be9d455..27f5fce2bdf 100644 --- a/homeassistant/components/mobile_app/translations/et.json +++ b/homeassistant/components/mobile_app/translations/et.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "install_app": "Home Assistantiga sidumiseks avage mobiilirakendus. \u00dchilduvate rakenduste loendi leiate jaotisest [dokumendid] ( {apps_url} )." + "install_app": "Home Assistantiga sidumiseks ava mobiilirakendus. \u00dchilduvate rakenduste loendi leiate jaotisest [dokumendid] ( {apps_url} )." }, "step": { "confirm": { diff --git a/homeassistant/components/motion_blinds/translations/pl.json b/homeassistant/components/motion_blinds/translations/pl.json index 1d34d22d65e..61e9d22c3cf 100644 --- a/homeassistant/components/motion_blinds/translations/pl.json +++ b/homeassistant/components/motion_blinds/translations/pl.json @@ -8,7 +8,7 @@ "error": { "discovery_error": "Nie uda\u0142o si\u0119 wykry\u0107 bramki ruchu" }, - "flow_title": "Motion Blinds", + "flow_title": "Rolety Motion", "step": { "connect": { "data": { @@ -30,7 +30,7 @@ "host": "Adres IP" }, "description": "Po\u0142\u0105cz si\u0119 z bram\u0105 ruchu. Je\u015bli adres IP nie jest ustawiony, u\u017cywane jest automatyczne wykrywanie", - "title": "Motion Blinds" + "title": "Rolety Motion" } } } diff --git a/homeassistant/components/mqtt/translations/et.json b/homeassistant/components/mqtt/translations/et.json index 53d6d391e8f..d2b863cab46 100644 --- a/homeassistant/components/mqtt/translations/et.json +++ b/homeassistant/components/mqtt/translations/et.json @@ -78,7 +78,7 @@ "will_retain": "L\u00f5petamisteate j\u00e4\u00e4dvustamine", "will_topic": "L\u00f5petamisteade" }, - "description": "Valige MQTT s\u00e4tted." + "description": "Vali MQTT s\u00e4tted." } } } diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index 6693c2e5614..f45c9ea489f 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -7,7 +7,7 @@ "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." + "unknown_authorize_url_generation": "Unknown error generating an authorize URL." }, "create_entry": { "default": "Successfully authenticated" diff --git a/homeassistant/components/nightscout/translations/et.json b/homeassistant/components/nightscout/translations/et.json index 1e77907f2af..361b4789328 100644 --- a/homeassistant/components/nightscout/translations/et.json +++ b/homeassistant/components/nightscout/translations/et.json @@ -15,7 +15,7 @@ "api_key": "API v\u00f5ti", "url": "" }, - "description": "- URL: NightScout eksemplari aadress. St: https://myhomeassistant.duckdns.org:5423\n - API v\u00f5ti (valikuline): kasutage ainult siis kui teie eksemplar on kaitstud (auth_default_roles! = readable).", + "description": "- URL: NightScout eksemplari aadress. St: https://myhomeassistant.duckdns.org:5423\n - API v\u00f5ti (valikuline): kasuta ainult siis kui teie eksemplar on kaitstud (auth_default_roles! = readable).", "title": "Sisesta oma Nightscouti serveri teave." } } diff --git a/homeassistant/components/openweathermap/translations/et.json b/homeassistant/components/openweathermap/translations/et.json index 26c688482fd..e548c07236e 100644 --- a/homeassistant/components/openweathermap/translations/et.json +++ b/homeassistant/components/openweathermap/translations/et.json @@ -17,7 +17,7 @@ "mode": "Re\u017eiim", "name": "Sidumise nimi" }, - "description": "Seadistage OpenWeatherMapi sidumine. API-v\u00f5tme loomiseks minge aadressile https://openweathermap.org/appid", + "description": "Seadista OpenWeatherMapi sidumine. API-v\u00f5tme loomiseks mine aadressile https://openweathermap.org/appid", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/owntracks/translations/et.json b/homeassistant/components/owntracks/translations/et.json index 16ef569d9a4..2ee171365a4 100644 --- a/homeassistant/components/owntracks/translations/et.json +++ b/homeassistant/components/owntracks/translations/et.json @@ -4,7 +4,7 @@ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." }, "create_entry": { - "default": "\n\nAva Android seadmes [rakendus OwnTracks] ( {android_url} ), mine eelistustele - > \u00fchendus. Muuda j\u00e4rgmisi seadeid:\n - Re\u017eiim: privaatne HTTP\n - Host: {webhook_url}\n - Identifitseerimine:\n - kasutajanimi: \" \"\n - seadme ID: \" \"\n\n IOS-is ava rakendus [OwnTracks] ( {ios_url} ), puuduta vasakus \u00fclanurgas ikooni (i) - > seaded. Muuda j\u00e4rgmisi seadeid:\n - Re\u017eiim: HTTP\n - URL: {webhook_url}\n - L\u00fclitage autentimine sisse\n - UserID: \" \" \"\n\n {secret}\n\n Lisateavet leiad [dokumentatsioonist] ( {docs_url} )." + "default": "\n\nAva Android seadmes [rakendus OwnTracks] ( {android_url} ), mine eelistustele - > \u00fchendus. Muuda j\u00e4rgmisi seadeid:\n - Re\u017eiim: privaatne HTTP\n - Host: {webhook_url}\n - Identifitseerimine:\n - kasutajanimi: \" \"\n - seadme ID: \" \"\n\n IOS-is ava rakendus [OwnTracks] ( {ios_url} ), puuduta vasakus \u00fclanurgas ikooni (i) - > seaded. Muuda j\u00e4rgmisi seadeid:\n - Re\u017eiim: HTTP\n - URL: {webhook_url}\n - L\u00fclita autentimine sisse\n - UserID: \" \" \"\n\n {secret}\n\n Lisateavet leiad [dokumentatsioonist] ( {docs_url} )." }, "step": { "user": { diff --git a/homeassistant/components/plaato/translations/it.json b/homeassistant/components/plaato/translations/it.json index ad289fa758f..acd2fcfa3f4 100644 --- a/homeassistant/components/plaato/translations/it.json +++ b/homeassistant/components/plaato/translations/it.json @@ -1,16 +1,53 @@ { "config": { "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, "create_entry": { - "default": "Per inviare eventi a Home Assistant, dovrai impostare la funzione webhook in Plaato Airlock. \n\n Inserisci le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Metodo: POST \n\n Vedi [la documentazione]({docs_url}) per ulteriori dettagli." + "default": "Il tuo Plaato {device_type} con nome **{device_name}** \u00e8 stato configurato con successo!" + }, + "error": { + "invalid_webhook_device": "Hai selezionato un dispositivo che non supporta l'invio di dati a un webhook. \u00c8 disponibile solo per Airlock", + "no_api_method": "Devi aggiungere un token di autenticazione o selezionare webhook", + "no_auth_token": "\u00c8 necessario aggiungere un token di autorizzazione" }, "step": { + "api_method": { + "data": { + "token": "Incolla il token di autenticazione qui", + "use_webhook": "Usa webhook" + }, + "description": "Per poter interrogare l'API \u00e8 necessario un `auth_token` che pu\u00f2 essere ottenuto seguendo [queste] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) istruzioni \n\n Dispositivo selezionato: **{device_type}** \n\n Se preferisci utilizzare il metodo webhook integrato (solo Airlock), seleziona la casella sottostante e lascia vuoto il token di autenticazione", + "title": "Seleziona il metodo API" + }, "user": { + "data": { + "device_name": "Assegna un nome al dispositivo", + "device_type": "Tipo di dispositivo Plaato" + }, "description": "Vuoi iniziare la configurazione?", - "title": "Configura il webhook di Plaato" + "title": "Imposta i dispositivi Plaato" + }, + "webhook": { + "description": "Per inviare eventi a Home Assistant, dovrai configurare la funzione webhook in Plaato Airlock. \n\n Compila le seguenti informazioni: \n\n - URL: \"{webhook_url}\"\n - Metodo: POST \n\n Vedere [la documentazione] ({docs_url}) per ulteriori dettagli.", + "title": "Webhook da utilizzare" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "Intervallo di aggiornamento (minuti)" + }, + "description": "Imposta l'intervallo di aggiornamento (minuti)", + "title": "Opzioni per Plaato" + }, + "webhook": { + "description": "Informazioni webhook:\n\n- URL: \"{webhook_url}\"\n- Metodo: POST\n\n", + "title": "Opzioni per Plaato Airlock" } } } diff --git a/homeassistant/components/plaato/translations/pl.json b/homeassistant/components/plaato/translations/pl.json index c849f574c9c..57df32c3f4e 100644 --- a/homeassistant/components/plaato/translations/pl.json +++ b/homeassistant/components/plaato/translations/pl.json @@ -46,7 +46,7 @@ "title": "Opcje dla Plaato" }, "webhook": { - "description": "Informacje o webhook: \n\n - URL: `{webhook_url}`\n - Metoda: POST \n\n", + "description": "Informacje o webhooku: \n\n - URL: `{webhook_url}`\n - Metoda: POST \n\n", "title": "Opcje dla areomierza Plaato Airlock" } } diff --git a/homeassistant/components/plaato/translations/ru.json b/homeassistant/components/plaato/translations/ru.json index 99e28ac9e04..befce3d7e84 100644 --- a/homeassistant/components/plaato/translations/ru.json +++ b/homeassistant/components/plaato/translations/ru.json @@ -1,15 +1,52 @@ { "config": { "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "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.", "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f Webhook-\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439." }, "create_entry": { "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Plaato Airlock.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, + "error": { + "invalid_webhook_device": "\u0412\u044b \u0432\u044b\u0431\u0440\u0430\u043b\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0443 \u0434\u0430\u043d\u043d\u044b\u0445 \u043d\u0430 Webhook. \u042d\u0442\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0448\u043b\u044e\u0437\u0430.", + "no_api_method": "\u041d\u0443\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0438\u043b\u0438 \u0432\u044b\u0431\u0440\u0430\u0442\u044c Webhook.", + "no_auth_token": "\u041d\u0443\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + }, + "step": { + "api_method": { + "data": { + "token": "\u0412\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0441\u044e\u0434\u0430", + "use_webhook": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c Webhook" + }, + "description": "\u0427\u0442\u043e\u0431\u044b \u0438\u043c\u0435\u0442\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0437\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0442\u044c API, \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f `auth_token`, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c, \u0441\u043b\u0435\u0434\u0443\u044f [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) \n\n\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e: **{device_type}** \n\n\u0415\u0441\u043b\u0438 \u0412\u044b \u043f\u0440\u0435\u0434\u043f\u043e\u0447\u0438\u0442\u0430\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0439 Webhook (\u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f Airlock), \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0435 \u0444\u043b\u0430\u0436\u043e\u043a \u043d\u0438\u0436\u0435 \u0438 \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u0442\u043e\u043a\u0435\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0443\u0441\u0442\u044b\u043c.", + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 API" + }, + "user": { + "data": { + "device_name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "device_type": "\u0422\u0438\u043f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Plaato" + }, + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?", + "title": "Plaato Airlock" + }, + "webhook": { + "description": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Plaato Airlock.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", + "title": "Webhook" + } + } + }, + "options": { "step": { "user": { - "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?", + "data": { + "update_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)" + }, + "description": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0435 \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)", + "title": "Plaato" + }, + "webhook": { + "description": "Webhook:\n\n- URL: `{webhook_url}`\n- Method: POST", "title": "Plaato Airlock" } } diff --git a/homeassistant/components/point/translations/en.json b/homeassistant/components/point/translations/en.json index 50e9e4f3ce0..685a16cbbf5 100644 --- a/homeassistant/components/point/translations/en.json +++ b/homeassistant/components/point/translations/en.json @@ -6,7 +6,7 @@ "authorize_url_timeout": "Timeout generating authorize URL.", "external_setup": "Point successfully configured from another flow.", "no_flows": "The component is not configured. Please follow the documentation.", - "unknown_authorize_url_generation": "Unknown error generating an authorize url." + "unknown_authorize_url_generation": "Unknown error generating an authorize URL." }, "create_entry": { "default": "Successfully authenticated" diff --git a/homeassistant/components/risco/translations/et.json b/homeassistant/components/risco/translations/et.json index 9bd35ec22db..c57d5d73fec 100644 --- a/homeassistant/components/risco/translations/et.json +++ b/homeassistant/components/risco/translations/et.json @@ -27,7 +27,7 @@ "armed_home": "Valves kodus", "armed_night": "Valves \u00f6ine" }, - "description": "Valige millisesse olekusse l\u00fcltub Risco alarm kui valvestada Home Assistant", + "description": "Vali millisesse olekusse l\u00fcltub Risco alarm kui valvestada Home Assistant", "title": "Lisa Risco olekud Home Assistanti olekutesse" }, "init": { diff --git a/homeassistant/components/risco/translations/pl.json b/homeassistant/components/risco/translations/pl.json index ef7ed9f13e0..b39c2cde23b 100644 --- a/homeassistant/components/risco/translations/pl.json +++ b/homeassistant/components/risco/translations/pl.json @@ -34,7 +34,7 @@ "data": { "code_arm_required": "Wymagaj kodu PIN do uzbrojenia", "code_disarm_required": "Wymagaj kodu PIN do rozbrojenia", - "scan_interval": "Cz\u0119stotliwo\u015b\u0107 od\u015bwie\u017cania (w sekundach)" + "scan_interval": "Jak cz\u0119sto odpytywa\u0107 Risco (w sekundach)" }, "title": "Opcje" }, diff --git a/homeassistant/components/roomba/translations/en.json b/homeassistant/components/roomba/translations/en.json index 8d449e18815..9c373d649aa 100644 --- a/homeassistant/components/roomba/translations/en.json +++ b/homeassistant/components/roomba/translations/en.json @@ -25,7 +25,7 @@ "data": { "password": "Password" }, - "description": "The password could not be retrivied from the device automatically. Please follow the steps outlined in the documentation at: {auth_help_url}", + "description": "The password could not be retrieved from the device automatically. Please follow the steps outlined in the documentation at: {auth_help_url}", "title": "Enter Password" }, "manual": { diff --git a/homeassistant/components/rpi_power/translations/et.json b/homeassistant/components/rpi_power/translations/et.json index fdf32414ba7..ec8475d2dac 100644 --- a/homeassistant/components/rpi_power/translations/et.json +++ b/homeassistant/components/rpi_power/translations/et.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "Ei leia selle komponendi jaoks vajalikku s\u00fcsteemiklassi. Veenduge, et teie kernel on v\u00e4rske ja riistvara on toetatud", + "no_devices_found": "Ei leia selle komponendi jaoks vajalikku s\u00fcsteemiklassi. Veendu, et kernel on v\u00e4rske ja riistvara on toetatud", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." }, "step": { diff --git a/homeassistant/components/simplisafe/translations/et.json b/homeassistant/components/simplisafe/translations/et.json index b98a121046a..7b6e317b922 100644 --- a/homeassistant/components/simplisafe/translations/et.json +++ b/homeassistant/components/simplisafe/translations/et.json @@ -12,7 +12,7 @@ }, "step": { "mfa": { - "description": "Kontrollige oma e-posti: link SimpliSafe-lt. P\u00e4rast lingi kontrollimist naase siia, et viia l\u00f5pule sidumise installimine.", + "description": "Kontrolli oma e-posti: link SimpliSafe-lt. P\u00e4rast lingi kontrollimist naase siia, et viia l\u00f5pule sidumise installimine.", "title": "SimpliSafe mitmeastmeline autentimine" }, "reauth_confirm": { diff --git a/homeassistant/components/soma/translations/en.json b/homeassistant/components/soma/translations/en.json index 6f28ee53ae2..fb5d17ac59d 100644 --- a/homeassistant/components/soma/translations/en.json +++ b/homeassistant/components/soma/translations/en.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "You can only configure one Soma account.", - "authorize_url_timeout": "Timeout generating authorize url.", + "authorize_url_timeout": "Timeout generating authorize URL.", "connection_error": "Failed to connect to SOMA Connect.", "missing_configuration": "The Soma component is not configured. Please follow the documentation.", "result_error": "SOMA Connect responded with error status." diff --git a/homeassistant/components/soma/translations/it.json b/homeassistant/components/soma/translations/it.json index 0119fca7388..237ce347cb0 100644 --- a/homeassistant/components/soma/translations/it.json +++ b/homeassistant/components/soma/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "\u00c8 possibile configurare un solo account Soma.", - "authorize_url_timeout": "Timeout durante la generazione dell'URL di autorizzazione.", + "authorize_url_timeout": "Tempo scaduto nella generazione dell'URL di autorizzazione.", "connection_error": "Impossibile connettersi a SOMA Connect.", "missing_configuration": "Il componente Soma non \u00e8 configurato. Si prega di seguire la documentazione.", "result_error": "SOMA Connect ha risposto con stato di errore." diff --git a/homeassistant/components/sonarr/translations/et.json b/homeassistant/components/sonarr/translations/et.json index 957b3c74eae..c95b2e9dc88 100644 --- a/homeassistant/components/sonarr/translations/et.json +++ b/homeassistant/components/sonarr/translations/et.json @@ -13,7 +13,7 @@ "step": { "reauth_confirm": { "description": "Sonarr-i sidumine tuleb k\u00e4sitsi taastuvastada Sonarr API abil: {host}", - "title": "Autentige uuesti Sonarriga" + "title": "Autendi Sonarriga uuesti" }, "user": { "data": { diff --git a/homeassistant/components/spotify/translations/en.json b/homeassistant/components/spotify/translations/en.json index 73ea219105b..7136e5a8e71 100644 --- a/homeassistant/components/spotify/translations/en.json +++ b/homeassistant/components/spotify/translations/en.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "authorize_url_timeout": "Timeout generating authorize url.", + "authorize_url_timeout": "Timeout generating authorize URL.", "missing_configuration": "The Spotify integration 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_account_mismatch": "The Spotify account authenticated with, does not match the account needed re-authentication." diff --git a/homeassistant/components/spotify/translations/et.json b/homeassistant/components/spotify/translations/et.json index 01583d1b0f0..c5cee44acca 100644 --- a/homeassistant/components/spotify/translations/et.json +++ b/homeassistant/components/spotify/translations/et.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Kinnituse URLi ajal\u00f5pp", - "missing_configuration": "Spotify sidumine pole h\u00e4\u00e4lestatud. Palun j\u00e4rgige dokumentatsiooni.", + "missing_configuration": "Spotify sidumine pole h\u00e4\u00e4lestatud. Palun j\u00e4rgi dokumentatsiooni.", "no_url_available": "URL pole saadaval. Rohkem teavet [check the help section]({docs_url})", "reauth_account_mismatch": "Spotify konto mida autenditi ei vasta kontole mis vajas uuesti autentimist." }, diff --git a/homeassistant/components/spotify/translations/it.json b/homeassistant/components/spotify/translations/it.json index 6911d38be00..595e13865e1 100644 --- a/homeassistant/components/spotify/translations/it.json +++ b/homeassistant/components/spotify/translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione", + "authorize_url_timeout": "Tempo scaduto nella generazione dell'URL di autorizzazione.", "missing_configuration": "L'integrazione di Spotify non \u00e8 configurata. 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_account_mismatch": "L'account Spotify con cui si \u00e8 autenticati non corrisponde all'account necessario per la ri-autenticazione." diff --git a/homeassistant/components/spotify/translations/pl.json b/homeassistant/components/spotify/translations/pl.json index f9e6f429214..52028d4d368 100644 --- a/homeassistant/components/spotify/translations/pl.json +++ b/homeassistant/components/spotify/translations/pl.json @@ -21,7 +21,7 @@ }, "system_health": { "info": { - "api_endpoint_reachable": "Dost\u0119pno\u015b\u0107 punktu ko\u0144cowego API Spotify" + "api_endpoint_reachable": "Punkt ko\u0144cowy Spotify API osi\u0105galny" } } } \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/en.json b/homeassistant/components/tellduslive/translations/en.json index 7b14df15fa8..b1b9cd9ab10 100644 --- a/homeassistant/components/tellduslive/translations/en.json +++ b/homeassistant/components/tellduslive/translations/en.json @@ -5,7 +5,7 @@ "authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_timeout": "Timeout generating authorize URL.", "unknown": "Unexpected error", - "unknown_authorize_url_generation": "Unknown error generating an authorize url." + "unknown_authorize_url_generation": "Unknown error generating an authorize URL." }, "error": { "invalid_auth": "Invalid authentication" diff --git a/homeassistant/components/toon/translations/en.json b/homeassistant/components/toon/translations/en.json index c64913cfb6c..3351c16d8d8 100644 --- a/homeassistant/components/toon/translations/en.json +++ b/homeassistant/components/toon/translations/en.json @@ -7,7 +7,7 @@ "missing_configuration": "The component is not configured. Please follow the documentation.", "no_agreements": "This account has no Toon displays.", "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", - "unknown_authorize_url_generation": "Unknown error generating an authorize url." + "unknown_authorize_url_generation": "Unknown error generating an authorize URL." }, "step": { "agreement": { diff --git a/homeassistant/components/toon/translations/et.json b/homeassistant/components/toon/translations/et.json index 7b70eae433e..f93fb684b25 100644 --- a/homeassistant/components/toon/translations/et.json +++ b/homeassistant/components/toon/translations/et.json @@ -18,7 +18,7 @@ "title": "Vali oma leping" }, "pick_implementation": { - "title": "Valige oma rentnik, kellega autentida" + "title": "Vali oma rentnik, kellega autentida" } } } diff --git a/homeassistant/components/traccar/translations/ca.json b/homeassistant/components/traccar/translations/ca.json index 1b00aab4b3e..62c15e0ca20 100644 --- a/homeassistant/components/traccar/translations/ca.json +++ b/homeassistant/components/traccar/translations/ca.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "La teva inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per poder rebre missatges webhook." }, "create_entry": { - "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar l'opci\u00f3 webhook de Traccar.\n\nUtilitza el seg\u00fcent enlla\u00e7: `{webhook_url}`\n\nConsulta la [documentaci\u00f3]({docs_url}) per a m\u00e9s detalls." + "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar l'opci\u00f3 webhook de Traccar.\n\nUtilitza el seg\u00fcent URL: `{webhook_url}`\n\nConsulta la [documentaci\u00f3]({docs_url}) per a m\u00e9s detalls." }, "step": { "user": { diff --git a/homeassistant/components/traccar/translations/en.json b/homeassistant/components/traccar/translations/en.json index 2231d53ceb8..c6d7f0f1892 100644 --- a/homeassistant/components/traccar/translations/en.json +++ b/homeassistant/components/traccar/translations/en.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive webhook messages." }, "create_entry": { - "default": "To send events to Home Assistant, you will need to setup the webhook feature in Traccar.\n\nUse the following url: `{webhook_url}`\n\nSee [the documentation]({docs_url}) for further details." + "default": "To send events to Home Assistant, you will need to setup the webhook feature in Traccar.\n\nUse the following URL: `{webhook_url}`\n\nSee [the documentation]({docs_url}) for further details." }, "step": { "user": { diff --git a/homeassistant/components/traccar/translations/it.json b/homeassistant/components/traccar/translations/it.json index 6d4de5bb13d..8c95b3cd022 100644 --- a/homeassistant/components/traccar/translations/it.json +++ b/homeassistant/components/traccar/translations/it.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, "create_entry": { - "default": "Per inviare eventi a Home Assistant, \u00e8 necessario configurare la funzionalit\u00e0 webhook in Traccar.\n\nUtilizzare l'URL seguente: `{webhook_url}`\n\nPer ulteriori dettagli, vedere [la documentazione]({docs_url}) ." + "default": "Per inviare eventi a Home Assistant, \u00e8 necessario impostare la funzione webhook in Traccar.\n\nUsa il seguente URL: `{webhook_url}`.\n\nVedi [la documentazione]({docs_url}) per ulteriori dettagli." }, "step": { "user": { diff --git a/homeassistant/components/traccar/translations/zh-Hant.json b/homeassistant/components/traccar/translations/zh-Hant.json index 2204e7c3323..ee7c75d8408 100644 --- a/homeassistant/components/traccar/translations/zh-Hant.json +++ b/homeassistant/components/traccar/translations/zh-Hant.json @@ -5,7 +5,7 @@ "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": { - "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Traccar \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u4f7f\u7528 url: `{webhook_url}`\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" + "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Traccar \u5167\u8a2d\u5b9a Webhook \u529f\u80fd\u3002\n\n\u8acb\u4f7f\u7528 URL\uff1a`{webhook_url}`\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6] ({docs_url}) \u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" }, "step": { "user": { diff --git a/homeassistant/components/tuya/translations/pl.json b/homeassistant/components/tuya/translations/pl.json index a24c1dbe265..dc57f064d57 100644 --- a/homeassistant/components/tuya/translations/pl.json +++ b/homeassistant/components/tuya/translations/pl.json @@ -53,7 +53,7 @@ "discovery_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania nowych urz\u0105dze\u0144 (w sekundach)", "list_devices": "Wybierz urz\u0105dzenia do skonfigurowania lub pozostaw puste, aby zapisa\u0107 konfiguracj\u0119", "query_device": "Wybierz urz\u0105dzenie, kt\u00f3re b\u0119dzie u\u017cywa\u0107 metody odpytywania w celu szybszej aktualizacji statusu", - "query_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania odpytywanego urz\u0105dzenia (w sekundach)" + "query_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania odpytywanego urz\u0105dzenia w sekundach" }, "description": "Nie ustawiaj zbyt niskich warto\u015bci skanowania, bo zako\u0144cz\u0105 si\u0119 niepowodzeniem, generuj\u0105c komunikat o b\u0142\u0119dzie w logu", "title": "Konfiguracja opcji Tuya" diff --git a/homeassistant/components/unifi/translations/pl.json b/homeassistant/components/unifi/translations/pl.json index 6c8c74e726a..719120eeb5e 100644 --- a/homeassistant/components/unifi/translations/pl.json +++ b/homeassistant/components/unifi/translations/pl.json @@ -70,7 +70,7 @@ "allow_uptime_sensors": "Sensory czasu pracy dla klient\u00f3w sieciowych" }, "description": "Konfiguracja sensora statystyk", - "title": "Opcje UniFi" + "title": "Opcje UniFi 3/3" } } } diff --git a/homeassistant/components/unifi/translations/ru.json b/homeassistant/components/unifi/translations/ru.json index 3b69bf0ee33..df2150c98ec 100644 --- a/homeassistant/components/unifi/translations/ru.json +++ b/homeassistant/components/unifi/translations/ru.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "configuration_updated": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430.", "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": { diff --git a/homeassistant/components/vera/translations/ca.json b/homeassistant/components/vera/translations/ca.json index 63ec236ef89..13a889cb7db 100644 --- a/homeassistant/components/vera/translations/ca.json +++ b/homeassistant/components/vera/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cannot_connect": "No s'ha pogut connectar amb el controlador amb l'URL {base_url}" + "cannot_connect": "No s'ha pogut connectar amb el controlador amb URL {base_url}" }, "step": { "user": { @@ -10,7 +10,7 @@ "lights": "Identificadors de dispositiu dels commutadors Vera a tractar com a llums a Home Assistant.", "vera_controller_url": "URL del controlador" }, - "description": "Proporciona un URL pel controlador Vera. Hauria de quedar aix\u00ed: http://192.168.1.161:3480.", + "description": "Proporciona un URL pel controlador Vera. Hauria de ser similar al seg\u00fcent: http://192.168.1.161:3480.", "title": "Configuraci\u00f3 del controlador Vera" } } diff --git a/homeassistant/components/vera/translations/en.json b/homeassistant/components/vera/translations/en.json index 5503d6b1034..94f490d71ee 100644 --- a/homeassistant/components/vera/translations/en.json +++ b/homeassistant/components/vera/translations/en.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cannot_connect": "Could not connect to controller with url {base_url}" + "cannot_connect": "Could not connect to controller with URL {base_url}" }, "step": { "user": { @@ -10,7 +10,7 @@ "lights": "Vera switch device ids to treat as lights in Home Assistant.", "vera_controller_url": "Controller URL" }, - "description": "Provide a Vera controller url below. It should look like this: http://192.168.1.161:3480.", + "description": "Provide a Vera controller URL below. It should look like this: http://192.168.1.161:3480.", "title": "Setup Vera controller" } } diff --git a/homeassistant/components/vera/translations/it.json b/homeassistant/components/vera/translations/it.json index e144bf251cd..3ec026beaac 100644 --- a/homeassistant/components/vera/translations/it.json +++ b/homeassistant/components/vera/translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cannot_connect": "Impossibile connettersi al controllore con l'url {base_url}" + "cannot_connect": "Impossibile connettersi al controller con l'URL {base_url}" }, "step": { "user": { @@ -10,7 +10,7 @@ "lights": "Gli ID dei dispositivi switch Vera da trattare come luci in Home Assistant.", "vera_controller_url": "URL del controller" }, - "description": "Fornire un url di controllo Vera di seguito. Dovrebbe avere questo aspetto: http://192.168.1.161:3480.", + "description": "Fornisci un URL controller Vera di seguito. Dovrebbe assomigliare a questo: http://192.168.1.161:3480.", "title": "Configurazione controller Vera" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/en.json b/homeassistant/components/xiaomi_aqara/translations/en.json index 075c8d4a194..d51687a0790 100644 --- a/homeassistant/components/xiaomi_aqara/translations/en.json +++ b/homeassistant/components/xiaomi_aqara/translations/en.json @@ -18,7 +18,7 @@ "data": { "select_ip": "IP Address" }, - "description": "Run the setup again if you want to connect aditional gateways", + "description": "Run the setup again if you want to connect additional gateways", "title": "Select the Xiaomi Aqara Gateway that you wish to connect" }, "settings": { @@ -35,7 +35,7 @@ "interface": "The network interface to use", "mac": "Mac Address (optional)" }, - "description": "Connect to your Xiaomi Aqara Gateway, if the IP and mac addresses are left empty, auto-discovery is used", + "description": "Connect to your Xiaomi Aqara Gateway, if the IP and MAC addresses are left empty, auto-discovery is used", "title": "Xiaomi Aqara Gateway" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/it.json b/homeassistant/components/xiaomi_aqara/translations/it.json index 3299fa092f8..275729e4e81 100644 --- a/homeassistant/components/xiaomi_aqara/translations/it.json +++ b/homeassistant/components/xiaomi_aqara/translations/it.json @@ -18,7 +18,7 @@ "data": { "select_ip": "Indirizzo IP" }, - "description": "Eseguire nuovamente l'installazione se si desidera connettere gateway adizionali", + "description": "Esegui di nuovo la configurazione se desideri connettere gateway aggiuntivi", "title": "Selezionare il Gateway Xiaomi Aqara che si desidera collegare" }, "settings": { @@ -35,7 +35,7 @@ "interface": "L'interfaccia di rete da utilizzare", "mac": "Indirizzo Mac (opzionale)" }, - "description": "Connettiti al tuo Xiaomi Aqara Gateway, se gli indirizzi IP e mac sono lasciati vuoti, verr\u00e0 utilizzato il rilevamento automatico", + "description": "Connettiti al tuo Xiaomi Aqara Gateway, se gli indirizzi IP e MAC sono lasciati vuoti, verr\u00e0 utilizzato il rilevamento automatico", "title": "Xiaomi Aqara Gateway" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json index 582aea354c6..5d2d097e832 100644 --- a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json @@ -18,7 +18,7 @@ "data": { "select_ip": "IP \u4f4d\u5740" }, - "description": "\u5982\u679c\u9084\u6709\u5176\u4ed6\u7db2\u95dc\u9700\u8981\u9023\u7dda\uff0c\u8acb\u518d\u57f7\u884c\u4e00\u6b21\u8a2d\u5b9a", + "description": "\u5982\u679c\u9084\u9700\u8981\u9023\u7dda\u81f3\u5176\u4ed6\u7db2\u95dc\uff0c\u8acb\u518d\u57f7\u884c\u4e00\u6b21\u8a2d\u5b9a", "title": "\u9078\u64c7\u6240\u8981\u9023\u7dda\u7684\u5c0f\u7c73 Aqara \u7db2\u95dc" }, "settings": { diff --git a/homeassistant/components/zoneminder/translations/et.json b/homeassistant/components/zoneminder/translations/et.json index 087158a450d..c8ccef3a44b 100644 --- a/homeassistant/components/zoneminder/translations/et.json +++ b/homeassistant/components/zoneminder/translations/et.json @@ -23,7 +23,7 @@ "password": "Salas\u00f5na", "path": "ZM aadress", "path_zms": "ZMS-i aadress", - "ssl": "Kasutage ZoneMinderiga \u00fchenduse loomiseks SSL-i", + "ssl": "Kasuta ZoneMinderiga \u00fchenduse loomiseks SSL-i", "username": "Kasutajanimi", "verify_ssl": "Kontrolli SSL sertifikaati" }, diff --git a/homeassistant/components/zwave/translations/it.json b/homeassistant/components/zwave/translations/it.json index 0534d54f32f..d3522cf0889 100644 --- a/homeassistant/components/zwave/translations/it.json +++ b/homeassistant/components/zwave/translations/it.json @@ -13,7 +13,7 @@ "network_key": "Chiave di rete (lascia vuoto per generare automaticamente)", "usb_path": "Percorso del dispositivo USB" }, - "description": "Vai su https://www.home-assistant.io/docs/z-wave/installation/ per le informazioni sulle variabili di configurazione", + "description": "Questa integrazione non viene pi\u00f9 mantenuta. Per le nuove installazioni, usa invece Z-Wave JS. \n\nVedere https://www.home-assistant.io/docs/z-wave/installation/ per informazioni sulle variabili di configurazione", "title": "Configura Z-Wave" } } diff --git a/homeassistant/components/zwave/translations/pl.json b/homeassistant/components/zwave/translations/pl.json index 90ff1a37894..0a4b6a4828c 100644 --- a/homeassistant/components/zwave/translations/pl.json +++ b/homeassistant/components/zwave/translations/pl.json @@ -13,7 +13,7 @@ "network_key": "Klucz sieciowy (pozostaw pusty, by generowa\u0107 automatycznie)", "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" }, - "description": "Przejd\u017a na https://www.home-assistant.io/docs/z-wave/installation/, aby uzyska\u0107 informacje na temat zmiennych konfiguracyjnych", + "description": "Ta integracja nie jest ju\u017c wspierana. Dla nowych instalacji, u\u017cyj Z-Wave JS.\n\nPrzejd\u017a na https://www.home-assistant.io/docs/z-wave/installation/, aby uzyska\u0107 informacje na temat zmiennych konfiguracyjnych", "title": "Konfiguracja Z-Wave" } } diff --git a/homeassistant/components/zwave/translations/zh-Hant.json b/homeassistant/components/zwave/translations/zh-Hant.json index f5c07a9efc9..545da7b2ee7 100644 --- a/homeassistant/components/zwave/translations/zh-Hant.json +++ b/homeassistant/components/zwave/translations/zh-Hant.json @@ -13,7 +13,7 @@ "network_key": "\u7db2\u8def\u5bc6\u9470\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u6703\u81ea\u52d5\u7522\u751f\uff09", "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/", + "description": "\u6b64\u6574\u5408\u5df2\u7d93\u4e0d\u518d\u9032\u884c\u7dad\u8b77\uff0c\u8acb\u4f7f\u7528 Z-Wave JS \u53d6\u4ee3\u70ba\u65b0\u5b89\u88dd\u65b9\u5f0f\u3002\n\n\u8acb\u53c3\u95b1 https://www.home-assistant.io/docs/z-wave/installation/ \u4ee5\n\u7372\u5f97\u8a2d\u5b9a\u8b8a\u6578\u8cc7\u8a0a", "title": "\u8a2d\u5b9a Z-Wave" } } From d1b7d25a5db8456958fe07eca836f93ee7a120ba Mon Sep 17 00:00:00 2001 From: obelix05 Date: Fri, 5 Feb 2021 02:31:47 +0100 Subject: [PATCH 197/796] Prevent fritzbox callmonitor phonebook_id 0 from being ignored (#45990) --- homeassistant/components/fritzbox_callmonitor/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritzbox_callmonitor/base.py b/homeassistant/components/fritzbox_callmonitor/base.py index 79f82de95b7..ec62e196855 100644 --- a/homeassistant/components/fritzbox_callmonitor/base.py +++ b/homeassistant/components/fritzbox_callmonitor/base.py @@ -41,7 +41,7 @@ class FritzBoxPhonebook: @Throttle(MIN_TIME_PHONEBOOK_UPDATE) def update_phonebook(self): """Update the phone book dictionary.""" - if not self.phonebook_id: + if self.phonebook_id is None: return self.phonebook_dict = self.fph.get_all_names(self.phonebook_id) From 374aa3aee1c7ee82a5d73b40b6f23439ad78e085 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Feb 2021 15:39:07 -1000 Subject: [PATCH 198/796] Fix homekit options not being prefilled (#45926) * Fix homekit options not being prefilled When changing homekit options, the existing ones were not being prefilled on the form. * hide camera screen if no cameras --- .../components/homekit/config_flow.py | 38 ++++++----- tests/components/homekit/test_config_flow.py | 65 ++++++++++++++++--- 2 files changed, 78 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index a20d9a5b843..f16d14e9330 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -91,7 +91,8 @@ DEFAULT_DOMAINS = [ DOMAINS_PREFER_ACCESSORY_MODE = ["camera", "media_player"] -CAMERA_ENTITY_PREFIX = "camera." +CAMERA_DOMAIN = "camera" +CAMERA_ENTITY_PREFIX = f"{CAMERA_DOMAIN}." _EMPTY_ENTITY_FILTER = { CONF_INCLUDE_DOMAINS: [], @@ -356,15 +357,26 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if domain not in domains_with_entities_selected ] - for entity_id in list(self.included_cameras): - if entity_id not in entities: - self.included_cameras.remove(entity_id) + self.included_cameras = { + entity_id + for entity_id in entities + if entity_id.startswith(CAMERA_ENTITY_PREFIX) + } else: entity_filter[CONF_INCLUDE_DOMAINS] = self.hk_options[CONF_DOMAINS] entity_filter[CONF_EXCLUDE_ENTITIES] = entities - for entity_id in entities: - if entity_id in self.included_cameras: - self.included_cameras.remove(entity_id) + if CAMERA_DOMAIN in entity_filter[CONF_INCLUDE_DOMAINS]: + camera_entities = _async_get_matching_entities( + self.hass, + domains=[CAMERA_DOMAIN], + ) + self.included_cameras = { + entity_id + for entity_id in camera_entities + if entity_id not in entities + } + else: + self.included_cameras = set() self.hk_options[CONF_FILTER] = entity_filter @@ -378,11 +390,6 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self.hass, domains=self.hk_options[CONF_DOMAINS], ) - self.included_cameras = { - entity_id - for entity_id in all_supported_entities - if entity_id.startswith(CAMERA_ENTITY_PREFIX) - } data_schema = {} entities = entity_filter.get(CONF_INCLUDE_ENTITIES, []) @@ -416,10 +423,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self.hk_options.update(user_input) return await self.async_step_include_exclude() - hk_options = dict(self.config_entry.options) - entity_filter = hk_options.get(CONF_FILTER, {}) - - homekit_mode = hk_options.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE) + self.hk_options = dict(self.config_entry.options) + entity_filter = self.hk_options.get(CONF_FILTER, {}) + homekit_mode = self.hk_options.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE) domains = entity_filter.get(CONF_INCLUDE_DOMAINS, []) include_entities = entity_filter.get(CONF_INCLUDE_ENTITIES) if include_entities: diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index 0e4114d566d..34c7ab2ecc0 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -492,6 +492,18 @@ async def test_options_flow_include_mode_with_cameras(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" + assert result["data_schema"]({}) == { + "domains": ["fan", "vacuum", "climate", "camera"], + "mode": "bridge", + } + schema = result["data_schema"].schema + assert _get_schema_default(schema, "domains") == [ + "fan", + "vacuum", + "climate", + "camera", + ] + assert _get_schema_default(schema, "mode") == "bridge" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -500,6 +512,16 @@ async def test_options_flow_include_mode_with_cameras(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "include_exclude" + assert result["data_schema"]({}) == { + "entities": ["camera.native_h264", "camera.transcode_h264"], + "include_exclude_mode": "include", + } + schema = result["data_schema"].schema + assert _get_schema_default(schema, "entities") == [ + "camera.native_h264", + "camera.transcode_h264", + ] + assert _get_schema_default(schema, "include_exclude_mode") == "include" result2 = await hass.config_entries.options.async_configure( result["flow_id"], @@ -510,6 +532,9 @@ async def test_options_flow_include_mode_with_cameras(hass): ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["step_id"] == "cameras" + assert result2["data_schema"]({}) == {"camera_copy": ["camera.native_h264"]} + schema = result2["data_schema"].schema + assert _get_schema_default(schema, "camera_copy") == ["camera.native_h264"] result3 = await hass.config_entries.options.async_configure( result2["flow_id"], @@ -519,14 +544,14 @@ async def test_options_flow_include_mode_with_cameras(hass): assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { "auto_start": True, - "mode": "bridge", + "entity_config": {"camera.native_h264": {}}, "filter": { "exclude_domains": [], "exclude_entities": ["climate.old", "camera.excluded"], "include_domains": ["fan", "vacuum", "climate", "camera"], "include_entities": [], }, - "entity_config": {}, + "mode": "bridge", } @@ -586,6 +611,17 @@ async def test_options_flow_include_mode_basic_accessory(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" + assert result["data_schema"]({}) == { + "domains": [ + "fan", + "humidifier", + "vacuum", + "media_player", + "climate", + "alarm_control_panel", + ], + "mode": "bridge", + } result2 = await hass.config_entries.options.async_configure( result["flow_id"], @@ -594,6 +630,7 @@ async def test_options_flow_include_mode_basic_accessory(hass): assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["step_id"] == "include_exclude" + assert _get_schema_default(result2["data_schema"].schema, "entities") == [] result3 = await hass.config_entries.options.async_configure( result2["flow_id"], @@ -612,7 +649,7 @@ async def test_options_flow_include_mode_basic_accessory(hass): } -async def test_converting_bridge_to_accessory_mode(hass): +async def test_converting_bridge_to_accessory_mode(hass, hk_driver): """Test we can convert a bridge to accessory mode.""" await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -639,12 +676,12 @@ async def test_converting_bridge_to_accessory_mode(hass): assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM assert result3["step_id"] == "pairing" + # We need to actually setup the config entry or the data + # will not get migrated to options with patch( - "homeassistant.components.homekit.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.homekit.async_setup_entry", + "homeassistant.components.homekit.HomeKit.async_start", return_value=True, - ) as mock_setup_entry: + ) as mock_async_start: result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], {}, @@ -665,8 +702,7 @@ async def test_converting_bridge_to_accessory_mode(hass): "name": bridge_name, "port": 12345, } - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_async_start.mock_calls) == 1 config_entry = result4["result"] @@ -681,6 +717,9 @@ async def test_converting_bridge_to_accessory_mode(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" + schema = result["data_schema"].schema + assert _get_schema_default(schema, "mode") == "bridge" + assert _get_schema_default(schema, "domains") == ["light"] result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -714,3 +753,11 @@ async def test_converting_bridge_to_accessory_mode(hass): "include_entities": ["camera.tv"], }, } + + +def _get_schema_default(schema, key_name): + """Iterate schema to find a key.""" + for schema_key in schema: + if schema_key == key_name: + return schema_key.default() + raise KeyError(f"{key_name} not found in schema") From bcefbe2dcaa38c86867a6b63db596c7c9b5daed0 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Thu, 4 Feb 2021 21:41:43 -0500 Subject: [PATCH 199/796] Use core constants for automation (#46016) --- homeassistant/components/automation/__init__.py | 2 +- homeassistant/components/automation/config.py | 3 +-- homeassistant/components/automation/const.py | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 201eeb5c456..295450e8211 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -10,6 +10,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, CONF_ALIAS, + CONF_CONDITION, CONF_DEVICE_ID, CONF_ENTITY_ID, CONF_ID, @@ -57,7 +58,6 @@ from .config import PLATFORM_SCHEMA # noqa from .config import async_validate_config_item from .const import ( CONF_ACTION, - CONF_CONDITION, CONF_INITIAL_STATE, CONF_TRIGGER, DEFAULT_INITIAL_STATE, diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 9c26f3552aa..89d5e184748 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -8,7 +8,7 @@ from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) from homeassistant.config import async_log_exception, config_without_domain -from homeassistant.const import CONF_ALIAS, CONF_ID, CONF_VARIABLES +from homeassistant.const import CONF_ALIAS, CONF_CONDITION, CONF_ID, CONF_VARIABLES from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, config_validation as cv, script from homeassistant.helpers.condition import async_validate_condition_config @@ -17,7 +17,6 @@ from homeassistant.loader import IntegrationNotFound from .const import ( CONF_ACTION, - CONF_CONDITION, CONF_DESCRIPTION, CONF_HIDE_ENTITY, CONF_INITIAL_STATE, diff --git a/homeassistant/components/automation/const.py b/homeassistant/components/automation/const.py index c8db3aa01e5..ffb89ba0907 100644 --- a/homeassistant/components/automation/const.py +++ b/homeassistant/components/automation/const.py @@ -1,7 +1,6 @@ """Constants for the automation integration.""" import logging -CONF_CONDITION = "condition" CONF_ACTION = "action" CONF_TRIGGER = "trigger" DOMAIN = "automation" From 6e9aa254d55c1e4ca0f84fda8837e73b13123d53 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Thu, 4 Feb 2021 21:43:04 -0500 Subject: [PATCH 200/796] Use core constants for delijn (#46027) --- homeassistant/components/delijn/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py index 8c73fecf26e..0058816d318 100644 --- a/homeassistant/components/delijn/sensor.py +++ b/homeassistant/components/delijn/sensor.py @@ -6,7 +6,7 @@ from pydelijn.common import HttpException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION, DEVICE_CLASS_TIMESTAMP +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, DEVICE_CLASS_TIMESTAMP from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -17,7 +17,6 @@ ATTRIBUTION = "Data provided by data.delijn.be" CONF_NEXT_DEPARTURE = "next_departure" CONF_STOP_ID = "stop_id" -CONF_API_KEY = "api_key" CONF_NUMBER_OF_DEPARTURES = "number_of_departures" DEFAULT_NAME = "De Lijn" From 2b17ba1dc49343f904b74eb295c14783947ac879 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Fri, 5 Feb 2021 04:29:59 -0500 Subject: [PATCH 201/796] User core constants for deutsche_bahn (#46028) --- homeassistant/components/deutsche_bahn/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/deutsche_bahn/sensor.py b/homeassistant/components/deutsche_bahn/sensor.py index 01752f0373f..b3e4cd432ac 100644 --- a/homeassistant/components/deutsche_bahn/sensor.py +++ b/homeassistant/components/deutsche_bahn/sensor.py @@ -5,13 +5,13 @@ import schiene import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_OFFSET import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util CONF_DESTINATION = "to" CONF_START = "from" -CONF_OFFSET = "offset" DEFAULT_OFFSET = timedelta(minutes=0) CONF_ONLY_DIRECT = "only_direct" DEFAULT_ONLY_DIRECT = False @@ -87,7 +87,6 @@ class SchieneData: def __init__(self, start, goal, offset, only_direct): """Initialize the sensor.""" - self.start = start self.goal = goal self.offset = offset From 92886cafe94735819591f73838da410b9108edec Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Fri, 5 Feb 2021 02:48:47 -0700 Subject: [PATCH 202/796] Fix zwave_js cover control for Up/Down and Open/Close (#45965) * Fix issue with control of cover when the target value is Up/Down instead of Open/Close * Adjust open/close/stop cover control to account for no Open/Up or Close/Down targets * Revert back to using values of 0/99 to close/open a cover since it is supported by all covers * Replace RELEASE_BUTTON with False and remove unused PRESS_BUTTON in zwave_js cover --- homeassistant/components/zwave_js/cover.py | 20 ++++++++--------- tests/components/zwave_js/test_cover.py | 26 +++++++++++++--------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index 5f473f80957..b86cbeba944 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -21,8 +21,6 @@ from .entity import ZWaveBaseEntity LOGGER = logging.getLogger(__name__) SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE -PRESS_BUTTON = True -RELEASE_BUTTON = False async def async_setup_entry( @@ -79,17 +77,19 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" - target_value = self.get_zwave_value("Open") - await self.info.node.async_set_value(target_value, PRESS_BUTTON) + target_value = self.get_zwave_value("targetValue") + await self.info.node.async_set_value(target_value, 99) async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" - target_value = self.get_zwave_value("Close") - await self.info.node.async_set_value(target_value, PRESS_BUTTON) + target_value = self.get_zwave_value("targetValue") + await self.info.node.async_set_value(target_value, 0) async def async_stop_cover(self, **kwargs: Any) -> None: """Stop cover.""" - target_value = self.get_zwave_value("Open") - await self.info.node.async_set_value(target_value, RELEASE_BUTTON) - target_value = self.get_zwave_value("Close") - await self.info.node.async_set_value(target_value, RELEASE_BUTTON) + target_value = self.get_zwave_value("Open") or self.get_zwave_value("Up") + if target_value: + await self.info.node.async_set_value(target_value, False) + target_value = self.get_zwave_value("Close") or self.get_zwave_value("Down") + if target_value: + await self.info.node.async_set_value(target_value, False) diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index f014245a5f8..52e0a444ec9 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -95,14 +95,16 @@ async def test_cover(hass, client, chain_actuator_zws12, integration): "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, - "property": "Open", - "propertyName": "Open", + "property": "targetValue", + "propertyName": "targetValue", "metadata": { - "type": "boolean", + "label": "Target value", + "max": 99, + "min": 0, + "type": "number", "readable": True, "writeable": True, - "label": "Perform a level change (Open)", - "ccSpecific": {"switchType": 3}, + "label": "Target value", }, } assert args["value"] @@ -194,17 +196,19 @@ async def test_cover(hass, client, chain_actuator_zws12, integration): "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, - "property": "Close", - "propertyName": "Close", + "property": "targetValue", + "propertyName": "targetValue", "metadata": { - "type": "boolean", + "label": "Target value", + "max": 99, + "min": 0, + "type": "number", "readable": True, "writeable": True, - "label": "Perform a level change (Close)", - "ccSpecific": {"switchType": 3}, + "label": "Target value", }, } - assert args["value"] + assert args["value"] == 0 client.async_send_command.reset_mock() From 6404f91d0090404c8169cf83c77bbd935a60236e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 5 Feb 2021 02:51:55 -0700 Subject: [PATCH 203/796] Standardize AirVisual helper method in config flow (#45999) * Standardize AirVisual helper method in config flow * Code review --- .../components/airvisual/config_flow.py | 84 +++++++++---------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index 266f7b7c2c2..12dec114349 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -84,47 +84,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } ) - async def _async_set_unique_id(self, unique_id): - """Set the unique ID of the config flow and abort if it already exists.""" - await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured() - - @staticmethod - @callback - def async_get_options_flow(config_entry): - """Define the config flow to handle options.""" - return AirVisualOptionsFlowHandler(config_entry) - - async def async_step_geography(self, user_input, integration_type): - """Handle the initialization of the integration via the cloud API.""" - self._geo_id = async_get_geography_id(user_input) - await self._async_set_unique_id(self._geo_id) - self._abort_if_unique_id_configured() - return await self.async_step_geography_finish(user_input, integration_type) - - async def async_step_geography_by_coords(self, user_input=None): - """Handle the initialization of the cloud API based on latitude/longitude.""" - if not user_input: - return self.async_show_form( - step_id="geography_by_coords", data_schema=self.geography_coords_schema - ) - - return await self.async_step_geography( - user_input, INTEGRATION_TYPE_GEOGRAPHY_COORDS - ) - - async def async_step_geography_by_name(self, user_input=None): - """Handle the initialization of the cloud API based on city/state/country.""" - if not user_input: - return self.async_show_form( - step_id="geography_by_name", data_schema=GEOGRAPHY_NAME_SCHEMA - ) - - return await self.async_step_geography( - user_input, INTEGRATION_TYPE_GEOGRAPHY_NAME - ) - - async def async_step_geography_finish(self, user_input, integration_type): + async def _async_finish_geography(self, user_input, integration_type): """Validate a Cloud API key.""" websession = aiohttp_client.async_get_clientsession(self.hass) cloud_api = CloudAPI(user_input[CONF_API_KEY], session=websession) @@ -183,6 +143,46 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data={**user_input, CONF_INTEGRATION_TYPE: integration_type}, ) + async def _async_init_geography(self, user_input, integration_type): + """Handle the initialization of the integration via the cloud API.""" + self._geo_id = async_get_geography_id(user_input) + await self._async_set_unique_id(self._geo_id) + self._abort_if_unique_id_configured() + return await self._async_finish_geography(user_input, integration_type) + + async def _async_set_unique_id(self, unique_id): + """Set the unique ID of the config flow and abort if it already exists.""" + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Define the config flow to handle options.""" + return AirVisualOptionsFlowHandler(config_entry) + + async def async_step_geography_by_coords(self, user_input=None): + """Handle the initialization of the cloud API based on latitude/longitude.""" + if not user_input: + return self.async_show_form( + step_id="geography_by_coords", data_schema=self.geography_coords_schema + ) + + return await self._async_init_geography( + user_input, INTEGRATION_TYPE_GEOGRAPHY_COORDS + ) + + async def async_step_geography_by_name(self, user_input=None): + """Handle the initialization of the cloud API based on city/state/country.""" + if not user_input: + return self.async_show_form( + step_id="geography_by_name", data_schema=GEOGRAPHY_NAME_SCHEMA + ) + + return await self._async_init_geography( + user_input, INTEGRATION_TYPE_GEOGRAPHY_NAME + ) + async def async_step_node_pro(self, user_input=None): """Handle the initialization of the integration with a Node/Pro.""" if not user_input: @@ -224,7 +224,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): conf = {CONF_API_KEY: user_input[CONF_API_KEY], **self._entry_data_for_reauth} - return await self.async_step_geography_finish( + return await self._async_finish_geography( conf, self._entry_data_for_reauth[CONF_INTEGRATION_TYPE] ) From 725dcb5cacd24059122eb71c5d73373c4745716a Mon Sep 17 00:00:00 2001 From: tkdrob Date: Fri, 5 Feb 2021 06:17:46 -0500 Subject: [PATCH 204/796] Use core constants for doods (#46043) --- homeassistant/components/doods/image_processing.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index f4180ffcffa..fb2b6daecda 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -16,7 +16,7 @@ from homeassistant.components.image_processing import ( PLATFORM_SCHEMA, ImageProcessingEntity, ) -from homeassistant.const import CONF_TIMEOUT +from homeassistant.const import CONF_COVERS, CONF_TIMEOUT, CONF_URL from homeassistant.core import split_entity_id from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv @@ -29,12 +29,10 @@ ATTR_SUMMARY = "summary" ATTR_TOTAL_MATCHES = "total_matches" ATTR_PROCESS_TIME = "process_time" -CONF_URL = "url" CONF_AUTH_KEY = "auth_key" CONF_DETECTOR = "detector" CONF_LABELS = "labels" CONF_AREA = "area" -CONF_COVERS = "covers" CONF_TOP = "top" CONF_BOTTOM = "bottom" CONF_RIGHT = "right" From 2a0c36589f7c57de86336b8d5794812168e11fb4 Mon Sep 17 00:00:00 2001 From: Yuval Aboulafia Date: Fri, 5 Feb 2021 13:41:36 +0200 Subject: [PATCH 205/796] Centralize some Airly constants (#45985) --- homeassistant/components/airly/__init__.py | 2 +- homeassistant/components/airly/air_quality.py | 5 ++--- homeassistant/components/airly/const.py | 7 +++++++ homeassistant/components/airly/sensor.py | 10 ++++------ 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index 9d6b46f82e5..6a9c23624f0 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -1,4 +1,4 @@ -"""The Airly component.""" +"""The Airly integration.""" import asyncio from datetime import timedelta import logging diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py index e43a76b3418..4c4c239a84b 100644 --- a/homeassistant/components/airly/air_quality.py +++ b/homeassistant/components/airly/air_quality.py @@ -19,14 +19,13 @@ from .const import ( ATTR_API_PM25, ATTR_API_PM25_LIMIT, ATTR_API_PM25_PERCENT, + ATTRIBUTION, DEFAULT_NAME, DOMAIN, + LABEL_ADVICE, MANUFACTURER, ) -ATTRIBUTION = "Data provided by Airly" - -LABEL_ADVICE = "advice" LABEL_AQI_DESCRIPTION = f"{ATTR_AQI}_description" LABEL_AQI_LEVEL = f"{ATTR_AQI}_level" LABEL_PM_2_5_LIMIT = f"{ATTR_PM_2_5}_limit" diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index b4711b50dd2..b8d2270c3c4 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -1,4 +1,5 @@ """Constants for Airly integration.""" + ATTR_API_ADVICE = "ADVICE" ATTR_API_CAQI = "CAQI" ATTR_API_CAQI_DESCRIPTION = "DESCRIPTION" @@ -13,9 +14,15 @@ ATTR_API_PM25_LIMIT = "PM25_LIMIT" ATTR_API_PM25_PERCENT = "PM25_PERCENT" ATTR_API_PRESSURE = "PRESSURE" ATTR_API_TEMPERATURE = "TEMPERATURE" + +ATTR_LABEL = "label" +ATTR_UNIT = "unit" + +ATTRIBUTION = "Data provided by Airly" CONF_USE_NEAREST = "use_nearest" DEFAULT_NAME = "Airly" DOMAIN = "airly" +LABEL_ADVICE = "advice" MANUFACTURER = "Airly sp. z o.o." MAX_REQUESTS_PER_DAY = 100 NO_AIRLY_SENSORS = "There are no Airly sensors in this area yet." diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 420d11a5963..789dbbb4657 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -2,6 +2,7 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, + ATTR_ICON, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONF_NAME, DEVICE_CLASS_HUMIDITY, @@ -18,17 +19,14 @@ from .const import ( ATTR_API_PM1, ATTR_API_PRESSURE, ATTR_API_TEMPERATURE, + ATTR_LABEL, + ATTR_UNIT, + ATTRIBUTION, DEFAULT_NAME, DOMAIN, MANUFACTURER, ) -ATTRIBUTION = "Data provided by Airly" - -ATTR_ICON = "icon" -ATTR_LABEL = "label" -ATTR_UNIT = "unit" - PARALLEL_UPDATES = 1 SENSOR_TYPES = { From e01ca40d5603c0d671a9c5cce58d2fa2ea12b7ff Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Fri, 5 Feb 2021 12:20:15 +0000 Subject: [PATCH 206/796] Force Vera refresh after starting subscription (#46001) * Force vera refresh after starting subscription. * Request refresh on device load. * Update init test. --- homeassistant/components/vera/binary_sensor.py | 3 ++- homeassistant/components/vera/climate.py | 3 ++- homeassistant/components/vera/cover.py | 3 ++- homeassistant/components/vera/light.py | 3 ++- homeassistant/components/vera/lock.py | 3 ++- homeassistant/components/vera/scene.py | 2 +- homeassistant/components/vera/sensor.py | 3 ++- homeassistant/components/vera/switch.py | 3 ++- tests/components/vera/test_init.py | 5 +++++ 9 files changed, 20 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/vera/binary_sensor.py b/homeassistant/components/vera/binary_sensor.py index a84764209b2..7932fa14f4c 100644 --- a/homeassistant/components/vera/binary_sensor.py +++ b/homeassistant/components/vera/binary_sensor.py @@ -27,7 +27,8 @@ async def async_setup_entry( [ VeraBinarySensor(device, controller_data) for device in controller_data.devices.get(PLATFORM_DOMAIN) - ] + ], + True, ) diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index 70694af012f..9abfc485268 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -44,7 +44,8 @@ async def async_setup_entry( [ VeraThermostat(device, controller_data) for device in controller_data.devices.get(PLATFORM_DOMAIN) - ] + ], + True, ) diff --git a/homeassistant/components/vera/cover.py b/homeassistant/components/vera/cover.py index 69e412bdade..43f68fba786 100644 --- a/homeassistant/components/vera/cover.py +++ b/homeassistant/components/vera/cover.py @@ -28,7 +28,8 @@ async def async_setup_entry( [ VeraCover(device, controller_data) for device in controller_data.devices.get(PLATFORM_DOMAIN) - ] + ], + True, ) diff --git a/homeassistant/components/vera/light.py b/homeassistant/components/vera/light.py index 7daaf095a5c..c52627d340f 100644 --- a/homeassistant/components/vera/light.py +++ b/homeassistant/components/vera/light.py @@ -32,7 +32,8 @@ async def async_setup_entry( [ VeraLight(device, controller_data) for device in controller_data.devices.get(PLATFORM_DOMAIN) - ] + ], + True, ) diff --git a/homeassistant/components/vera/lock.py b/homeassistant/components/vera/lock.py index 36a5f4cf2f3..b77f17d3b0a 100644 --- a/homeassistant/components/vera/lock.py +++ b/homeassistant/components/vera/lock.py @@ -31,7 +31,8 @@ async def async_setup_entry( [ VeraLock(device, controller_data) for device in controller_data.devices.get(PLATFORM_DOMAIN) - ] + ], + True, ) diff --git a/homeassistant/components/vera/scene.py b/homeassistant/components/vera/scene.py index a031aadb66c..2274b67f683 100644 --- a/homeassistant/components/vera/scene.py +++ b/homeassistant/components/vera/scene.py @@ -21,7 +21,7 @@ async def async_setup_entry( """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) async_add_entities( - [VeraScene(device, controller_data) for device in controller_data.scenes] + [VeraScene(device, controller_data) for device in controller_data.scenes], True ) diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py index e39b752bbf3..ea7dbf0ae30 100644 --- a/homeassistant/components/vera/sensor.py +++ b/homeassistant/components/vera/sensor.py @@ -28,7 +28,8 @@ async def async_setup_entry( [ VeraSensor(device, controller_data) for device in controller_data.devices.get(PLATFORM_DOMAIN) - ] + ], + True, ) diff --git a/homeassistant/components/vera/switch.py b/homeassistant/components/vera/switch.py index d0cbeba669c..5dfeba6f5b2 100644 --- a/homeassistant/components/vera/switch.py +++ b/homeassistant/components/vera/switch.py @@ -28,7 +28,8 @@ async def async_setup_entry( [ VeraSwitch(device, controller_data) for device in controller_data.devices.get(PLATFORM_DOMAIN) - ] + ], + True, ) diff --git a/tests/components/vera/test_init.py b/tests/components/vera/test_init.py index b1d6010336a..33b6843d7e5 100644 --- a/tests/components/vera/test_init.py +++ b/tests/components/vera/test_init.py @@ -206,6 +206,7 @@ async def test_exclude_and_light_ids( vera_device3.name = "dev3" vera_device3.category = pv.CATEGORY_SWITCH vera_device3.is_switched_on = MagicMock(return_value=False) + entity_id3 = "switch.dev3_3" vera_device4 = MagicMock(spec=pv.VeraSwitch) # type: pv.VeraSwitch @@ -214,6 +215,10 @@ async def test_exclude_and_light_ids( vera_device4.name = "dev4" vera_device4.category = pv.CATEGORY_SWITCH vera_device4.is_switched_on = MagicMock(return_value=False) + vera_device4.get_brightness = MagicMock(return_value=0) + vera_device4.get_color = MagicMock(return_value=[0, 0, 0]) + vera_device4.is_dimmable = True + entity_id4 = "light.dev4_4" component_data = await vera_component_factory.configure_component( From 9a570d7c3215d1196a8a80f4148d4c19bbdde8ec Mon Sep 17 00:00:00 2001 From: tkdrob Date: Fri, 5 Feb 2021 08:02:28 -0500 Subject: [PATCH 207/796] Use core constants for bmw_connected_drive (#46042) --- homeassistant/components/bmw_connected_drive/__init__.py | 2 +- homeassistant/components/bmw_connected_drive/config_flow.py | 4 ++-- homeassistant/components/bmw_connected_drive/const.py | 1 - tests/components/bmw_connected_drive/test_config_flow.py | 3 +-- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index e9f6a0d7f6f..ac25636a066 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_NAME, CONF_PASSWORD, + CONF_REGION, CONF_USERNAME, ) from homeassistant.core import HomeAssistant, callback @@ -28,7 +29,6 @@ from .const import ( CONF_ACCOUNT, CONF_ALLOWED_REGIONS, CONF_READ_ONLY, - CONF_REGION, CONF_USE_LOCATION, DATA_ENTRIES, DATA_HASS_CONFIG, diff --git a/homeassistant/components/bmw_connected_drive/config_flow.py b/homeassistant/components/bmw_connected_drive/config_flow.py index a6081d5ccc1..8315a1322e2 100644 --- a/homeassistant/components/bmw_connected_drive/config_flow.py +++ b/homeassistant/components/bmw_connected_drive/config_flow.py @@ -6,11 +6,11 @@ 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.const import CONF_PASSWORD, CONF_REGION, 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 +from .const import CONF_ALLOWED_REGIONS, CONF_READ_ONLY, CONF_USE_LOCATION _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bmw_connected_drive/const.py b/homeassistant/components/bmw_connected_drive/const.py index 65dc7fde595..7af24496838 100644 --- a/homeassistant/components/bmw_connected_drive/const.py +++ b/homeassistant/components/bmw_connected_drive/const.py @@ -1,7 +1,6 @@ """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" diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index 52433f2f58f..d56978deb27 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -5,10 +5,9 @@ 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 homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME from tests.common import MockConfigEntry From 6fdb12c09d00cbc7eef243f0295e5d72d951d11f Mon Sep 17 00:00:00 2001 From: tkdrob Date: Fri, 5 Feb 2021 09:07:17 -0500 Subject: [PATCH 208/796] Use core constants for bluetooth_tracker (#46041) --- .../components/bluetooth_tracker/device_tracker.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index af49266bef4..380d8091bd6 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -20,6 +20,7 @@ from homeassistant.components.device_tracker.legacy import ( YAML_DEVICES, async_load_config, ) +from homeassistant.const import CONF_DEVICE_ID import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType @@ -32,8 +33,6 @@ BT_PREFIX = "BT_" CONF_REQUEST_RSSI = "request_rssi" -CONF_DEVICE_ID = "device_id" - DEFAULT_DEVICE_ID = -1 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -131,7 +130,6 @@ async def async_setup_scanner( async def perform_bluetooth_update(): """Discover Bluetooth devices and update status.""" - _LOGGER.debug("Performing Bluetooth devices discovery and update") tasks = [] @@ -164,7 +162,6 @@ async def async_setup_scanner( async def update_bluetooth(now=None): """Lookup Bluetooth devices and update status.""" - # If an update is in progress, we don't do anything if update_bluetooth_lock.locked(): _LOGGER.debug( @@ -178,7 +175,6 @@ async def async_setup_scanner( async def handle_manual_update_bluetooth(call): """Update bluetooth devices on demand.""" - await update_bluetooth() hass.async_create_task(update_bluetooth()) From 7144c5f316c3008960f58c7beddb8006a2ac1425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Hansl=C3=ADk?= Date: Fri, 5 Feb 2021 15:08:28 +0100 Subject: [PATCH 209/796] Fix demo number entity (#45991) --- homeassistant/components/demo/number.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/demo/number.py b/homeassistant/components/demo/number.py index 5a6ce5f5c64..f3fd815f621 100644 --- a/homeassistant/components/demo/number.py +++ b/homeassistant/components/demo/number.py @@ -21,7 +21,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= DemoNumber( "pwm1", "PWM 1", - 42.0, + 0.42, "mdi:square-wave", False, 0.0, From ae2c7e4c742c9f83dd157a0999fe45c0bd552ae2 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 5 Feb 2021 16:31:47 +0100 Subject: [PATCH 210/796] Improve UniFi tests (#45871) --- tests/components/unifi/test_config_flow.py | 30 +-- tests/components/unifi/test_controller.py | 194 +++++++++++------- tests/components/unifi/test_device_tracker.py | 127 +++++++----- tests/components/unifi/test_init.py | 13 +- tests/components/unifi/test_sensor.py | 35 ++-- tests/components/unifi/test_switch.py | 190 +++++++++-------- 6 files changed, 315 insertions(+), 274 deletions(-) diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 790e204c1dd..302564dbbd5 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -191,7 +191,7 @@ async def test_flow_works_multiple_sites(hass, aioclient_mock): async def test_flow_raise_already_configured(hass, aioclient_mock): """Test config flow aborts since a connected config entry already exists.""" - await setup_unifi_integration(hass) + await setup_unifi_integration(hass, aioclient_mock) result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": "user"} @@ -200,6 +200,8 @@ async def test_flow_raise_already_configured(hass, aioclient_mock): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" + aioclient_mock.clear_requests() + aioclient_mock.get("https://1.2.3.4:1234", status=302) aioclient_mock.post( @@ -340,17 +342,19 @@ async def test_flow_fails_controller_unavailable(hass, aioclient_mock): async def test_reauth_flow_update_configuration(hass, aioclient_mock): """Verify reauth flow can update controller configuration.""" - controller = await setup_unifi_integration(hass) + config_entry = await setup_unifi_integration(hass, aioclient_mock) result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": SOURCE_REAUTH}, - data=controller.config_entry, + data=config_entry, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == SOURCE_USER + aioclient_mock.clear_requests() + aioclient_mock.get("https://1.2.3.4:1234", status=302) aioclient_mock.post( @@ -381,15 +385,16 @@ async def test_reauth_flow_update_configuration(hass, aioclient_mock): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "reauth_successful" - assert controller.host == "1.2.3.4" - assert controller.config_entry.data[CONF_CONTROLLER][CONF_USERNAME] == "new_name" - assert controller.config_entry.data[CONF_CONTROLLER][CONF_PASSWORD] == "new_pass" + assert config_entry.data[CONF_CONTROLLER][CONF_HOST] == "1.2.3.4" + assert config_entry.data[CONF_CONTROLLER][CONF_USERNAME] == "new_name" + assert config_entry.data[CONF_CONTROLLER][CONF_PASSWORD] == "new_pass" -async def test_advanced_option_flow(hass): +async def test_advanced_option_flow(hass, aioclient_mock): """Test advanced config flow options.""" - controller = await setup_unifi_integration( + config_entry = await setup_unifi_integration( hass, + aioclient_mock, clients_response=CLIENTS, devices_response=DEVICES, wlans_response=WLANS, @@ -398,7 +403,7 @@ async def test_advanced_option_flow(hass): ) result = await hass.config_entries.options.async_init( - controller.config_entry.entry_id, context={"show_advanced_options": True} + config_entry.entry_id, context={"show_advanced_options": True} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -457,10 +462,11 @@ async def test_advanced_option_flow(hass): } -async def test_simple_option_flow(hass): +async def test_simple_option_flow(hass, aioclient_mock): """Test simple config flow options.""" - controller = await setup_unifi_integration( + config_entry = await setup_unifi_integration( hass, + aioclient_mock, clients_response=CLIENTS, wlans_response=WLANS, dpigroup_response=DPI_GROUPS, @@ -468,7 +474,7 @@ async def test_simple_option_flow(hass): ) result = await hass.config_entries.options.async_init( - controller.config_entry.entry_id, context={"show_advanced_options": False} + config_entry.entry_id, context={"show_advanced_options": False} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index e484e041a88..077481a9320 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -1,5 +1,5 @@ """Test UniFi Controller.""" -from collections import deque + from copy import deepcopy from datetime import timedelta from unittest.mock import patch @@ -33,14 +33,18 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, CONF_VERIFY_SSL, + CONTENT_TYPE_JSON, ) from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry +DEFAULT_HOST = "1.2.3.4" +DEFAULT_SITE = "site_id" + CONTROLLER_HOST = { "hostname": "controller_host", - "ip": "1.2.3.4", + "ip": DEFAULT_HOST, "is_wired": True, "last_seen": 1562600145, "mac": "10:00:00:00:00:01", @@ -54,11 +58,11 @@ CONTROLLER_HOST = { } CONTROLLER_DATA = { - CONF_HOST: "1.2.3.4", + CONF_HOST: DEFAULT_HOST, CONF_USERNAME: "username", CONF_PASSWORD: "password", CONF_PORT: 1234, - CONF_SITE_ID: "site_id", + CONF_SITE_ID: DEFAULT_SITE, CONF_VERIFY_SSL: False, } @@ -67,22 +71,90 @@ ENTRY_OPTIONS = {} CONFIGURATION = [] -SITES = {"Site name": {"desc": "Site name", "name": "site_id", "role": "admin"}} +SITE = [{"desc": "Site name", "name": "site_id", "role": "admin"}] DESCRIPTION = [{"name": "username", "site_name": "site_id", "site_role": "admin"}] +def mock_default_unifi_requests( + aioclient_mock, + host, + site_id, + sites=None, + description=None, + clients_response=None, + clients_all_response=None, + devices_response=None, + dpiapp_response=None, + dpigroup_response=None, + wlans_response=None, +): + """Mock default UniFi requests responses.""" + aioclient_mock.get(f"https://{host}:1234", status=302) # Check UniFi OS + + aioclient_mock.post( + f"https://{host}:1234/api/login", + json={"data": "login successful", "meta": {"rc": "ok"}}, + headers={"content-type": CONTENT_TYPE_JSON}, + ) + + aioclient_mock.get( + f"https://{host}:1234/api/self/sites", + json={"data": sites or [], "meta": {"rc": "ok"}}, + headers={"content-type": CONTENT_TYPE_JSON}, + ) + + aioclient_mock.get( + f"https://{host}:1234/api/s/{site_id}/self", + json={"data": description or [], "meta": {"rc": "ok"}}, + headers={"content-type": CONTENT_TYPE_JSON}, + ) + + aioclient_mock.get( + f"https://{host}:1234/api/s/{site_id}/stat/sta", + json={"data": clients_response or [], "meta": {"rc": "ok"}}, + headers={"content-type": CONTENT_TYPE_JSON}, + ) + aioclient_mock.get( + f"https://{host}:1234/api/s/{site_id}/rest/user", + json={"data": clients_all_response or [], "meta": {"rc": "ok"}}, + headers={"content-type": CONTENT_TYPE_JSON}, + ) + aioclient_mock.get( + f"https://{host}:1234/api/s/{site_id}/stat/device", + json={"data": devices_response or [], "meta": {"rc": "ok"}}, + headers={"content-type": CONTENT_TYPE_JSON}, + ) + aioclient_mock.get( + f"https://{host}:1234/api/s/{site_id}/rest/dpiapp", + json={"data": dpiapp_response or [], "meta": {"rc": "ok"}}, + headers={"content-type": CONTENT_TYPE_JSON}, + ) + aioclient_mock.get( + f"https://{host}:1234/api/s/{site_id}/rest/dpigroup", + json={"data": dpigroup_response or [], "meta": {"rc": "ok"}}, + headers={"content-type": CONTENT_TYPE_JSON}, + ) + aioclient_mock.get( + f"https://{host}:1234/api/s/{site_id}/rest/wlanconf", + json={"data": wlans_response or [], "meta": {"rc": "ok"}}, + headers={"content-type": CONTENT_TYPE_JSON}, + ) + + async def setup_unifi_integration( hass, + aioclient_mock=None, + *, config=ENTRY_CONFIG, options=ENTRY_OPTIONS, - sites=SITES, + sites=SITE, site_description=DESCRIPTION, clients_response=None, - devices_response=None, clients_all_response=None, - wlans_response=None, - dpigroup_response=None, + devices_response=None, dpiapp_response=None, + dpigroup_response=None, + wlans_response=None, known_wireless_clients=None, controllers=None, ): @@ -102,82 +174,39 @@ async def setup_unifi_integration( known_wireless_clients, config_entry ) - mock_client_responses = deque() - if clients_response: - mock_client_responses.append(clients_response) + if aioclient_mock: + mock_default_unifi_requests( + aioclient_mock, + host=config_entry.data[CONF_CONTROLLER][CONF_HOST], + site_id=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID], + sites=sites, + description=site_description, + clients_response=clients_response, + clients_all_response=clients_all_response, + devices_response=devices_response, + dpiapp_response=dpiapp_response, + dpigroup_response=dpigroup_response, + wlans_response=wlans_response, + ) - mock_device_responses = deque() - if devices_response: - mock_device_responses.append(devices_response) - - mock_client_all_responses = deque() - if clients_all_response: - mock_client_all_responses.append(clients_all_response) - - mock_wlans_responses = deque() - if wlans_response: - mock_wlans_responses.append(wlans_response) - - mock_dpigroup_responses = deque() - if dpigroup_response: - mock_dpigroup_responses.append(dpigroup_response) - - mock_dpiapp_responses = deque() - if dpiapp_response: - mock_dpiapp_responses.append(dpiapp_response) - - mock_requests = [] - - async def mock_request(self, method, path, json=None): - mock_requests.append({"method": method, "path": path, "json": json}) - - if path == "/stat/sta" and mock_client_responses: - return mock_client_responses.popleft() - if path == "/stat/device" and mock_device_responses: - return mock_device_responses.popleft() - if path == "/rest/user" and mock_client_all_responses: - return mock_client_all_responses.popleft() - if path == "/rest/wlanconf" and mock_wlans_responses: - return mock_wlans_responses.popleft() - if path == "/rest/dpigroup" and mock_dpigroup_responses: - return mock_dpigroup_responses.popleft() - if path == "/rest/dpiapp" and mock_dpiapp_responses: - return mock_dpiapp_responses.popleft() - return {} - - with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( - "aiounifi.Controller.login", - return_value=True, - ), patch("aiounifi.Controller.sites", return_value=sites), patch( - "aiounifi.Controller.site_description", return_value=site_description - ), patch( - "aiounifi.Controller.request", new=mock_request - ), patch.object( - aiounifi.websocket.WSClient, "start", return_value=True - ): + with patch.object(aiounifi.websocket.WSClient, "start", return_value=True): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() if config_entry.entry_id not in hass.data[UNIFI_DOMAIN]: return None - controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - controller.mock_client_responses = mock_client_responses - controller.mock_device_responses = mock_device_responses - controller.mock_client_all_responses = mock_client_all_responses - controller.mock_wlans_responses = mock_wlans_responses - controller.mock_requests = mock_requests - - return controller + return config_entry -async def test_controller_setup(hass): +async def test_controller_setup(hass, aioclient_mock): """Successful setup.""" with patch( "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", return_value=True, ) as forward_entry_setup: - controller = await setup_unifi_integration(hass) + config_entry = await setup_unifi_integration(hass, aioclient_mock) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] entry = controller.config_entry assert len(forward_entry_setup.mock_calls) == len(SUPPORTED_PLATFORMS) @@ -187,8 +216,8 @@ async def test_controller_setup(hass): assert controller.host == CONTROLLER_DATA[CONF_HOST] assert controller.site == CONTROLLER_DATA[CONF_SITE_ID] - assert controller.site_name in SITES - assert controller.site_role == SITES[controller.site_name]["role"] + assert controller.site_name == SITE[0]["desc"] + assert controller.site_role == SITE[0]["role"] assert controller.option_allow_bandwidth_sensors == DEFAULT_ALLOW_BANDWIDTH_SENSORS assert controller.option_allow_uptime_sensors == DEFAULT_ALLOW_UPTIME_SENSORS @@ -208,9 +237,12 @@ async def test_controller_setup(hass): assert controller.signal_heartbeat_missed == "unifi-heartbeat-missed" -async def test_controller_mac(hass): +async def test_controller_mac(hass, aioclient_mock): """Test that it is possible to identify controller mac.""" - controller = await setup_unifi_integration(hass, clients_response=[CONTROLLER_HOST]) + config_entry = await setup_unifi_integration( + hass, aioclient_mock, clients_response=[CONTROLLER_HOST] + ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert controller.mac == CONTROLLER_HOST["mac"] @@ -245,9 +277,10 @@ async def test_controller_unknown_error(hass): assert hass.data[UNIFI_DOMAIN] == {} -async def test_reset_after_successful_setup(hass): +async def test_reset_after_successful_setup(hass, aioclient_mock): """Calling reset when the entry has been setup.""" - controller = await setup_unifi_integration(hass) + config_entry = await setup_unifi_integration(hass, aioclient_mock) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert len(controller.listeners) == 6 @@ -258,9 +291,12 @@ async def test_reset_after_successful_setup(hass): assert len(controller.listeners) == 0 -async def test_wireless_client_event_calls_update_wireless_devices(hass): +async def test_wireless_client_event_calls_update_wireless_devices( + hass, aioclient_mock +): """Call update_wireless_devices method when receiving wireless client event.""" - controller = await setup_unifi_integration(hass) + config_entry = await setup_unifi_integration(hass, aioclient_mock) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] with patch( "homeassistant.components.unifi.controller.UniFiController.update_wireless_clients", diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 6462fcba943..39465a34aef 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -25,7 +25,6 @@ from homeassistant.components.unifi.const import ( ) from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import entity_registry -from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from .test_controller import ENTRY_CONFIG, setup_unifi_integration @@ -151,27 +150,19 @@ EVENT_DEVICE_2_UPGRADED = { } -async def test_platform_manually_configured(hass): - """Test that nothing happens when configuring unifi through device tracker platform.""" - assert ( - await async_setup_component( - hass, TRACKER_DOMAIN, {TRACKER_DOMAIN: {"platform": UNIFI_DOMAIN}} - ) - is False - ) - assert UNIFI_DOMAIN not in hass.data - - -async def test_no_clients(hass): +async def test_no_clients(hass, aioclient_mock): """Test the update_clients function when no clients are found.""" - await setup_unifi_integration(hass) + await setup_unifi_integration(hass, aioclient_mock) assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 0 -async def test_tracked_wireless_clients(hass): +async def test_tracked_wireless_clients(hass, aioclient_mock): """Test the update_items function with some clients.""" - controller = await setup_unifi_integration(hass, clients_response=[CLIENT_1]) + config_entry = await setup_unifi_integration( + hass, aioclient_mock, clients_response=[CLIENT_1] + ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 client_1 = hass.states.get("device_tracker.client_1") @@ -224,17 +215,19 @@ async def test_tracked_wireless_clients(hass): assert client_1.state == "home" -async def test_tracked_clients(hass): +async def test_tracked_clients(hass, aioclient_mock): """Test the update_items function with some clients.""" client_4_copy = copy(CLIENT_4) client_4_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - controller = await setup_unifi_integration( + config_entry = await setup_unifi_integration( hass, + aioclient_mock, options={CONF_SSID_FILTER: ["ssid"]}, clients_response=[CLIENT_1, CLIENT_2, CLIENT_3, CLIENT_5, client_4_copy], known_wireless_clients=(CLIENT_4["mac"],), ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 4 client_1 = hass.states.get("device_tracker.client_1") @@ -269,12 +262,14 @@ async def test_tracked_clients(hass): assert client_1.state == "home" -async def test_tracked_devices(hass): +async def test_tracked_devices(hass, aioclient_mock): """Test the update_items function with some devices.""" - controller = await setup_unifi_integration( + config_entry = await setup_unifi_integration( hass, + aioclient_mock, devices_response=[DEVICE_1, DEVICE_2], ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 device_1 = hass.states.get("device_tracker.device_1") @@ -338,11 +333,12 @@ async def test_tracked_devices(hass): assert device.sw_version == EVENT_DEVICE_2_UPGRADED["version_to"] -async def test_remove_clients(hass): +async def test_remove_clients(hass, aioclient_mock): """Test the remove_items function with some clients.""" - controller = await setup_unifi_integration( - hass, clients_response=[CLIENT_1, CLIENT_2] + config_entry = await setup_unifi_integration( + hass, aioclient_mock, clients_response=[CLIENT_1, CLIENT_2] ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 client_1 = hass.states.get("device_tracker.client_1") @@ -367,13 +363,15 @@ async def test_remove_clients(hass): assert wired_client is not None -async def test_controller_state_change(hass): +async def test_controller_state_change(hass, aioclient_mock): """Verify entities state reflect on controller becoming unavailable.""" - controller = await setup_unifi_integration( + config_entry = await setup_unifi_integration( hass, + aioclient_mock, clients_response=[CLIENT_1], devices_response=[DEVICE_1], ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 client_1 = hass.states.get("device_tracker.client_1") @@ -405,10 +403,11 @@ async def test_controller_state_change(hass): assert device_1.state == "home" -async def test_option_track_clients(hass): +async def test_option_track_clients(hass, aioclient_mock): """Test the tracking of clients can be turned off.""" - controller = await setup_unifi_integration( + config_entry = await setup_unifi_integration( hass, + aioclient_mock, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1], ) @@ -424,7 +423,7 @@ async def test_option_track_clients(hass): assert device_1 is not None hass.config_entries.async_update_entry( - controller.config_entry, + config_entry, options={CONF_TRACK_CLIENTS: False}, ) await hass.async_block_till_done() @@ -439,7 +438,7 @@ async def test_option_track_clients(hass): assert device_1 is not None hass.config_entries.async_update_entry( - controller.config_entry, + config_entry, options={CONF_TRACK_CLIENTS: True}, ) await hass.async_block_till_done() @@ -454,10 +453,11 @@ async def test_option_track_clients(hass): assert device_1 is not None -async def test_option_track_wired_clients(hass): +async def test_option_track_wired_clients(hass, aioclient_mock): """Test the tracking of wired clients can be turned off.""" - controller = await setup_unifi_integration( + config_entry = await setup_unifi_integration( hass, + aioclient_mock, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1], ) @@ -473,7 +473,7 @@ async def test_option_track_wired_clients(hass): assert device_1 is not None hass.config_entries.async_update_entry( - controller.config_entry, + config_entry, options={CONF_TRACK_WIRED_CLIENTS: False}, ) await hass.async_block_till_done() @@ -488,7 +488,7 @@ async def test_option_track_wired_clients(hass): assert device_1 is not None hass.config_entries.async_update_entry( - controller.config_entry, + config_entry, options={CONF_TRACK_WIRED_CLIENTS: True}, ) await hass.async_block_till_done() @@ -503,10 +503,11 @@ async def test_option_track_wired_clients(hass): assert device_1 is not None -async def test_option_track_devices(hass): +async def test_option_track_devices(hass, aioclient_mock): """Test the tracking of devices can be turned off.""" - controller = await setup_unifi_integration( + config_entry = await setup_unifi_integration( hass, + aioclient_mock, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1], ) @@ -522,7 +523,7 @@ async def test_option_track_devices(hass): assert device_1 is not None hass.config_entries.async_update_entry( - controller.config_entry, + config_entry, options={CONF_TRACK_DEVICES: False}, ) await hass.async_block_till_done() @@ -537,7 +538,7 @@ async def test_option_track_devices(hass): assert device_1 is None hass.config_entries.async_update_entry( - controller.config_entry, + config_entry, options={CONF_TRACK_DEVICES: True}, ) await hass.async_block_till_done() @@ -552,7 +553,7 @@ async def test_option_track_devices(hass): assert device_1 is not None -async def test_option_ssid_filter(hass): +async def test_option_ssid_filter(hass, aioclient_mock): """Test the SSID filter works. Client 1 will travel from a supported SSID to an unsupported ssid. @@ -561,9 +562,10 @@ async def test_option_ssid_filter(hass): client_1_copy = copy(CLIENT_1) client_1_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - controller = await setup_unifi_integration( - hass, clients_response=[client_1_copy, CLIENT_3] + config_entry = await setup_unifi_integration( + hass, aioclient_mock, clients_response=[client_1_copy, CLIENT_3] ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 client_1 = hass.states.get("device_tracker.client_1") @@ -574,7 +576,7 @@ async def test_option_ssid_filter(hass): # Setting SSID filter will remove clients outside of filter hass.config_entries.async_update_entry( - controller.config_entry, + config_entry, options={CONF_SSID_FILTER: ["ssid"]}, ) await hass.async_block_till_done() @@ -609,7 +611,7 @@ async def test_option_ssid_filter(hass): # Remove SSID filter hass.config_entries.async_update_entry( - controller.config_entry, + config_entry, options={CONF_SSID_FILTER: []}, ) await hass.async_block_till_done() @@ -656,7 +658,7 @@ async def test_option_ssid_filter(hass): assert client_3.state == "not_home" -async def test_wireless_client_go_wired_issue(hass): +async def test_wireless_client_go_wired_issue(hass, aioclient_mock): """Test the solution to catch wireless device go wired UniFi issue. UniFi has a known issue that when a wireless device goes away it sometimes gets marked as wired. @@ -664,7 +666,10 @@ async def test_wireless_client_go_wired_issue(hass): client_1_client = copy(CLIENT_1) client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - controller = await setup_unifi_integration(hass, clients_response=[client_1_client]) + config_entry = await setup_unifi_integration( + hass, aioclient_mock, clients_response=[client_1_client] + ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 # Client is wireless @@ -717,14 +722,18 @@ async def test_wireless_client_go_wired_issue(hass): assert client_1.attributes["is_wired"] is False -async def test_option_ignore_wired_bug(hass): +async def test_option_ignore_wired_bug(hass, aioclient_mock): """Test option to ignore wired bug.""" client_1_client = copy(CLIENT_1) client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - controller = await setup_unifi_integration( - hass, options={CONF_IGNORE_WIRED_BUG: True}, clients_response=[client_1_client] + config_entry = await setup_unifi_integration( + hass, + aioclient_mock, + options={CONF_IGNORE_WIRED_BUG: True}, + clients_response=[client_1_client], ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 # Client is wireless @@ -777,7 +786,7 @@ async def test_option_ignore_wired_bug(hass): assert client_1.attributes["is_wired"] is False -async def test_restoring_client(hass): +async def test_restoring_client(hass, aioclient_mock): """Test the update_items function with some clients.""" config_entry = config_entries.ConfigEntry( version=1, @@ -809,6 +818,7 @@ async def test_restoring_client(hass): await setup_unifi_integration( hass, + aioclient_mock, options={CONF_BLOCK_CLIENT: True}, clients_response=[CLIENT_2], clients_all_response=[CLIENT_1], @@ -819,10 +829,11 @@ async def test_restoring_client(hass): assert device_1 is not None -async def test_dont_track_clients(hass): +async def test_dont_track_clients(hass, aioclient_mock): """Test don't track clients config works.""" - controller = await setup_unifi_integration( + config_entry = await setup_unifi_integration( hass, + aioclient_mock, options={CONF_TRACK_CLIENTS: False}, clients_response=[CLIENT_1], devices_response=[DEVICE_1], @@ -836,7 +847,7 @@ async def test_dont_track_clients(hass): assert device_1 is not None hass.config_entries.async_update_entry( - controller.config_entry, + config_entry, options={CONF_TRACK_CLIENTS: True}, ) await hass.async_block_till_done() @@ -850,10 +861,11 @@ async def test_dont_track_clients(hass): assert device_1 is not None -async def test_dont_track_devices(hass): +async def test_dont_track_devices(hass, aioclient_mock): """Test don't track devices config works.""" - controller = await setup_unifi_integration( + config_entry = await setup_unifi_integration( hass, + aioclient_mock, options={CONF_TRACK_DEVICES: False}, clients_response=[CLIENT_1], devices_response=[DEVICE_1], @@ -867,7 +879,7 @@ async def test_dont_track_devices(hass): assert device_1 is None hass.config_entries.async_update_entry( - controller.config_entry, + config_entry, options={CONF_TRACK_DEVICES: True}, ) await hass.async_block_till_done() @@ -881,10 +893,11 @@ async def test_dont_track_devices(hass): assert device_1 is not None -async def test_dont_track_wired_clients(hass): +async def test_dont_track_wired_clients(hass, aioclient_mock): """Test don't track wired clients config works.""" - controller = await setup_unifi_integration( + config_entry = await setup_unifi_integration( hass, + aioclient_mock, options={CONF_TRACK_WIRED_CLIENTS: False}, clients_response=[CLIENT_1, CLIENT_2], ) @@ -897,7 +910,7 @@ async def test_dont_track_wired_clients(hass): assert client_2 is None hass.config_entries.async_update_entry( - controller.config_entry, + config_entry, options={CONF_TRACK_WIRED_CLIENTS: True}, ) await hass.async_block_till_done() diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index cc2a4b3e4a3..841e9ec7576 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -11,14 +11,14 @@ from tests.common import MockConfigEntry, mock_coro async def test_setup_with_no_config(hass): - """Test that we do not discover anything or try to set up a bridge.""" + """Test that we do not discover anything or try to set up a controller.""" assert await async_setup_component(hass, UNIFI_DOMAIN, {}) is True assert UNIFI_DOMAIN not in hass.data -async def test_successful_config_entry(hass): +async def test_successful_config_entry(hass, aioclient_mock): """Test that configured options for a host are loaded via config entry.""" - await setup_unifi_integration(hass) + await setup_unifi_integration(hass, aioclient_mock) assert hass.data[UNIFI_DOMAIN] @@ -44,7 +44,6 @@ async def test_controller_no_mac(hass): "site": "default", "verify_ssl": True, }, - "poe_control": True, }, ) entry.add_to_hass(hass) @@ -64,10 +63,10 @@ async def test_controller_no_mac(hass): assert len(mock_registry.mock_calls) == 0 -async def test_unload_entry(hass): +async def test_unload_entry(hass, aioclient_mock): """Test being able to unload an entry.""" - controller = await setup_unifi_integration(hass) + config_entry = await setup_unifi_integration(hass, aioclient_mock) assert hass.data[UNIFI_DOMAIN] - assert await unifi.async_unload_entry(hass, controller.config_entry) + assert await unifi.async_unload_entry(hass, config_entry) assert not hass.data[UNIFI_DOMAIN] diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index dc2fea634c9..c668bf3789f 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -14,7 +14,6 @@ from homeassistant.components.unifi.const import ( DOMAIN as UNIFI_DOMAIN, ) from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.setup import async_setup_component from .test_controller import setup_unifi_integration @@ -50,35 +49,25 @@ CLIENTS = [ ] -async def test_platform_manually_configured(hass): - """Test that we do not discover anything or try to set up a controller.""" - assert ( - await async_setup_component( - hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: {"platform": UNIFI_DOMAIN}} - ) - is True - ) - assert UNIFI_DOMAIN not in hass.data - - -async def test_no_clients(hass): +async def test_no_clients(hass, aioclient_mock): """Test the update_clients function when no clients are found.""" - controller = await setup_unifi_integration( + await setup_unifi_integration( hass, + aioclient_mock, options={ CONF_ALLOW_BANDWIDTH_SENSORS: True, CONF_ALLOW_UPTIME_SENSORS: True, }, ) - assert len(controller.mock_requests) == 6 assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 0 -async def test_sensors(hass): +async def test_sensors(hass, aioclient_mock): """Test the update_items function with some clients.""" - controller = await setup_unifi_integration( + config_entry = await setup_unifi_integration( hass, + aioclient_mock, options={ CONF_ALLOW_BANDWIDTH_SENSORS: True, CONF_ALLOW_UPTIME_SENSORS: True, @@ -87,8 +76,8 @@ async def test_sensors(hass): }, clients_response=CLIENTS, ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - assert len(controller.mock_requests) == 6 assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6 wired_client_rx = hass.states.get("sensor.wired_client_name_rx") @@ -129,7 +118,7 @@ async def test_sensors(hass): assert wireless_client_uptime.state == "2020-09-15T14:41:00+00:00" hass.config_entries.async_update_entry( - controller.config_entry, + config_entry, options={ CONF_ALLOW_BANDWIDTH_SENSORS: False, CONF_ALLOW_UPTIME_SENSORS: False, @@ -150,7 +139,7 @@ async def test_sensors(hass): assert wireless_client_uptime is None hass.config_entries.async_update_entry( - controller.config_entry, + config_entry, options={ CONF_ALLOW_BANDWIDTH_SENSORS: True, CONF_ALLOW_UPTIME_SENSORS: True, @@ -189,16 +178,18 @@ async def test_sensors(hass): assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6 -async def test_remove_sensors(hass): +async def test_remove_sensors(hass, aioclient_mock): """Test the remove_items function with some clients.""" - controller = await setup_unifi_integration( + config_entry = await setup_unifi_integration( hass, + aioclient_mock, options={ CONF_ALLOW_BANDWIDTH_SENSORS: True, CONF_ALLOW_UPTIME_SENSORS: True, }, clients_response=CLIENTS, ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6 assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 903db479d34..e5a3a7eccc4 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -17,7 +17,6 @@ from homeassistant.components.unifi.const import ( ) from homeassistant.components.unifi.switch import POE_SWITCH from homeassistant.helpers import entity_registry -from homeassistant.setup import async_setup_component from .test_controller import ( CONTROLLER_HOST, @@ -282,21 +281,11 @@ DPI_APPS = [ ] -async def test_platform_manually_configured(hass): - """Test that we do not discover anything or try to set up a controller.""" - assert ( - await async_setup_component( - hass, SWITCH_DOMAIN, {SWITCH_DOMAIN: {"platform": UNIFI_DOMAIN}} - ) - is True - ) - assert UNIFI_DOMAIN not in hass.data - - -async def test_no_clients(hass): +async def test_no_clients(hass, aioclient_mock): """Test the update_clients function when no clients are found.""" - controller = await setup_unifi_integration( + await setup_unifi_integration( hass, + aioclient_mock, options={ CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False, @@ -304,45 +293,46 @@ async def test_no_clients(hass): }, ) - assert len(controller.mock_requests) == 6 + assert aioclient_mock.call_count == 10 assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 -async def test_controller_not_client(hass): +async def test_controller_not_client(hass, aioclient_mock): """Test that the controller doesn't become a switch.""" - controller = await setup_unifi_integration( + await setup_unifi_integration( hass, + aioclient_mock, options={CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False}, clients_response=[CONTROLLER_HOST], devices_response=[DEVICE_1], ) - assert len(controller.mock_requests) == 6 assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 cloudkey = hass.states.get("switch.cloud_key") assert cloudkey is None -async def test_not_admin(hass): +async def test_not_admin(hass, aioclient_mock): """Test that switch platform only work on an admin account.""" description = deepcopy(DESCRIPTION) description[0]["site_role"] = "not admin" - controller = await setup_unifi_integration( + await setup_unifi_integration( hass, + aioclient_mock, options={CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False}, site_description=description, clients_response=[CLIENT_1], devices_response=[DEVICE_1], ) - assert len(controller.mock_requests) == 6 assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 -async def test_switches(hass): +async def test_switches(hass, aioclient_mock): """Test the update_items function with some clients.""" - controller = await setup_unifi_integration( + config_entry = await setup_unifi_integration( hass, + aioclient_mock, options={ CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]], CONF_TRACK_CLIENTS: False, @@ -354,8 +344,8 @@ async def test_switches(hass): dpigroup_response=DPI_GROUPS, dpiapp_response=DPI_APPS, ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - assert len(controller.mock_requests) == 6 assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 4 switch_1 = hass.states.get("switch.poe_client_1") @@ -381,38 +371,44 @@ async def test_switches(hass): assert dpi_switch is not None assert dpi_switch.state == "on" + # Block and unblock client + + aioclient_mock.post( + f"https://{controller.host}:1234/api/s/{controller.site}/cmd/stamgr", + ) + await hass.services.async_call( SWITCH_DOMAIN, "turn_off", {"entity_id": "switch.block_client_1"}, blocking=True ) - assert len(controller.mock_requests) == 7 - assert controller.mock_requests[6] == { - "json": {"mac": "00:00:00:00:01:01", "cmd": "block-sta"}, - "method": "post", - "path": "/cmd/stamgr", + assert aioclient_mock.call_count == 11 + assert aioclient_mock.mock_calls[10][2] == { + "mac": "00:00:00:00:01:01", + "cmd": "block-sta", } await hass.services.async_call( SWITCH_DOMAIN, "turn_on", {"entity_id": "switch.block_client_1"}, blocking=True ) - assert len(controller.mock_requests) == 8 - assert controller.mock_requests[7] == { - "json": {"mac": "00:00:00:00:01:01", "cmd": "unblock-sta"}, - "method": "post", - "path": "/cmd/stamgr", + assert aioclient_mock.call_count == 12 + assert aioclient_mock.mock_calls[11][2] == { + "mac": "00:00:00:00:01:01", + "cmd": "unblock-sta", } + # Enable and disable DPI + + aioclient_mock.put( + f"https://{controller.host}:1234/api/s/{controller.site}/rest/dpiapp/5f976f62e3c58f018ec7e17d", + ) + await hass.services.async_call( SWITCH_DOMAIN, "turn_off", {"entity_id": "switch.block_media_streaming"}, blocking=True, ) - assert len(controller.mock_requests) == 9 - assert controller.mock_requests[8] == { - "json": {"enabled": False}, - "method": "put", - "path": "/rest/dpiapp/5f976f62e3c58f018ec7e17d", - } + assert aioclient_mock.call_count == 13 + assert aioclient_mock.mock_calls[12][2] == {"enabled": False} await hass.services.async_call( SWITCH_DOMAIN, @@ -420,22 +416,20 @@ async def test_switches(hass): {"entity_id": "switch.block_media_streaming"}, blocking=True, ) - assert len(controller.mock_requests) == 10 - assert controller.mock_requests[9] == { - "json": {"enabled": True}, - "method": "put", - "path": "/rest/dpiapp/5f976f62e3c58f018ec7e17d", - } + assert aioclient_mock.call_count == 14 + assert aioclient_mock.mock_calls[13][2] == {"enabled": True} -async def test_remove_switches(hass): +async def test_remove_switches(hass, aioclient_mock): """Test the update_items function with some clients.""" - controller = await setup_unifi_integration( + config_entry = await setup_unifi_integration( hass, + aioclient_mock, options={CONF_BLOCK_CLIENT: [UNBLOCKED["mac"]]}, clients_response=[CLIENT_1, UNBLOCKED], devices_response=[DEVICE_1], ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 poe_switch = hass.states.get("switch.poe_client_1") @@ -460,10 +454,11 @@ async def test_remove_switches(hass): assert block_switch is None -async def test_block_switches(hass): +async def test_block_switches(hass, aioclient_mock): """Test the update_items function with some clients.""" - controller = await setup_unifi_integration( + config_entry = await setup_unifi_integration( hass, + aioclient_mock, options={ CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]], CONF_TRACK_CLIENTS: False, @@ -472,6 +467,7 @@ async def test_block_switches(hass): clients_response=[UNBLOCKED], clients_all_response=[BLOCKED], ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 @@ -507,31 +503,34 @@ async def test_block_switches(hass): assert blocked is not None assert blocked.state == "off" + aioclient_mock.post( + f"https://{controller.host}:1234/api/s/{controller.site}/cmd/stamgr", + ) + await hass.services.async_call( SWITCH_DOMAIN, "turn_off", {"entity_id": "switch.block_client_1"}, blocking=True ) - assert len(controller.mock_requests) == 7 - assert controller.mock_requests[6] == { - "json": {"mac": "00:00:00:00:01:01", "cmd": "block-sta"}, - "method": "post", - "path": "/cmd/stamgr", + assert aioclient_mock.call_count == 11 + assert aioclient_mock.mock_calls[10][2] == { + "mac": "00:00:00:00:01:01", + "cmd": "block-sta", } await hass.services.async_call( SWITCH_DOMAIN, "turn_on", {"entity_id": "switch.block_client_1"}, blocking=True ) - assert len(controller.mock_requests) == 8 - assert controller.mock_requests[7] == { - "json": {"mac": "00:00:00:00:01:01", "cmd": "unblock-sta"}, - "method": "post", - "path": "/cmd/stamgr", + assert aioclient_mock.call_count == 12 + assert aioclient_mock.mock_calls[11][2] == { + "mac": "00:00:00:00:01:01", + "cmd": "unblock-sta", } -async def test_new_client_discovered_on_block_control(hass): +async def test_new_client_discovered_on_block_control(hass, aioclient_mock): """Test if 2nd update has a new client.""" - controller = await setup_unifi_integration( + config_entry = await setup_unifi_integration( hass, + aioclient_mock, options={ CONF_BLOCK_CLIENT: [BLOCKED["mac"]], CONF_TRACK_CLIENTS: False, @@ -539,8 +538,8 @@ async def test_new_client_discovered_on_block_control(hass): CONF_DPI_RESTRICTIONS: False, }, ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - assert len(controller.mock_requests) == 6 assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 blocked = hass.states.get("switch.block_client_1") @@ -567,10 +566,11 @@ async def test_new_client_discovered_on_block_control(hass): assert blocked is not None -async def test_option_block_clients(hass): +async def test_option_block_clients(hass, aioclient_mock): """Test the changes to option reflects accordingly.""" - controller = await setup_unifi_integration( + config_entry = await setup_unifi_integration( hass, + aioclient_mock, options={CONF_BLOCK_CLIENT: [BLOCKED["mac"]]}, clients_all_response=[BLOCKED, UNBLOCKED], ) @@ -578,7 +578,7 @@ async def test_option_block_clients(hass): # Add a second switch hass.config_entries.async_update_entry( - controller.config_entry, + config_entry, options={CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]]}, ) await hass.async_block_till_done() @@ -586,7 +586,7 @@ async def test_option_block_clients(hass): # Remove the second switch again hass.config_entries.async_update_entry( - controller.config_entry, + config_entry, options={CONF_BLOCK_CLIENT: [BLOCKED["mac"]]}, ) await hass.async_block_till_done() @@ -594,7 +594,7 @@ async def test_option_block_clients(hass): # Enable one and remove another one hass.config_entries.async_update_entry( - controller.config_entry, + config_entry, options={CONF_BLOCK_CLIENT: [UNBLOCKED["mac"]]}, ) await hass.async_block_till_done() @@ -602,17 +602,18 @@ async def test_option_block_clients(hass): # Remove one hass.config_entries.async_update_entry( - controller.config_entry, + config_entry, options={CONF_BLOCK_CLIENT: []}, ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 -async def test_option_remove_switches(hass): +async def test_option_remove_switches(hass, aioclient_mock): """Test removal of DPI switch when options updated.""" - controller = await setup_unifi_integration( + config_entry = await setup_unifi_integration( hass, + aioclient_mock, options={ CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False, @@ -626,23 +627,24 @@ async def test_option_remove_switches(hass): # Disable DPI Switches hass.config_entries.async_update_entry( - controller.config_entry, + config_entry, options={CONF_DPI_RESTRICTIONS: False, CONF_POE_CLIENTS: False}, ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 -async def test_new_client_discovered_on_poe_control(hass): +async def test_new_client_discovered_on_poe_control(hass, aioclient_mock): """Test if 2nd update has a new client.""" - controller = await setup_unifi_integration( + config_entry = await setup_unifi_integration( hass, + aioclient_mock, options={CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False}, clients_response=[CLIENT_1], devices_response=[DEVICE_1], ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - assert len(controller.mock_requests) == 6 assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 controller.api.websocket._data = { @@ -665,47 +667,41 @@ async def test_new_client_discovered_on_poe_control(hass): switch_2 = hass.states.get("switch.poe_client_2") assert switch_2 is not None + aioclient_mock.put( + f"https://{controller.host}:1234/api/s/{controller.site}/rest/device/mock-id", + ) + await hass.services.async_call( SWITCH_DOMAIN, "turn_off", {"entity_id": "switch.poe_client_1"}, blocking=True ) - assert len(controller.mock_requests) == 7 assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 - assert controller.mock_requests[6] == { - "json": { - "port_overrides": [{"port_idx": 1, "portconf_id": "1a1", "poe_mode": "off"}] - }, - "method": "put", - "path": "/rest/device/mock-id", + assert aioclient_mock.call_count == 11 + assert aioclient_mock.mock_calls[10][2] == { + "port_overrides": [{"port_idx": 1, "portconf_id": "1a1", "poe_mode": "off"}] } await hass.services.async_call( SWITCH_DOMAIN, "turn_on", {"entity_id": "switch.poe_client_1"}, blocking=True ) - assert len(controller.mock_requests) == 8 - assert controller.mock_requests[7] == { - "json": { - "port_overrides": [ - {"port_idx": 1, "portconf_id": "1a1", "poe_mode": "auto"} - ] - }, - "method": "put", - "path": "/rest/device/mock-id", + assert aioclient_mock.call_count == 12 + assert aioclient_mock.mock_calls[11][2] == { + "port_overrides": [{"port_idx": 1, "portconf_id": "1a1", "poe_mode": "auto"}] } -async def test_ignore_multiple_poe_clients_on_same_port(hass): +async def test_ignore_multiple_poe_clients_on_same_port(hass, aioclient_mock): """Ignore when there are multiple POE driven clients on same port. If there is a non-UniFi switch powered by POE, clients will be transparently marked as having POE as well. """ - controller = await setup_unifi_integration( + await setup_unifi_integration( hass, + aioclient_mock, clients_response=POE_SWITCH_CLIENTS, devices_response=[DEVICE_1], ) - assert len(controller.mock_requests) == 6 assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3 switch_1 = hass.states.get("switch.poe_client_1") @@ -714,7 +710,7 @@ async def test_ignore_multiple_poe_clients_on_same_port(hass): assert switch_2 is None -async def test_restoring_client(hass): +async def test_restoring_client(hass, aioclient_mock): """Test the update_items function with some clients.""" config_entry = config_entries.ConfigEntry( version=1, @@ -744,8 +740,9 @@ async def test_restoring_client(hass): config_entry=config_entry, ) - controller = await setup_unifi_integration( + await setup_unifi_integration( hass, + aioclient_mock, options={ CONF_BLOCK_CLIENT: ["random mac"], CONF_TRACK_CLIENTS: False, @@ -756,7 +753,6 @@ async def test_restoring_client(hass): clients_all_response=[CLIENT_1], ) - assert len(controller.mock_requests) == 6 assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 device_1 = hass.states.get("switch.client_1") From 55f81a8a0414373a9a8e37602e84c6e77fdce1de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Nenz=C3=A9n?= Date: Fri, 5 Feb 2021 18:28:06 +0100 Subject: [PATCH 211/796] Address Plaato post merge review (#46024) --- .../components/plaato/binary_sensor.py | 6 +- .../components/plaato/config_flow.py | 8 +- homeassistant/components/plaato/const.py | 9 +++ homeassistant/components/plaato/entity.py | 10 ++- homeassistant/components/plaato/sensor.py | 4 +- tests/components/plaato/test_config_flow.py | 73 ++++++++++++------- 6 files changed, 72 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/plaato/binary_sensor.py b/homeassistant/components/plaato/binary_sensor.py index 0ee61b7668b..27150692d6f 100644 --- a/homeassistant/components/plaato/binary_sensor.py +++ b/homeassistant/components/plaato/binary_sensor.py @@ -20,7 +20,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Plaato from a config entry.""" if config_entry.data[CONF_USE_WEBHOOK]: - return False + return coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] async_add_entities( @@ -29,11 +29,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensor_type, coordinator, ) - for sensor_type in coordinator.data.binary_sensors.keys() + for sensor_type in coordinator.data.binary_sensors ) - return True - class PlaatoBinarySensor(PlaatoEntity, BinarySensorEntity): """Representation of a Binary Sensor.""" diff --git a/homeassistant/components/plaato/config_flow.py b/homeassistant/components/plaato/config_flow.py index 290776f47c1..8dbf6d50fca 100644 --- a/homeassistant/components/plaato/config_flow.py +++ b/homeassistant/components/plaato/config_flow.py @@ -30,7 +30,7 @@ _LOGGER = logging.getLogger(__package__) class PlaatoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handles a Plaato config flow.""" - VERSION = 2 + VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): @@ -128,16 +128,16 @@ class PlaatoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _show_api_method_form( self, device_type: PlaatoDeviceType, errors: dict = None ): - data_scheme = vol.Schema({vol.Optional(CONF_TOKEN, default=""): str}) + data_schema = vol.Schema({vol.Optional(CONF_TOKEN, default=""): str}) if device_type == PlaatoDeviceType.Airlock: - data_scheme = data_scheme.extend( + data_schema = data_schema.extend( {vol.Optional(CONF_USE_WEBHOOK, default=False): bool} ) return self.async_show_form( step_id="api_method", - data_schema=data_scheme, + data_schema=data_schema, errors=errors, description_placeholders={PLACEHOLDER_DEVICE_TYPE: device_type.name}, ) diff --git a/homeassistant/components/plaato/const.py b/homeassistant/components/plaato/const.py index f50eaaac0ed..1700b803775 100644 --- a/homeassistant/components/plaato/const.py +++ b/homeassistant/components/plaato/const.py @@ -25,3 +25,12 @@ DEVICE_ID = "device_id" UNDO_UPDATE_LISTENER = "undo_update_listener" DEFAULT_SCAN_INTERVAL = 5 MIN_UPDATE_INTERVAL = timedelta(minutes=1) + +DEVICE_STATE_ATTRIBUTES = { + "beer_name": "beer_name", + "keg_date": "keg_date", + "mode": "mode", + "original_gravity": "original_gravity", + "final_gravity": "final_gravity", + "alcohol_by_volume": "alcohol_by_volume", +} diff --git a/homeassistant/components/plaato/entity.py b/homeassistant/components/plaato/entity.py index 6f72c3419a4..7cb1a77a9fb 100644 --- a/homeassistant/components/plaato/entity.py +++ b/homeassistant/components/plaato/entity.py @@ -7,6 +7,7 @@ from .const import ( DEVICE, DEVICE_ID, DEVICE_NAME, + DEVICE_STATE_ATTRIBUTES, DEVICE_TYPE, DOMAIN, SENSOR_DATA, @@ -69,8 +70,13 @@ class PlaatoEntity(entity.Entity): @property def device_state_attributes(self): """Return the state attributes of the monitored installation.""" - if self._attributes is not None: - return self._attributes + if self._attributes: + return { + attr_key: self._attributes[plaato_key] + for attr_key, plaato_key in DEVICE_STATE_ATTRIBUTES.items() + if plaato_key in self._attributes + and self._attributes[plaato_key] is not None + } @property def available(self): diff --git a/homeassistant/components/plaato/sensor.py b/homeassistant/components/plaato/sensor.py index 3f5e467f504..1dd347305e1 100644 --- a/homeassistant/components/plaato/sensor.py +++ b/homeassistant/components/plaato/sensor.py @@ -59,11 +59,9 @@ async def async_setup_entry(hass, entry, async_add_entities): coordinator = entry_data[COORDINATOR] async_add_entities( PlaatoSensor(entry_data, sensor_type, coordinator) - for sensor_type in coordinator.data.sensors.keys() + for sensor_type in coordinator.data.sensors ) - return True - class PlaatoSensor(PlaatoEntity): """Representation of a Plaato Sensor.""" diff --git a/tests/components/plaato/test_config_flow.py b/tests/components/plaato/test_config_flow.py index 10562b6aa60..7966882a977 100644 --- a/tests/components/plaato/test_config_flow.py +++ b/tests/components/plaato/test_config_flow.py @@ -95,15 +95,12 @@ async def test_show_config_form_validate_webhook(hass, webhook_id): assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "api_method" - async def return_async_value(val): - return val - hass.config.components.add("cloud") with patch( "homeassistant.components.cloud.async_active_subscription", return_value=True ), patch( "homeassistant.components.cloud.async_create_cloudhook", - return_value=return_async_value("https://hooks.nabu.casa/ABCD"), + return_value="https://hooks.nabu.casa/ABCD", ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -243,21 +240,34 @@ async def test_options(hass): data={}, options={CONF_SCAN_INTERVAL: 5}, ) - config_entry.add_to_hass(hass) - with patch("homeassistant.components.plaato.async_setup_entry", return_value=True): + + with patch( + "homeassistant.components.plaato.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.plaato.async_setup_entry", return_value=True + ) as mock_setup_entry: + + 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"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={CONF_SCAN_INTERVAL: 10}, - ) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_SCAN_INTERVAL: 10}, + ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"][CONF_SCAN_INTERVAL] == 10 + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_SCAN_INTERVAL] == 10 + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 async def test_options_webhook(hass, webhook_id): @@ -268,19 +278,32 @@ async def test_options_webhook(hass, webhook_id): data={CONF_USE_WEBHOOK: True, CONF_WEBHOOK_ID: None}, options={CONF_SCAN_INTERVAL: 5}, ) - config_entry.add_to_hass(hass) - with patch("homeassistant.components.plaato.async_setup_entry", return_value=True): + + with patch( + "homeassistant.components.plaato.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.plaato.async_setup_entry", return_value=True + ) as mock_setup_entry: + + 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"] == "webhook" - assert result["description_placeholders"] == {"webhook_url": ""} + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "webhook" + assert result["description_placeholders"] == {"webhook_url": ""} - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={CONF_WEBHOOK_ID: WEBHOOK_ID}, - ) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_WEBHOOK_ID: WEBHOOK_ID}, + ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"][CONF_WEBHOOK_ID] == CONF_WEBHOOK_ID + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_WEBHOOK_ID] == CONF_WEBHOOK_ID + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 From 55f9d985234e51e450add2370c4dd171a848e498 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 5 Feb 2021 19:12:23 +0100 Subject: [PATCH 212/796] Fix deprecated method isAlive() (#46062) --- homeassistant/components/zwave/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave/light.py b/homeassistant/components/zwave/light.py index 244b4a557e1..52014e37eea 100644 --- a/homeassistant/components/zwave/light.py +++ b/homeassistant/components/zwave/light.py @@ -175,7 +175,7 @@ class ZwaveDimmer(ZWaveDeviceEntity, LightEntity): self._refreshing = True self.values.primary.refresh() - if self._timer is not None and self._timer.isAlive(): + if self._timer is not None and self._timer.is_alive(): self._timer.cancel() self._timer = Timer(self._delay, _refresh_value) From 0d620eb7c3139d4e425715584f7f11c43737ee35 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 5 Feb 2021 19:38:08 +0100 Subject: [PATCH 213/796] Add unique id to UniFi config entries using the unique id of the site it is controlling (#45737) * Add unique id to UniFi config entries using the unique id of the site it is controlling * Fix failing test --- homeassistant/components/unifi/__init__.py | 5 ++ homeassistant/components/unifi/config_flow.py | 51 ++++++++++--------- homeassistant/components/unifi/controller.py | 2 + tests/components/unifi/test_config_flow.py | 36 +++++++++---- tests/components/unifi/test_controller.py | 3 +- tests/components/unifi/test_init.py | 1 + 6 files changed, 61 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index a9d39251838..5f19a01ce45 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -32,6 +32,11 @@ async def async_setup_entry(hass, config_entry): if not await controller.async_setup(): return False + if config_entry.unique_id is None: + hass.config_entries.async_update_entry( + config_entry, unique_id=controller.site_id + ) + hass.data[UNIFI_DOMAIN][config_entry.entry_id] = controller hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, controller.shutdown) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 07b81621750..1d89215dc89 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -70,7 +70,8 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): def __init__(self): """Initialize the UniFi flow.""" self.config = {} - self.sites = None + self.site_ids = {} + self.site_names = {} self.reauth_config_entry = None self.reauth_config = {} self.reauth_schema = {} @@ -101,11 +102,15 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): errors["base"] = "service_unavailable" else: - self.sites = {site["name"]: site["desc"] for site in sites.values()} + self.site_ids = {site["_id"]: site["name"] for site in sites.values()} + self.site_names = {site["_id"]: site["desc"] for site in sites.values()} - if self.reauth_config.get(CONF_SITE_ID) in self.sites: + if ( + self.reauth_config_entry + and self.reauth_config_entry.unique_id in self.site_names + ): return await self.async_step_site( - {CONF_SITE_ID: self.reauth_config[CONF_SITE_ID]} + {CONF_SITE_ID: self.reauth_config_entry.unique_id} ) return await self.async_step_site() @@ -136,26 +141,18 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): if user_input is not None: - self.config[CONF_SITE_ID] = user_input[CONF_SITE_ID] + unique_id = user_input[CONF_SITE_ID] + self.config[CONF_SITE_ID] = self.site_ids[unique_id] data = {CONF_CONTROLLER: self.config} + config_entry = await self.async_set_unique_id(unique_id) + abort_reason = "configuration_updated" + if self.reauth_config_entry: - self.hass.config_entries.async_update_entry( - self.reauth_config_entry, data=data - ) - await self.hass.config_entries.async_reload( - self.reauth_config_entry.entry_id - ) - return self.async_abort(reason="reauth_successful") - - for config_entry in self._async_current_entries(): - controller_data = config_entry.data[CONF_CONTROLLER] - if ( - controller_data[CONF_HOST] != self.config[CONF_HOST] - or controller_data[CONF_SITE_ID] != self.config[CONF_SITE_ID] - ): - continue + config_entry = self.reauth_config_entry + abort_reason = "reauth_successful" + if config_entry: controller = self.hass.data.get(UNIFI_DOMAIN, {}).get( config_entry.entry_id ) @@ -165,17 +162,21 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): self.hass.config_entries.async_update_entry(config_entry, data=data) await self.hass.config_entries.async_reload(config_entry.entry_id) - return self.async_abort(reason="configuration_updated") + return self.async_abort(reason=abort_reason) - site_nice_name = self.sites[self.config[CONF_SITE_ID]] + site_nice_name = self.site_names[unique_id] return self.async_create_entry(title=site_nice_name, data=data) - if len(self.sites) == 1: - return await self.async_step_site({CONF_SITE_ID: next(iter(self.sites))}) + if len(self.site_names) == 1: + return await self.async_step_site( + {CONF_SITE_ID: next(iter(self.site_names))} + ) return self.async_show_form( step_id="site", - data_schema=vol.Schema({vol.Required(CONF_SITE_ID): vol.In(self.sites)}), + data_schema=vol.Schema( + {vol.Required(CONF_SITE_ID): vol.In(self.site_names)} + ), errors=errors, ) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index bd55f4479fa..5d5e679e75e 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -95,6 +95,7 @@ class UniFiController: self.wireless_clients = None self.listeners = [] + self.site_id: str = "" self._site_name = None self._site_role = None @@ -321,6 +322,7 @@ class UniFiController: for site in sites.values(): if self.site == site["name"]: + self.site_id = site["_id"] self._site_name = site["desc"] break diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 302564dbbd5..096e6ba7791 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -112,7 +112,9 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery): aioclient_mock.get( "https://1.2.3.4:1234/api/self/sites", json={ - "data": [{"desc": "Site name", "name": "site_id", "role": "admin"}], + "data": [ + {"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"} + ], "meta": {"rc": "ok"}, }, headers={"content-type": CONTENT_TYPE_JSON}, @@ -143,7 +145,7 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery): } -async def test_flow_works_multiple_sites(hass, aioclient_mock): +async def test_flow_multiple_sites(hass, aioclient_mock): """Test config flow works when finding multiple sites.""" result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": "user"} @@ -164,8 +166,8 @@ async def test_flow_works_multiple_sites(hass, aioclient_mock): "https://1.2.3.4:1234/api/self/sites", json={ "data": [ - {"name": "default", "role": "admin", "desc": "site name"}, - {"name": "site2", "role": "admin", "desc": "site2 name"}, + {"name": "default", "role": "admin", "desc": "site name", "_id": "1"}, + {"name": "site2", "role": "admin", "desc": "site2 name", "_id": "2"}, ], "meta": {"rc": "ok"}, }, @@ -185,8 +187,8 @@ async def test_flow_works_multiple_sites(hass, aioclient_mock): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "site" - assert result["data_schema"]({"site": "default"}) - assert result["data_schema"]({"site": "site2"}) + assert result["data_schema"]({"site": "1"}) + assert result["data_schema"]({"site": "2"}) async def test_flow_raise_already_configured(hass, aioclient_mock): @@ -213,7 +215,9 @@ async def test_flow_raise_already_configured(hass, aioclient_mock): aioclient_mock.get( "https://1.2.3.4:1234/api/self/sites", json={ - "data": [{"desc": "Site name", "name": "site_id", "role": "admin"}], + "data": [ + {"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"} + ], "meta": {"rc": "ok"}, }, headers={"content-type": CONTENT_TYPE_JSON}, @@ -237,12 +241,16 @@ async def test_flow_raise_already_configured(hass, aioclient_mock): async def test_flow_aborts_configuration_updated(hass, aioclient_mock): """Test config flow aborts since a connected config entry already exists.""" entry = MockConfigEntry( - domain=UNIFI_DOMAIN, data={"controller": {"host": "1.2.3.4", "site": "office"}} + domain=UNIFI_DOMAIN, + data={"controller": {"host": "1.2.3.4", "site": "office"}}, + unique_id="2", ) entry.add_to_hass(hass) entry = MockConfigEntry( - domain=UNIFI_DOMAIN, data={"controller": {"host": "1.2.3.4", "site": "site_id"}} + domain=UNIFI_DOMAIN, + data={"controller": {"host": "1.2.3.4", "site": "site_id"}}, + unique_id="1", ) entry.add_to_hass(hass) @@ -264,7 +272,9 @@ async def test_flow_aborts_configuration_updated(hass, aioclient_mock): aioclient_mock.get( "https://1.2.3.4:1234/api/self/sites", json={ - "data": [{"desc": "Site name", "name": "site_id", "role": "admin"}], + "data": [ + {"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"} + ], "meta": {"rc": "ok"}, }, headers={"content-type": CONTENT_TYPE_JSON}, @@ -343,6 +353,8 @@ async def test_flow_fails_controller_unavailable(hass, aioclient_mock): async def test_reauth_flow_update_configuration(hass, aioclient_mock): """Verify reauth flow can update controller configuration.""" config_entry = await setup_unifi_integration(hass, aioclient_mock) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + controller.available = False result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, @@ -366,7 +378,9 @@ async def test_reauth_flow_update_configuration(hass, aioclient_mock): aioclient_mock.get( "https://1.2.3.4:1234/api/self/sites", json={ - "data": [{"desc": "Site name", "name": "site_id", "role": "admin"}], + "data": [ + {"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"} + ], "meta": {"rc": "ok"}, }, headers={"content-type": CONTENT_TYPE_JSON}, diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 077481a9320..3ecd44b3db7 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -71,7 +71,7 @@ ENTRY_OPTIONS = {} CONFIGURATION = [] -SITE = [{"desc": "Site name", "name": "site_id", "role": "admin"}] +SITE = [{"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"}] DESCRIPTION = [{"name": "username", "site_name": "site_id", "site_role": "admin"}] @@ -166,6 +166,7 @@ async def setup_unifi_integration( data=deepcopy(config), options=deepcopy(options), entry_id=1, + unique_id="1", ) config_entry.add_to_hass(hass) diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index 841e9ec7576..9de8b0a0990 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -45,6 +45,7 @@ async def test_controller_no_mac(hass): "verify_ssl": True, }, }, + unique_id="1", ) entry.add_to_hass(hass) mock_registry = Mock() From c01e01f7973cb0f577d2d62eb044c72649d1deba Mon Sep 17 00:00:00 2001 From: functionpointer Date: Fri, 5 Feb 2021 22:13:57 +0100 Subject: [PATCH 214/796] MySensors config flow (#45421) * MySensors: Add type annotations Adds a bunch of type annotations that were created while understanding the code. * MySensors: Change GatewayId to string In preparation for config flow. The GatewayId used to be id(gateway). With config flows, every gateway will have its own ConfigEntry. Every ConfigEntry has a unique id. Thus we would have two separate but one-to-one related ID systems. This commit removes this unneeded duplication by using the id of the ConfigEntry as GatewayId. * MySensors: Add unique_id to all entities This allows entities to work well with the frontend. * MySensors: Add device_info to all entities Entities belonging to the same node_id will now by grouped as a device. * MySensors: clean up device.py a bit * MySensors: Add config flow support With this change the MySensors can be fully configured from the GUI. Legacy configuration.yaml configs will be migrated by reading them once. Note that custom node names are not migrated. Users will have to re-enter the names in the front-end. Since there is no straight-forward way to configure global settings, all previously global settings are now per-gateway. These settings include: - MQTT retain - optimistic - persistence enable - MySensors version When a MySensors integration is loaded, it works as follows: 1. __init__.async_setup_entry is called 2. for every platform, async_forward_entry_setup is called 3. the platform's async_setup_entry is called 4. __init__.setup_mysensors_platform is called 5. the entity's constructor (e.g. MySensorsCover) is called 6. the created entity is stored in a dict in the hass object * MySensors: Fix linter errors * MySensors: Remove unused import * MySensors: Feedback from @MartinHjelmare * MySensors: Multi-step config flow * MySensors: More feedback * MySensors: Move all storage in hass object under DOMAIN The integration now stores everything under hass.data["mysensors"] instead of using several top level keys. * MySensors: await shutdown of gateway instead of creating a task * MySensors: Rename Ethernet to TCP * MySensors: Absolute imports and cosmetic changes * MySensors: fix gw_stop * MySensors: Allow user to specify persistence file * MySensors: Nicer log message * MySensors: Add lots of unit tests * MySensors: Fix legacy import of persistence file name Turns out tests help to find bugs :D * MySensors: Improve test coverage * MySensors: Use json persistence files by default * MySensors: Code style improvements * MySensors: Stop adding attributes to existing objects This commit removes the extra attributes that were being added to the gateway objects from pymysensors. Most attributes were easy to remove, except for the gateway id. The MySensorsDevice class needs the gateway id as it is part of its DevId as well as the unique_id and device_info. Most MySensorsDevices actually end up being Entities. Entities have access to their ConfigEntry via self.platform.config_entry. However, the device_tracker platform does not become an Entity. For this reason, the gateway id is not fetched from self.plaform but given as an argument. Additionally, MySensorsDevices expose the address of the gateway (CONF_DEVICE). Entities can easily fetch this information via self.platform, but the device_tracker cannot. This commit chooses to remove the gateway address from device_tracker. While this could in theory break some automations, the simplicity of this solution was deemed worth it. The alternative of adding the entire ConfigEntry as an argument to MySensorsDevices is not viable, because device_tracker is initialized by the async_setup_scanner function that isn't supplied a ConfigEntry. It only gets discovery_info. Adding the entire ConfigEntry doesn't seem appropriate for this edge case. * MySensors: Fix gw_stop and the translations * MySensors: Fix incorrect function calls * MySensors: Fewer comments in const.py * MySensors: Remove union from _get_gateway and remove id from try_connect * MySensors: Deprecate nodes option in configuration.yaml * MySensors: Use version parser from packaging * MySensors: Remove prefix from unique_id and change some private property names * MySensors: Change _get_gateway function signature * MySensors: add packaging==20.8 for the version parser * MySensors: Rename some stuff * MySensors: use pytest.mark.parametrize * MySensors: Clean up test cases * MySensors: Remove unneeded parameter from devices * Revert "MySensors: add packaging==20.8 for the version parser" This reverts commit 6b200ee01a3c0eee98176380bdd0b73e5a25b2dd. * MySensors: Use core interface for testing configuration.yaml import * MySensors: Fix test_init * MySensors: Rename a few variables * MySensors: cosmetic changes * MySensors: Update strings.json * MySensors: Still more feedback from @MartinHjelmare * MySensors: Remove unused strings Co-authored-by: Martin Hjelmare * MySensors: Fix typo and remove another unused string * MySensors: More strings.json * MySensors: Fix gateway ready handler * MySensors: Add duplicate detection to config flows * MySensors: Deal with non-existing topics and ports. Includes unit tests for these cases. * MySensors: Use awesomeversion instead of packaging * Add string already_configured * MySensors: Abort config flow when config is found to be invalid while importing * MySensors: Copy all error messages to also be abort messages All error strings may now also be used as an abort reason, so the strings should be defined * Use string references Co-authored-by: Martin Hjelmare --- .coveragerc | 15 +- CODEOWNERS | 2 +- .../components/mysensors/__init__.py | 209 +++-- .../components/mysensors/binary_sensor.py | 38 +- homeassistant/components/mysensors/climate.py | 49 +- .../components/mysensors/config_flow.py | 300 +++++++ homeassistant/components/mysensors/const.py | 110 ++- homeassistant/components/mysensors/cover.py | 50 +- homeassistant/components/mysensors/device.py | 130 +++- .../components/mysensors/device_tracker.py | 33 +- homeassistant/components/mysensors/gateway.py | 234 ++++-- homeassistant/components/mysensors/handler.py | 72 +- homeassistant/components/mysensors/helpers.py | 120 ++- homeassistant/components/mysensors/light.py | 55 +- .../components/mysensors/manifest.json | 14 +- homeassistant/components/mysensors/sensor.py | 38 +- .../components/mysensors/strings.json | 79 ++ homeassistant/components/mysensors/switch.py | 54 +- .../components/mysensors/translations/en.json | 79 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + tests/components/mysensors/__init__.py | 1 + .../components/mysensors/test_config_flow.py | 735 ++++++++++++++++++ tests/components/mysensors/test_gateway.py | 30 + tests/components/mysensors/test_init.py | 251 ++++++ 26 files changed, 2371 insertions(+), 333 deletions(-) create mode 100644 homeassistant/components/mysensors/config_flow.py create mode 100644 homeassistant/components/mysensors/strings.json create mode 100644 homeassistant/components/mysensors/translations/en.json create mode 100644 tests/components/mysensors/__init__.py create mode 100644 tests/components/mysensors/test_config_flow.py create mode 100644 tests/components/mysensors/test_gateway.py create mode 100644 tests/components/mysensors/test_init.py diff --git a/.coveragerc b/.coveragerc index 47d9c84ba0e..3a274cd004f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -578,7 +578,20 @@ omit = homeassistant/components/mychevy/* homeassistant/components/mycroft/* homeassistant/components/mycroft/notify.py - homeassistant/components/mysensors/* + homeassistant/components/mysensors/__init__.py + homeassistant/components/mysensors/binary_sensor.py + homeassistant/components/mysensors/climate.py + homeassistant/components/mysensors/const.py + homeassistant/components/mysensors/cover.py + homeassistant/components/mysensors/device.py + homeassistant/components/mysensors/device_tracker.py + homeassistant/components/mysensors/gateway.py + homeassistant/components/mysensors/handler.py + homeassistant/components/mysensors/helpers.py + homeassistant/components/mysensors/light.py + homeassistant/components/mysensors/notify.py + homeassistant/components/mysensors/sensor.py + homeassistant/components/mysensors/switch.py homeassistant/components/mystrom/binary_sensor.py homeassistant/components/mystrom/light.py homeassistant/components/mystrom/switch.py diff --git a/CODEOWNERS b/CODEOWNERS index efb338dd4b4..8785ce382cb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -288,7 +288,7 @@ homeassistant/components/mpd/* @fabaff homeassistant/components/mqtt/* @home-assistant/core @emontnemery homeassistant/components/msteams/* @peroyvind homeassistant/components/myq/* @bdraco -homeassistant/components/mysensors/* @MartinHjelmare +homeassistant/components/mysensors/* @MartinHjelmare @functionpointer homeassistant/components/mystrom/* @fabaff homeassistant/components/neato/* @dshokouhi @Santobert homeassistant/components/nederlandse_spoorwegen/* @YarmoM diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 43e398b142f..25b4d3106da 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -1,12 +1,18 @@ """Connect to a MySensors gateway via pymysensors API.""" +import asyncio import logging +from typing import Callable, Dict, List, Optional, Tuple, Type, Union +from mysensors import BaseAsyncGateway import voluptuous as vol +from homeassistant import config_entries from homeassistant.components.mqtt import valid_publish_topic, valid_subscribe_topic +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_OPTIMISTIC from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .const import ( ATTR_DEVICES, @@ -23,9 +29,14 @@ from .const import ( CONF_VERSION, DOMAIN, MYSENSORS_GATEWAYS, + MYSENSORS_ON_UNLOAD, + SUPPORTED_PLATFORMS_WITH_ENTRY_SUPPORT, + DevId, + GatewayId, + SensorType, ) -from .device import get_mysensors_devices -from .gateway import finish_setup, get_mysensors_gateway, setup_gateways +from .device import MySensorsDevice, MySensorsEntity, get_mysensors_devices +from .gateway import finish_setup, get_mysensors_gateway, gw_stop, setup_gateway _LOGGER = logging.getLogger(__name__) @@ -81,29 +92,38 @@ def deprecated(key): NODE_SCHEMA = vol.Schema({cv.positive_int: {vol.Required(CONF_NODE_NAME): cv.string}}) -GATEWAY_SCHEMA = { - vol.Required(CONF_DEVICE): cv.string, - vol.Optional(CONF_PERSISTENCE_FILE): vol.All(cv.string, is_persistence_file), - vol.Optional(CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE): cv.positive_int, - vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT): cv.port, - vol.Optional(CONF_TOPIC_IN_PREFIX): valid_subscribe_topic, - vol.Optional(CONF_TOPIC_OUT_PREFIX): valid_publish_topic, - vol.Optional(CONF_NODES, default={}): NODE_SCHEMA, -} +GATEWAY_SCHEMA = vol.Schema( + vol.All( + deprecated(CONF_NODES), + { + vol.Required(CONF_DEVICE): cv.string, + vol.Optional(CONF_PERSISTENCE_FILE): vol.All( + cv.string, is_persistence_file + ), + vol.Optional(CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE): cv.positive_int, + vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT): cv.port, + vol.Optional(CONF_TOPIC_IN_PREFIX): valid_subscribe_topic, + vol.Optional(CONF_TOPIC_OUT_PREFIX): valid_publish_topic, + vol.Optional(CONF_NODES, default={}): NODE_SCHEMA, + }, + ) +) CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( vol.All( deprecated(CONF_DEBUG), + deprecated(CONF_OPTIMISTIC), + deprecated(CONF_PERSISTENCE), { vol.Required(CONF_GATEWAYS): vol.All( cv.ensure_list, has_all_unique_files, [GATEWAY_SCHEMA] ), - vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, - vol.Optional(CONF_PERSISTENCE, default=True): cv.boolean, vol.Optional(CONF_RETAIN, default=True): cv.boolean, vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + vol.Optional(CONF_PERSISTENCE, default=True): cv.boolean, }, ) ) @@ -112,69 +132,168 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Set up the MySensors component.""" - gateways = await setup_gateways(hass, config) + if DOMAIN not in config or bool(hass.config_entries.async_entries(DOMAIN)): + return True - if not gateways: - _LOGGER.error("No devices could be setup as gateways, check your configuration") - return False + config = config[DOMAIN] + user_inputs = [ + { + CONF_DEVICE: gw[CONF_DEVICE], + CONF_BAUD_RATE: gw[CONF_BAUD_RATE], + CONF_TCP_PORT: gw[CONF_TCP_PORT], + CONF_TOPIC_OUT_PREFIX: gw.get(CONF_TOPIC_OUT_PREFIX, ""), + CONF_TOPIC_IN_PREFIX: gw.get(CONF_TOPIC_IN_PREFIX, ""), + CONF_RETAIN: config[CONF_RETAIN], + CONF_VERSION: config[CONF_VERSION], + CONF_PERSISTENCE_FILE: gw.get(CONF_PERSISTENCE_FILE) + # nodes config ignored at this time. renaming nodes can now be done from the frontend. + } + for gw in config[CONF_GATEWAYS] + ] + user_inputs = [ + {k: v for k, v in userinput.items() if v is not None} + for userinput in user_inputs + ] - hass.data[MYSENSORS_GATEWAYS] = gateways - - hass.async_create_task(finish_setup(hass, config, gateways)) + # there is an actual configuration in configuration.yaml, so we have to process it + for user_input in user_inputs: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=user_input, + ) + ) return True -def _get_mysensors_name(gateway, node_id, child_id): - """Return a name for a node child.""" - node_name = f"{gateway.sensors[node_id].sketch_name} {node_id}" - node_name = next( - ( - node[CONF_NODE_NAME] - for conf_id, node in gateway.nodes_config.items() - if node.get(CONF_NODE_NAME) is not None and conf_id == node_id - ), - node_name, +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Set up an instance of the MySensors integration. + + Every instance has a connection to exactly one Gateway. + """ + gateway = await setup_gateway(hass, entry) + + if not gateway: + _LOGGER.error("Gateway setup failed for %s", entry.data) + return False + + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + + if MYSENSORS_GATEWAYS not in hass.data[DOMAIN]: + hass.data[DOMAIN][MYSENSORS_GATEWAYS] = {} + hass.data[DOMAIN][MYSENSORS_GATEWAYS][entry.entry_id] = gateway + + async def finish(): + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_setup(entry, platform) + for platform in SUPPORTED_PLATFORMS_WITH_ENTRY_SUPPORT + ] + ) + await finish_setup(hass, entry, gateway) + + hass.async_create_task(finish()) + + return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Remove an instance of the MySensors integration.""" + + gateway = get_mysensors_gateway(hass, entry.entry_id) + + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in SUPPORTED_PLATFORMS_WITH_ENTRY_SUPPORT + ] + ) ) - return f"{node_name} {child_id}" + if not unload_ok: + return False + + key = MYSENSORS_ON_UNLOAD.format(entry.entry_id) + if key in hass.data[DOMAIN]: + for fnct in hass.data[DOMAIN][key]: + fnct() + + del hass.data[DOMAIN][MYSENSORS_GATEWAYS][entry.entry_id] + + await gw_stop(hass, entry, gateway) + return True + + +async def on_unload( + hass: HomeAssistantType, entry: Union[ConfigEntry, GatewayId], fnct: Callable +) -> None: + """Register a callback to be called when entry is unloaded. + + This function is used by platforms to cleanup after themselves + """ + if isinstance(entry, GatewayId): + uniqueid = entry + else: + uniqueid = entry.entry_id + key = MYSENSORS_ON_UNLOAD.format(uniqueid) + if key not in hass.data[DOMAIN]: + hass.data[DOMAIN][key] = [] + hass.data[DOMAIN][key].append(fnct) @callback def setup_mysensors_platform( hass, - domain, - discovery_info, - device_class, - device_args=None, - async_add_entities=None, -): - """Set up a MySensors platform.""" + domain: str, # hass platform name + discovery_info: Optional[Dict[str, List[DevId]]], + device_class: Union[Type[MySensorsDevice], Dict[SensorType, Type[MySensorsEntity]]], + device_args: Optional[ + Tuple + ] = None, # extra arguments that will be given to the entity constructor + async_add_entities: Callable = None, +) -> Optional[List[MySensorsDevice]]: + """Set up a MySensors platform. + + Sets up a bunch of instances of a single platform that is supported by this integration. + The function is given a list of device ids, each one describing an instance to set up. + The function is also given a class. + A new instance of the class is created for every device id, and the device id is given to the constructor of the class + """ # Only act if called via MySensors by discovery event. # Otherwise gateway is not set up. if not discovery_info: + _LOGGER.debug("Skipping setup due to no discovery info") return None if device_args is None: device_args = () - new_devices = [] - new_dev_ids = discovery_info[ATTR_DEVICES] + new_devices: List[MySensorsDevice] = [] + new_dev_ids: List[DevId] = discovery_info[ATTR_DEVICES] for dev_id in new_dev_ids: - devices = get_mysensors_devices(hass, domain) + devices: Dict[DevId, MySensorsDevice] = get_mysensors_devices(hass, domain) if dev_id in devices: + _LOGGER.debug( + "Skipping setup of %s for platform %s as it already exists", + dev_id, + domain, + ) continue gateway_id, node_id, child_id, value_type = dev_id - gateway = get_mysensors_gateway(hass, gateway_id) + gateway: Optional[BaseAsyncGateway] = get_mysensors_gateway(hass, gateway_id) if not gateway: + _LOGGER.warning("Skipping setup of %s, no gateway found", dev_id) continue device_class_copy = device_class if isinstance(device_class, dict): child = gateway.sensors[node_id].children[child_id] s_type = gateway.const.Presentation(child.type).name device_class_copy = device_class[s_type] - name = _get_mysensors_name(gateway, node_id, child_id) - args_copy = (*device_args, gateway, node_id, child_id, name, value_type) + args_copy = (*device_args, gateway_id, gateway, node_id, child_id, value_type) devices[dev_id] = device_class_copy(*args_copy) new_devices.append(devices[dev_id]) if new_devices: diff --git a/homeassistant/components/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py index 4ec3c6e0abd..c4e12d170c0 100644 --- a/homeassistant/components/mysensors/binary_sensor.py +++ b/homeassistant/components/mysensors/binary_sensor.py @@ -1,4 +1,6 @@ """Support for MySensors binary sensors.""" +from typing import Callable + from homeassistant.components import mysensors from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOISTURE, @@ -10,7 +12,13 @@ from homeassistant.components.binary_sensor import ( DOMAIN, BinarySensorEntity, ) +from homeassistant.components.mysensors import on_unload +from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY +from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType SENSORS = { "S_DOOR": "door", @@ -24,14 +32,30 @@ SENSORS = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the mysensors platform for binary sensors.""" - mysensors.setup_mysensors_platform( +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable +): + """Set up this platform for a specific ConfigEntry(==Gateway).""" + + @callback + def async_discover(discovery_info): + """Discover and add a MySensors binary_sensor.""" + mysensors.setup_mysensors_platform( + hass, + DOMAIN, + discovery_info, + MySensorsBinarySensor, + async_add_entities=async_add_entities, + ) + + await on_unload( hass, - DOMAIN, - discovery_info, - MySensorsBinarySensor, - async_add_entities=async_add_entities, + config_entry, + async_dispatcher_connect( + hass, + MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN), + async_discover, + ), ) diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index c318ccf7ec6..b1916fc4ed1 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -1,4 +1,6 @@ """MySensors platform that offers a Climate (MySensors-HVAC) component.""" +from typing import Callable + from homeassistant.components import mysensors from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -13,7 +15,12 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) +from homeassistant.components.mysensors import on_unload +from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType DICT_HA_TO_MYS = { HVAC_MODE_AUTO: "AutoChangeOver", @@ -32,14 +39,29 @@ FAN_LIST = ["Auto", "Min", "Normal", "Max"] OPERATION_LIST = [HVAC_MODE_OFF, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT] -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the mysensors climate.""" - mysensors.setup_mysensors_platform( +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable +): + """Set up this platform for a specific ConfigEntry(==Gateway).""" + + async def async_discover(discovery_info): + """Discover and add a MySensors climate.""" + mysensors.setup_mysensors_platform( + hass, + DOMAIN, + discovery_info, + MySensorsHVAC, + async_add_entities=async_add_entities, + ) + + await on_unload( hass, - DOMAIN, - discovery_info, - MySensorsHVAC, - async_add_entities=async_add_entities, + config_entry, + async_dispatcher_connect( + hass, + MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN), + async_discover, + ), ) @@ -62,15 +84,10 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity): features = features | SUPPORT_TARGET_TEMPERATURE return features - @property - def assumed_state(self): - """Return True if unable to access real state of entity.""" - return self.gateway.optimistic - @property def temperature_unit(self): """Return the unit of measurement.""" - return TEMP_CELSIUS if self.gateway.metric else TEMP_FAHRENHEIT + return TEMP_CELSIUS if self.hass.config.units.is_metric else TEMP_FAHRENHEIT @property def current_temperature(self): @@ -159,7 +176,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity): self.gateway.set_child_value( self.node_id, self.child_id, value_type, value, ack=1 ) - if self.gateway.optimistic: + if self.assumed_state: # Optimistically assume that device has changed state self._values[value_type] = value self.async_write_ha_state() @@ -170,7 +187,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity): self.gateway.set_child_value( self.node_id, self.child_id, set_req.V_HVAC_SPEED, fan_mode, ack=1 ) - if self.gateway.optimistic: + if self.assumed_state: # Optimistically assume that device has changed state self._values[set_req.V_HVAC_SPEED] = fan_mode self.async_write_ha_state() @@ -184,7 +201,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity): DICT_HA_TO_MYS[hvac_mode], ack=1, ) - if self.gateway.optimistic: + if self.assumed_state: # Optimistically assume that device has changed state self._values[self.value_type] = hvac_mode self.async_write_ha_state() diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py new file mode 100644 index 00000000000..058b782d208 --- /dev/null +++ b/homeassistant/components/mysensors/config_flow.py @@ -0,0 +1,300 @@ +"""Config flow for MySensors.""" +import logging +import os +from typing import Any, Dict, Optional + +from awesomeversion import ( + AwesomeVersion, + AwesomeVersionStrategy, + AwesomeVersionStrategyException, +) +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.mqtt import valid_publish_topic, valid_subscribe_topic +from homeassistant.components.mysensors import ( + CONF_DEVICE, + DEFAULT_BAUD_RATE, + DEFAULT_TCP_PORT, + is_persistence_file, +) +from homeassistant.config_entries import ConfigEntry +import homeassistant.helpers.config_validation as cv + +from . import CONF_RETAIN, CONF_VERSION, DEFAULT_VERSION + +# pylint: disable=unused-import +from .const import ( + CONF_BAUD_RATE, + CONF_GATEWAY_TYPE, + CONF_GATEWAY_TYPE_ALL, + CONF_GATEWAY_TYPE_MQTT, + CONF_GATEWAY_TYPE_SERIAL, + CONF_GATEWAY_TYPE_TCP, + CONF_PERSISTENCE_FILE, + CONF_TCP_PORT, + CONF_TOPIC_IN_PREFIX, + CONF_TOPIC_OUT_PREFIX, + DOMAIN, + ConfGatewayType, +) +from .gateway import MQTT_COMPONENT, is_serial_port, is_socket_address, try_connect + +_LOGGER = logging.getLogger(__name__) + + +def _get_schema_common() -> dict: + """Create a schema with options common to all gateway types.""" + schema = { + vol.Required( + CONF_VERSION, default="", description={"suggested_value": DEFAULT_VERSION} + ): str, + vol.Optional( + CONF_PERSISTENCE_FILE, + ): str, + } + return schema + + +def _validate_version(version: str) -> Dict[str, str]: + """Validate a version string from the user.""" + version_okay = False + try: + version_okay = bool( + AwesomeVersion.ensure_strategy( + version, + [AwesomeVersionStrategy.SIMPLEVER, AwesomeVersionStrategy.SEMVER], + ) + ) + except AwesomeVersionStrategyException: + pass + if version_okay: + return {} + return {CONF_VERSION: "invalid_version"} + + +def _is_same_device( + gw_type: ConfGatewayType, user_input: Dict[str, str], entry: ConfigEntry +): + """Check if another ConfigDevice is actually the same as user_input. + + This function only compares addresses and tcp ports, so it is possible to fool it with tricks like port forwarding. + """ + if entry.data[CONF_DEVICE] != user_input[CONF_DEVICE]: + return False + if gw_type == CONF_GATEWAY_TYPE_TCP: + return entry.data[CONF_TCP_PORT] == user_input[CONF_TCP_PORT] + if gw_type == CONF_GATEWAY_TYPE_MQTT: + entry_topics = { + entry.data[CONF_TOPIC_IN_PREFIX], + entry.data[CONF_TOPIC_OUT_PREFIX], + } + return ( + user_input.get(CONF_TOPIC_IN_PREFIX) in entry_topics + or user_input.get(CONF_TOPIC_OUT_PREFIX) in entry_topics + ) + return True + + +class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + async def async_step_import(self, user_input: Optional[Dict[str, str]] = None): + """Import a config entry. + + This method is called by async_setup and it has already + prepared the dict to be compatible with what a user would have + entered from the frontend. + Therefore we process it as though it came from the frontend. + """ + if user_input[CONF_DEVICE] == MQTT_COMPONENT: + user_input[CONF_GATEWAY_TYPE] = CONF_GATEWAY_TYPE_MQTT + else: + try: + await self.hass.async_add_executor_job( + is_serial_port, user_input[CONF_DEVICE] + ) + except vol.Invalid: + user_input[CONF_GATEWAY_TYPE] = CONF_GATEWAY_TYPE_TCP + else: + user_input[CONF_GATEWAY_TYPE] = CONF_GATEWAY_TYPE_SERIAL + + result: Dict[str, Any] = await self.async_step_user(user_input=user_input) + if result["type"] == "form": + return self.async_abort(reason=next(iter(result["errors"].values()))) + return result + + async def async_step_user(self, user_input: Optional[Dict[str, str]] = None): + """Create a config entry from frontend user input.""" + schema = {vol.Required(CONF_GATEWAY_TYPE): vol.In(CONF_GATEWAY_TYPE_ALL)} + schema = vol.Schema(schema) + + if user_input is not None: + gw_type = user_input[CONF_GATEWAY_TYPE] + input_pass = user_input if CONF_DEVICE in user_input else None + if gw_type == CONF_GATEWAY_TYPE_MQTT: + return await self.async_step_gw_mqtt(input_pass) + if gw_type == CONF_GATEWAY_TYPE_TCP: + return await self.async_step_gw_tcp(input_pass) + if gw_type == CONF_GATEWAY_TYPE_SERIAL: + return await self.async_step_gw_serial(input_pass) + + return self.async_show_form(step_id="user", data_schema=schema) + + async def async_step_gw_serial(self, user_input: Optional[Dict[str, str]] = None): + """Create config entry for a serial gateway.""" + errors = {} + if user_input is not None: + errors.update( + await self.validate_common(CONF_GATEWAY_TYPE_SERIAL, errors, user_input) + ) + if not errors: + return self.async_create_entry( + title=f"{user_input[CONF_DEVICE]}", data=user_input + ) + + schema = _get_schema_common() + schema[ + vol.Required(CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE) + ] = cv.positive_int + schema[vol.Required(CONF_DEVICE, default="/dev/ttyACM0")] = str + + schema = vol.Schema(schema) + return self.async_show_form( + step_id="gw_serial", data_schema=schema, errors=errors + ) + + async def async_step_gw_tcp(self, user_input: Optional[Dict[str, str]] = None): + """Create a config entry for a tcp gateway.""" + errors = {} + if user_input is not None: + if CONF_TCP_PORT in user_input: + port: int = user_input[CONF_TCP_PORT] + if not (0 < port <= 65535): + errors[CONF_TCP_PORT] = "port_out_of_range" + + errors.update( + await self.validate_common(CONF_GATEWAY_TYPE_TCP, errors, user_input) + ) + if not errors: + return self.async_create_entry( + title=f"{user_input[CONF_DEVICE]}", data=user_input + ) + + schema = _get_schema_common() + schema[vol.Required(CONF_DEVICE, default="127.0.0.1")] = str + # Don't use cv.port as that would show a slider *facepalm* + schema[vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT)] = vol.Coerce(int) + + schema = vol.Schema(schema) + return self.async_show_form(step_id="gw_tcp", data_schema=schema, errors=errors) + + def _check_topic_exists(self, topic: str) -> bool: + for other_config in self.hass.config_entries.async_entries(DOMAIN): + if topic == other_config.data.get( + CONF_TOPIC_IN_PREFIX + ) or topic == other_config.data.get(CONF_TOPIC_OUT_PREFIX): + return True + return False + + async def async_step_gw_mqtt(self, user_input: Optional[Dict[str, str]] = None): + """Create a config entry for a mqtt gateway.""" + errors = {} + if user_input is not None: + user_input[CONF_DEVICE] = MQTT_COMPONENT + + try: + valid_subscribe_topic(user_input[CONF_TOPIC_IN_PREFIX]) + except vol.Invalid: + errors[CONF_TOPIC_IN_PREFIX] = "invalid_subscribe_topic" + else: + if self._check_topic_exists(user_input[CONF_TOPIC_IN_PREFIX]): + errors[CONF_TOPIC_IN_PREFIX] = "duplicate_topic" + + try: + valid_publish_topic(user_input[CONF_TOPIC_OUT_PREFIX]) + except vol.Invalid: + errors[CONF_TOPIC_OUT_PREFIX] = "invalid_publish_topic" + if not errors: + if ( + user_input[CONF_TOPIC_IN_PREFIX] + == user_input[CONF_TOPIC_OUT_PREFIX] + ): + errors[CONF_TOPIC_OUT_PREFIX] = "same_topic" + elif self._check_topic_exists(user_input[CONF_TOPIC_OUT_PREFIX]): + errors[CONF_TOPIC_OUT_PREFIX] = "duplicate_topic" + + errors.update( + await self.validate_common(CONF_GATEWAY_TYPE_MQTT, errors, user_input) + ) + if not errors: + return self.async_create_entry( + title=f"{user_input[CONF_DEVICE]}", data=user_input + ) + schema = _get_schema_common() + schema[vol.Required(CONF_RETAIN, default=True)] = bool + schema[vol.Required(CONF_TOPIC_IN_PREFIX)] = str + schema[vol.Required(CONF_TOPIC_OUT_PREFIX)] = str + + schema = vol.Schema(schema) + return self.async_show_form( + step_id="gw_mqtt", data_schema=schema, errors=errors + ) + + def _normalize_persistence_file(self, path: str) -> str: + return os.path.realpath(os.path.normcase(self.hass.config.path(path))) + + async def validate_common( + self, + gw_type: ConfGatewayType, + errors: Dict[str, str], + user_input: Optional[Dict[str, str]] = None, + ) -> Dict[str, str]: + """Validate parameters common to all gateway types.""" + if user_input is not None: + errors.update(_validate_version(user_input.get(CONF_VERSION))) + + if gw_type != CONF_GATEWAY_TYPE_MQTT: + if gw_type == CONF_GATEWAY_TYPE_TCP: + verification_func = is_socket_address + else: + verification_func = is_serial_port + + try: + await self.hass.async_add_executor_job( + verification_func, user_input.get(CONF_DEVICE) + ) + except vol.Invalid: + errors[CONF_DEVICE] = ( + "invalid_ip" + if gw_type == CONF_GATEWAY_TYPE_TCP + else "invalid_serial" + ) + if CONF_PERSISTENCE_FILE in user_input: + try: + is_persistence_file(user_input[CONF_PERSISTENCE_FILE]) + except vol.Invalid: + errors[CONF_PERSISTENCE_FILE] = "invalid_persistence_file" + else: + real_persistence_path = self._normalize_persistence_file( + user_input[CONF_PERSISTENCE_FILE] + ) + for other_entry in self.hass.config_entries.async_entries(DOMAIN): + if CONF_PERSISTENCE_FILE not in other_entry.data: + continue + if real_persistence_path == self._normalize_persistence_file( + other_entry.data[CONF_PERSISTENCE_FILE] + ): + errors[CONF_PERSISTENCE_FILE] = "duplicate_persistence_file" + break + + for other_entry in self.hass.config_entries.async_entries(DOMAIN): + if _is_same_device(gw_type, user_input, other_entry): + errors["base"] = "already_configured" + break + + # if no errors so far, try to connect + if not errors and not await try_connect(self.hass, user_input): + errors["base"] = "cannot_connect" + + return errors diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index ccb646eb47e..66bee128d4d 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -1,33 +1,69 @@ """MySensors constants.""" from collections import defaultdict +from typing import Dict, List, Literal, Set, Tuple -ATTR_DEVICES = "devices" +ATTR_DEVICES: str = "devices" +ATTR_GATEWAY_ID: str = "gateway_id" -CONF_BAUD_RATE = "baud_rate" -CONF_DEVICE = "device" -CONF_GATEWAYS = "gateways" -CONF_NODES = "nodes" -CONF_PERSISTENCE = "persistence" -CONF_PERSISTENCE_FILE = "persistence_file" -CONF_RETAIN = "retain" -CONF_TCP_PORT = "tcp_port" -CONF_TOPIC_IN_PREFIX = "topic_in_prefix" -CONF_TOPIC_OUT_PREFIX = "topic_out_prefix" -CONF_VERSION = "version" +CONF_BAUD_RATE: str = "baud_rate" +CONF_DEVICE: str = "device" +CONF_GATEWAYS: str = "gateways" +CONF_NODES: str = "nodes" +CONF_PERSISTENCE: str = "persistence" +CONF_PERSISTENCE_FILE: str = "persistence_file" +CONF_RETAIN: str = "retain" +CONF_TCP_PORT: str = "tcp_port" +CONF_TOPIC_IN_PREFIX: str = "topic_in_prefix" +CONF_TOPIC_OUT_PREFIX: str = "topic_out_prefix" +CONF_VERSION: str = "version" +CONF_GATEWAY_TYPE: str = "gateway_type" +ConfGatewayType = Literal["Serial", "TCP", "MQTT"] +CONF_GATEWAY_TYPE_SERIAL: ConfGatewayType = "Serial" +CONF_GATEWAY_TYPE_TCP: ConfGatewayType = "TCP" +CONF_GATEWAY_TYPE_MQTT: ConfGatewayType = "MQTT" +CONF_GATEWAY_TYPE_ALL: List[str] = [ + CONF_GATEWAY_TYPE_MQTT, + CONF_GATEWAY_TYPE_SERIAL, + CONF_GATEWAY_TYPE_TCP, +] -DOMAIN = "mysensors" -MYSENSORS_GATEWAY_READY = "mysensors_gateway_ready_{}" -MYSENSORS_GATEWAYS = "mysensors_gateways" -PLATFORM = "platform" -SCHEMA = "schema" -CHILD_CALLBACK = "mysensors_child_callback_{}_{}_{}_{}" -NODE_CALLBACK = "mysensors_node_callback_{}_{}" -TYPE = "type" -UPDATE_DELAY = 0.1 -SERVICE_SEND_IR_CODE = "send_ir_code" +DOMAIN: str = "mysensors" +MYSENSORS_GATEWAY_READY: str = "mysensors_gateway_ready_{}" +MYSENSORS_GATEWAY_START_TASK: str = "mysensors_gateway_start_task_{}" +MYSENSORS_GATEWAYS: str = "mysensors_gateways" +PLATFORM: str = "platform" +SCHEMA: str = "schema" +CHILD_CALLBACK: str = "mysensors_child_callback_{}_{}_{}_{}" +NODE_CALLBACK: str = "mysensors_node_callback_{}_{}" +MYSENSORS_DISCOVERY = "mysensors_discovery_{}_{}" +MYSENSORS_ON_UNLOAD = "mysensors_on_unload_{}" +TYPE: str = "type" +UPDATE_DELAY: float = 0.1 -BINARY_SENSOR_TYPES = { +SERVICE_SEND_IR_CODE: str = "send_ir_code" + +SensorType = str +# S_DOOR, S_MOTION, S_SMOKE, ... + +ValueType = str +# V_TRIPPED, V_ARMED, V_STATUS, V_PERCENTAGE, ... + +GatewayId = str +# a unique id generated by config_flow.py and stored in the ConfigEntry as the entry id. +# +# Gateway may be fetched by giving the gateway id to get_mysensors_gateway() + +DevId = Tuple[GatewayId, int, int, int] +# describes the backend of a hass entity. Contents are: GatewayId, node_id, child_id, v_type as int +# +# The string version of v_type can be looked up in the enum gateway.const.SetReq of the appropriate BaseAsyncGateway +# Home Assistant Entities are quite limited and only ever do one thing. +# MySensors Nodes have multiple child_ids each with a s_type several associated v_types +# The MySensors integration brings these together by creating an entity for every v_type of every child_id of every node. +# The DevId tuple perfectly captures this. + +BINARY_SENSOR_TYPES: Dict[SensorType, Set[ValueType]] = { "S_DOOR": {"V_TRIPPED"}, "S_MOTION": {"V_TRIPPED"}, "S_SMOKE": {"V_TRIPPED"}, @@ -38,21 +74,23 @@ BINARY_SENSOR_TYPES = { "S_MOISTURE": {"V_TRIPPED"}, } -CLIMATE_TYPES = {"S_HVAC": {"V_HVAC_FLOW_STATE"}} +CLIMATE_TYPES: Dict[SensorType, Set[ValueType]] = {"S_HVAC": {"V_HVAC_FLOW_STATE"}} -COVER_TYPES = {"S_COVER": {"V_DIMMER", "V_PERCENTAGE", "V_LIGHT", "V_STATUS"}} +COVER_TYPES: Dict[SensorType, Set[ValueType]] = { + "S_COVER": {"V_DIMMER", "V_PERCENTAGE", "V_LIGHT", "V_STATUS"} +} -DEVICE_TRACKER_TYPES = {"S_GPS": {"V_POSITION"}} +DEVICE_TRACKER_TYPES: Dict[SensorType, Set[ValueType]] = {"S_GPS": {"V_POSITION"}} -LIGHT_TYPES = { +LIGHT_TYPES: Dict[SensorType, Set[ValueType]] = { "S_DIMMER": {"V_DIMMER", "V_PERCENTAGE"}, "S_RGB_LIGHT": {"V_RGB"}, "S_RGBW_LIGHT": {"V_RGBW"}, } -NOTIFY_TYPES = {"S_INFO": {"V_TEXT"}} +NOTIFY_TYPES: Dict[SensorType, Set[ValueType]] = {"S_INFO": {"V_TEXT"}} -SENSOR_TYPES = { +SENSOR_TYPES: Dict[SensorType, Set[ValueType]] = { "S_SOUND": {"V_LEVEL"}, "S_VIBRATION": {"V_LEVEL"}, "S_MOISTURE": {"V_LEVEL"}, @@ -80,7 +118,7 @@ SENSOR_TYPES = { "S_DUST": {"V_DUST_LEVEL", "V_LEVEL"}, } -SWITCH_TYPES = { +SWITCH_TYPES: Dict[SensorType, Set[ValueType]] = { "S_LIGHT": {"V_LIGHT"}, "S_BINARY": {"V_STATUS"}, "S_DOOR": {"V_ARMED"}, @@ -97,7 +135,7 @@ SWITCH_TYPES = { } -PLATFORM_TYPES = { +PLATFORM_TYPES: Dict[str, Dict[SensorType, Set[ValueType]]] = { "binary_sensor": BINARY_SENSOR_TYPES, "climate": CLIMATE_TYPES, "cover": COVER_TYPES, @@ -108,13 +146,19 @@ PLATFORM_TYPES = { "switch": SWITCH_TYPES, } -FLAT_PLATFORM_TYPES = { +FLAT_PLATFORM_TYPES: Dict[Tuple[str, SensorType], Set[ValueType]] = { (platform, s_type_name): v_type_name for platform, platform_types in PLATFORM_TYPES.items() for s_type_name, v_type_name in platform_types.items() } -TYPE_TO_PLATFORMS = defaultdict(list) +TYPE_TO_PLATFORMS: Dict[SensorType, List[str]] = defaultdict(list) + for platform, platform_types in PLATFORM_TYPES.items(): for s_type_name in platform_types: TYPE_TO_PLATFORMS[s_type_name].append(platform) + +SUPPORTED_PLATFORMS_WITH_ENTRY_SUPPORT = set(PLATFORM_TYPES.keys()) - { + "notify", + "device_tracker", +} diff --git a/homeassistant/components/mysensors/cover.py b/homeassistant/components/mysensors/cover.py index f2ede69793f..782ab88c488 100644 --- a/homeassistant/components/mysensors/cover.py +++ b/homeassistant/components/mysensors/cover.py @@ -1,28 +1,48 @@ """Support for MySensors covers.""" +import logging +from typing import Callable + from homeassistant.components import mysensors from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverEntity +from homeassistant.components.mysensors import on_unload +from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY +from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType + +_LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the mysensors platform for covers.""" - mysensors.setup_mysensors_platform( +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable +): + """Set up this platform for a specific ConfigEntry(==Gateway).""" + + async def async_discover(discovery_info): + """Discover and add a MySensors cover.""" + mysensors.setup_mysensors_platform( + hass, + DOMAIN, + discovery_info, + MySensorsCover, + async_add_entities=async_add_entities, + ) + + await on_unload( hass, - DOMAIN, - discovery_info, - MySensorsCover, - async_add_entities=async_add_entities, + config_entry.entry_id, + async_dispatcher_connect( + hass, + MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN), + async_discover, + ), ) class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity): """Representation of the value of a MySensors Cover child node.""" - @property - def assumed_state(self): - """Return True if unable to access real state of entity.""" - return self.gateway.optimistic - @property def is_closed(self): """Return True if cover is closed.""" @@ -46,7 +66,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity): self.gateway.set_child_value( self.node_id, self.child_id, set_req.V_UP, 1, ack=1 ) - if self.gateway.optimistic: + if self.assumed_state: # Optimistically assume that cover has changed state. if set_req.V_DIMMER in self._values: self._values[set_req.V_DIMMER] = 100 @@ -60,7 +80,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity): self.gateway.set_child_value( self.node_id, self.child_id, set_req.V_DOWN, 1, ack=1 ) - if self.gateway.optimistic: + if self.assumed_state: # Optimistically assume that cover has changed state. if set_req.V_DIMMER in self._values: self._values[set_req.V_DIMMER] = 0 @@ -75,7 +95,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity): self.gateway.set_child_value( self.node_id, self.child_id, set_req.V_DIMMER, position, ack=1 ) - if self.gateway.optimistic: + if self.assumed_state: # Optimistically assume that cover has changed state. self._values[set_req.V_DIMMER] = position self.async_write_ha_state() diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py index 9c1c4b54367..68414867345 100644 --- a/homeassistant/components/mysensors/device.py +++ b/homeassistant/components/mysensors/device.py @@ -1,13 +1,26 @@ """Handle MySensors devices.""" from functools import partial import logging +from typing import Any, Dict, Optional + +from mysensors import BaseAsyncGateway, Sensor +from mysensors.sensor import ChildSensor from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from .const import CHILD_CALLBACK, NODE_CALLBACK, UPDATE_DELAY +from .const import ( + CHILD_CALLBACK, + CONF_DEVICE, + DOMAIN, + NODE_CALLBACK, + PLATFORM_TYPES, + UPDATE_DELAY, + DevId, + GatewayId, +) _LOGGER = logging.getLogger(__name__) @@ -19,33 +32,94 @@ ATTR_HEARTBEAT = "heartbeat" MYSENSORS_PLATFORM_DEVICES = "mysensors_devices_{}" -def get_mysensors_devices(hass, domain): - """Return MySensors devices for a platform.""" - if MYSENSORS_PLATFORM_DEVICES.format(domain) not in hass.data: - hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)] = {} - return hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)] - - class MySensorsDevice: """Representation of a MySensors device.""" - def __init__(self, gateway, node_id, child_id, name, value_type): + def __init__( + self, + gateway_id: GatewayId, + gateway: BaseAsyncGateway, + node_id: int, + child_id: int, + value_type: int, + ): """Set up the MySensors device.""" - self.gateway = gateway - self.node_id = node_id - self.child_id = child_id - self._name = name - self.value_type = value_type - child = gateway.sensors[node_id].children[child_id] - self.child_type = child.type + self.gateway_id: GatewayId = gateway_id + self.gateway: BaseAsyncGateway = gateway + self.node_id: int = node_id + self.child_id: int = child_id + self.value_type: int = value_type # value_type as int. string variant can be looked up in gateway consts + self.child_type = self._child.type self._values = {} self._update_scheduled = False self.hass = None + @property + def dev_id(self) -> DevId: + """Return the DevId of this device. + + It is used to route incoming MySensors messages to the correct device/entity. + """ + return self.gateway_id, self.node_id, self.child_id, self.value_type + + @property + def _logger(self): + return logging.getLogger(f"{__name__}.{self.name}") + + async def async_will_remove_from_hass(self): + """Remove this entity from home assistant.""" + for platform in PLATFORM_TYPES: + platform_str = MYSENSORS_PLATFORM_DEVICES.format(platform) + if platform_str in self.hass.data[DOMAIN]: + platform_dict = self.hass.data[DOMAIN][platform_str] + if self.dev_id in platform_dict: + del platform_dict[self.dev_id] + self._logger.debug( + "deleted %s from platform %s", self.dev_id, platform + ) + + @property + def _node(self) -> Sensor: + return self.gateway.sensors[self.node_id] + + @property + def _child(self) -> ChildSensor: + return self._node.children[self.child_id] + + @property + def sketch_name(self) -> str: + """Return the name of the sketch running on the whole node (will be the same for several entities!).""" + return self._node.sketch_name + + @property + def sketch_version(self) -> str: + """Return the version of the sketch running on the whole node (will be the same for several entities!).""" + return self._node.sketch_version + + @property + def node_name(self) -> str: + """Name of the whole node (will be the same for several entities!).""" + return f"{self.sketch_name} {self.node_id}" + + @property + def unique_id(self) -> str: + """Return a unique ID for use in home assistant.""" + return f"{self.gateway_id}-{self.node_id}-{self.child_id}-{self.value_type}" + + @property + def device_info(self) -> Optional[Dict[str, Any]]: + """Return a dict that allows home assistant to puzzle all entities belonging to a node together.""" + return { + "identifiers": {(DOMAIN, f"{self.gateway_id}-{self.node_id}")}, + "name": self.node_name, + "manufacturer": DOMAIN, + "sw_version": self.sketch_version, + } + @property def name(self): """Return the name of this entity.""" - return self._name + return f"{self.node_name} {self.child_id}" @property def device_state_attributes(self): @@ -57,9 +131,12 @@ class MySensorsDevice: ATTR_HEARTBEAT: node.heartbeat, ATTR_CHILD_ID: self.child_id, ATTR_DESCRIPTION: child.description, - ATTR_DEVICE: self.gateway.device, ATTR_NODE_ID: self.node_id, } + # This works when we are actually an Entity (i.e. all platforms except device_tracker) + if hasattr(self, "platform"): + # pylint: disable=no-member + attr[ATTR_DEVICE] = self.platform.config_entry.data[CONF_DEVICE] set_req = self.gateway.const.SetReq @@ -76,7 +153,7 @@ class MySensorsDevice: for value_type, value in child.values.items(): _LOGGER.debug( "Entity update: %s: value_type %s, value = %s", - self._name, + self.name, value_type, value, ) @@ -116,6 +193,13 @@ class MySensorsDevice: self.hass.loop.call_later(UPDATE_DELAY, delayed_update) +def get_mysensors_devices(hass, domain: str) -> Dict[DevId, MySensorsDevice]: + """Return MySensors devices for a hass platform name.""" + if MYSENSORS_PLATFORM_DEVICES.format(domain) not in hass.data[DOMAIN]: + hass.data[DOMAIN][MYSENSORS_PLATFORM_DEVICES.format(domain)] = {} + return hass.data[DOMAIN][MYSENSORS_PLATFORM_DEVICES.format(domain)] + + class MySensorsEntity(MySensorsDevice, Entity): """Representation of a MySensors entity.""" @@ -135,17 +219,17 @@ class MySensorsEntity(MySensorsDevice, Entity): async def async_added_to_hass(self): """Register update callback.""" - gateway_id = id(self.gateway) - dev_id = gateway_id, self.node_id, self.child_id, self.value_type self.async_on_remove( async_dispatcher_connect( - self.hass, CHILD_CALLBACK.format(*dev_id), self.async_update_callback + self.hass, + CHILD_CALLBACK.format(*self.dev_id), + self.async_update_callback, ) ) self.async_on_remove( async_dispatcher_connect( self.hass, - NODE_CALLBACK.format(gateway_id, self.node_id), + NODE_CALLBACK.format(self.gateway_id, self.node_id), self.async_update_callback, ) ) diff --git a/homeassistant/components/mysensors/device_tracker.py b/homeassistant/components/mysensors/device_tracker.py index 1bf1e072ceb..b395a48f28b 100644 --- a/homeassistant/components/mysensors/device_tracker.py +++ b/homeassistant/components/mysensors/device_tracker.py @@ -1,11 +1,16 @@ """Support for tracking MySensors devices.""" from homeassistant.components import mysensors from homeassistant.components.device_tracker import DOMAIN +from homeassistant.components.mysensors import DevId, on_unload +from homeassistant.components.mysensors.const import ATTR_GATEWAY_ID, GatewayId from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify -async def async_setup_scanner(hass, config, async_see, discovery_info=None): +async def async_setup_scanner( + hass: HomeAssistantType, config, async_see, discovery_info=None +): """Set up the MySensors device scanner.""" new_devices = mysensors.setup_mysensors_platform( hass, @@ -18,17 +23,25 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): return False for device in new_devices: - gateway_id = id(device.gateway) - dev_id = (gateway_id, device.node_id, device.child_id, device.value_type) - async_dispatcher_connect( + gateway_id: GatewayId = discovery_info[ATTR_GATEWAY_ID] + dev_id: DevId = (gateway_id, device.node_id, device.child_id, device.value_type) + await on_unload( hass, - mysensors.const.CHILD_CALLBACK.format(*dev_id), - device.async_update_callback, + gateway_id, + async_dispatcher_connect( + hass, + mysensors.const.CHILD_CALLBACK.format(*dev_id), + device.async_update_callback, + ), ) - async_dispatcher_connect( + await on_unload( hass, - mysensors.const.NODE_CALLBACK.format(gateway_id, device.node_id), - device.async_update_callback, + gateway_id, + async_dispatcher_connect( + hass, + mysensors.const.NODE_CALLBACK.format(gateway_id, device.node_id), + device.async_update_callback, + ), ) return True @@ -37,7 +50,7 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): class MySensorsDeviceScanner(mysensors.device.MySensorsDevice): """Represent a MySensors scanner.""" - def __init__(self, hass, async_see, *args): + def __init__(self, hass: HomeAssistantType, async_see, *args): """Set up instance.""" super().__init__(*args) self.async_see = async_see diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index f9450b798ac..b618004b622 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -4,22 +4,21 @@ from collections import defaultdict import logging import socket import sys +from typing import Any, Callable, Coroutine, Dict, Optional import async_timeout -from mysensors import mysensors +from mysensors import BaseAsyncGateway, Message, Sensor, mysensors import voluptuous as vol -from homeassistant.const import CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Event, callback import homeassistant.helpers.config_validation as cv -from homeassistant.setup import async_setup_component +from homeassistant.helpers.typing import HomeAssistantType from .const import ( CONF_BAUD_RATE, CONF_DEVICE, - CONF_GATEWAYS, - CONF_NODES, - CONF_PERSISTENCE, CONF_PERSISTENCE_FILE, CONF_RETAIN, CONF_TCP_PORT, @@ -28,7 +27,9 @@ from .const import ( CONF_VERSION, DOMAIN, MYSENSORS_GATEWAY_READY, + MYSENSORS_GATEWAY_START_TASK, MYSENSORS_GATEWAYS, + GatewayId, ) from .handler import HANDLERS from .helpers import discover_mysensors_platform, validate_child, validate_node @@ -58,48 +59,114 @@ def is_socket_address(value): raise vol.Invalid("Device is not a valid domain name or ip address") from err -def get_mysensors_gateway(hass, gateway_id): - """Return MySensors gateway.""" - if MYSENSORS_GATEWAYS not in hass.data: - hass.data[MYSENSORS_GATEWAYS] = {} - gateways = hass.data.get(MYSENSORS_GATEWAYS) +async def try_connect(hass: HomeAssistantType, user_input: Dict[str, str]) -> bool: + """Try to connect to a gateway and report if it worked.""" + if user_input[CONF_DEVICE] == MQTT_COMPONENT: + return True # dont validate mqtt. mqtt gateways dont send ready messages :( + try: + gateway_ready = asyncio.Future() + + def gateway_ready_callback(msg): + msg_type = msg.gateway.const.MessageType(msg.type) + _LOGGER.debug("Received MySensors msg type %s: %s", msg_type.name, msg) + if msg_type.name != "internal": + return + internal = msg.gateway.const.Internal(msg.sub_type) + if internal.name != "I_GATEWAY_READY": + return + _LOGGER.debug("Received gateway ready") + gateway_ready.set_result(True) + + gateway: Optional[BaseAsyncGateway] = await _get_gateway( + hass, + device=user_input[CONF_DEVICE], + version=user_input[CONF_VERSION], + event_callback=gateway_ready_callback, + persistence_file=None, + baud_rate=user_input.get(CONF_BAUD_RATE), + tcp_port=user_input.get(CONF_TCP_PORT), + topic_in_prefix=None, + topic_out_prefix=None, + retain=False, + persistence=False, + ) + if gateway is None: + return False + + connect_task = None + try: + connect_task = asyncio.create_task(gateway.start()) + with async_timeout.timeout(5): + await gateway_ready + return True + except asyncio.TimeoutError: + _LOGGER.info("Try gateway connect failed with timeout") + return False + finally: + if connect_task is not None and not connect_task.done(): + connect_task.cancel() + asyncio.create_task(gateway.stop()) + except OSError as err: + _LOGGER.info("Try gateway connect failed with exception", exc_info=err) + return False + + +def get_mysensors_gateway( + hass: HomeAssistantType, gateway_id: GatewayId +) -> Optional[BaseAsyncGateway]: + """Return the Gateway for a given GatewayId.""" + if MYSENSORS_GATEWAYS not in hass.data[DOMAIN]: + hass.data[DOMAIN][MYSENSORS_GATEWAYS] = {} + gateways = hass.data[DOMAIN].get(MYSENSORS_GATEWAYS) return gateways.get(gateway_id) -async def setup_gateways(hass, config): - """Set up all gateways.""" - conf = config[DOMAIN] - gateways = {} +async def setup_gateway( + hass: HomeAssistantType, entry: ConfigEntry +) -> Optional[BaseAsyncGateway]: + """Set up the Gateway for the given ConfigEntry.""" - for index, gateway_conf in enumerate(conf[CONF_GATEWAYS]): - persistence_file = gateway_conf.get( - CONF_PERSISTENCE_FILE, - hass.config.path(f"mysensors{index + 1}.pickle"), - ) - ready_gateway = await _get_gateway(hass, config, gateway_conf, persistence_file) - if ready_gateway is not None: - gateways[id(ready_gateway)] = ready_gateway - - return gateways + ready_gateway = await _get_gateway( + hass, + device=entry.data[CONF_DEVICE], + version=entry.data[CONF_VERSION], + event_callback=_gw_callback_factory(hass, entry.entry_id), + persistence_file=entry.data.get( + CONF_PERSISTENCE_FILE, f"mysensors_{entry.entry_id}.json" + ), + baud_rate=entry.data.get(CONF_BAUD_RATE), + tcp_port=entry.data.get(CONF_TCP_PORT), + topic_in_prefix=entry.data.get(CONF_TOPIC_IN_PREFIX), + topic_out_prefix=entry.data.get(CONF_TOPIC_OUT_PREFIX), + retain=entry.data.get(CONF_RETAIN, False), + ) + return ready_gateway -async def _get_gateway(hass, config, gateway_conf, persistence_file): +async def _get_gateway( + hass: HomeAssistantType, + device: str, + version: str, + event_callback: Callable[[Message], None], + persistence_file: Optional[str] = None, + baud_rate: Optional[int] = None, + tcp_port: Optional[int] = None, + topic_in_prefix: Optional[str] = None, + topic_out_prefix: Optional[str] = None, + retain: bool = False, + persistence: bool = True, # old persistence option has been deprecated. kwarg is here so we can run try_connect() without persistence +) -> Optional[BaseAsyncGateway]: """Return gateway after setup of the gateway.""" - conf = config[DOMAIN] - persistence = conf[CONF_PERSISTENCE] - version = conf[CONF_VERSION] - device = gateway_conf[CONF_DEVICE] - baud_rate = gateway_conf[CONF_BAUD_RATE] - tcp_port = gateway_conf[CONF_TCP_PORT] - in_prefix = gateway_conf.get(CONF_TOPIC_IN_PREFIX, "") - out_prefix = gateway_conf.get(CONF_TOPIC_OUT_PREFIX, "") + if persistence_file is not None: + # interpret relative paths to be in hass config folder. absolute paths will be left as they are + persistence_file = hass.config.path(persistence_file) if device == MQTT_COMPONENT: - if not await async_setup_component(hass, MQTT_COMPONENT, config): - return None + # what is the purpose of this? + # if not await async_setup_component(hass, MQTT_COMPONENT, entry): + # return None mqtt = hass.components.mqtt - retain = conf[CONF_RETAIN] def pub_callback(topic, payload, qos, retain): """Call MQTT publish function.""" @@ -118,8 +185,8 @@ async def _get_gateway(hass, config, gateway_conf, persistence_file): gateway = mysensors.AsyncMQTTGateway( pub_callback, sub_callback, - in_prefix=in_prefix, - out_prefix=out_prefix, + in_prefix=topic_in_prefix, + out_prefix=topic_out_prefix, retain=retain, loop=hass.loop, event_callback=None, @@ -154,25 +221,23 @@ async def _get_gateway(hass, config, gateway_conf, persistence_file): ) except vol.Invalid: # invalid ip address + _LOGGER.error("Connect failed: Invalid device %s", device) return None - gateway.metric = hass.config.units.is_metric - gateway.optimistic = conf[CONF_OPTIMISTIC] - gateway.device = device - gateway.event_callback = _gw_callback_factory(hass, config) - gateway.nodes_config = gateway_conf[CONF_NODES] + gateway.event_callback = event_callback if persistence: await gateway.start_persistence() return gateway -async def finish_setup(hass, hass_config, gateways): +async def finish_setup( + hass: HomeAssistantType, entry: ConfigEntry, gateway: BaseAsyncGateway +): """Load any persistent devices and platforms and start gateway.""" discover_tasks = [] start_tasks = [] - for gateway in gateways.values(): - discover_tasks.append(_discover_persistent_devices(hass, hass_config, gateway)) - start_tasks.append(_gw_start(hass, gateway)) + discover_tasks.append(_discover_persistent_devices(hass, entry, gateway)) + start_tasks.append(_gw_start(hass, entry, gateway)) if discover_tasks: # Make sure all devices and platforms are loaded before gateway start. await asyncio.wait(discover_tasks) @@ -180,43 +245,58 @@ async def finish_setup(hass, hass_config, gateways): await asyncio.wait(start_tasks) -async def _discover_persistent_devices(hass, hass_config, gateway): +async def _discover_persistent_devices( + hass: HomeAssistantType, entry: ConfigEntry, gateway: BaseAsyncGateway +): """Discover platforms for devices loaded via persistence file.""" tasks = [] new_devices = defaultdict(list) for node_id in gateway.sensors: if not validate_node(gateway, node_id): continue - node = gateway.sensors[node_id] - for child in node.children.values(): - validated = validate_child(gateway, node_id, child) + node: Sensor = gateway.sensors[node_id] + for child in node.children.values(): # child is of type ChildSensor + validated = validate_child(entry.entry_id, gateway, node_id, child) for platform, dev_ids in validated.items(): new_devices[platform].extend(dev_ids) + _LOGGER.debug("discovering persistent devices: %s", new_devices) for platform, dev_ids in new_devices.items(): - tasks.append(discover_mysensors_platform(hass, hass_config, platform, dev_ids)) + discover_mysensors_platform(hass, entry.entry_id, platform, dev_ids) if tasks: await asyncio.wait(tasks) -async def _gw_start(hass, gateway): +async def gw_stop(hass, entry: ConfigEntry, gateway: BaseAsyncGateway): + """Stop the gateway.""" + connect_task = hass.data[DOMAIN].get( + MYSENSORS_GATEWAY_START_TASK.format(entry.entry_id) + ) + if connect_task is not None and not connect_task.done(): + connect_task.cancel() + await gateway.stop() + + +async def _gw_start( + hass: HomeAssistantType, entry: ConfigEntry, gateway: BaseAsyncGateway +): """Start the gateway.""" # Don't use hass.async_create_task to avoid holding up setup indefinitely. - connect_task = hass.loop.create_task(gateway.start()) + hass.data[DOMAIN][ + MYSENSORS_GATEWAY_START_TASK.format(entry.entry_id) + ] = asyncio.create_task( + gateway.start() + ) # store the connect task so it can be cancelled in gw_stop - @callback - def gw_stop(event): - """Trigger to stop the gateway.""" - hass.async_create_task(gateway.stop()) - if not connect_task.done(): - connect_task.cancel() + async def stop_this_gw(_: Event): + await gw_stop(hass, entry, gateway) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gw_stop) - if gateway.device == "mqtt": + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_this_gw) + if entry.data[CONF_DEVICE] == MQTT_COMPONENT: # Gatways connected via mqtt doesn't send gateway ready message. return gateway_ready = asyncio.Future() - gateway_ready_key = MYSENSORS_GATEWAY_READY.format(id(gateway)) - hass.data[gateway_ready_key] = gateway_ready + gateway_ready_key = MYSENSORS_GATEWAY_READY.format(entry.entry_id) + hass.data[DOMAIN][gateway_ready_key] = gateway_ready try: with async_timeout.timeout(GATEWAY_READY_TIMEOUT): @@ -224,27 +304,35 @@ async def _gw_start(hass, gateway): except asyncio.TimeoutError: _LOGGER.warning( "Gateway %s not ready after %s secs so continuing with setup", - gateway.device, + entry.data[CONF_DEVICE], GATEWAY_READY_TIMEOUT, ) finally: - hass.data.pop(gateway_ready_key, None) + hass.data[DOMAIN].pop(gateway_ready_key, None) -def _gw_callback_factory(hass, hass_config): +def _gw_callback_factory( + hass: HomeAssistantType, gateway_id: GatewayId +) -> Callable[[Message], None]: """Return a new callback for the gateway.""" @callback - def mysensors_callback(msg): - """Handle messages from a MySensors gateway.""" + def mysensors_callback(msg: Message): + """Handle messages from a MySensors gateway. + + All MySenors messages are received here. + The messages are passed to handler functions depending on their type. + """ _LOGGER.debug("Node update: node %s child %s", msg.node_id, msg.child_id) msg_type = msg.gateway.const.MessageType(msg.type) - msg_handler = HANDLERS.get(msg_type.name) + msg_handler: Callable[ + [Any, GatewayId, Message], Coroutine[None] + ] = HANDLERS.get(msg_type.name) if msg_handler is None: return - hass.async_create_task(msg_handler(hass, hass_config, msg)) + hass.async_create_task(msg_handler(hass, gateway_id, msg)) return mysensors_callback diff --git a/homeassistant/components/mysensors/handler.py b/homeassistant/components/mysensors/handler.py index b5b8b511aee..10165a171e0 100644 --- a/homeassistant/components/mysensors/handler.py +++ b/homeassistant/components/mysensors/handler.py @@ -1,9 +1,21 @@ """Handle MySensors messages.""" +from typing import Dict, List + +from mysensors import Message + from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import decorator -from .const import CHILD_CALLBACK, MYSENSORS_GATEWAY_READY, NODE_CALLBACK +from .const import ( + CHILD_CALLBACK, + DOMAIN, + MYSENSORS_GATEWAY_READY, + NODE_CALLBACK, + DevId, + GatewayId, +) from .device import get_mysensors_devices from .helpers import discover_mysensors_platform, validate_set_msg @@ -11,75 +23,91 @@ HANDLERS = decorator.Registry() @HANDLERS.register("set") -async def handle_set(hass, hass_config, msg): +async def handle_set( + hass: HomeAssistantType, gateway_id: GatewayId, msg: Message +) -> None: """Handle a mysensors set message.""" - validated = validate_set_msg(msg) - _handle_child_update(hass, hass_config, validated) + validated = validate_set_msg(gateway_id, msg) + _handle_child_update(hass, gateway_id, validated) @HANDLERS.register("internal") -async def handle_internal(hass, hass_config, msg): +async def handle_internal( + hass: HomeAssistantType, gateway_id: GatewayId, msg: Message +) -> None: """Handle a mysensors internal message.""" internal = msg.gateway.const.Internal(msg.sub_type) handler = HANDLERS.get(internal.name) if handler is None: return - await handler(hass, hass_config, msg) + await handler(hass, gateway_id, msg) @HANDLERS.register("I_BATTERY_LEVEL") -async def handle_battery_level(hass, hass_config, msg): +async def handle_battery_level( + hass: HomeAssistantType, gateway_id: GatewayId, msg: Message +) -> None: """Handle an internal battery level message.""" - _handle_node_update(hass, msg) + _handle_node_update(hass, gateway_id, msg) @HANDLERS.register("I_HEARTBEAT_RESPONSE") -async def handle_heartbeat(hass, hass_config, msg): +async def handle_heartbeat( + hass: HomeAssistantType, gateway_id: GatewayId, msg: Message +) -> None: """Handle an heartbeat.""" - _handle_node_update(hass, msg) + _handle_node_update(hass, gateway_id, msg) @HANDLERS.register("I_SKETCH_NAME") -async def handle_sketch_name(hass, hass_config, msg): +async def handle_sketch_name( + hass: HomeAssistantType, gateway_id: GatewayId, msg: Message +) -> None: """Handle an internal sketch name message.""" - _handle_node_update(hass, msg) + _handle_node_update(hass, gateway_id, msg) @HANDLERS.register("I_SKETCH_VERSION") -async def handle_sketch_version(hass, hass_config, msg): +async def handle_sketch_version( + hass: HomeAssistantType, gateway_id: GatewayId, msg: Message +) -> None: """Handle an internal sketch version message.""" - _handle_node_update(hass, msg) + _handle_node_update(hass, gateway_id, msg) @HANDLERS.register("I_GATEWAY_READY") -async def handle_gateway_ready(hass, hass_config, msg): +async def handle_gateway_ready( + hass: HomeAssistantType, gateway_id: GatewayId, msg: Message +) -> None: """Handle an internal gateway ready message. Set asyncio future result if gateway is ready. """ - gateway_ready = hass.data.get(MYSENSORS_GATEWAY_READY.format(id(msg.gateway))) + gateway_ready = hass.data[DOMAIN].get(MYSENSORS_GATEWAY_READY.format(gateway_id)) if gateway_ready is None or gateway_ready.cancelled(): return gateway_ready.set_result(True) @callback -def _handle_child_update(hass, hass_config, validated): +def _handle_child_update( + hass: HomeAssistantType, gateway_id: GatewayId, validated: Dict[str, List[DevId]] +): """Handle a child update.""" - signals = [] + signals: List[str] = [] # Update all platforms for the device via dispatcher. # Add/update entity for validated children. for platform, dev_ids in validated.items(): devices = get_mysensors_devices(hass, platform) - new_dev_ids = [] + new_dev_ids: List[DevId] = [] for dev_id in dev_ids: if dev_id in devices: signals.append(CHILD_CALLBACK.format(*dev_id)) else: new_dev_ids.append(dev_id) if new_dev_ids: - discover_mysensors_platform(hass, hass_config, platform, new_dev_ids) + discover_mysensors_platform(hass, gateway_id, platform, new_dev_ids) for signal in set(signals): # Only one signal per device is needed. # A device can have multiple platforms, ie multiple schemas. @@ -87,7 +115,7 @@ def _handle_child_update(hass, hass_config, validated): @callback -def _handle_node_update(hass, msg): +def _handle_node_update(hass: HomeAssistantType, gateway_id: GatewayId, msg: Message): """Handle a node update.""" - signal = NODE_CALLBACK.format(id(msg.gateway), msg.node_id) + signal = NODE_CALLBACK.format(gateway_id, msg.node_id) async_dispatcher_send(hass, signal) diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py index 20b266e550e..d06bf0dee2f 100644 --- a/homeassistant/components/mysensors/helpers.py +++ b/homeassistant/components/mysensors/helpers.py @@ -1,78 +1,109 @@ """Helper functions for mysensors package.""" from collections import defaultdict +from enum import IntEnum import logging +from typing import DefaultDict, Dict, List, Optional, Set +from mysensors import BaseAsyncGateway, Message +from mysensors.sensor import ChildSensor import voluptuous as vol from homeassistant.const import CONF_NAME from homeassistant.core import callback -from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util.decorator import Registry -from .const import ATTR_DEVICES, DOMAIN, FLAT_PLATFORM_TYPES, TYPE_TO_PLATFORMS +from .const import ( + ATTR_DEVICES, + ATTR_GATEWAY_ID, + DOMAIN, + FLAT_PLATFORM_TYPES, + MYSENSORS_DISCOVERY, + TYPE_TO_PLATFORMS, + DevId, + GatewayId, + SensorType, + ValueType, +) _LOGGER = logging.getLogger(__name__) SCHEMAS = Registry() @callback -def discover_mysensors_platform(hass, hass_config, platform, new_devices): +def discover_mysensors_platform( + hass, gateway_id: GatewayId, platform: str, new_devices: List[DevId] +) -> None: """Discover a MySensors platform.""" - task = hass.async_create_task( - discovery.async_load_platform( - hass, - platform, - DOMAIN, - {ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN}, - hass_config, - ) + _LOGGER.debug("Discovering platform %s with devIds: %s", platform, new_devices) + async_dispatcher_send( + hass, + MYSENSORS_DISCOVERY.format(gateway_id, platform), + { + ATTR_DEVICES: new_devices, + CONF_NAME: DOMAIN, + ATTR_GATEWAY_ID: gateway_id, + }, ) - return task -def default_schema(gateway, child, value_type_name): +def default_schema( + gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType +) -> vol.Schema: """Return a default validation schema for value types.""" schema = {value_type_name: cv.string} return get_child_schema(gateway, child, value_type_name, schema) @SCHEMAS.register(("light", "V_DIMMER")) -def light_dimmer_schema(gateway, child, value_type_name): +def light_dimmer_schema( + gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType +) -> vol.Schema: """Return a validation schema for V_DIMMER.""" schema = {"V_DIMMER": cv.string, "V_LIGHT": cv.string} return get_child_schema(gateway, child, value_type_name, schema) @SCHEMAS.register(("light", "V_PERCENTAGE")) -def light_percentage_schema(gateway, child, value_type_name): +def light_percentage_schema( + gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType +) -> vol.Schema: """Return a validation schema for V_PERCENTAGE.""" schema = {"V_PERCENTAGE": cv.string, "V_STATUS": cv.string} return get_child_schema(gateway, child, value_type_name, schema) @SCHEMAS.register(("light", "V_RGB")) -def light_rgb_schema(gateway, child, value_type_name): +def light_rgb_schema( + gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType +) -> vol.Schema: """Return a validation schema for V_RGB.""" schema = {"V_RGB": cv.string, "V_STATUS": cv.string} return get_child_schema(gateway, child, value_type_name, schema) @SCHEMAS.register(("light", "V_RGBW")) -def light_rgbw_schema(gateway, child, value_type_name): +def light_rgbw_schema( + gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType +) -> vol.Schema: """Return a validation schema for V_RGBW.""" schema = {"V_RGBW": cv.string, "V_STATUS": cv.string} return get_child_schema(gateway, child, value_type_name, schema) @SCHEMAS.register(("switch", "V_IR_SEND")) -def switch_ir_send_schema(gateway, child, value_type_name): +def switch_ir_send_schema( + gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType +) -> vol.Schema: """Return a validation schema for V_IR_SEND.""" schema = {"V_IR_SEND": cv.string, "V_LIGHT": cv.string} return get_child_schema(gateway, child, value_type_name, schema) -def get_child_schema(gateway, child, value_type_name, schema): +def get_child_schema( + gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType, schema +) -> vol.Schema: """Return a child schema.""" set_req = gateway.const.SetReq child_schema = child.get_schema(gateway.protocol_version) @@ -88,7 +119,9 @@ def get_child_schema(gateway, child, value_type_name, schema): return schema -def invalid_msg(gateway, child, value_type_name): +def invalid_msg( + gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType +): """Return a message for an invalid child during schema validation.""" pres = gateway.const.Presentation set_req = gateway.const.SetReq @@ -97,15 +130,15 @@ def invalid_msg(gateway, child, value_type_name): ) -def validate_set_msg(msg): +def validate_set_msg(gateway_id: GatewayId, msg: Message) -> Dict[str, List[DevId]]: """Validate a set message.""" if not validate_node(msg.gateway, msg.node_id): return {} child = msg.gateway.sensors[msg.node_id].children[msg.child_id] - return validate_child(msg.gateway, msg.node_id, child, msg.sub_type) + return validate_child(gateway_id, msg.gateway, msg.node_id, child, msg.sub_type) -def validate_node(gateway, node_id): +def validate_node(gateway: BaseAsyncGateway, node_id: int) -> bool: """Validate a node.""" if gateway.sensors[node_id].sketch_name is None: _LOGGER.debug("Node %s is missing sketch name", node_id) @@ -113,31 +146,39 @@ def validate_node(gateway, node_id): return True -def validate_child(gateway, node_id, child, value_type=None): - """Validate a child.""" - validated = defaultdict(list) - pres = gateway.const.Presentation - set_req = gateway.const.SetReq - child_type_name = next( +def validate_child( + gateway_id: GatewayId, + gateway: BaseAsyncGateway, + node_id: int, + child: ChildSensor, + value_type: Optional[int] = None, +) -> DefaultDict[str, List[DevId]]: + """Validate a child. Returns a dict mapping hass platform names to list of DevId.""" + validated: DefaultDict[str, List[DevId]] = defaultdict(list) + pres: IntEnum = gateway.const.Presentation + set_req: IntEnum = gateway.const.SetReq + child_type_name: Optional[SensorType] = next( (member.name for member in pres if member.value == child.type), None ) - value_types = {value_type} if value_type else {*child.values} - value_type_names = { + value_types: Set[int] = {value_type} if value_type else {*child.values} + value_type_names: Set[ValueType] = { member.name for member in set_req if member.value in value_types } - platforms = TYPE_TO_PLATFORMS.get(child_type_name, []) + platforms: List[str] = TYPE_TO_PLATFORMS.get(child_type_name, []) if not platforms: _LOGGER.warning("Child type %s is not supported", child.type) return validated for platform in platforms: - platform_v_names = FLAT_PLATFORM_TYPES[platform, child_type_name] - v_names = platform_v_names & value_type_names + platform_v_names: Set[ValueType] = FLAT_PLATFORM_TYPES[ + platform, child_type_name + ] + v_names: Set[ValueType] = platform_v_names & value_type_names if not v_names: - child_value_names = { + child_value_names: Set[ValueType] = { member.name for member in set_req if member.value in child.values } - v_names = platform_v_names & child_value_names + v_names: Set[ValueType] = platform_v_names & child_value_names for v_name in v_names: child_schema_gen = SCHEMAS.get((platform, v_name), default_schema) @@ -153,7 +194,12 @@ def validate_child(gateway, node_id, child, value_type=None): exc, ) continue - dev_id = id(gateway), node_id, child.id, set_req[v_name].value + dev_id: DevId = ( + gateway_id, + node_id, + child.id, + set_req[v_name].value, + ) validated[platform].append(dev_id) return validated diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py index ffbcba6f032..f90f9c5c81c 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -1,4 +1,6 @@ """Support for MySensors lights.""" +from typing import Callable + from homeassistant.components import mysensors from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -10,27 +12,47 @@ from homeassistant.components.light import ( SUPPORT_WHITE_VALUE, LightEntity, ) +from homeassistant.components.mysensors import on_unload +from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY +from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.color as color_util from homeassistant.util.color import rgb_hex_to_rgb_list SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the mysensors platform for lights.""" +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable +): + """Set up this platform for a specific ConfigEntry(==Gateway).""" device_class_map = { "S_DIMMER": MySensorsLightDimmer, "S_RGB_LIGHT": MySensorsLightRGB, "S_RGBW_LIGHT": MySensorsLightRGBW, } - mysensors.setup_mysensors_platform( + + async def async_discover(discovery_info): + """Discover and add a MySensors light.""" + mysensors.setup_mysensors_platform( + hass, + DOMAIN, + discovery_info, + device_class_map, + async_add_entities=async_add_entities, + ) + + await on_unload( hass, - DOMAIN, - discovery_info, - device_class_map, - async_add_entities=async_add_entities, + config_entry, + async_dispatcher_connect( + hass, + MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN), + async_discover, + ), ) @@ -60,11 +82,6 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity): """Return the white value of this light between 0..255.""" return self._white - @property - def assumed_state(self): - """Return true if unable to access real state of entity.""" - return self.gateway.optimistic - @property def is_on(self): """Return true if device is on.""" @@ -80,7 +97,7 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity): self.node_id, self.child_id, set_req.V_LIGHT, 1, ack=1 ) - if self.gateway.optimistic: + if self.assumed_state: # optimistically assume that light has changed state self._state = True self._values[set_req.V_LIGHT] = STATE_ON @@ -102,7 +119,7 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity): self.node_id, self.child_id, set_req.V_DIMMER, percent, ack=1 ) - if self.gateway.optimistic: + if self.assumed_state: # optimistically assume that light has changed state self._brightness = brightness self._values[set_req.V_DIMMER] = percent @@ -135,7 +152,7 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity): self.node_id, self.child_id, self.value_type, hex_color, ack=1 ) - if self.gateway.optimistic: + if self.assumed_state: # optimistically assume that light has changed state self._hs = color_util.color_RGB_to_hs(*rgb) self._white = white @@ -145,7 +162,7 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity): """Turn the device off.""" value_type = self.gateway.const.SetReq.V_LIGHT self.gateway.set_child_value(self.node_id, self.child_id, value_type, 0, ack=1) - if self.gateway.optimistic: + if self.assumed_state: # optimistically assume that light has changed state self._state = False self._values[value_type] = STATE_OFF @@ -188,7 +205,7 @@ class MySensorsLightDimmer(MySensorsLight): """Turn the device on.""" self._turn_on_light() self._turn_on_dimmer(**kwargs) - if self.gateway.optimistic: + if self.assumed_state: self.async_write_ha_state() async def async_update(self): @@ -214,7 +231,7 @@ class MySensorsLightRGB(MySensorsLight): self._turn_on_light() self._turn_on_dimmer(**kwargs) self._turn_on_rgb_and_w("%02x%02x%02x", **kwargs) - if self.gateway.optimistic: + if self.assumed_state: self.async_write_ha_state() async def async_update(self): @@ -241,5 +258,5 @@ class MySensorsLightRGBW(MySensorsLightRGB): self._turn_on_light() self._turn_on_dimmer(**kwargs) self._turn_on_rgb_and_w("%02x%02x%02x%02x", **kwargs) - if self.gateway.optimistic: + if self.assumed_state: self.async_write_ha_state() diff --git a/homeassistant/components/mysensors/manifest.json b/homeassistant/components/mysensors/manifest.json index afeeb5d57cc..8371f2930c2 100644 --- a/homeassistant/components/mysensors/manifest.json +++ b/homeassistant/components/mysensors/manifest.json @@ -2,7 +2,15 @@ "domain": "mysensors", "name": "MySensors", "documentation": "https://www.home-assistant.io/integrations/mysensors", - "requirements": ["pymysensors==0.18.0"], - "after_dependencies": ["mqtt"], - "codeowners": ["@MartinHjelmare"] + "requirements": [ + "pymysensors==0.20.1" + ], + "after_dependencies": [ + "mqtt" + ], + "codeowners": [ + "@MartinHjelmare", + "@functionpointer" + ], + "config_flow": true } diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index bab6bf3fc40..a09f8af1394 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -1,6 +1,11 @@ """Support for MySensors sensors.""" +from typing import Callable + from homeassistant.components import mysensors +from homeassistant.components.mysensors import on_unload +from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY from homeassistant.components.sensor import DOMAIN +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONDUCTIVITY, DEGREE, @@ -18,6 +23,8 @@ from homeassistant.const import ( VOLT, VOLUME_CUBIC_METERS, ) +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType SENSORS = { "V_TEMP": [None, "mdi:thermometer"], @@ -54,14 +61,29 @@ SENSORS = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the MySensors platform for sensors.""" - mysensors.setup_mysensors_platform( +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable +): + """Set up this platform for a specific ConfigEntry(==Gateway).""" + + async def async_discover(discovery_info): + """Discover and add a MySensors sensor.""" + mysensors.setup_mysensors_platform( + hass, + DOMAIN, + discovery_info, + MySensorsSensor, + async_add_entities=async_add_entities, + ) + + await on_unload( hass, - DOMAIN, - discovery_info, - MySensorsSensor, - async_add_entities=async_add_entities, + config_entry, + async_dispatcher_connect( + hass, + MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN), + async_discover, + ), ) @@ -105,7 +127,7 @@ class MySensorsSensor(mysensors.device.MySensorsEntity): pres = self.gateway.const.Presentation set_req = self.gateway.const.SetReq SENSORS[set_req.V_TEMP.name][0] = ( - TEMP_CELSIUS if self.gateway.metric else TEMP_FAHRENHEIT + TEMP_CELSIUS if self.hass.config.units.is_metric else TEMP_FAHRENHEIT ) sensor_type = SENSORS.get(set_req(self.value_type).name, [None, None]) if isinstance(sensor_type, dict): diff --git a/homeassistant/components/mysensors/strings.json b/homeassistant/components/mysensors/strings.json new file mode 100644 index 00000000000..43a68f61e24 --- /dev/null +++ b/homeassistant/components/mysensors/strings.json @@ -0,0 +1,79 @@ +{ + "title": "MySensors", + "config": { + "step": { + "user": { + "data": { + "gateway_type": "Gateway type" + }, + "description": "Choose connection method to the gateway" + }, + "gw_tcp": { + "description": "Ethernet gateway setup", + "data": { + "device": "IP address of the gateway", + "tcp_port": "port", + "version": "MySensors version", + "persistence_file": "persistence file (leave empty to auto-generate)" + } + }, + "gw_serial": { + "description": "Serial gateway setup", + "data": { + "device": "Serial port", + "baud_rate": "baud rate", + "version": "MySensors version", + "persistence_file": "persistence file (leave empty to auto-generate)" + } + }, + "gw_mqtt": { + "description": "MQTT gateway setup", + "data": { + "retain": "mqtt retain", + "topic_in_prefix": "prefix for input topics (topic_in_prefix)", + "topic_out_prefix": "prefix for output topics (topic_out_prefix)", + "version": "MySensors version", + "persistence_file": "persistence file (leave empty to auto-generate)" + } + } + }, + "error": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_subscribe_topic": "Invalid subscribe topic", + "invalid_publish_topic": "Invalid publish topic", + "duplicate_topic": "Topic already in use", + "same_topic": "Subscribe and publish topics are the same", + "invalid_port": "Invalid port number", + "invalid_persistence_file": "Invalid persistence file", + "duplicate_persistence_file": "Persistence file already in use", + "invalid_ip": "Invalid IP address", + "invalid_serial": "Invalid serial port", + "invalid_device": "Invalid device", + "invalid_version": "Invalid MySensors version", + "not_a_number": "Please enter a number", + "port_out_of_range": "Port number must be at least 1 and at most 65535", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_subscribe_topic": "Invalid subscribe topic", + "invalid_publish_topic": "Invalid publish topic", + "duplicate_topic": "Topic already in use", + "same_topic": "Subscribe and publish topics are the same", + "invalid_port": "Invalid port number", + "invalid_persistence_file": "Invalid persistence file", + "duplicate_persistence_file": "Persistence file already in use", + "invalid_ip": "Invalid IP address", + "invalid_serial": "Invalid serial port", + "invalid_device": "Invalid device", + "invalid_version": "Invalid MySensors version", + "not_a_number": "Please enter a number", + "port_out_of_range": "Port number must be at least 1 and at most 65535", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py index 0da8bfe7030..14911e11090 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -1,4 +1,6 @@ """Support for MySensors switches.""" +from typing import Callable + import voluptuous as vol from homeassistant.components import mysensors @@ -6,7 +8,11 @@ from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv -from .const import DOMAIN as MYSENSORS_DOMAIN, SERVICE_SEND_IR_CODE +from . import on_unload +from ...config_entries import ConfigEntry +from ...helpers.dispatcher import async_dispatcher_connect +from ...helpers.typing import HomeAssistantType +from .const import DOMAIN as MYSENSORS_DOMAIN, MYSENSORS_DISCOVERY, SERVICE_SEND_IR_CODE ATTR_IR_CODE = "V_IR_SEND" @@ -15,8 +21,10 @@ SEND_IR_CODE_SERVICE_SCHEMA = vol.Schema( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the mysensors platform for switches.""" +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable +): + """Set up this platform for a specific ConfigEntry(==Gateway).""" device_class_map = { "S_DOOR": MySensorsSwitch, "S_MOTION": MySensorsSwitch, @@ -32,13 +40,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "S_MOISTURE": MySensorsSwitch, "S_WATER_QUALITY": MySensorsSwitch, } - mysensors.setup_mysensors_platform( - hass, - DOMAIN, - discovery_info, - device_class_map, - async_add_entities=async_add_entities, - ) + + async def async_discover(discovery_info): + """Discover and add a MySensors switch.""" + mysensors.setup_mysensors_platform( + hass, + DOMAIN, + discovery_info, + device_class_map, + async_add_entities=async_add_entities, + ) async def async_send_ir_code_service(service): """Set IR code as device state attribute.""" @@ -71,15 +82,20 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= schema=SEND_IR_CODE_SERVICE_SCHEMA, ) + await on_unload( + hass, + config_entry, + async_dispatcher_connect( + hass, + MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN), + async_discover, + ), + ) + class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchEntity): """Representation of the value of a MySensors Switch child node.""" - @property - def assumed_state(self): - """Return True if unable to access real state of entity.""" - return self.gateway.optimistic - @property def current_power_w(self): """Return the current power usage in W.""" @@ -96,7 +112,7 @@ class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchEntity): self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, 1, ack=1 ) - if self.gateway.optimistic: + if self.assumed_state: # Optimistically assume that switch has changed state self._values[self.value_type] = STATE_ON self.async_write_ha_state() @@ -106,7 +122,7 @@ class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchEntity): self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, 0, ack=1 ) - if self.gateway.optimistic: + if self.assumed_state: # Optimistically assume that switch has changed state self._values[self.value_type] = STATE_OFF self.async_write_ha_state() @@ -137,7 +153,7 @@ class MySensorsIRSwitch(MySensorsSwitch): self.gateway.set_child_value( self.node_id, self.child_id, set_req.V_LIGHT, 1, ack=1 ) - if self.gateway.optimistic: + if self.assumed_state: # Optimistically assume that switch has changed state self._values[self.value_type] = self._ir_code self._values[set_req.V_LIGHT] = STATE_ON @@ -151,7 +167,7 @@ class MySensorsIRSwitch(MySensorsSwitch): self.gateway.set_child_value( self.node_id, self.child_id, set_req.V_LIGHT, 0, ack=1 ) - if self.gateway.optimistic: + if self.assumed_state: # Optimistically assume that switch has changed state self._values[set_req.V_LIGHT] = STATE_OFF self.async_write_ha_state() diff --git a/homeassistant/components/mysensors/translations/en.json b/homeassistant/components/mysensors/translations/en.json new file mode 100644 index 00000000000..d7730ba09b6 --- /dev/null +++ b/homeassistant/components/mysensors/translations/en.json @@ -0,0 +1,79 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect", + "invalid_subscribe_topic": "Invalid subscribe topic", + "invalid_publish_topic": "Invalid publish topic", + "duplicate_topic": "Topic already in use", + "same_topic": "Subscribe and publish topics are the same", + "invalid_port": "Invalid port number", + "invalid_persistence_file": "Invalid persistence file", + "duplicate_persistence_file": "Persistence file already in use", + "invalid_ip": "Invalid IP address", + "invalid_serial": "Invalid serial port", + "invalid_device": "Invalid device", + "invalid_version": "Invalid MySensors version", + "not_a_number": "Please enter a number", + "port_out_of_range": "Port number must be at least 1 and at most 65535", + "unknown": "Unexpected error" + }, + "error": { + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect", + "invalid_subscribe_topic": "Invalid subscribe topic", + "invalid_publish_topic": "Invalid publish topic", + "duplicate_topic": "Topic already in use", + "same_topic": "Subscribe and publish topics are the same", + "invalid_port": "Invalid port number", + "invalid_persistence_file": "Invalid persistence file", + "duplicate_persistence_file": "Persistence file already in use", + "invalid_ip": "Invalid IP address", + "invalid_serial": "Invalid serial port", + "invalid_device": "Invalid device", + "invalid_version": "Invalid MySensors version", + "not_a_number": "Please enter a number", + "port_out_of_range": "Port number must be at least 1 and at most 65535", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "optimistic": "optimistic", + "persistence": "persistence", + "gateway_type": "Gateway type" + }, + "description": "Choose connection method to the gateway" + }, + "gw_tcp": { + "description": "Ethernet gateway setup", + "data": { + "device": "IP address of the gateway", + "tcp_port": "port", + "version": "MySensors version", + "persistence_file": "persistence file (leave empty to auto-generate)" + } + }, + "gw_serial": { + "description": "Serial gateway setup", + "data": { + "device": "Serial port", + "baud_rate": "baud rate", + "version": "MySensors version", + "persistence_file": "persistence file (leave empty to auto-generate)" + } + }, + "gw_mqtt": { + "description": "MQTT gateway setup", + "data": { + "retain": "mqtt retain", + "topic_in_prefix": "prefix for input topics (topic_in_prefix)", + "topic_out_prefix": "prefix for output topics (topic_out_prefix)", + "version": "MySensors version", + "persistence_file": "persistence file (leave empty to auto-generate)" + } + } + } + }, + "title": "MySensors" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index c2e36d9f846..6366a3eb887 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -136,6 +136,7 @@ FLOWS = [ "motion_blinds", "mqtt", "myq", + "mysensors", "neato", "nest", "netatmo", diff --git a/requirements_all.txt b/requirements_all.txt index 2a78b25a162..2cc29c3be4b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1551,7 +1551,7 @@ pymusiccast==0.1.6 pymyq==2.0.14 # homeassistant.components.mysensors -pymysensors==0.18.0 +pymysensors==0.20.1 # homeassistant.components.nanoleaf pynanoleaf==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 024de6596b6..2a447b539ef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -810,6 +810,9 @@ pymonoprice==0.3 # homeassistant.components.myq pymyq==2.0.14 +# homeassistant.components.mysensors +pymysensors==0.20.1 + # homeassistant.components.nuki pynuki==1.3.8 diff --git a/tests/components/mysensors/__init__.py b/tests/components/mysensors/__init__.py new file mode 100644 index 00000000000..68fc6d7b4d7 --- /dev/null +++ b/tests/components/mysensors/__init__.py @@ -0,0 +1 @@ +"""Tests for the MySensors integration.""" diff --git a/tests/components/mysensors/test_config_flow.py b/tests/components/mysensors/test_config_flow.py new file mode 100644 index 00000000000..6bfec3b102e --- /dev/null +++ b/tests/components/mysensors/test_config_flow.py @@ -0,0 +1,735 @@ +"""Test the MySensors config flow.""" +from typing import Dict, Optional, Tuple +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries, setup +from homeassistant.components.mysensors.const import ( + CONF_BAUD_RATE, + CONF_DEVICE, + CONF_GATEWAY_TYPE, + CONF_GATEWAY_TYPE_MQTT, + CONF_GATEWAY_TYPE_SERIAL, + CONF_GATEWAY_TYPE_TCP, + CONF_PERSISTENCE, + CONF_PERSISTENCE_FILE, + CONF_RETAIN, + CONF_TCP_PORT, + CONF_TOPIC_IN_PREFIX, + CONF_TOPIC_OUT_PREFIX, + CONF_VERSION, + DOMAIN, + ConfGatewayType, +) +from homeassistant.helpers.typing import HomeAssistantType + +from tests.common import MockConfigEntry + + +async def get_form( + hass: HomeAssistantType, gatway_type: ConfGatewayType, expected_step_id: str +): + """Get a form for the given gateway type.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + stepuser = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert stepuser["type"] == "form" + assert not stepuser["errors"] + + result = await hass.config_entries.flow.async_configure( + stepuser["flow_id"], + {CONF_GATEWAY_TYPE: gatway_type}, + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["step_id"] == expected_step_id + + return result + + +async def test_config_mqtt(hass: HomeAssistantType): + """Test configuring a mqtt gateway.""" + step = await get_form(hass, CONF_GATEWAY_TYPE_MQTT, "gw_mqtt") + flow_id = step["flow_id"] + + with patch( + "homeassistant.components.mysensors.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.mysensors.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + flow_id, + { + CONF_RETAIN: True, + CONF_TOPIC_IN_PREFIX: "bla", + CONF_TOPIC_OUT_PREFIX: "blub", + CONF_VERSION: "2.4", + }, + ) + await hass.async_block_till_done() + + if "errors" in result2: + assert not result2["errors"] + assert result2["type"] == "create_entry" + assert result2["title"] == "mqtt" + assert result2["data"] == { + CONF_DEVICE: "mqtt", + CONF_RETAIN: True, + CONF_TOPIC_IN_PREFIX: "bla", + CONF_TOPIC_OUT_PREFIX: "blub", + CONF_VERSION: "2.4", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_config_serial(hass: HomeAssistantType): + """Test configuring a gateway via serial.""" + step = await get_form(hass, CONF_GATEWAY_TYPE_SERIAL, "gw_serial") + flow_id = step["flow_id"] + + with patch( # mock is_serial_port because otherwise the test will be platform dependent (/dev/ttyACMx vs COMx) + "homeassistant.components.mysensors.config_flow.is_serial_port", + return_value=True, + ), patch( + "homeassistant.components.mysensors.config_flow.try_connect", return_value=True + ), patch( + "homeassistant.components.mysensors.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.mysensors.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + flow_id, + { + CONF_BAUD_RATE: 115200, + CONF_DEVICE: "/dev/ttyACM0", + CONF_VERSION: "2.4", + }, + ) + await hass.async_block_till_done() + + if "errors" in result2: + assert not result2["errors"] + assert result2["type"] == "create_entry" + assert result2["title"] == "/dev/ttyACM0" + assert result2["data"] == { + CONF_DEVICE: "/dev/ttyACM0", + CONF_BAUD_RATE: 115200, + CONF_VERSION: "2.4", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_config_tcp(hass: HomeAssistantType): + """Test configuring a gateway via tcp.""" + step = await get_form(hass, CONF_GATEWAY_TYPE_TCP, "gw_tcp") + flow_id = step["flow_id"] + + with patch( + "homeassistant.components.mysensors.config_flow.try_connect", return_value=True + ), patch( + "homeassistant.components.mysensors.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.mysensors.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + flow_id, + { + CONF_TCP_PORT: 5003, + CONF_DEVICE: "127.0.0.1", + CONF_VERSION: "2.4", + }, + ) + await hass.async_block_till_done() + + if "errors" in result2: + assert not result2["errors"] + assert result2["type"] == "create_entry" + assert result2["title"] == "127.0.0.1" + assert result2["data"] == { + CONF_DEVICE: "127.0.0.1", + CONF_TCP_PORT: 5003, + CONF_VERSION: "2.4", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_fail_to_connect(hass: HomeAssistantType): + """Test configuring a gateway via tcp.""" + step = await get_form(hass, CONF_GATEWAY_TYPE_TCP, "gw_tcp") + flow_id = step["flow_id"] + + with patch( + "homeassistant.components.mysensors.config_flow.try_connect", return_value=False + ), patch( + "homeassistant.components.mysensors.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.mysensors.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + flow_id, + { + CONF_TCP_PORT: 5003, + CONF_DEVICE: "127.0.0.1", + CONF_VERSION: "2.4", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert "errors" in result2 + assert "base" in result2["errors"] + assert result2["errors"]["base"] == "cannot_connect" + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + + +@pytest.mark.parametrize( + "gateway_type, expected_step_id, user_input, err_field, err_string", + [ + ( + CONF_GATEWAY_TYPE_TCP, + "gw_tcp", + { + CONF_TCP_PORT: 600_000, + CONF_DEVICE: "127.0.0.1", + CONF_VERSION: "2.4", + }, + CONF_TCP_PORT, + "port_out_of_range", + ), + ( + CONF_GATEWAY_TYPE_TCP, + "gw_tcp", + { + CONF_TCP_PORT: 0, + CONF_DEVICE: "127.0.0.1", + CONF_VERSION: "2.4", + }, + CONF_TCP_PORT, + "port_out_of_range", + ), + ( + CONF_GATEWAY_TYPE_TCP, + "gw_tcp", + { + CONF_TCP_PORT: 5003, + CONF_DEVICE: "127.0.0.1", + CONF_VERSION: "a", + }, + CONF_VERSION, + "invalid_version", + ), + ( + CONF_GATEWAY_TYPE_TCP, + "gw_tcp", + { + CONF_TCP_PORT: 5003, + CONF_DEVICE: "127.0.0.1", + CONF_VERSION: "a.b", + }, + CONF_VERSION, + "invalid_version", + ), + ( + CONF_GATEWAY_TYPE_TCP, + "gw_tcp", + { + CONF_TCP_PORT: 5003, + CONF_DEVICE: "127.0.0.1", + }, + CONF_VERSION, + "invalid_version", + ), + ( + CONF_GATEWAY_TYPE_TCP, + "gw_tcp", + { + CONF_TCP_PORT: 5003, + CONF_DEVICE: "127.0.0.1", + CONF_VERSION: "4", + }, + CONF_VERSION, + "invalid_version", + ), + ( + CONF_GATEWAY_TYPE_TCP, + "gw_tcp", + { + CONF_TCP_PORT: 5003, + CONF_DEVICE: "127.0.0.1", + CONF_VERSION: "v3", + }, + CONF_VERSION, + "invalid_version", + ), + ( + CONF_GATEWAY_TYPE_TCP, + "gw_tcp", + { + CONF_TCP_PORT: 5003, + CONF_DEVICE: "127.0.0.", + }, + CONF_DEVICE, + "invalid_ip", + ), + ( + CONF_GATEWAY_TYPE_TCP, + "gw_tcp", + { + CONF_TCP_PORT: 5003, + CONF_DEVICE: "abcd", + }, + CONF_DEVICE, + "invalid_ip", + ), + ( + CONF_GATEWAY_TYPE_MQTT, + "gw_mqtt", + { + CONF_RETAIN: True, + CONF_TOPIC_IN_PREFIX: "bla", + CONF_TOPIC_OUT_PREFIX: "blub", + CONF_PERSISTENCE_FILE: "asdf.zip", + CONF_VERSION: "2.4", + }, + CONF_PERSISTENCE_FILE, + "invalid_persistence_file", + ), + ( + CONF_GATEWAY_TYPE_MQTT, + "gw_mqtt", + { + CONF_RETAIN: True, + CONF_TOPIC_IN_PREFIX: "/#/#", + CONF_TOPIC_OUT_PREFIX: "blub", + CONF_VERSION: "2.4", + }, + CONF_TOPIC_IN_PREFIX, + "invalid_subscribe_topic", + ), + ( + CONF_GATEWAY_TYPE_MQTT, + "gw_mqtt", + { + CONF_RETAIN: True, + CONF_TOPIC_IN_PREFIX: "asdf", + CONF_TOPIC_OUT_PREFIX: "/#/#", + CONF_VERSION: "2.4", + }, + CONF_TOPIC_OUT_PREFIX, + "invalid_publish_topic", + ), + ( + CONF_GATEWAY_TYPE_MQTT, + "gw_mqtt", + { + CONF_RETAIN: True, + CONF_TOPIC_IN_PREFIX: "asdf", + CONF_TOPIC_OUT_PREFIX: "asdf", + CONF_VERSION: "2.4", + }, + CONF_TOPIC_OUT_PREFIX, + "same_topic", + ), + ], +) +async def test_config_invalid( + hass: HomeAssistantType, + gateway_type: ConfGatewayType, + expected_step_id: str, + user_input: Dict[str, any], + err_field, + err_string, +): + """Perform a test that is expected to generate an error.""" + step = await get_form(hass, gateway_type, expected_step_id) + flow_id = step["flow_id"] + + with patch( + "homeassistant.components.mysensors.config_flow.try_connect", return_value=True + ), patch( + "homeassistant.components.mysensors.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.mysensors.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + flow_id, + user_input, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert "errors" in result2 + assert err_field in result2["errors"] + assert result2["errors"][err_field] == err_string + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + + +@pytest.mark.parametrize( + "user_input", + [ + { + CONF_DEVICE: "COM5", + CONF_BAUD_RATE: 57600, + CONF_TCP_PORT: 5003, + CONF_RETAIN: True, + CONF_VERSION: "2.3", + CONF_PERSISTENCE_FILE: "bla.json", + }, + { + CONF_DEVICE: "COM5", + CONF_PERSISTENCE_FILE: "bla.json", + CONF_BAUD_RATE: 57600, + CONF_TCP_PORT: 5003, + CONF_VERSION: "2.3", + CONF_PERSISTENCE: False, + CONF_RETAIN: True, + }, + { + CONF_DEVICE: "mqtt", + CONF_BAUD_RATE: 115200, + CONF_TCP_PORT: 5003, + CONF_TOPIC_IN_PREFIX: "intopic", + CONF_TOPIC_OUT_PREFIX: "outtopic", + CONF_VERSION: "2.4", + CONF_PERSISTENCE: False, + CONF_RETAIN: False, + }, + { + CONF_DEVICE: "127.0.0.1", + CONF_PERSISTENCE_FILE: "blub.pickle", + CONF_BAUD_RATE: 115200, + CONF_TCP_PORT: 343, + CONF_VERSION: "2.4", + CONF_PERSISTENCE: False, + CONF_RETAIN: False, + }, + ], +) +async def test_import(hass: HomeAssistantType, user_input: Dict): + """Test importing a gateway.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch("sys.platform", "win32"), patch( + "homeassistant.components.mysensors.config_flow.try_connect", return_value=True + ), patch( + "homeassistant.components.mysensors.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, data=user_input, context={"source": config_entries.SOURCE_IMPORT} + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + + +@pytest.mark.parametrize( + "first_input, second_input, expected_result", + [ + ( + { + CONF_DEVICE: "mqtt", + CONF_VERSION: "2.3", + CONF_TOPIC_IN_PREFIX: "same1", + CONF_TOPIC_OUT_PREFIX: "same2", + }, + { + CONF_DEVICE: "mqtt", + CONF_VERSION: "2.3", + CONF_TOPIC_IN_PREFIX: "same1", + CONF_TOPIC_OUT_PREFIX: "same2", + }, + (CONF_TOPIC_IN_PREFIX, "duplicate_topic"), + ), + ( + { + CONF_DEVICE: "mqtt", + CONF_VERSION: "2.3", + CONF_TOPIC_IN_PREFIX: "different1", + CONF_TOPIC_OUT_PREFIX: "different2", + }, + { + CONF_DEVICE: "mqtt", + CONF_VERSION: "2.3", + CONF_TOPIC_IN_PREFIX: "different3", + CONF_TOPIC_OUT_PREFIX: "different4", + }, + None, + ), + ( + { + CONF_DEVICE: "mqtt", + CONF_VERSION: "2.3", + CONF_TOPIC_IN_PREFIX: "same1", + CONF_TOPIC_OUT_PREFIX: "different2", + }, + { + CONF_DEVICE: "mqtt", + CONF_VERSION: "2.3", + CONF_TOPIC_IN_PREFIX: "same1", + CONF_TOPIC_OUT_PREFIX: "different4", + }, + (CONF_TOPIC_IN_PREFIX, "duplicate_topic"), + ), + ( + { + CONF_DEVICE: "mqtt", + CONF_VERSION: "2.3", + CONF_TOPIC_IN_PREFIX: "same1", + CONF_TOPIC_OUT_PREFIX: "different2", + }, + { + CONF_DEVICE: "mqtt", + CONF_VERSION: "2.3", + CONF_TOPIC_IN_PREFIX: "different1", + CONF_TOPIC_OUT_PREFIX: "same1", + }, + (CONF_TOPIC_OUT_PREFIX, "duplicate_topic"), + ), + ( + { + CONF_DEVICE: "mqtt", + CONF_VERSION: "2.3", + CONF_TOPIC_IN_PREFIX: "same1", + CONF_TOPIC_OUT_PREFIX: "different2", + }, + { + CONF_DEVICE: "mqtt", + CONF_VERSION: "2.3", + CONF_TOPIC_IN_PREFIX: "same1", + CONF_TOPIC_OUT_PREFIX: "different1", + }, + (CONF_TOPIC_IN_PREFIX, "duplicate_topic"), + ), + ( + { + CONF_DEVICE: "127.0.0.1", + CONF_PERSISTENCE_FILE: "same.json", + CONF_TCP_PORT: 343, + CONF_VERSION: "2.3", + CONF_PERSISTENCE: False, + CONF_RETAIN: False, + }, + { + CONF_DEVICE: "192.168.1.2", + CONF_PERSISTENCE_FILE: "same.json", + CONF_TCP_PORT: 343, + CONF_VERSION: "2.3", + CONF_PERSISTENCE: False, + CONF_RETAIN: False, + }, + ("persistence_file", "duplicate_persistence_file"), + ), + ( + { + CONF_DEVICE: "127.0.0.1", + CONF_TCP_PORT: 343, + CONF_VERSION: "2.3", + CONF_PERSISTENCE: False, + CONF_RETAIN: False, + }, + { + CONF_DEVICE: "192.168.1.2", + CONF_PERSISTENCE_FILE: "same.json", + CONF_TCP_PORT: 343, + CONF_VERSION: "2.3", + CONF_PERSISTENCE: False, + CONF_RETAIN: False, + }, + None, + ), + ( + { + CONF_DEVICE: "127.0.0.1", + CONF_TCP_PORT: 343, + CONF_VERSION: "2.3", + CONF_PERSISTENCE: False, + CONF_RETAIN: False, + }, + { + CONF_DEVICE: "192.168.1.2", + CONF_TCP_PORT: 343, + CONF_VERSION: "2.3", + CONF_PERSISTENCE: False, + CONF_RETAIN: False, + }, + None, + ), + ( + { + CONF_DEVICE: "192.168.1.2", + CONF_PERSISTENCE_FILE: "different1.json", + CONF_TCP_PORT: 343, + CONF_VERSION: "2.3", + CONF_PERSISTENCE: False, + CONF_RETAIN: False, + }, + { + CONF_DEVICE: "192.168.1.2", + CONF_PERSISTENCE_FILE: "different2.json", + CONF_TCP_PORT: 343, + CONF_VERSION: "2.3", + CONF_PERSISTENCE: False, + CONF_RETAIN: False, + }, + ("base", "already_configured"), + ), + ( + { + CONF_DEVICE: "192.168.1.2", + CONF_PERSISTENCE_FILE: "different1.json", + CONF_TCP_PORT: 343, + CONF_VERSION: "2.3", + CONF_PERSISTENCE: False, + CONF_RETAIN: False, + }, + { + CONF_DEVICE: "192.168.1.2", + CONF_PERSISTENCE_FILE: "different2.json", + CONF_TCP_PORT: 5003, + CONF_VERSION: "2.3", + CONF_PERSISTENCE: False, + CONF_RETAIN: False, + }, + None, + ), + ( + { + CONF_DEVICE: "192.168.1.2", + CONF_TCP_PORT: 5003, + CONF_VERSION: "2.3", + CONF_PERSISTENCE: False, + CONF_RETAIN: False, + }, + { + CONF_DEVICE: "192.168.1.3", + CONF_TCP_PORT: 5003, + CONF_VERSION: "2.3", + CONF_PERSISTENCE: False, + CONF_RETAIN: False, + }, + None, + ), + ( + { + CONF_DEVICE: "COM5", + CONF_TCP_PORT: 5003, + CONF_RETAIN: True, + CONF_VERSION: "2.3", + CONF_PERSISTENCE_FILE: "different1.json", + }, + { + CONF_DEVICE: "COM5", + CONF_TCP_PORT: 5003, + CONF_RETAIN: True, + CONF_VERSION: "2.3", + CONF_PERSISTENCE_FILE: "different2.json", + }, + ("base", "already_configured"), + ), + ( + { + CONF_DEVICE: "COM6", + CONF_BAUD_RATE: 57600, + CONF_RETAIN: True, + CONF_VERSION: "2.3", + }, + { + CONF_DEVICE: "COM5", + CONF_TCP_PORT: 5003, + CONF_RETAIN: True, + CONF_VERSION: "2.3", + }, + None, + ), + ( + { + CONF_DEVICE: "COM5", + CONF_BAUD_RATE: 115200, + CONF_RETAIN: True, + CONF_VERSION: "2.3", + CONF_PERSISTENCE_FILE: "different1.json", + }, + { + CONF_DEVICE: "COM5", + CONF_BAUD_RATE: 57600, + CONF_RETAIN: True, + CONF_VERSION: "2.3", + CONF_PERSISTENCE_FILE: "different2.json", + }, + ("base", "already_configured"), + ), + ( + { + CONF_DEVICE: "COM5", + CONF_BAUD_RATE: 115200, + CONF_RETAIN: True, + CONF_VERSION: "2.3", + CONF_PERSISTENCE_FILE: "same.json", + }, + { + CONF_DEVICE: "COM6", + CONF_BAUD_RATE: 57600, + CONF_RETAIN: True, + CONF_VERSION: "2.3", + CONF_PERSISTENCE_FILE: "same.json", + }, + ("persistence_file", "duplicate_persistence_file"), + ), + ( + { + CONF_DEVICE: "mqtt", + CONF_PERSISTENCE_FILE: "bla.json", + CONF_BAUD_RATE: 115200, + CONF_TCP_PORT: 5003, + CONF_VERSION: "1.4", + }, + { + CONF_DEVICE: "COM6", + CONF_PERSISTENCE_FILE: "bla2.json", + CONF_BAUD_RATE: 115200, + CONF_TCP_PORT: 5003, + CONF_VERSION: "1.4", + }, + None, + ), + ], +) +async def test_duplicate( + hass: HomeAssistantType, + first_input: Dict, + second_input: Dict, + expected_result: Optional[Tuple[str, str]], +): + """Test duplicate detection.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch("sys.platform", "win32"), patch( + "homeassistant.components.mysensors.config_flow.try_connect", return_value=True + ), patch( + "homeassistant.components.mysensors.async_setup_entry", + return_value=True, + ): + MockConfigEntry(domain=DOMAIN, data=first_input).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, data=second_input, context={"source": config_entries.SOURCE_IMPORT} + ) + await hass.async_block_till_done() + if expected_result is None: + assert result["type"] == "create_entry" + else: + assert result["type"] == "abort" + assert result["reason"] == expected_result[1] diff --git a/tests/components/mysensors/test_gateway.py b/tests/components/mysensors/test_gateway.py new file mode 100644 index 00000000000..d3e360e0b9f --- /dev/null +++ b/tests/components/mysensors/test_gateway.py @@ -0,0 +1,30 @@ +"""Test function in gateway.py.""" +from unittest.mock import patch + +import pytest +import voluptuous as vol + +from homeassistant.components.mysensors.gateway import is_serial_port +from homeassistant.helpers.typing import HomeAssistantType + + +@pytest.mark.parametrize( + "port, expect_valid", + [ + ("COM5", True), + ("asdf", False), + ("COM17", True), + ("COM", False), + ("/dev/ttyACM0", False), + ], +) +def test_is_serial_port_windows(hass: HomeAssistantType, port: str, expect_valid: bool): + """Test windows serial port.""" + + with patch("sys.platform", "win32"): + try: + is_serial_port(port) + except vol.Invalid: + assert not expect_valid + else: + assert expect_valid diff --git a/tests/components/mysensors/test_init.py b/tests/components/mysensors/test_init.py new file mode 100644 index 00000000000..2775b73efd6 --- /dev/null +++ b/tests/components/mysensors/test_init.py @@ -0,0 +1,251 @@ +"""Test function in __init__.py.""" +from typing import Dict +from unittest.mock import patch + +import pytest + +from homeassistant.components.mysensors import ( + CONF_BAUD_RATE, + CONF_DEVICE, + CONF_GATEWAYS, + CONF_PERSISTENCE, + CONF_PERSISTENCE_FILE, + CONF_RETAIN, + CONF_TCP_PORT, + CONF_VERSION, + DEFAULT_VERSION, + DOMAIN, +) +from homeassistant.components.mysensors.const import ( + CONF_GATEWAY_TYPE, + CONF_GATEWAY_TYPE_MQTT, + CONF_GATEWAY_TYPE_SERIAL, + CONF_GATEWAY_TYPE_TCP, + CONF_TOPIC_IN_PREFIX, + CONF_TOPIC_OUT_PREFIX, +) +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.setup import async_setup_component + + +@pytest.mark.parametrize( + "config, expected_calls, expected_to_succeed, expected_config_flow_user_input", + [ + ( + { + DOMAIN: { + CONF_GATEWAYS: [ + { + CONF_DEVICE: "COM5", + CONF_PERSISTENCE_FILE: "bla.json", + CONF_BAUD_RATE: 57600, + CONF_TCP_PORT: 5003, + } + ], + CONF_VERSION: "2.3", + CONF_PERSISTENCE: False, + CONF_RETAIN: True, + } + }, + 1, + True, + { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, + CONF_DEVICE: "COM5", + CONF_PERSISTENCE_FILE: "bla.json", + CONF_BAUD_RATE: 57600, + CONF_VERSION: "2.3", + }, + ), + ( + { + DOMAIN: { + CONF_GATEWAYS: [ + { + CONF_DEVICE: "127.0.0.1", + CONF_PERSISTENCE_FILE: "blub.pickle", + CONF_BAUD_RATE: 115200, + CONF_TCP_PORT: 343, + } + ], + CONF_VERSION: "2.4", + CONF_PERSISTENCE: False, + CONF_RETAIN: False, + } + }, + 1, + True, + { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, + CONF_DEVICE: "127.0.0.1", + CONF_PERSISTENCE_FILE: "blub.pickle", + CONF_TCP_PORT: 343, + CONF_VERSION: "2.4", + }, + ), + ( + { + DOMAIN: { + CONF_GATEWAYS: [ + { + CONF_DEVICE: "127.0.0.1", + } + ], + CONF_PERSISTENCE: False, + CONF_RETAIN: False, + } + }, + 1, + True, + { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, + CONF_DEVICE: "127.0.0.1", + CONF_TCP_PORT: 5003, + CONF_VERSION: DEFAULT_VERSION, + }, + ), + ( + { + DOMAIN: { + CONF_GATEWAYS: [ + { + CONF_DEVICE: "mqtt", + CONF_BAUD_RATE: 115200, + CONF_TCP_PORT: 5003, + CONF_TOPIC_IN_PREFIX: "intopic", + CONF_TOPIC_OUT_PREFIX: "outtopic", + } + ], + CONF_PERSISTENCE: False, + CONF_RETAIN: False, + } + }, + 1, + True, + { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT, + CONF_DEVICE: "mqtt", + CONF_VERSION: DEFAULT_VERSION, + CONF_TOPIC_OUT_PREFIX: "outtopic", + CONF_TOPIC_IN_PREFIX: "intopic", + }, + ), + ( + { + DOMAIN: { + CONF_GATEWAYS: [ + { + CONF_DEVICE: "mqtt", + CONF_BAUD_RATE: 115200, + CONF_TCP_PORT: 5003, + } + ], + CONF_PERSISTENCE: False, + CONF_RETAIN: False, + } + }, + 0, + True, + {}, + ), + ( + { + DOMAIN: { + CONF_GATEWAYS: [ + { + CONF_DEVICE: "mqtt", + CONF_PERSISTENCE_FILE: "bla.json", + CONF_TOPIC_OUT_PREFIX: "out", + CONF_TOPIC_IN_PREFIX: "in", + CONF_BAUD_RATE: 115200, + CONF_TCP_PORT: 5003, + }, + { + CONF_DEVICE: "COM6", + CONF_PERSISTENCE_FILE: "bla2.json", + CONF_BAUD_RATE: 115200, + CONF_TCP_PORT: 5003, + }, + ], + CONF_VERSION: "2.4", + CONF_PERSISTENCE: False, + CONF_RETAIN: False, + } + }, + 2, + True, + {}, + ), + ( + { + DOMAIN: { + CONF_GATEWAYS: [ + { + CONF_DEVICE: "mqtt", + CONF_PERSISTENCE_FILE: "bla.json", + CONF_BAUD_RATE: 115200, + CONF_TCP_PORT: 5003, + }, + { + CONF_DEVICE: "COM6", + CONF_PERSISTENCE_FILE: "bla.json", + CONF_BAUD_RATE: 115200, + CONF_TCP_PORT: 5003, + }, + ], + CONF_VERSION: "2.4", + CONF_PERSISTENCE: False, + CONF_RETAIN: False, + } + }, + 0, + False, + {}, + ), + ( + { + DOMAIN: { + CONF_GATEWAYS: [ + { + CONF_DEVICE: "COMx", + CONF_PERSISTENCE_FILE: "bla.json", + CONF_BAUD_RATE: 115200, + CONF_TCP_PORT: 5003, + }, + ], + CONF_VERSION: "2.4", + CONF_PERSISTENCE: False, + CONF_RETAIN: False, + } + }, + 0, + True, + {}, + ), + ], +) +async def test_import( + hass: HomeAssistantType, + config: ConfigType, + expected_calls: int, + expected_to_succeed: bool, + expected_config_flow_user_input: Dict[str, any], +): + """Test importing a gateway.""" + with patch("sys.platform", "win32"), patch( + "homeassistant.components.mysensors.config_flow.try_connect", return_value=True + ), patch( + "homeassistant.components.mysensors.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await async_setup_component(hass, DOMAIN, config) + assert result == expected_to_succeed + await hass.async_block_till_done() + + assert len(mock_setup_entry.mock_calls) == expected_calls + + if expected_calls > 0: + config_flow_user_input = mock_setup_entry.mock_calls[0][1][1].data + for key, value in expected_config_flow_user_input.items(): + assert key in config_flow_user_input + assert config_flow_user_input[key] == value From 2c74befd4f6c2dd5aace996cd1e6b0088296a425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Conde=20G=C3=B3mez?= Date: Fri, 5 Feb 2021 22:39:31 +0100 Subject: [PATCH 215/796] Fix foscam to work again with non-admin accounts and make RTSP port configurable again (#45975) * Do not require admin account for foscam cameras. Foscam cameras require admin account for getting the MAC address, requiring an admin account in the integration is not desirable as an operator one is good enough (and a good practice). Old entries using the MAC address as unique_id are migrated to the new unique_id format so everything is consistent. Also fixed unhandled invalid responses from the camera in the config flow process. * Make RTSP port configurable again as some cameras reports wrong port * Remove periods from new log lines * Set new Config Flow version to 2 and adjust the entity migration * Create a proper error message for the InvalidResponse exception * Change crafted unique_id to use entry_id in the entity * Abort if same host and port is already configured * Fix entry tracking to use entry_id instead of unique_id * Remove unique_id from mocked config entry in tests --- homeassistant/components/foscam/__init__.py | 55 +++++++- homeassistant/components/foscam/camera.py | 45 ++++--- .../components/foscam/config_flow.py | 54 +++++++- homeassistant/components/foscam/const.py | 1 + homeassistant/components/foscam/strings.json | 2 + .../components/foscam/translations/en.json | 2 + tests/components/foscam/test_config_flow.py | 124 +++++++++++++++--- 7 files changed, 235 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/foscam/__init__.py b/homeassistant/components/foscam/__init__.py index e5b82817d4b..6a2c961544f 100644 --- a/homeassistant/components/foscam/__init__.py +++ b/homeassistant/components/foscam/__init__.py @@ -1,10 +1,15 @@ """The foscam component.""" import asyncio -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from libpyfoscam import FoscamCamera -from .const import DOMAIN, SERVICE_PTZ, SERVICE_PTZ_PRESET +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_registry import async_migrate_entries + +from .config_flow import DEFAULT_RTSP_PORT +from .const import CONF_RTSP_PORT, DOMAIN, LOGGER, SERVICE_PTZ, SERVICE_PTZ_PRESET PLATFORMS = ["camera"] @@ -22,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.config_entries.async_forward_entry_setup(entry, component) ) - hass.data[DOMAIN][entry.unique_id] = entry.data + hass.data[DOMAIN][entry.entry_id] = entry.data return True @@ -39,10 +44,50 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) if unload_ok: - hass.data[DOMAIN].pop(entry.unique_id) + hass.data[DOMAIN].pop(entry.entry_id) if not hass.data[DOMAIN]: hass.services.async_remove(domain=DOMAIN, service=SERVICE_PTZ) hass.services.async_remove(domain=DOMAIN, service=SERVICE_PTZ_PRESET) return unload_ok + + +async def async_migrate_entry(hass, config_entry: ConfigEntry): + """Migrate old entry.""" + LOGGER.debug("Migrating from version %s", config_entry.version) + + if config_entry.version == 1: + # Change unique id + @callback + def update_unique_id(entry): + return {"new_unique_id": config_entry.entry_id} + + await async_migrate_entries(hass, config_entry.entry_id, update_unique_id) + + config_entry.unique_id = None + + # Get RTSP port from the camera or use the fallback one and store it in data + camera = FoscamCamera( + config_entry.data[CONF_HOST], + config_entry.data[CONF_PORT], + config_entry.data[CONF_USERNAME], + config_entry.data[CONF_PASSWORD], + verbose=False, + ) + + ret, response = await hass.async_add_executor_job(camera.get_port_info) + + rtsp_port = DEFAULT_RTSP_PORT + + if ret != 0: + rtsp_port = response.get("rtspPort") or response.get("mediaPort") + + config_entry.data = {**config_entry.data, CONF_RTSP_PORT: rtsp_port} + + # Change entry version + config_entry.version = 2 + + LOGGER.info("Migration to version %s successful", config_entry.version) + + return True diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index f66ad31c2a8..d600546c3b0 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -15,7 +15,14 @@ from homeassistant.const import ( ) from homeassistant.helpers import config_validation as cv, entity_platform -from .const import CONF_STREAM, DOMAIN, LOGGER, SERVICE_PTZ, SERVICE_PTZ_PRESET +from .const import ( + CONF_RTSP_PORT, + CONF_STREAM, + DOMAIN, + LOGGER, + SERVICE_PTZ, + SERVICE_PTZ_PRESET, +) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -24,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Required(CONF_USERNAME): cv.string, vol.Optional(CONF_NAME, default="Foscam Camera"): cv.string, vol.Optional(CONF_PORT, default=88): cv.port, - vol.Optional("rtsp_port"): cv.port, + vol.Optional(CONF_RTSP_PORT): cv.port, } ) @@ -71,6 +78,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= CONF_USERNAME: config[CONF_USERNAME], CONF_PASSWORD: config[CONF_PASSWORD], CONF_STREAM: "Main", + CONF_RTSP_PORT: config.get(CONF_RTSP_PORT, 554), } hass.async_create_task( @@ -134,8 +142,8 @@ class HassFoscamCamera(Camera): self._username = config_entry.data[CONF_USERNAME] self._password = config_entry.data[CONF_PASSWORD] self._stream = config_entry.data[CONF_STREAM] - self._unique_id = config_entry.unique_id - self._rtsp_port = None + self._unique_id = config_entry.entry_id + self._rtsp_port = config_entry.data[CONF_RTSP_PORT] self._motion_status = False async def async_added_to_hass(self): @@ -145,7 +153,13 @@ class HassFoscamCamera(Camera): self._foscam_session.get_motion_detect_config ) - if ret != 0: + if ret == -3: + LOGGER.info( + "Can't get motion detection status, camera %s configured with non-admin user", + self._name, + ) + + elif ret != 0: LOGGER.error( "Error getting motion detection status of %s: %s", self._name, ret ) @@ -153,17 +167,6 @@ class HassFoscamCamera(Camera): else: self._motion_status = response == 1 - # Get RTSP port - ret, response = await self.hass.async_add_executor_job( - self._foscam_session.get_port_info - ) - - if ret != 0: - LOGGER.error("Error getting RTSP port of %s: %s", self._name, ret) - - else: - self._rtsp_port = response.get("rtspPort") or response.get("mediaPort") - @property def unique_id(self): """Return the entity unique ID.""" @@ -205,6 +208,11 @@ class HassFoscamCamera(Camera): ret = self._foscam_session.enable_motion_detection() if ret != 0: + if ret == -3: + LOGGER.info( + "Can't set motion detection status, camera %s configured with non-admin user", + self._name, + ) return self._motion_status = True @@ -220,6 +228,11 @@ class HassFoscamCamera(Camera): ret = self._foscam_session.disable_motion_detection() if ret != 0: + if ret == -3: + LOGGER.info( + "Can't set motion detection status, camera %s configured with non-admin user", + self._name, + ) return self._motion_status = False diff --git a/homeassistant/components/foscam/config_flow.py b/homeassistant/components/foscam/config_flow.py index 7bb8cb50a51..bfeefb9e406 100644 --- a/homeassistant/components/foscam/config_flow.py +++ b/homeassistant/components/foscam/config_flow.py @@ -1,6 +1,10 @@ """Config flow for foscam integration.""" from libpyfoscam import FoscamCamera -from libpyfoscam.foscam import ERROR_FOSCAM_AUTH, ERROR_FOSCAM_UNAVAILABLE +from libpyfoscam.foscam import ( + ERROR_FOSCAM_AUTH, + ERROR_FOSCAM_UNAVAILABLE, + FOSCAM_SUCCESS, +) import voluptuous as vol from homeassistant import config_entries, exceptions @@ -13,12 +17,13 @@ from homeassistant.const import ( ) from homeassistant.data_entry_flow import AbortFlow -from .const import CONF_STREAM, LOGGER +from .const import CONF_RTSP_PORT, CONF_STREAM, LOGGER from .const import DOMAIN # pylint:disable=unused-import STREAMS = ["Main", "Sub"] DEFAULT_PORT = 88 +DEFAULT_RTSP_PORT = 554 DATA_SCHEMA = vol.Schema( @@ -28,6 +33,7 @@ DATA_SCHEMA = vol.Schema( vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, vol.Required(CONF_STREAM, default=STREAMS[0]): vol.In(STREAMS), + vol.Required(CONF_RTSP_PORT, default=DEFAULT_RTSP_PORT): int, } ) @@ -35,7 +41,7 @@ DATA_SCHEMA = vol.Schema( class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for foscam.""" - VERSION = 1 + VERSION = 2 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL async def _validate_and_create(self, data): @@ -43,6 +49,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): Data has the keys from DATA_SCHEMA with values provided by the user. """ + + for entry in self.hass.config_entries.async_entries(DOMAIN): + if ( + entry.data[CONF_HOST] == data[CONF_HOST] + and entry.data[CONF_PORT] == data[CONF_PORT] + ): + raise AbortFlow("already_configured") + camera = FoscamCamera( data[CONF_HOST], data[CONF_PORT], @@ -52,7 +66,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) # Validate data by sending a request to the camera - ret, response = await self.hass.async_add_executor_job(camera.get_dev_info) + ret, _ = await self.hass.async_add_executor_job(camera.get_product_all_info) if ret == ERROR_FOSCAM_UNAVAILABLE: raise CannotConnect @@ -60,10 +74,23 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if ret == ERROR_FOSCAM_AUTH: raise InvalidAuth - await self.async_set_unique_id(response["mac"]) - self._abort_if_unique_id_configured() + if ret != FOSCAM_SUCCESS: + LOGGER.error( + "Unexpected error code from camera %s:%s: %s", + data[CONF_HOST], + data[CONF_PORT], + ret, + ) + raise InvalidResponse - name = data.pop(CONF_NAME, response["devName"]) + # Try to get camera name (only possible with admin account) + ret, response = await self.hass.async_add_executor_job(camera.get_dev_info) + + dev_name = response.get( + "devName", f"Foscam {data[CONF_HOST]}:{data[CONF_PORT]}" + ) + + name = data.pop(CONF_NAME, dev_name) return self.async_create_entry(title=name, data=data) @@ -81,6 +108,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except InvalidAuth: errors["base"] = "invalid_auth" + except InvalidResponse: + errors["base"] = "invalid_response" + except AbortFlow: raise @@ -105,6 +135,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): LOGGER.error("Error importing foscam platform config: invalid auth.") return self.async_abort(reason="invalid_auth") + except InvalidResponse: + LOGGER.exception( + "Error importing foscam platform config: invalid response from camera." + ) + return self.async_abort(reason="invalid_response") + except AbortFlow: raise @@ -121,3 +157,7 @@ class CannotConnect(exceptions.HomeAssistantError): class InvalidAuth(exceptions.HomeAssistantError): """Error to indicate there is invalid auth.""" + + +class InvalidResponse(exceptions.HomeAssistantError): + """Error to indicate there is invalid response.""" diff --git a/homeassistant/components/foscam/const.py b/homeassistant/components/foscam/const.py index a42b430993e..d5ac0f5c567 100644 --- a/homeassistant/components/foscam/const.py +++ b/homeassistant/components/foscam/const.py @@ -5,6 +5,7 @@ LOGGER = logging.getLogger(__package__) DOMAIN = "foscam" +CONF_RTSP_PORT = "rtsp_port" CONF_STREAM = "stream" SERVICE_PTZ = "ptz" diff --git a/homeassistant/components/foscam/strings.json b/homeassistant/components/foscam/strings.json index 6033fa099cd..5c0622af9d1 100644 --- a/homeassistant/components/foscam/strings.json +++ b/homeassistant/components/foscam/strings.json @@ -8,6 +8,7 @@ "port": "[%key:common::config_flow::data::port%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", + "rtsp_port": "RTSP port", "stream": "Stream" } } @@ -15,6 +16,7 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_response": "Invalid response from the device", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { diff --git a/homeassistant/components/foscam/translations/en.json b/homeassistant/components/foscam/translations/en.json index 3d1454a4ebd..16a7d0b7800 100644 --- a/homeassistant/components/foscam/translations/en.json +++ b/homeassistant/components/foscam/translations/en.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", + "invalid_response": "Invalid response from the device", "unknown": "Unexpected error" }, "step": { @@ -14,6 +15,7 @@ "host": "Host", "password": "Password", "port": "Port", + "rtsp_port": "RTSP port", "stream": "Stream", "username": "Username" } diff --git a/tests/components/foscam/test_config_flow.py b/tests/components/foscam/test_config_flow.py index 8087ac1894f..3b8910c4dbc 100644 --- a/tests/components/foscam/test_config_flow.py +++ b/tests/components/foscam/test_config_flow.py @@ -1,7 +1,12 @@ """Test the Foscam config flow.""" from unittest.mock import patch -from libpyfoscam.foscam import ERROR_FOSCAM_AUTH, ERROR_FOSCAM_UNAVAILABLE +from libpyfoscam.foscam import ( + ERROR_FOSCAM_AUTH, + ERROR_FOSCAM_CMD, + ERROR_FOSCAM_UNAVAILABLE, + ERROR_FOSCAM_UNKNOWN, +) from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.foscam import config_flow @@ -14,6 +19,13 @@ VALID_CONFIG = { config_flow.CONF_USERNAME: "admin", config_flow.CONF_PASSWORD: "1234", config_flow.CONF_STREAM: "Main", + config_flow.CONF_RTSP_PORT: 554, +} +OPERATOR_CONFIG = { + config_flow.CONF_USERNAME: "operator", +} +INVALID_RESPONSE_CONFIG = { + config_flow.CONF_USERNAME: "interr", } CAMERA_NAME = "Mocked Foscam Camera" CAMERA_MAC = "C0:C1:D0:F4:B4:D4" @@ -23,26 +35,39 @@ def setup_mock_foscam_camera(mock_foscam_camera): """Mock FoscamCamera simulating behaviour using a base valid config.""" def configure_mock_on_init(host, port, user, passwd, verbose=False): - return_code = 0 - data = {} + product_all_info_rc = 0 + dev_info_rc = 0 + dev_info_data = {} if ( host != VALID_CONFIG[config_flow.CONF_HOST] or port != VALID_CONFIG[config_flow.CONF_PORT] ): - return_code = ERROR_FOSCAM_UNAVAILABLE + product_all_info_rc = dev_info_rc = ERROR_FOSCAM_UNAVAILABLE elif ( - user != VALID_CONFIG[config_flow.CONF_USERNAME] + user + not in [ + VALID_CONFIG[config_flow.CONF_USERNAME], + OPERATOR_CONFIG[config_flow.CONF_USERNAME], + INVALID_RESPONSE_CONFIG[config_flow.CONF_USERNAME], + ] or passwd != VALID_CONFIG[config_flow.CONF_PASSWORD] ): - return_code = ERROR_FOSCAM_AUTH + product_all_info_rc = dev_info_rc = ERROR_FOSCAM_AUTH + + elif user == INVALID_RESPONSE_CONFIG[config_flow.CONF_USERNAME]: + product_all_info_rc = dev_info_rc = ERROR_FOSCAM_UNKNOWN + + elif user == OPERATOR_CONFIG[config_flow.CONF_USERNAME]: + dev_info_rc = ERROR_FOSCAM_CMD else: - data["devName"] = CAMERA_NAME - data["mac"] = CAMERA_MAC + dev_info_data["devName"] = CAMERA_NAME + dev_info_data["mac"] = CAMERA_MAC - mock_foscam_camera.get_dev_info.return_value = (return_code, data) + mock_foscam_camera.get_product_all_info.return_value = (product_all_info_rc, {}) + mock_foscam_camera.get_dev_info.return_value = (dev_info_rc, dev_info_data) return mock_foscam_camera @@ -142,12 +167,44 @@ async def test_user_cannot_connect(hass): assert result["errors"] == {"base": "cannot_connect"} +async def test_user_invalid_response(hass): + """Test we handle invalid response error from user input.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera: + setup_mock_foscam_camera(mock_foscam_camera) + + invalid_response = VALID_CONFIG.copy() + invalid_response[config_flow.CONF_USERNAME] = INVALID_RESPONSE_CONFIG[ + config_flow.CONF_USERNAME + ] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + invalid_response, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_response"} + + async def test_user_already_configured(hass): """Test we handle already configured from user input.""" await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( - domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC + domain=config_flow.DOMAIN, + data=VALID_CONFIG, ) entry.add_to_hass(hass) @@ -201,6 +258,8 @@ async def test_user_unknown_exception(hass): async def test_import_user_valid(hass): """Test valid config from import.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( "homeassistant.components.foscam.config_flow.FoscamCamera", ) as mock_foscam_camera, patch( @@ -229,6 +288,8 @@ async def test_import_user_valid(hass): async def test_import_user_valid_with_name(hass): """Test valid config with extra name from import.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( "homeassistant.components.foscam.config_flow.FoscamCamera", ) as mock_foscam_camera, patch( @@ -261,10 +322,7 @@ async def test_import_user_valid_with_name(hass): async def test_import_invalid_auth(hass): """Test we handle invalid auth from import.""" - entry = MockConfigEntry( - domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC - ) - entry.add_to_hass(hass) + await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.foscam.config_flow.FoscamCamera", @@ -287,11 +345,8 @@ async def test_import_invalid_auth(hass): async def test_import_cannot_connect(hass): - """Test we handle invalid auth from import.""" - entry = MockConfigEntry( - domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC - ) - entry.add_to_hass(hass) + """Test we handle cannot connect error from import.""" + await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.foscam.config_flow.FoscamCamera", @@ -313,10 +368,39 @@ async def test_import_cannot_connect(hass): assert result["reason"] == "cannot_connect" +async def test_import_invalid_response(hass): + """Test we handle invalid response error from import.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera: + setup_mock_foscam_camera(mock_foscam_camera) + + invalid_response = VALID_CONFIG.copy() + invalid_response[config_flow.CONF_USERNAME] = INVALID_RESPONSE_CONFIG[ + config_flow.CONF_USERNAME + ] + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=invalid_response, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "invalid_response" + + async def test_import_already_configured(hass): """Test we handle already configured from import.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( - domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC + domain=config_flow.DOMAIN, + data=VALID_CONFIG, ) entry.add_to_hass(hass) From 434b4dfa583037c755cbf86f53465941329f6fc9 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 5 Feb 2021 23:07:12 +0100 Subject: [PATCH 216/796] Improve deCONZ logbook to be more robust in different situations (#46063) --- homeassistant/components/deconz/logbook.py | 38 ++++++++++--- tests/components/deconz/test_logbook.py | 62 ++++++++++++++++++++++ 2 files changed, 93 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/deconz/logbook.py b/homeassistant/components/deconz/logbook.py index 73c157ac8f6..85982244364 100644 --- a/homeassistant/components/deconz/logbook.py +++ b/homeassistant/components/deconz/logbook.py @@ -6,7 +6,7 @@ from homeassistant.const import ATTR_DEVICE_ID, CONF_EVENT from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.event import Event -from .const import DOMAIN as DECONZ_DOMAIN +from .const import CONF_GESTURE, DOMAIN as DECONZ_DOMAIN from .deconz_event import CONF_DECONZ_EVENT, DeconzEvent from .device_trigger import ( CONF_BOTH_BUTTONS, @@ -107,8 +107,12 @@ def _get_device_event_description(modelid: str, event: str) -> tuple: device_event_descriptions: dict = REMOTES[modelid] for event_type_tuple, event_dict in device_event_descriptions.items(): - if event == event_dict[CONF_EVENT]: + if event == event_dict.get(CONF_EVENT): return event_type_tuple + if event == event_dict.get(CONF_GESTURE): + return event_type_tuple + + return (None, None) @callback @@ -125,15 +129,35 @@ def async_describe_events( hass, event.data[ATTR_DEVICE_ID] ) - if deconz_event.device.modelid not in REMOTES: + action = None + interface = None + data = event.data.get(CONF_EVENT) or event.data.get(CONF_GESTURE, "") + + if data and deconz_event.device.modelid in REMOTES: + action, interface = _get_device_event_description( + deconz_event.device.modelid, data + ) + + # Unknown event + if not data: return { "name": f"{deconz_event.device.name}", - "message": f"fired event '{event.data[CONF_EVENT]}'.", + "message": "fired an unknown event.", } - action, interface = _get_device_event_description( - deconz_event.device.modelid, event.data[CONF_EVENT] - ) + # No device event match + if not action: + return { + "name": f"{deconz_event.device.name}", + "message": f"fired event '{data}'.", + } + + # Gesture event + if not interface: + return { + "name": f"{deconz_event.device.name}", + "message": f"fired event '{ACTIONS[action]}'.", + } return { "name": f"{deconz_event.device.name}", diff --git a/tests/components/deconz/test_logbook.py b/tests/components/deconz/test_logbook.py index 7315a766d5c..500ca03b7ed 100644 --- a/tests/components/deconz/test_logbook.py +++ b/tests/components/deconz/test_logbook.py @@ -3,6 +3,7 @@ from copy import deepcopy from homeassistant.components import logbook +from homeassistant.components.deconz.const import CONF_GESTURE from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT from homeassistant.components.deconz.gateway import get_gateway_from_config_entry from homeassistant.const import CONF_DEVICE_ID, CONF_EVENT, CONF_ID, CONF_UNIQUE_ID @@ -34,6 +35,23 @@ async def test_humanifying_deconz_event(hass): "config": {}, "uniqueid": "00:00:00:00:00:00:00:02-00", }, + "2": { + "id": "Xiaomi cube id", + "name": "Xiaomi cube", + "type": "ZHASwitch", + "modelid": "lumi.sensor_cube", + "state": {"buttonevent": 1000, "gesture": 1}, + "config": {}, + "uniqueid": "00:00:00:00:00:00:00:03-00", + }, + "3": { + "id": "faulty", + "name": "Faulty event", + "type": "ZHASwitch", + "state": {}, + "config": {}, + "uniqueid": "00:00:00:00:00:00:00:04-00", + }, } config_entry = await setup_deconz_integration(hass, get_state_response=data) gateway = get_gateway_from_config_entry(hass, config_entry) @@ -46,6 +64,7 @@ async def test_humanifying_deconz_event(hass): logbook.humanify( hass, [ + # Event without matching device trigger MockLazyEventPartialState( CONF_DECONZ_EVENT, { @@ -55,6 +74,7 @@ async def test_humanifying_deconz_event(hass): CONF_UNIQUE_ID: gateway.events[0].serial, }, ), + # Event with matching device trigger MockLazyEventPartialState( CONF_DECONZ_EVENT, { @@ -64,6 +84,36 @@ async def test_humanifying_deconz_event(hass): CONF_UNIQUE_ID: gateway.events[1].serial, }, ), + # Gesture with matching device trigger + MockLazyEventPartialState( + CONF_DECONZ_EVENT, + { + CONF_DEVICE_ID: gateway.events[2].device_id, + CONF_GESTURE: 1, + CONF_ID: gateway.events[2].event_id, + CONF_UNIQUE_ID: gateway.events[2].serial, + }, + ), + # Unsupported device trigger + MockLazyEventPartialState( + CONF_DECONZ_EVENT, + { + CONF_DEVICE_ID: gateway.events[2].device_id, + CONF_GESTURE: "unsupported_gesture", + CONF_ID: gateway.events[2].event_id, + CONF_UNIQUE_ID: gateway.events[2].serial, + }, + ), + # Unknown event + MockLazyEventPartialState( + CONF_DECONZ_EVENT, + { + CONF_DEVICE_ID: gateway.events[3].device_id, + "unknown_event": None, + CONF_ID: gateway.events[3].event_id, + CONF_UNIQUE_ID: gateway.events[3].serial, + }, + ), ], entity_attr_cache, {}, @@ -77,3 +127,15 @@ async def test_humanifying_deconz_event(hass): assert events[1]["name"] == "Hue remote" assert events[1]["domain"] == "deconz" assert events[1]["message"] == "'Long press' event for 'Dim up' was fired." + + assert events[2]["name"] == "Xiaomi cube" + assert events[2]["domain"] == "deconz" + assert events[2]["message"] == "fired event 'Shake'." + + assert events[3]["name"] == "Xiaomi cube" + assert events[3]["domain"] == "deconz" + assert events[3]["message"] == "fired event 'unsupported_gesture'." + + assert events[4]["name"] == "Faulty event" + assert events[4]["domain"] == "deconz" + assert events[4]["message"] == "fired an unknown event." From 67392338da9babb705525723862c551f8dda1ccd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 5 Feb 2021 23:23:47 +0100 Subject: [PATCH 217/796] Activate manual ZHA config flow when no comports detected (#46077) --- homeassistant/components/zha/config_flow.py | 4 ++++ tests/components/zha/test_config_flow.py | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 473d39c6f7a..f59f53c7995 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -45,6 +45,10 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + (f" - {p.manufacturer}" if p.manufacturer else "") for p in ports ] + + if not list_of_ports: + return await self.async_step_pick_radio() + list_of_ports.append(CONF_MANUAL_PATH) if user_input is not None: diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index fe65def839d..b3dbefbdbf0 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -74,6 +74,7 @@ async def test_user_flow_not_detected(detect_mock, hass): assert detect_mock.await_args[0][0] == port.device +@patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) async def test_user_flow_show_form(hass): """Test user step form.""" result = await hass.config_entries.flow.async_init( @@ -85,6 +86,17 @@ async def test_user_flow_show_form(hass): assert result["step_id"] == "user" +async def test_user_flow_show_manual(hass): + """Test user flow manual entry when no comport detected.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "pick_radio" + + async def test_user_flow_manual(hass): """Test user flow manual entry.""" From 33169cf8cd96fff693367f489f4d40ac85cfaec6 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 5 Feb 2021 16:36:42 -0600 Subject: [PATCH 218/796] Fix zwave_js Notification CC sensors and binary sensors (#46072) * only include property key name in sensor name if it exists * add endpoint to binary_sensor and sensor notification CC entities if > 0 * refactor to have helper method generate name * change default behavior of generate_name * return value for notification sensor when we can't find the state * store generated name --- .../components/zwave_js/binary_sensor.py | 12 ++---- homeassistant/components/zwave_js/entity.py | 31 ++++++++++--- homeassistant/components/zwave_js/sensor.py | 43 +++++++++++-------- 3 files changed, 53 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index f17d893e371..bb2e4355f16 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -293,6 +293,10 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity): """Initialize a ZWaveNotificationBinarySensor entity.""" super().__init__(config_entry, client, info) self.state_key = state_key + self._name = self.generate_name( + self.info.primary_value.property_name, + [self.info.primary_value.metadata.states[self.state_key]], + ) # check if we have a custom mapping for this value self._mapping_info = self._get_sensor_mapping() @@ -301,14 +305,6 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity): """Return if the sensor is on or off.""" return int(self.info.primary_value.value) == int(self.state_key) - @property - def name(self) -> str: - """Return default name from device name and value name combination.""" - node_name = self.info.node.name or self.info.node.device_config.description - value_name = self.info.primary_value.property_name - state_label = self.info.primary_value.metadata.states[self.state_key] - return f"{node_name}: {value_name} - {state_label}" - @property def device_class(self) -> Optional[str]: """Return device class.""" diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 334a2cccd4f..08571ad5d8c 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -1,7 +1,7 @@ """Generic Z-Wave Entity Class.""" import logging -from typing import Optional, Tuple, Union +from typing import List, Optional, Tuple, Union from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.node import Node as ZwaveNode @@ -35,6 +35,7 @@ class ZWaveBaseEntity(Entity): self.config_entry = config_entry self.client = client self.info = info + self._name = self.generate_name() # entities requiring additional values, can add extra ids to this list self.watched_value_ids = {self.info.primary_value.value_id} @@ -61,19 +62,35 @@ class ZWaveBaseEntity(Entity): "identifiers": {get_device_id(self.client, self.info.node)}, } - @property - def name(self) -> str: - """Return default name from device name and value name combination.""" + def generate_name( + self, + alternate_value_name: Optional[str] = None, + additional_info: Optional[List[str]] = None, + ) -> str: + """Generate entity name.""" + if additional_info is None: + additional_info = [] node_name = self.info.node.name or self.info.node.device_config.description value_name = ( - self.info.primary_value.metadata.label + alternate_value_name + or self.info.primary_value.metadata.label or self.info.primary_value.property_key_name or self.info.primary_value.property_name ) + name = f"{node_name}: {value_name}" + for item in additional_info: + if item: + name += f" - {item}" # append endpoint if > 1 if self.info.primary_value.endpoint > 1: - value_name += f" ({self.info.primary_value.endpoint})" - return f"{node_name}: {value_name}" + name += f" ({self.info.primary_value.endpoint})" + + return name + + @property + def name(self) -> str: + """Return default name from device name and value name combination.""" + return self._name @property def unique_id(self) -> str: diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 3d3f782bc1b..78b536b81f7 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -123,6 +123,17 @@ class ZWaveStringSensor(ZwaveSensorBase): class ZWaveNumericSensor(ZwaveSensorBase): """Representation of a Z-Wave Numeric sensor.""" + def __init__( + self, + config_entry: ConfigEntry, + client: ZwaveClient, + info: ZwaveDiscoveryInfo, + ) -> None: + """Initialize a ZWaveNumericSensor entity.""" + super().__init__(config_entry, client, info) + if self.info.primary_value.command_class == CommandClass.BASIC: + self._name = self.generate_name(self.info.primary_value.command_class_name) + @property def state(self) -> float: """Return state of the sensor.""" @@ -142,19 +153,23 @@ class ZWaveNumericSensor(ZwaveSensorBase): return str(self.info.primary_value.metadata.unit) - @property - def name(self) -> str: - """Return default name from device name and value name combination.""" - if self.info.primary_value.command_class == CommandClass.BASIC: - node_name = self.info.node.name or self.info.node.device_config.description - label = self.info.primary_value.command_class_name - return f"{node_name}: {label}" - return super().name - class ZWaveListSensor(ZwaveSensorBase): """Representation of a Z-Wave Numeric sensor with multiple states.""" + def __init__( + self, + config_entry: ConfigEntry, + client: ZwaveClient, + info: ZwaveDiscoveryInfo, + ) -> None: + """Initialize a ZWaveListSensor entity.""" + super().__init__(config_entry, client, info) + self._name = self.generate_name( + self.info.primary_value.property_name, + [self.info.primary_value.property_key_name], + ) + @property def state(self) -> Optional[str]: """Return state of the sensor.""" @@ -164,7 +179,7 @@ class ZWaveListSensor(ZwaveSensorBase): not str(self.info.primary_value.value) in self.info.primary_value.metadata.states ): - return None + return str(self.info.primary_value.value) return str( self.info.primary_value.metadata.states[str(self.info.primary_value.value)] ) @@ -174,11 +189,3 @@ class ZWaveListSensor(ZwaveSensorBase): """Return the device specific state attributes.""" # add the value's int value as property for multi-value (list) items return {"value": self.info.primary_value.value} - - @property - def name(self) -> str: - """Return default name from device name and value name combination.""" - node_name = self.info.node.name or self.info.node.device_config.description - prop_name = self.info.primary_value.property_name - prop_key_name = self.info.primary_value.property_key_name - return f"{node_name}: {prop_name} - {prop_key_name}" From ce159d7db33cc494bc3038f71e340acea1922ab6 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 6 Feb 2021 00:07:22 +0000 Subject: [PATCH 219/796] [ci skip] Translation update --- .../components/airvisual/translations/fr.json | 22 ++++- .../components/airvisual/translations/sv.json | 3 +- .../components/august/translations/sv.json | 8 +- .../bmw_connected_drive/translations/fr.json | 3 +- .../cert_expiry/translations/sv.json | 3 + .../components/cover/translations/sv.json | 4 + .../components/deconz/translations/sv.json | 3 +- .../components/doorbird/translations/sv.json | 3 +- .../components/econet/translations/fr.json | 22 +++++ .../fireservicerota/translations/fr.json | 3 +- .../components/fritzbox/translations/fr.json | 7 ++ .../fritzbox_callmonitor/translations/fr.json | 41 +++++++++ .../components/hassio/translations/fr.json | 1 + .../components/homekit/translations/fr.json | 17 +++- .../huisbaasje/translations/fr.json | 21 +++++ .../components/hyperion/translations/fr.json | 37 +++++++- .../components/icloud/translations/sv.json | 3 +- .../components/ipma/translations/fr.json | 5 + .../components/kodi/translations/fr.json | 1 + .../components/kulersky/translations/fr.json | 3 +- .../components/light/translations/sv.json | 2 + .../lutron_caseta/translations/fr.json | 51 +++++++++- .../components/lyric/translations/fr.json | 7 ++ .../components/mazda/translations/fr.json | 34 +++++++ .../components/mazda/translations/no.json | 35 +++++++ .../mobile_app/translations/fr.json | 5 + .../motion_blinds/translations/fr.json | 23 ++++- .../components/myq/translations/sv.json | 7 +- .../components/mysensors/translations/en.json | 92 +++++++++---------- .../components/mysensors/translations/no.json | 34 +++++++ .../components/neato/translations/fr.json | 6 +- .../components/nest/translations/fr.json | 11 +++ .../components/nexia/translations/sv.json | 3 +- .../components/nuheat/translations/sv.json | 5 +- .../components/nuki/translations/fr.json | 17 ++++ .../components/number/translations/fr.json | 8 ++ .../ondilo_ico/translations/fr.json | 3 + .../ovo_energy/translations/fr.json | 5 +- .../components/ozw/translations/fr.json | 3 + .../components/pi_hole/translations/fr.json | 6 ++ .../components/plaato/translations/fr.json | 37 ++++++++ .../components/plugwise/translations/fr.json | 3 +- .../components/powerwall/translations/fr.json | 1 + .../components/rachio/translations/sv.json | 3 +- .../recollect_waste/translations/fr.json | 28 ++++++ .../components/rfxtrx/translations/fr.json | 3 +- .../components/roku/translations/fr.json | 8 ++ .../components/roku/translations/sv.json | 12 +++ .../components/roomba/translations/fr.json | 5 +- .../components/shelly/translations/fr.json | 15 +++ .../shopping_list/translations/sv.json | 14 +++ .../simplisafe/translations/sv.json | 3 + .../components/solaredge/translations/fr.json | 3 + .../somfy_mylink/translations/fr.json | 14 ++- .../components/spotify/translations/fr.json | 5 + .../srp_energy/translations/fr.json | 14 ++- .../components/twinkly/translations/fr.json | 3 +- .../components/unifi/translations/fr.json | 4 +- .../components/unifi/translations/sv.json | 9 +- .../components/vizio/translations/fr.json | 1 + .../components/vizio/translations/sv.json | 13 +++ .../components/wled/translations/sv.json | 11 ++- .../components/zwave_js/translations/fr.json | 36 +++++++- 63 files changed, 732 insertions(+), 80 deletions(-) create mode 100644 homeassistant/components/econet/translations/fr.json create mode 100644 homeassistant/components/fritzbox_callmonitor/translations/fr.json create mode 100644 homeassistant/components/huisbaasje/translations/fr.json create mode 100644 homeassistant/components/lyric/translations/fr.json create mode 100644 homeassistant/components/mazda/translations/fr.json create mode 100644 homeassistant/components/mazda/translations/no.json create mode 100644 homeassistant/components/mysensors/translations/no.json create mode 100644 homeassistant/components/nuki/translations/fr.json create mode 100644 homeassistant/components/number/translations/fr.json create mode 100644 homeassistant/components/ondilo_ico/translations/fr.json create mode 100644 homeassistant/components/recollect_waste/translations/fr.json create mode 100644 homeassistant/components/shopping_list/translations/sv.json diff --git a/homeassistant/components/airvisual/translations/fr.json b/homeassistant/components/airvisual/translations/fr.json index d1a0d3d511a..62c144e075d 100644 --- a/homeassistant/components/airvisual/translations/fr.json +++ b/homeassistant/components/airvisual/translations/fr.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "\u00c9chec de connexion", "general_error": "Erreur inattendue", - "invalid_api_key": "Cl\u00e9 API invalide" + "invalid_api_key": "Cl\u00e9 API invalide", + "location_not_found": "Emplacement introuvable" }, "step": { "geography": { @@ -19,6 +20,25 @@ "description": "Utilisez l'API cloud AirVisual pour surveiller une position g\u00e9ographique.", "title": "Configurer une g\u00e9ographie" }, + "geography_by_coords": { + "data": { + "api_key": "Clef d'API", + "latitude": "Latitude", + "longitude": "Longitude" + }, + "description": "Utilisez l'API cloud AirVisual pour surveiller une latitude / longitude.", + "title": "Configurer un lieu g\u00e9ographique" + }, + "geography_by_name": { + "data": { + "api_key": "Clef d'API", + "city": "Ville", + "country": "Pays", + "state": "Etat" + }, + "description": "Utilisez l'API cloud AirVisual pour surveiller une ville / un \u00e9tat / un pays.", + "title": "Configurer un lieu g\u00e9ographique" + }, "node_pro": { "data": { "ip_address": "H\u00f4te", diff --git a/homeassistant/components/airvisual/translations/sv.json b/homeassistant/components/airvisual/translations/sv.json index f375b4fc598..ecc1c397ec4 100644 --- a/homeassistant/components/airvisual/translations/sv.json +++ b/homeassistant/components/airvisual/translations/sv.json @@ -21,7 +21,8 @@ "data": { "cloud_api": "Geografisk Plats", "type": "Integrationstyp" - } + }, + "title": "Konfigurera AirVisual" } } } diff --git a/homeassistant/components/august/translations/sv.json b/homeassistant/components/august/translations/sv.json index df72f5daaf3..a3a0b891bc6 100644 --- a/homeassistant/components/august/translations/sv.json +++ b/homeassistant/components/august/translations/sv.json @@ -13,10 +13,16 @@ "data": { "login_method": "Inloggningsmetod", "password": "L\u00f6senord", + "timeout": "Timeout (sekunder)", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Om inloggningsmetoden \u00e4r \"e-post\" \u00e4r anv\u00e4ndarnamnet e-postadressen. Om inloggningsmetoden \u00e4r \"telefon\" \u00e4r anv\u00e4ndarnamnet telefonnumret i formatet \"+ NNNNNNNN\".", + "title": "St\u00e4ll in ett August-konto" }, "validation": { + "data": { + "code": "Verifieringskod" + }, "title": "Tv\u00e5faktorsautentisering" } } diff --git a/homeassistant/components/bmw_connected_drive/translations/fr.json b/homeassistant/components/bmw_connected_drive/translations/fr.json index 1b8f562669f..900b352ecb6 100644 --- a/homeassistant/components/bmw_connected_drive/translations/fr.json +++ b/homeassistant/components/bmw_connected_drive/translations/fr.json @@ -21,7 +21,8 @@ "step": { "account_options": { "data": { - "read_only": "Lecture seule (uniquement capteurs et notification, pas d'ex\u00e9cution de services, pas de verrouillage)" + "read_only": "Lecture seule (uniquement capteurs et notification, pas d'ex\u00e9cution de services, pas de verrouillage)", + "use_location": "Utilisez la localisation de Home Assistant pour les sondages de localisation de voiture (obligatoire pour les v\u00e9hicules non i3 / i8 produits avant 7/2014)" } } } diff --git a/homeassistant/components/cert_expiry/translations/sv.json b/homeassistant/components/cert_expiry/translations/sv.json index 23703f11e5b..f00fc236d09 100644 --- a/homeassistant/components/cert_expiry/translations/sv.json +++ b/homeassistant/components/cert_expiry/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten har redan konfigurerats" + }, "error": { "connection_refused": "Anslutningen blev tillbakavisad under anslutning till v\u00e4rd.", "connection_timeout": "Timeout vid anslutning till den h\u00e4r v\u00e4rden", diff --git a/homeassistant/components/cover/translations/sv.json b/homeassistant/components/cover/translations/sv.json index 0a8dbecf124..a9509740330 100644 --- a/homeassistant/components/cover/translations/sv.json +++ b/homeassistant/components/cover/translations/sv.json @@ -1,5 +1,9 @@ { "device_automation": { + "action_type": { + "close": "St\u00e4ng {entity_name}", + "open": "\u00d6ppna {entity_name}" + }, "condition_type": { "is_closed": "{entity_name} \u00e4r st\u00e4ngd", "is_closing": "{entity_name} st\u00e4ngs", diff --git a/homeassistant/components/deconz/translations/sv.json b/homeassistant/components/deconz/translations/sv.json index 225b9f0b4e3..4d709a43af1 100644 --- a/homeassistant/components/deconz/translations/sv.json +++ b/homeassistant/components/deconz/translations/sv.json @@ -87,7 +87,8 @@ "allow_clip_sensor": "Till\u00e5t deCONZ CLIP-sensorer", "allow_deconz_groups": "Till\u00e5t deCONZ ljusgrupper" }, - "description": "Konfigurera synlighet f\u00f6r deCONZ-enhetstyper" + "description": "Konfigurera synlighet f\u00f6r deCONZ-enhetstyper", + "title": "deCONZ-inst\u00e4llningar" } } } diff --git a/homeassistant/components/doorbird/translations/sv.json b/homeassistant/components/doorbird/translations/sv.json index b2a809a576e..546535fb937 100644 --- a/homeassistant/components/doorbird/translations/sv.json +++ b/homeassistant/components/doorbird/translations/sv.json @@ -9,7 +9,8 @@ "host": "V\u00e4rd (IP-adress)", "name": "Enhetsnamn", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Anslut till DoorBird" } } } diff --git a/homeassistant/components/econet/translations/fr.json b/homeassistant/components/econet/translations/fr.json new file mode 100644 index 00000000000..64fd39c852a --- /dev/null +++ b/homeassistant/components/econet/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9ja configur\u00e9 ", + "cannot_connect": "\u00c9chec de la connexion ", + "invalid_auth": "Authentification invalide " + }, + "error": { + "cannot_connect": "\u00c9chec de la connexion", + "invalid_auth": "Authentification invalide " + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Mot de passe" + }, + "title": "Configurer le compte Rheem EcoNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/fr.json b/homeassistant/components/fireservicerota/translations/fr.json index a8803f63fca..d0ce81458e3 100644 --- a/homeassistant/components/fireservicerota/translations/fr.json +++ b/homeassistant/components/fireservicerota/translations/fr.json @@ -13,7 +13,8 @@ "reauth": { "data": { "password": "Mot de passe" - } + }, + "description": "Les jetons d'authentification sont invalides, connectez-vous pour les recr\u00e9er." }, "user": { "data": { diff --git a/homeassistant/components/fritzbox/translations/fr.json b/homeassistant/components/fritzbox/translations/fr.json index e7a8acaa762..0cd425410e6 100644 --- a/homeassistant/components/fritzbox/translations/fr.json +++ b/homeassistant/components/fritzbox/translations/fr.json @@ -18,6 +18,13 @@ }, "description": "Voulez-vous configurer {name} ?" }, + "reauth_confirm": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "Mettez \u00e0 jour vos informations de connexion pour {name} ." + }, "user": { "data": { "host": "Nom d'h\u00f4te ou adresse IP", diff --git a/homeassistant/components/fritzbox_callmonitor/translations/fr.json b/homeassistant/components/fritzbox_callmonitor/translations/fr.json new file mode 100644 index 00000000000..cde9023273c --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/fr.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9ja configur\u00e9 ", + "insufficient_permissions": "L'utilisateur ne dispose pas des autorisations n\u00e9cessaires pour acc\u00e9der aux param\u00e8tres d'AVM FRITZ! Box et \u00e0 ses r\u00e9pertoires.", + "no_devices_found": "Aucun appreil trouv\u00e9 sur le r\u00e9seau " + }, + "error": { + "invalid_auth": "Authentification invalide" + }, + "flow_title": "Moniteur d'appels AVM FRITZ! Box: {name}", + "step": { + "phonebook": { + "data": { + "phonebook": "Annuaire" + } + }, + "user": { + "data": { + "host": "Hote", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur " + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "Les pr\u00e9fixes sont mal form\u00e9s, veuillez v\u00e9rifier leur format." + }, + "step": { + "init": { + "data": { + "prefixes": "Pr\u00e9fixes (liste s\u00e9par\u00e9e par des virgules)" + }, + "title": "Configurer les pr\u00e9fixes" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/fr.json b/homeassistant/components/hassio/translations/fr.json index 2bb52c3c54c..cef14b258c4 100644 --- a/homeassistant/components/hassio/translations/fr.json +++ b/homeassistant/components/hassio/translations/fr.json @@ -6,6 +6,7 @@ "disk_used": "Taille du disque utilis\u00e9", "docker_version": "Version de Docker", "healthy": "Sain", + "host_os": "Syst\u00e8me d'exploitation h\u00f4te", "installed_addons": "Add-ons install\u00e9s", "supervisor_api": "API du superviseur", "supervisor_version": "Version du supervisor", diff --git a/homeassistant/components/homekit/translations/fr.json b/homeassistant/components/homekit/translations/fr.json index be7d30c30ee..a0f10c9684a 100644 --- a/homeassistant/components/homekit/translations/fr.json +++ b/homeassistant/components/homekit/translations/fr.json @@ -4,6 +4,20 @@ "port_name_in_use": "Une passerelle avec le m\u00eame nom ou port est d\u00e9j\u00e0 configur\u00e9e." }, "step": { + "accessory_mode": { + "data": { + "entity_id": "Entit\u00e9" + }, + "description": "Choisissez l'entit\u00e9 \u00e0 inclure. En mode accessoire, une seule entit\u00e9 est incluse.", + "title": "S\u00e9lectionnez l'entit\u00e9 \u00e0 inclure" + }, + "bridge_mode": { + "data": { + "include_domains": "Domaines \u00e0 inclure" + }, + "description": "Choisissez les domaines \u00e0 inclure. Toutes les entit\u00e9s prises en charge dans le domaine seront incluses.", + "title": "S\u00e9lectionnez les domaines \u00e0 inclure" + }, "pairing": { "description": "D\u00e8s que le pont {name} est pr\u00eat, l'appairage sera disponible dans \"Notifications\" sous \"Configuration de la Passerelle HomeKit\".", "title": "Appairage de la Passerelle Homekit" @@ -11,7 +25,8 @@ "user": { "data": { "auto_start": "D\u00e9marrage automatique (d\u00e9sactiver si vous utilisez Z-Wave ou un autre syst\u00e8me de d\u00e9marrage diff\u00e9r\u00e9)", - "include_domains": "Domaines \u00e0 inclure" + "include_domains": "Domaines \u00e0 inclure", + "mode": "Mode" }, "description": "La passerelle HomeKit vous permettra d'acc\u00e9der \u00e0 vos entit\u00e9s Home Assistant dans HomeKit. Les passerelles HomeKit sont limit\u00e9es \u00e0 150 accessoires par instance, y compris la passerelle elle-m\u00eame. Si vous souhaitez connecter plus que le nombre maximum d'accessoires, il est recommand\u00e9 d'utiliser plusieurs passerelles HomeKit pour diff\u00e9rents domaines. La configuration d\u00e9taill\u00e9e des entit\u00e9s est uniquement disponible via YAML pour la passerelle principale.", "title": "Activer la Passerelle HomeKit" diff --git a/homeassistant/components/huisbaasje/translations/fr.json b/homeassistant/components/huisbaasje/translations/fr.json new file mode 100644 index 00000000000..9f78d7d8826 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9ja configur\u00e9 " + }, + "error": { + "connection_exception": "\u00c9chec de la connexion ", + "invalid_auth": "Authentification invalide ", + "unauthenticated_exception": "Authentification invalide ", + "unknown": "Erreur inatendue" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/fr.json b/homeassistant/components/hyperion/translations/fr.json index 90733a8968b..4b374f097a4 100644 --- a/homeassistant/components/hyperion/translations/fr.json +++ b/homeassistant/components/hyperion/translations/fr.json @@ -1,10 +1,36 @@ { "config": { "abort": { + "already_configured": "Le service est d\u00e9ja configur\u00e9 ", + "auth_new_token_not_granted_error": "Le jeton nouvellement cr\u00e9\u00e9 n'a pas \u00e9t\u00e9 approuv\u00e9 sur l'interface utilisateur Hyperion", "auth_new_token_not_work_error": "\u00c9chec de l'authentification \u00e0 l'aide du jeton nouvellement cr\u00e9\u00e9", - "cannot_connect": "Echec de connection" + "auth_required_error": "Impossible de d\u00e9terminer si une autorisation est requise", + "cannot_connect": "Echec de connection", + "no_id": "L'instance Hyperion Ambilight n'a pas signal\u00e9 son identifiant" + }, + "error": { + "cannot_connect": "Echec de la connexion ", + "invalid_access_token": "jeton d'acc\u00e8s Invalide" }, "step": { + "auth": { + "data": { + "create_token": "Cr\u00e9er automatiquement un nouveau jeton", + "token": "Ou fournir un jeton pr\u00e9existant" + }, + "description": "Configurer l'autorisation sur votre serveur Hyperion Ambilight" + }, + "confirm": { + "description": "Voulez-vous ajouter cet Hyperion Ambilight \u00e0 Home Assistant? \n\n ** H\u00f4te: ** {host}\n ** Port: ** {port}\n ** ID **: {id}", + "title": "Confirmer l'ajout du service Hyperion Ambilight" + }, + "create_token": { + "description": "Choisissez ** Soumettre ** ci-dessous pour demander un nouveau jeton d'authentification. Vous serez redirig\u00e9 vers l'interface utilisateur Hyperion pour approuver la demande. Veuillez v\u00e9rifier que l'identifiant affich\u00e9 est \" {auth_id} \"", + "title": "Cr\u00e9er automatiquement un nouveau jeton d'authentification" + }, + "create_token_external": { + "title": "Accepter un nouveau jeton dans l'interface utilisateur Hyperion" + }, "user": { "data": { "host": "H\u00f4te", @@ -12,5 +38,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "priority": "Priorit\u00e9 Hyperion \u00e0 utiliser pour les couleurs et les effets" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/sv.json b/homeassistant/components/icloud/translations/sv.json index 6caf02f56c5..2bba72d49df 100644 --- a/homeassistant/components/icloud/translations/sv.json +++ b/homeassistant/components/icloud/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Kontot har redan konfigurerats" + "already_configured": "Kontot har redan konfigurerats", + "no_device": "Ingen av dina enheter har \"Hitta min iPhone\" aktiverat" }, "error": { "send_verification_code": "Det gick inte att skicka verifieringskod", diff --git a/homeassistant/components/ipma/translations/fr.json b/homeassistant/components/ipma/translations/fr.json index 9a3a11a7a73..eaff9d211db 100644 --- a/homeassistant/components/ipma/translations/fr.json +++ b/homeassistant/components/ipma/translations/fr.json @@ -15,5 +15,10 @@ "title": "Emplacement" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Point de terminaison de l'API IPMA accessible" + } } } \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/fr.json b/homeassistant/components/kodi/translations/fr.json index fd0d3e38e81..a7c4b3f34a1 100644 --- a/homeassistant/components/kodi/translations/fr.json +++ b/homeassistant/components/kodi/translations/fr.json @@ -4,6 +4,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification erron\u00e9e", + "no_uuid": "L'instance Kodi n'a pas d'identifiant unique. Cela est probablement d\u00fb \u00e0 une ancienne version de Kodi (17.x ou inf\u00e9rieure). Vous pouvez configurer l'int\u00e9gration manuellement ou passer \u00e0 une version plus r\u00e9cente de Kodi.", "unknown": "Erreur inattendue" }, "error": { diff --git a/homeassistant/components/kulersky/translations/fr.json b/homeassistant/components/kulersky/translations/fr.json index 4c984a55690..649a3d387bd 100644 --- a/homeassistant/components/kulersky/translations/fr.json +++ b/homeassistant/components/kulersky/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Aucun appareil n'a \u00e9t\u00e9 d\u00e9tect\u00e9 sur le r\u00e9seau" + "no_devices_found": "Aucun appareil n'a \u00e9t\u00e9 d\u00e9tect\u00e9 sur le r\u00e9seau", + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Seulement une seule configuration est possible " } } } \ No newline at end of file diff --git a/homeassistant/components/light/translations/sv.json b/homeassistant/components/light/translations/sv.json index 0d0e29a87ed..d5f0bdaf767 100644 --- a/homeassistant/components/light/translations/sv.json +++ b/homeassistant/components/light/translations/sv.json @@ -1,6 +1,8 @@ { "device_automation": { "action_type": { + "brightness_decrease": "Minska ljusstyrkan f\u00f6r {entity_name}", + "brightness_increase": "\u00d6ka ljusstyrkan f\u00f6r {entity_name}", "toggle": "V\u00e4xla {entity_name}", "turn_off": "St\u00e4ng av {entity_name}", "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" diff --git a/homeassistant/components/lutron_caseta/translations/fr.json b/homeassistant/components/lutron_caseta/translations/fr.json index 0674172e975..4d7dccd0acf 100644 --- a/homeassistant/components/lutron_caseta/translations/fr.json +++ b/homeassistant/components/lutron_caseta/translations/fr.json @@ -2,16 +2,65 @@ "config": { "abort": { "already_configured": "Pont Cas\u00e9ta d\u00e9j\u00e0 configur\u00e9.", - "cannot_connect": "Installation annul\u00e9e du pont Cas\u00e9ta en raison d'un \u00e9chec de connexion." + "cannot_connect": "Installation annul\u00e9e du pont Cas\u00e9ta en raison d'un \u00e9chec de connexion.", + "not_lutron_device": "L'appareil d\u00e9couvert n'est pas un appareil Lutron" }, "error": { "cannot_connect": "\u00c9chec de la connexion \u00e0 la passerelle Cas\u00e9ta; v\u00e9rifiez la configuration de votre h\u00f4te et de votre certificat." }, + "flow_title": "Lutron Cas\u00e9ta {name} ( {host} )", "step": { "import_failed": { "description": "Impossible de configurer la passerelle (h\u00f4te: {host} ) import\u00e9 \u00e0 partir de configuration.yaml.", "title": "\u00c9chec de l'importation de la configuration de la passerelle Cas\u00e9ta." + }, + "link": { + "description": "Pour jumeler avec {name} ( {host} ), apr\u00e8s avoir soumis ce formulaire, appuyez sur le bouton noir \u00e0 l'arri\u00e8re du pont.", + "title": "Paire avec le pont" + }, + "user": { + "data": { + "host": "Hote" + }, + "description": "Saisissez l'adresse IP de l'appareil.", + "title": "Se connecter automatiquement au pont" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Premier bouton", + "button_2": "Deuxi\u00e8me bouton", + "button_3": "Troisi\u00e8me bouton", + "button_4": "Quatri\u00e8me bouton", + "close_1": "Fermer 1", + "close_2": "Fermer 2", + "close_3": "Fermer 3", + "close_4": "Fermer 4", + "close_all": "Ferme tout", + "group_1_button_1": "Premier bouton du premier groupe", + "group_1_button_2": "Premier groupe deuxi\u00e8me bouton", + "group_2_button_1": "Premier bouton du deuxi\u00e8me groupe", + "group_2_button_2": "Deuxi\u00e8me bouton du deuxi\u00e8me groupe", + "lower_all": "Tout baisser", + "off": "Eteint", + "on": "Allumer", + "open_1": "Ouvrir 1", + "open_2": "Ouvrir 2", + "open_3": "Ouvrir 3", + "open_4": "Ouvrir 4", + "open_all": "Ouvre tout", + "raise_all": "Lever tout", + "stop": "Stop (favori)", + "stop_1": "Arr\u00eat 1", + "stop_2": "Arr\u00eat 2", + "stop_3": "Arr\u00eat 3", + "stop_4": "Arr\u00eat 4", + "stop_all": "Arr\u00eate tout" + }, + "trigger_type": { + "press": "\" {subtype} \" appuy\u00e9", + "release": "\" {subtype} \" publi\u00e9" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/fr.json b/homeassistant/components/lyric/translations/fr.json new file mode 100644 index 00000000000..794e85b7fa6 --- /dev/null +++ b/homeassistant/components/lyric/translations/fr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/fr.json b/homeassistant/components/mazda/translations/fr.json new file mode 100644 index 00000000000..e9ccb013b5e --- /dev/null +++ b/homeassistant/components/mazda/translations/fr.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9ja configur\u00e9" + }, + "error": { + "account_locked": "Compte bloqu\u00e9. Veuillez r\u00e9essayer plus tard.", + "cannot_connect": "Echec de la connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "reauth": { + "data": { + "email": "Email", + "password": "Mot de passe", + "region": "R\u00e9gion" + }, + "description": "L'authentification a \u00e9chou\u00e9 pour les services connect\u00e9s Mazda. Veuillez saisir vos informations d'identification actuelles.", + "title": "Services connect\u00e9s Mazda - \u00c9chec de l'authentification" + }, + "user": { + "data": { + "email": "Email", + "password": "Mot de passe", + "region": "R\u00e9gion" + }, + "description": "Veuillez saisir l'adresse e-mail et le mot de passe que vous utilisez pour vous connecter \u00e0 l'application mobile MyMazda.", + "title": "Services connect\u00e9s Mazda - Ajouter un compte" + } + } + }, + "title": "Services connect\u00e9s Mazda" +} \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/no.json b/homeassistant/components/mazda/translations/no.json new file mode 100644 index 00000000000..e3a05de51f9 --- /dev/null +++ b/homeassistant/components/mazda/translations/no.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "account_locked": "Kontoen er l\u00e5st. Pr\u00f8v igjen senere.", + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "reauth": { + "data": { + "email": "E-post", + "password": "Passord", + "region": "Region" + }, + "description": "Autentisering mislyktes for Mazda Connected Services. Vennligst skriv inn din n\u00e5v\u00e6rende legitimasjon.", + "title": "Mazda Connected Services - Autentisering mislyktes" + }, + "user": { + "data": { + "email": "E-post", + "password": "Passord", + "region": "Region" + }, + "description": "Vennligst skriv inn e-postadressen og passordet du bruker for \u00e5 logge p\u00e5 MyMazda-mobilappen.", + "title": "Mazda Connected Services - Legg til konto" + } + } + }, + "title": "Mazda Connected Services" +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/fr.json b/homeassistant/components/mobile_app/translations/fr.json index 09317e4a00d..f4b0f590e48 100644 --- a/homeassistant/components/mobile_app/translations/fr.json +++ b/homeassistant/components/mobile_app/translations/fr.json @@ -8,5 +8,10 @@ "description": "Voulez-vous configurer le composant Application mobile?" } } + }, + "device_automation": { + "action_type": { + "notify": "Envoyer une notification" + } } } \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/fr.json b/homeassistant/components/motion_blinds/translations/fr.json index 86d008b9e6d..da8abbcc564 100644 --- a/homeassistant/components/motion_blinds/translations/fr.json +++ b/homeassistant/components/motion_blinds/translations/fr.json @@ -1,16 +1,35 @@ { "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9ja configur\u00e9 ", + "connection_error": "\u00c9chec de la connexion " + }, + "error": { + "discovery_error": "Impossible de d\u00e9couvrir une Motion Gateway" + }, + "flow_title": "Stores de mouvement", "step": { "connect": { "data": { "api_key": "Cl\u00e9 API" }, - "description": "Vous aurez besoin de la cl\u00e9 API de 16 caract\u00e8res, voir https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key pour les instructions" + "description": "Vous aurez besoin de la cl\u00e9 API de 16 caract\u00e8res, voir https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key pour les instructions", + "title": "Stores de mouvement" }, "select": { "data": { "select_ip": "Adresse IP" - } + }, + "description": "Ex\u00e9cutez \u00e0 nouveau la configuration si vous souhaitez connecter des passerelles Motion suppl\u00e9mentaires", + "title": "S\u00e9lectionnez la Motion Gateway que vous souhaitez connecter" + }, + "user": { + "data": { + "api_key": "Clef d'API", + "host": "Adresse IP" + }, + "description": "Connectez-vous \u00e0 votre Motion Gateway, si l'adresse IP n'est pas d\u00e9finie, la d\u00e9tection automatique est utilis\u00e9e", + "title": "Stores de mouvement" } } } diff --git a/homeassistant/components/myq/translations/sv.json b/homeassistant/components/myq/translations/sv.json index 1243ca600f0..da06f32aa92 100644 --- a/homeassistant/components/myq/translations/sv.json +++ b/homeassistant/components/myq/translations/sv.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten har redan konfigurerats" + }, "error": { + "cannot_connect": "Anslutningen misslyckades", "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, @@ -9,7 +13,8 @@ "data": { "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Anslut till MyQ Gateway" } } } diff --git a/homeassistant/components/mysensors/translations/en.json b/homeassistant/components/mysensors/translations/en.json index d7730ba09b6..63af85488f0 100644 --- a/homeassistant/components/mysensors/translations/en.json +++ b/homeassistant/components/mysensors/translations/en.json @@ -3,75 +3,75 @@ "abort": { "already_configured": "Device is already configured", "cannot_connect": "Failed to connect", - "invalid_subscribe_topic": "Invalid subscribe topic", - "invalid_publish_topic": "Invalid publish topic", - "duplicate_topic": "Topic already in use", - "same_topic": "Subscribe and publish topics are the same", - "invalid_port": "Invalid port number", - "invalid_persistence_file": "Invalid persistence file", "duplicate_persistence_file": "Persistence file already in use", - "invalid_ip": "Invalid IP address", - "invalid_serial": "Invalid serial port", + "duplicate_topic": "Topic already in use", + "invalid_auth": "Invalid authentication", "invalid_device": "Invalid device", + "invalid_ip": "Invalid IP address", + "invalid_persistence_file": "Invalid persistence file", + "invalid_port": "Invalid port number", + "invalid_publish_topic": "Invalid publish topic", + "invalid_serial": "Invalid serial port", + "invalid_subscribe_topic": "Invalid subscribe topic", "invalid_version": "Invalid MySensors version", "not_a_number": "Please enter a number", "port_out_of_range": "Port number must be at least 1 and at most 65535", + "same_topic": "Subscribe and publish topics are the same", "unknown": "Unexpected error" }, "error": { "already_configured": "Device is already configured", "cannot_connect": "Failed to connect", - "invalid_subscribe_topic": "Invalid subscribe topic", - "invalid_publish_topic": "Invalid publish topic", - "duplicate_topic": "Topic already in use", - "same_topic": "Subscribe and publish topics are the same", - "invalid_port": "Invalid port number", - "invalid_persistence_file": "Invalid persistence file", "duplicate_persistence_file": "Persistence file already in use", - "invalid_ip": "Invalid IP address", - "invalid_serial": "Invalid serial port", + "duplicate_topic": "Topic already in use", + "invalid_auth": "Invalid authentication", "invalid_device": "Invalid device", + "invalid_ip": "Invalid IP address", + "invalid_persistence_file": "Invalid persistence file", + "invalid_port": "Invalid port number", + "invalid_publish_topic": "Invalid publish topic", + "invalid_serial": "Invalid serial port", + "invalid_subscribe_topic": "Invalid subscribe topic", "invalid_version": "Invalid MySensors version", "not_a_number": "Please enter a number", "port_out_of_range": "Port number must be at least 1 and at most 65535", + "same_topic": "Subscribe and publish topics are the same", "unknown": "Unexpected error" }, "step": { - "user": { - "data": { - "optimistic": "optimistic", - "persistence": "persistence", - "gateway_type": "Gateway type" - }, - "description": "Choose connection method to the gateway" - }, - "gw_tcp": { - "description": "Ethernet gateway setup", - "data": { - "device": "IP address of the gateway", - "tcp_port": "port", - "version": "MySensors version", - "persistence_file": "persistence file (leave empty to auto-generate)" - } - }, - "gw_serial": { - "description": "Serial gateway setup", - "data": { - "device": "Serial port", - "baud_rate": "baud rate", - "version": "MySensors version", - "persistence_file": "persistence file (leave empty to auto-generate)" - } - }, "gw_mqtt": { - "description": "MQTT gateway setup", "data": { + "persistence_file": "persistence file (leave empty to auto-generate)", "retain": "mqtt retain", "topic_in_prefix": "prefix for input topics (topic_in_prefix)", "topic_out_prefix": "prefix for output topics (topic_out_prefix)", - "version": "MySensors version", - "persistence_file": "persistence file (leave empty to auto-generate)" - } + "version": "MySensors version" + }, + "description": "MQTT gateway setup" + }, + "gw_serial": { + "data": { + "baud_rate": "baud rate", + "device": "Serial port", + "persistence_file": "persistence file (leave empty to auto-generate)", + "version": "MySensors version" + }, + "description": "Serial gateway setup" + }, + "gw_tcp": { + "data": { + "device": "IP address of the gateway", + "persistence_file": "persistence file (leave empty to auto-generate)", + "tcp_port": "port", + "version": "MySensors version" + }, + "description": "Ethernet gateway setup" + }, + "user": { + "data": { + "gateway_type": "Gateway type" + }, + "description": "Choose connection method to the gateway" } } }, diff --git a/homeassistant/components/mysensors/translations/no.json b/homeassistant/components/mysensors/translations/no.json new file mode 100644 index 00000000000..a13140df640 --- /dev/null +++ b/homeassistant/components/mysensors/translations/no.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "error": { + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "gw_mqtt": { + "data": { + "version": "MySensors versjon" + } + }, + "gw_serial": { + "data": { + "version": "MySensors versjon" + } + }, + "user": { + "data": { + "gateway_type": "" + } + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/fr.json b/homeassistant/components/neato/translations/fr.json index 69f2186c54c..4b71a93a783 100644 --- a/homeassistant/components/neato/translations/fr.json +++ b/homeassistant/components/neato/translations/fr.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "D\u00e9j\u00e0 configur\u00e9", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification invalide", + "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation " }, "create_entry": { "default": "Voir [Documentation Neato]({docs_url})." @@ -22,5 +23,6 @@ "title": "Informations compte Neato" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/fr.json b/homeassistant/components/nest/translations/fr.json index 03b55458e9b..0d1f5b761f5 100644 --- a/homeassistant/components/nest/translations/fr.json +++ b/homeassistant/components/nest/translations/fr.json @@ -34,7 +34,18 @@ }, "pick_implementation": { "title": "S\u00e9lectionner une m\u00e9thode d'authentification" + }, + "reauth_confirm": { + "description": "L'int\u00e9gration Nest doit r\u00e9-authentifier votre compte" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Mouvement d\u00e9tect\u00e9", + "camera_person": "Personne d\u00e9tect\u00e9e", + "camera_sound": "Son d\u00e9tect\u00e9", + "doorbell_chime": "Sonnette enfonc\u00e9e" + } } } \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/sv.json b/homeassistant/components/nexia/translations/sv.json index 9cfd620ac73..60044361f65 100644 --- a/homeassistant/components/nexia/translations/sv.json +++ b/homeassistant/components/nexia/translations/sv.json @@ -10,7 +10,8 @@ "data": { "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Anslut till mynexia.com" } } } diff --git a/homeassistant/components/nuheat/translations/sv.json b/homeassistant/components/nuheat/translations/sv.json index 9cfd620ac73..327bdf8c4ca 100644 --- a/homeassistant/components/nuheat/translations/sv.json +++ b/homeassistant/components/nuheat/translations/sv.json @@ -3,14 +3,17 @@ "error": { "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", "invalid_auth": "Ogiltig autentisering", + "invalid_thermostat": "Termostatens serienummer \u00e4r ogiltigt.", "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { "password": "L\u00f6senord", + "serial_number": "Termostatens serienummer.", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "F\u00e5 tillg\u00e5ng till din termostats serienummer eller ID genom att logga in p\u00e5 https://MyNuHeat.com och v\u00e4lja din termostat." } } } diff --git a/homeassistant/components/nuki/translations/fr.json b/homeassistant/components/nuki/translations/fr.json new file mode 100644 index 00000000000..26a949038d5 --- /dev/null +++ b/homeassistant/components/nuki/translations/fr.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00c9chec de la connexion ", + "invalid_auth": "Authentification invalide " + }, + "step": { + "user": { + "data": { + "host": "Hote", + "port": "Port", + "token": "jeton d'acc\u00e8s" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/fr.json b/homeassistant/components/number/translations/fr.json new file mode 100644 index 00000000000..9f49c3fb962 --- /dev/null +++ b/homeassistant/components/number/translations/fr.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "D\u00e9finir la valeur de {entity_name}" + } + }, + "title": "Nombre" +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/fr.json b/homeassistant/components/ondilo_ico/translations/fr.json new file mode 100644 index 00000000000..33271e594a3 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/fr.json @@ -0,0 +1,3 @@ +{ + "title": "Ondilo ICO" +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/fr.json b/homeassistant/components/ovo_energy/translations/fr.json index 351e20641aa..9be6b4d3c11 100644 --- a/homeassistant/components/ovo_energy/translations/fr.json +++ b/homeassistant/components/ovo_energy/translations/fr.json @@ -5,11 +5,14 @@ "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide" }, + "flow_title": "OVO Energy: {username}", "step": { "reauth": { "data": { "password": "Mot de passe" - } + }, + "description": "L'authentification a \u00e9chou\u00e9 pour OVO Energy. Veuillez saisir vos informations d'identification actuelles.", + "title": "R\u00e9authentification" }, "user": { "data": { diff --git a/homeassistant/components/ozw/translations/fr.json b/homeassistant/components/ozw/translations/fr.json index c4ea835d86c..5eed478549d 100644 --- a/homeassistant/components/ozw/translations/fr.json +++ b/homeassistant/components/ozw/translations/fr.json @@ -18,6 +18,9 @@ "hassio_confirm": { "title": "Configurer l\u2019int\u00e9gration OpenZWave avec l\u2019add-on OpenZWave" }, + "install_addon": { + "title": "L'installation du module compl\u00e9mentaire OpenZWave a commenc\u00e9" + }, "on_supervisor": { "data": { "use_addon": "Utiliser l'add-on OpenZWave Supervisor" diff --git a/homeassistant/components/pi_hole/translations/fr.json b/homeassistant/components/pi_hole/translations/fr.json index 1ccc5ac7d76..152fb0f3def 100644 --- a/homeassistant/components/pi_hole/translations/fr.json +++ b/homeassistant/components/pi_hole/translations/fr.json @@ -7,6 +7,11 @@ "cannot_connect": "Connexion impossible" }, "step": { + "api_key": { + "data": { + "api_key": "Clef d'API" + } + }, "user": { "data": { "api_key": "Cl\u00e9 d'API", @@ -15,6 +20,7 @@ "name": "Nom", "port": "Port", "ssl": "Utiliser SSL", + "statistics_only": "Statistiques uniquement", "verify_ssl": "V\u00e9rifier le certificat SSL" } } diff --git a/homeassistant/components/plaato/translations/fr.json b/homeassistant/components/plaato/translations/fr.json index bc442a04c60..ab3c01144dd 100644 --- a/homeassistant/components/plaato/translations/fr.json +++ b/homeassistant/components/plaato/translations/fr.json @@ -1,16 +1,53 @@ { "config": { "abort": { + "already_configured": "Le compte est d\u00e9ja configur\u00e9", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, "create_entry": { "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer la fonction Webhook dans Plaato Airlock. \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n\n Voir [la documentation] ( {docs_url} ) pour plus de d\u00e9tails." }, + "error": { + "invalid_webhook_device": "Vous avez s\u00e9lectionn\u00e9 un appareil qui ne prend pas en charge l'envoi de donn\u00e9es vers un webhook. Il n'est disponible que pour le sas", + "no_api_method": "Vous devez ajouter un jeton d'authentification ou s\u00e9lectionner un webhook", + "no_auth_token": "Vous devez ajouter un jeton d'authentification" + }, "step": { + "api_method": { + "data": { + "token": "Collez le jeton d'authentification ici", + "use_webhook": "Utiliser le webhook" + }, + "description": "Pour pouvoir interroger l'API, un \u00abauth_token\u00bb est n\u00e9cessaire. Il peut \u00eatre obtenu en suivant [ces] instructions (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) \n\n Appareil s\u00e9lectionn\u00e9: ** {device_type} ** \n\n Si vous pr\u00e9f\u00e9rez utiliser la m\u00e9thode Webhook int\u00e9gr\u00e9e (Airlock uniquement), veuillez cocher la case ci-dessous et laisser le jeton d'authentification vide", + "title": "S\u00e9lectionnez la m\u00e9thode API" + }, "user": { + "data": { + "device_name": "Nommez votre appareil", + "device_type": "Type d'appareil Plaato" + }, "description": "\u00cates-vous s\u00fbr de vouloir installer le Plaato Airlock ?", "title": "Configurer le Webhook Plaato" + }, + "webhook": { + "description": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer la fonction Webhook dans Plaato Airlock. \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n\n Voir [la documentation] ( {docs_url} ) pour plus de d\u00e9tails.", + "title": "Webhook \u00e0 utiliser" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "Intervalle de mise \u00e0 jour (minutes)" + }, + "description": "D\u00e9finir l'intervalle de mise \u00e0 jour (minutes)", + "title": "Options pour Plaato" + }, + "webhook": { + "description": "Informations sur le webhook: \n\n - URL: ` {webhook_url} `\n - M\u00e9thode: POST \n\n", + "title": "Options pour Plaato Airlock" } } } diff --git a/homeassistant/components/plugwise/translations/fr.json b/homeassistant/components/plugwise/translations/fr.json index 2fdc3502571..f89c8509136 100644 --- a/homeassistant/components/plugwise/translations/fr.json +++ b/homeassistant/components/plugwise/translations/fr.json @@ -21,7 +21,8 @@ "data": { "host": "Adresse IP", "password": "ID Smile", - "port": "Port" + "port": "Port", + "username": "Nom d'utilisateur de sourire" }, "description": "Veuillez saisir :", "title": "Se connecter \u00e0 Smile" diff --git a/homeassistant/components/powerwall/translations/fr.json b/homeassistant/components/powerwall/translations/fr.json index 3ddc6634557..2086393dfef 100644 --- a/homeassistant/components/powerwall/translations/fr.json +++ b/homeassistant/components/powerwall/translations/fr.json @@ -8,6 +8,7 @@ "unknown": "Erreur inattendue", "wrong_version": "Votre Powerwall utilise une version logicielle qui n'est pas prise en charge. Veuillez envisager de mettre \u00e0 niveau ou de signaler ce probl\u00e8me afin qu'il puisse \u00eatre r\u00e9solu." }, + "flow_title": "Tesla Powerwall ( {ip_address} )", "step": { "user": { "data": { diff --git a/homeassistant/components/rachio/translations/sv.json b/homeassistant/components/rachio/translations/sv.json index c2da7a1c01d..4932b17ebfa 100644 --- a/homeassistant/components/rachio/translations/sv.json +++ b/homeassistant/components/rachio/translations/sv.json @@ -12,7 +12,8 @@ "user": { "data": { "api_key": "API nyckel" - } + }, + "title": "Anslut till Rachio-enheten" } } } diff --git a/homeassistant/components/recollect_waste/translations/fr.json b/homeassistant/components/recollect_waste/translations/fr.json new file mode 100644 index 00000000000..dc62c8f520a --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/fr.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9ja configur\u00e9 " + }, + "error": { + "invalid_place_or_service_id": "ID de lieu ou de service non valide" + }, + "step": { + "user": { + "data": { + "place_id": "Identifiant de lieu", + "service_id": "ID de service" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "Utilisez des noms conviviaux pour les types de ramassage (si possible)" + }, + "title": "Configurer Recollect Waste" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/fr.json b/homeassistant/components/rfxtrx/translations/fr.json index baf8d0f5148..c0df7233458 100644 --- a/homeassistant/components/rfxtrx/translations/fr.json +++ b/homeassistant/components/rfxtrx/translations/fr.json @@ -64,7 +64,8 @@ "off_delay": "D\u00e9lai d'arr\u00eat", "off_delay_enabled": "Activer le d\u00e9lai d'arr\u00eat", "replace_device": "S\u00e9lectionnez l'appareil \u00e0 remplacer", - "signal_repetitions": "Nombre de r\u00e9p\u00e9titions du signal" + "signal_repetitions": "Nombre de r\u00e9p\u00e9titions du signal", + "venetian_blind_mode": "Mode store v\u00e9nitien" }, "title": "Configurer les options de l'appareil" } diff --git a/homeassistant/components/roku/translations/fr.json b/homeassistant/components/roku/translations/fr.json index 7aba1ef0489..6d237992592 100644 --- a/homeassistant/components/roku/translations/fr.json +++ b/homeassistant/components/roku/translations/fr.json @@ -9,6 +9,14 @@ }, "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "data": { + "one": "Vide", + "other": "Vide" + }, + "description": "Voulez-vous configurer {name} ?", + "title": "Roku" + }, "ssdp_confirm": { "data": { "one": "Vide", diff --git a/homeassistant/components/roku/translations/sv.json b/homeassistant/components/roku/translations/sv.json index 6d6f9223466..524e7753548 100644 --- a/homeassistant/components/roku/translations/sv.json +++ b/homeassistant/components/roku/translations/sv.json @@ -2,6 +2,18 @@ "config": { "abort": { "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "Roku: {name}", + "step": { + "ssdp_confirm": { + "description": "Vill du konfigurera {name}?", + "title": "Roku" + }, + "user": { + "data": { + "host": "V\u00e4rd" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/fr.json b/homeassistant/components/roomba/translations/fr.json index 8142d3acf13..b4bc615e4e3 100644 --- a/homeassistant/components/roomba/translations/fr.json +++ b/homeassistant/components/roomba/translations/fr.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "cannot_connect": "Echec de connection" + "already_configured": "L'appareil est d\u00e9ja configur\u00e9 ", + "cannot_connect": "Echec de connection", + "not_irobot_device": "L'appareil d\u00e9couvert n'est pas un appareil iRobot" }, "error": { "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer" }, + "flow_title": "iRobot {name} ( {host} )", "step": { "init": { "data": { diff --git a/homeassistant/components/shelly/translations/fr.json b/homeassistant/components/shelly/translations/fr.json index e40da9f5e68..0dea9111c62 100644 --- a/homeassistant/components/shelly/translations/fr.json +++ b/homeassistant/components/shelly/translations/fr.json @@ -27,5 +27,20 @@ "description": "Avant la configuration, l'appareil aliment\u00e9 par batterie doit \u00eatre r\u00e9veill\u00e9 en appuyant sur le bouton de l'appareil." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Bouton", + "button1": "Premier bouton", + "button2": "Deuxi\u00e8me bouton", + "button3": "Troisi\u00e8me bouton" + }, + "trigger_type": { + "double": "{subtype} double-cliqu\u00e9", + "long": " {sous-type} long cliqu\u00e9", + "single": "{subtype} simple clic", + "single_long": "{subtype} simple clic, puis un clic long", + "triple": "{subtype} cliqu\u00e9 trois fois" + } } } \ No newline at end of file diff --git a/homeassistant/components/shopping_list/translations/sv.json b/homeassistant/components/shopping_list/translations/sv.json new file mode 100644 index 00000000000..0202ae3a53f --- /dev/null +++ b/homeassistant/components/shopping_list/translations/sv.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten har redan konfigurerats" + }, + "step": { + "user": { + "description": "Vill du konfigurera ink\u00f6pslistan?", + "title": "Ink\u00f6pslista" + } + } + }, + "title": "Ink\u00f6pslista" +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/sv.json b/homeassistant/components/simplisafe/translations/sv.json index a1bfb4400be..a4e8e052073 100644 --- a/homeassistant/components/simplisafe/translations/sv.json +++ b/homeassistant/components/simplisafe/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Det h\u00e4r SimpliSafe-kontot har redan konfigurerats." + }, "error": { "identifier_exists": "Kontot \u00e4r redan registrerat" }, diff --git a/homeassistant/components/solaredge/translations/fr.json b/homeassistant/components/solaredge/translations/fr.json index 6fa6fdf264f..3eea6678d03 100644 --- a/homeassistant/components/solaredge/translations/fr.json +++ b/homeassistant/components/solaredge/translations/fr.json @@ -1,9 +1,12 @@ { "config": { "abort": { + "already_configured": "L'appareil est d\u00e9ja configur\u00e9 ", "site_exists": "Ce site est d\u00e9j\u00e0 configur\u00e9" }, "error": { + "already_configured": "L'appareil est d\u00e9ja configur\u00e9 ", + "could_not_connect": "Impossible de se connecter \u00e0 l'API solaredge", "invalid_api_key": "Cl\u00e9 API invalide", "site_exists": "Ce site est d\u00e9j\u00e0 configur\u00e9", "site_not_active": "The site n'est pas actif" diff --git a/homeassistant/components/somfy_mylink/translations/fr.json b/homeassistant/components/somfy_mylink/translations/fr.json index 96904b6038d..bee2ea3ba13 100644 --- a/homeassistant/components/somfy_mylink/translations/fr.json +++ b/homeassistant/components/somfy_mylink/translations/fr.json @@ -1,13 +1,22 @@ { "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9ja configur\u00e9 " + }, "error": { + "cannot_connect": "\u00c9chec de la connexion ", + "invalid_auth": "Authentification invalide ", "unknown": "Erreur inattendue" }, + "flow_title": "Somfy MyLink {mac} ( {ip} )", "step": { "user": { "data": { - "host": "H\u00f4te" - } + "host": "H\u00f4te", + "port": "Port", + "system_id": "ID syst\u00e8me" + }, + "description": "L'ID syst\u00e8me peut \u00eatre obtenu dans l'application MyLink sous Int\u00e9gration en s\u00e9lectionnant n'importe quel service non cloud." } } }, @@ -20,6 +29,7 @@ "data": { "reverse": "La couverture est invers\u00e9e" }, + "description": "Configurer les options pour \u00ab {entity_id} \u00bb", "title": "Configurez une entit\u00e9 sp\u00e9cifique" }, "init": { diff --git a/homeassistant/components/spotify/translations/fr.json b/homeassistant/components/spotify/translations/fr.json index f4f6566e88d..d6b5838feb5 100644 --- a/homeassistant/components/spotify/translations/fr.json +++ b/homeassistant/components/spotify/translations/fr.json @@ -18,5 +18,10 @@ "title": "R\u00e9-authentifier avec Spotify" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Point de terminaison de l'API Spotify accessible" + } } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/fr.json b/homeassistant/components/srp_energy/translations/fr.json index 0cc85aff649..b9b33cfa930 100644 --- a/homeassistant/components/srp_energy/translations/fr.json +++ b/homeassistant/components/srp_energy/translations/fr.json @@ -1,14 +1,24 @@ { "config": { + "abort": { + "single_instance_allowed": "D\u00e9ja configur\u00e9. Seulement une seule configuration est possible " + }, "error": { + "cannot_connect": "\u00c9chec de la connexion ", + "invalid_account": "L'ID de compte doit \u00eatre un num\u00e9ro \u00e0 9 chiffres", + "invalid_auth": "Authentification invalide ", "unknown": "Erreur inattendue" }, "step": { "user": { "data": { - "password": "Mot de passe" + "id": "Identifiant de compte", + "is_tou": "Est le plan de temps d'utilisation", + "password": "Mot de passe", + "username": "Nom d'utilisateur " } } } - } + }, + "title": "\u00c9nergie SRP" } \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/fr.json b/homeassistant/components/twinkly/translations/fr.json index 5071b7e302a..c26edea54ee 100644 --- a/homeassistant/components/twinkly/translations/fr.json +++ b/homeassistant/components/twinkly/translations/fr.json @@ -11,7 +11,8 @@ "data": { "host": "Nom r\u00e9seau (ou adresse IP) de votre Twinkly" }, - "description": "Configurer votre Twinkly" + "description": "Configurer votre Twinkly", + "title": "Twinkly" } } } diff --git a/homeassistant/components/unifi/translations/fr.json b/homeassistant/components/unifi/translations/fr.json index 6e5412ba3d2..49d9c68c01c 100644 --- a/homeassistant/components/unifi/translations/fr.json +++ b/homeassistant/components/unifi/translations/fr.json @@ -1,13 +1,15 @@ { "config": { "abort": { - "already_configured": "Le contr\u00f4leur est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Le contr\u00f4leur est d\u00e9j\u00e0 configur\u00e9", + "configuration_updated": "Configuration mise \u00e0 jour." }, "error": { "faulty_credentials": "Authentification invalide", "service_unavailable": "\u00c9chec de connexion", "unknown_client_mac": "Aucun client disponible sur cette adresse MAC" }, + "flow_title": "UniFi Network {site} ( {host} )", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/sv.json b/homeassistant/components/unifi/translations/sv.json index eb4e0d8ee3d..2e4851e70ed 100644 --- a/homeassistant/components/unifi/translations/sv.json +++ b/homeassistant/components/unifi/translations/sv.json @@ -24,13 +24,17 @@ }, "options": { "step": { + "client_control": { + "title": "UniFi-inst\u00e4llningar 2/3" + }, "device_tracker": { "data": { "detection_time": "Tid i sekunder fr\u00e5n senast sett tills den anses borta", "track_clients": "Sp\u00e5ra n\u00e4tverksklienter", "track_devices": "Sp\u00e5ra n\u00e4tverksenheter (Ubiquiti-enheter)", "track_wired_clients": "Inkludera tr\u00e5dbundna n\u00e4tverksklienter" - } + }, + "title": "UniFi-inst\u00e4llningar 1/3" }, "init": { "data": { @@ -44,7 +48,8 @@ "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Skapa bandbreddsanv\u00e4ndningssensorer f\u00f6r n\u00e4tverksklienter" - } + }, + "title": "UniFi-inst\u00e4llningar 2/3" } } } diff --git a/homeassistant/components/vizio/translations/fr.json b/homeassistant/components/vizio/translations/fr.json index 89c46fd5959..5fc9158c803 100644 --- a/homeassistant/components/vizio/translations/fr.json +++ b/homeassistant/components/vizio/translations/fr.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured_device": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de la connexion ", "updated_entry": "Cette entr\u00e9e a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e mais le nom et/ou les options d\u00e9finis dans la configuration ne correspondent pas \u00e0 la configuration pr\u00e9c\u00e9demment import\u00e9e, de sorte que l'entr\u00e9e de configuration a \u00e9t\u00e9 mise \u00e0 jour en cons\u00e9quence." }, "error": { diff --git a/homeassistant/components/vizio/translations/sv.json b/homeassistant/components/vizio/translations/sv.json index 8e5ebe47c43..82483d80fe8 100644 --- a/homeassistant/components/vizio/translations/sv.json +++ b/homeassistant/components/vizio/translations/sv.json @@ -4,6 +4,19 @@ "updated_entry": "Den h\u00e4r posten har redan konfigurerats, men namnet och/eller alternativen som definierats i konfigurationen matchar inte den tidigare importerade konfigurationen och d\u00e4rf\u00f6r har konfigureringsposten uppdaterats i enlighet med detta." }, "step": { + "pair_tv": { + "data": { + "pin": "PIN-kod" + }, + "description": "Din TV borde visa en kod. Skriv koden i formul\u00e4ret och forts\u00e4tt sedan till n\u00e4sta steg f\u00f6r att slutf\u00f6ra parningen.", + "title": "Slutf\u00f6r parningsprocessen" + }, + "pairing_complete": { + "title": "Parkopplingen slutf\u00f6rd" + }, + "pairing_complete_import": { + "title": "Parkopplingen slutf\u00f6rd" + }, "user": { "data": { "access_token": "\u00c5tkomstnyckel", diff --git a/homeassistant/components/wled/translations/sv.json b/homeassistant/components/wled/translations/sv.json index 3c802a87007..aea858c5bfc 100644 --- a/homeassistant/components/wled/translations/sv.json +++ b/homeassistant/components/wled/translations/sv.json @@ -1,10 +1,19 @@ { "config": { + "abort": { + "already_configured": "Enheten har redan konfigurerats" + }, + "flow_title": "WLED: {name}", "step": { "user": { "data": { "host": "V\u00e4rd eller IP-adress" - } + }, + "description": "St\u00e4ll in din WLED f\u00f6r att integrera med Home Assistant." + }, + "zeroconf_confirm": { + "description": "Vill du l\u00e4gga till WLED-enheten `{name}` till Home Assistant?", + "title": "Uppt\u00e4ckte WLED-enhet" } } } diff --git a/homeassistant/components/zwave_js/translations/fr.json b/homeassistant/components/zwave_js/translations/fr.json index f3a9aff1a29..e52552fa986 100644 --- a/homeassistant/components/zwave_js/translations/fr.json +++ b/homeassistant/components/zwave_js/translations/fr.json @@ -1,14 +1,48 @@ { "config": { "abort": { - "already_configured": "Le p\u00e9riph\u00e9rique est d\u00e9j\u00e0 configur\u00e9" + "addon_get_discovery_info_failed": "Impossible d'obtenir les informations de d\u00e9couverte du module compl\u00e9mentaire Z-Wave JS.", + "addon_info_failed": "Impossible d'obtenir les informations sur le module compl\u00e9mentaire Z-Wave JS.", + "addon_install_failed": "\u00c9chec de l'installation du module compl\u00e9mentaire Z-Wave JS.", + "addon_missing_discovery_info": "Informations manquantes sur la d\u00e9couverte du module compl\u00e9mentaire Z-Wave JS.", + "addon_set_config_failed": "\u00c9chec de la d\u00e9finition de la configuration Z-Wave JS.", + "already_configured": "Le p\u00e9riph\u00e9rique est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de la connexion " }, "error": { + "addon_start_failed": "\u00c9chec du d\u00e9marrage du module compl\u00e9mentaire Z-Wave JS. V\u00e9rifiez la configuration.", "cannot_connect": "Erreur de connection", "invalid_ws_url": "URL websocket invalide", "unknown": "Erreur inattendue" }, + "progress": { + "install_addon": "Veuillez patienter pendant l'installation du module compl\u00e9mentaire Z-Wave JS. Cela peut prendre plusieurs minutes." + }, "step": { + "hassio_confirm": { + "title": "Configurer l'int\u00e9gration Z-Wave JS avec le module compl\u00e9mentaire Z-Wave JS" + }, + "install_addon": { + "title": "L'installation du module compl\u00e9mentaire Z-Wave JS a d\u00e9marr\u00e9" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Utiliser le module compl\u00e9mentaire Z-Wave JS Supervisor" + }, + "description": "Voulez-vous utiliser le module compl\u00e9mentaire Z-Wave JS Supervisor?", + "title": "S\u00e9lectionner la m\u00e9thode de connexion" + }, + "start_addon": { + "data": { + "network_key": "Cl\u00e9 r\u00e9seau" + }, + "title": "Entrez la configuration du module compl\u00e9mentaire Z-Wave JS" + }, "user": { "data": { "url": "URL" From f2d9e6f70c7f73489705a086482ac7f52d093f56 Mon Sep 17 00:00:00 2001 From: djtimca <60706061+djtimca@users.noreply.github.com> Date: Sat, 6 Feb 2021 02:05:39 -0500 Subject: [PATCH 220/796] Add sensor platform for Aurora integration (#43148) * Removed pylint disable on unused after updating CI files that were out of date. * Pylint still fails due to bug on DOMAIN import. Added disable check. * Addressed PR comments * Added import for ClientError to test_config_flow.py Co-authored-by: Paulus Schoutsen --- .coveragerc | 1 + homeassistant/components/aurora/__init__.py | 67 +++++++++++++++++-- .../components/aurora/binary_sensor.py | 59 ++-------------- .../components/aurora/config_flow.py | 12 +++- homeassistant/components/aurora/const.py | 1 + homeassistant/components/aurora/sensor.py | 36 ++++++++++ tests/components/aurora/test_config_flow.py | 4 +- 7 files changed, 119 insertions(+), 61 deletions(-) create mode 100644 homeassistant/components/aurora/sensor.py diff --git a/.coveragerc b/.coveragerc index 3a274cd004f..581c6350a05 100644 --- a/.coveragerc +++ b/.coveragerc @@ -72,6 +72,7 @@ omit = homeassistant/components/aurora/__init__.py homeassistant/components/aurora/binary_sensor.py homeassistant/components/aurora/const.py + homeassistant/components/aurora/sensor.py homeassistant/components/aurora_abb_powerone/sensor.py homeassistant/components/avea/light.py homeassistant/components/avion/light.py diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py index 260a3bd735d..a187288e2e4 100644 --- a/homeassistant/components/aurora/__init__.py +++ b/homeassistant/components/aurora/__init__.py @@ -4,16 +4,26 @@ import asyncio from datetime import timedelta import logging +from aiohttp import ClientError from auroranoaa import AuroraForecast from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.const import ATTR_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from .const import ( + ATTR_ENTRY_TYPE, + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTRIBUTION, AURORA_API, CONF_THRESHOLD, COORDINATOR, @@ -24,7 +34,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["binary_sensor"] +PLATFORMS = ["binary_sensor", "sensor"] async def async_setup(hass: HomeAssistant, config: dict): @@ -126,5 +136,54 @@ class AuroraDataUpdateCoordinator(DataUpdateCoordinator): try: return await self.api.get_forecast_data(self.longitude, self.latitude) - except ConnectionError as error: + except ClientError as error: raise UpdateFailed(f"Error updating from NOAA: {error}") from error + + +class AuroraEntity(CoordinatorEntity): + """Implementation of the base Aurora Entity.""" + + def __init__( + self, + coordinator: AuroraDataUpdateCoordinator, + name: str, + icon: str, + ): + """Initialize the Aurora Entity.""" + + super().__init__(coordinator=coordinator) + + self._name = name + self._unique_id = f"{self.coordinator.latitude}_{self.coordinator.longitude}" + self._icon = icon + + @property + def unique_id(self): + """Define the unique id based on the latitude and longitude.""" + return self._unique_id + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return {"attribution": ATTRIBUTION} + + @property + def icon(self): + """Return the icon for the sensor.""" + return self._icon + + @property + def device_info(self): + """Define the device based on name.""" + return { + ATTR_IDENTIFIERS: {(DOMAIN, self._unique_id)}, + ATTR_NAME: self.coordinator.name, + ATTR_MANUFACTURER: "NOAA", + ATTR_MODEL: "Aurora Visibility Sensor", + ATTR_ENTRY_TYPE: "service", + } diff --git a/homeassistant/components/aurora/binary_sensor.py b/homeassistant/components/aurora/binary_sensor.py index 82be366ce6d..3ea1faa7949 100644 --- a/homeassistant/components/aurora/binary_sensor.py +++ b/homeassistant/components/aurora/binary_sensor.py @@ -1,19 +1,10 @@ -"""Support for aurora forecast data sensor.""" +"""Support for Aurora Forecast binary sensor.""" import logging from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.const import ATTR_NAME -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import AuroraDataUpdateCoordinator -from .const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTRIBUTION, - COORDINATOR, - DOMAIN, -) +from . import AuroraEntity +from .const import COORDINATOR, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -21,55 +12,17 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, entry, async_add_entries): """Set up the binary_sensor platform.""" coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR] - name = coordinator.name + name = f"{coordinator.name} Aurora Visibility Alert" - entity = AuroraSensor(coordinator, name) + entity = AuroraSensor(coordinator=coordinator, name=name, icon="mdi:hazard-lights") async_add_entries([entity]) -class AuroraSensor(CoordinatorEntity, BinarySensorEntity): +class AuroraSensor(AuroraEntity, BinarySensorEntity): """Implementation of an aurora sensor.""" - def __init__(self, coordinator: AuroraDataUpdateCoordinator, name): - """Define the binary sensor for the Aurora integration.""" - super().__init__(coordinator=coordinator) - - self._name = name - self.coordinator = coordinator - self._unique_id = f"{self.coordinator.latitude}_{self.coordinator.longitude}" - - @property - def unique_id(self): - """Define the unique id based on the latitude and longitude.""" - return self._unique_id - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - @property def is_on(self): """Return true if aurora is visible.""" return self.coordinator.data > self.coordinator.threshold - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return {"attribution": ATTRIBUTION} - - @property - def icon(self): - """Return the icon for the sensor.""" - return "mdi:hazard-lights" - - @property - def device_info(self): - """Define the device based on name.""" - return { - ATTR_IDENTIFIERS: {(DOMAIN, self._unique_id)}, - ATTR_NAME: self.coordinator.name, - ATTR_MANUFACTURER: "NOAA", - ATTR_MODEL: "Aurora Visibility Sensor", - } diff --git a/homeassistant/components/aurora/config_flow.py b/homeassistant/components/aurora/config_flow.py index 37885cc87cf..24161c059c1 100644 --- a/homeassistant/components/aurora/config_flow.py +++ b/homeassistant/components/aurora/config_flow.py @@ -1,6 +1,7 @@ """Config flow for SpaceX Launches and Starman.""" import logging +from aiohttp import ClientError from auroranoaa import AuroraForecast import voluptuous as vol @@ -9,7 +10,12 @@ from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client -from .const import CONF_THRESHOLD, DEFAULT_NAME, DEFAULT_THRESHOLD, DOMAIN +from .const import ( # pylint: disable=unused-import + CONF_THRESHOLD, + DEFAULT_NAME, + DEFAULT_THRESHOLD, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) @@ -40,14 +46,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: await api.get_forecast_data(longitude, latitude) - except ConnectionError: + except ClientError: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: await self.async_set_unique_id( - f"{DOMAIN}_{user_input[CONF_LONGITUDE]}_{user_input[CONF_LATITUDE]}" + f"{user_input[CONF_LONGITUDE]}_{user_input[CONF_LATITUDE]}" ) self._abort_if_unique_id_configured() return self.async_create_entry( diff --git a/homeassistant/components/aurora/const.py b/homeassistant/components/aurora/const.py index f4451de863d..cd6f54a3d0c 100644 --- a/homeassistant/components/aurora/const.py +++ b/homeassistant/components/aurora/const.py @@ -6,6 +6,7 @@ AURORA_API = "aurora_api" ATTR_IDENTIFIERS = "identifiers" ATTR_MANUFACTURER = "manufacturer" ATTR_MODEL = "model" +ATTR_ENTRY_TYPE = "entry_type" DEFAULT_POLLING_INTERVAL = 5 CONF_THRESHOLD = "forecast_threshold" DEFAULT_THRESHOLD = 75 diff --git a/homeassistant/components/aurora/sensor.py b/homeassistant/components/aurora/sensor.py new file mode 100644 index 00000000000..51ccb3dc4dd --- /dev/null +++ b/homeassistant/components/aurora/sensor.py @@ -0,0 +1,36 @@ +"""Support for Aurora Forecast sensor.""" +import logging + +from homeassistant.const import PERCENTAGE + +from . import AuroraEntity +from .const import COORDINATOR, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entries): + """Set up the sensor platform.""" + coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR] + + entity = AuroraSensor( + coordinator=coordinator, + name=f"{coordinator.name} Aurora Visibility %", + icon="mdi:gauge", + ) + + async_add_entries([entity]) + + +class AuroraSensor(AuroraEntity): + """Implementation of an aurora sensor.""" + + @property + def state(self): + """Return % chance the aurora is visible.""" + return self.coordinator.data + + @property + def unit_of_measurement(self): + """Return the unit of measure.""" + return PERCENTAGE diff --git a/tests/components/aurora/test_config_flow.py b/tests/components/aurora/test_config_flow.py index b9e0496f668..d9d0c4fd128 100644 --- a/tests/components/aurora/test_config_flow.py +++ b/tests/components/aurora/test_config_flow.py @@ -2,6 +2,8 @@ from unittest.mock import patch +from aiohttp import ClientError + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.aurora.const import DOMAIN @@ -55,7 +57,7 @@ async def test_form_cannot_connect(hass): with patch( "homeassistant.components.aurora.AuroraForecast.get_forecast_data", - side_effect=ConnectionError, + side_effect=ClientError, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], From 8a7e0241ab0c91e2ed6b8672188988969b64ef14 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 6 Feb 2021 10:01:30 +0100 Subject: [PATCH 221/796] Fix race in script wait for trigger step (#46055) * Fix race in script wait for trigger step * Update script.py * Update script.py --- homeassistant/helpers/script.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index f197664f7e6..60a1be6103a 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -623,6 +623,8 @@ class _ScriptRun: variables = {**self._variables} self._variables["wait"] = {"remaining": delay, "trigger": None} + done = asyncio.Event() + async def async_done(variables, context=None): self._variables["wait"] = { "remaining": to_context.remaining if to_context else delay, @@ -647,7 +649,6 @@ class _ScriptRun: return self._changed() - done = asyncio.Event() tasks = [ self._hass.async_create_task(flag.wait()) for flag in (self._stop, done) ] From ee98ea89ddffe9e7363d8f687a04bc69a9ecfe55 Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Sat, 6 Feb 2021 02:55:21 -0700 Subject: [PATCH 222/796] Bump aioharmony from 0.2.6 to 0.2.7 (#46075) --- homeassistant/components/harmony/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/harmony/manifest.json b/homeassistant/components/harmony/manifest.json index 7509f3d4f4d..eb7a99fffa8 100644 --- a/homeassistant/components/harmony/manifest.json +++ b/homeassistant/components/harmony/manifest.json @@ -2,7 +2,7 @@ "domain": "harmony", "name": "Logitech Harmony Hub", "documentation": "https://www.home-assistant.io/integrations/harmony", - "requirements": ["aioharmony==0.2.6"], + "requirements": ["aioharmony==0.2.7"], "codeowners": ["@ehendrix23", "@bramkragten", "@bdraco", "@mkeesey"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 2cc29c3be4b..1962b4d393e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -163,7 +163,7 @@ aioftp==0.12.0 aioguardian==1.0.4 # homeassistant.components.harmony -aioharmony==0.2.6 +aioharmony==0.2.7 # homeassistant.components.homekit_controller aiohomekit==0.2.60 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a447b539ef..65b19d7aabb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -97,7 +97,7 @@ aioflo==0.4.1 aioguardian==1.0.4 # homeassistant.components.harmony -aioharmony==0.2.6 +aioharmony==0.2.7 # homeassistant.components.homekit_controller aiohomekit==0.2.60 From af4e6f856f076453aa74d0981d885825c269b728 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 6 Feb 2021 05:08:25 -0600 Subject: [PATCH 223/796] Use better names for zwave_js platforms that are self describing (#46083) * use better names for platforms that are self describing * add missing light change * fix tests * only use value_name in sensors and binary_sensors --- .../components/zwave_js/binary_sensor.py | 16 +++++++++++++-- homeassistant/components/zwave_js/entity.py | 18 +++++++++-------- homeassistant/components/zwave_js/sensor.py | 20 ++++++++++++++++--- tests/components/zwave_js/common.py | 2 +- tests/components/zwave_js/test_climate.py | 6 +++--- tests/components/zwave_js/test_cover.py | 2 +- tests/components/zwave_js/test_fan.py | 2 +- tests/components/zwave_js/test_light.py | 4 ++-- tests/components/zwave_js/test_lock.py | 2 +- 9 files changed, 50 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index bb2e4355f16..8a483c34e12 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -257,6 +257,16 @@ async def async_setup_entry( class ZWaveBooleanBinarySensor(ZWaveBaseEntity, BinarySensorEntity): """Representation of a Z-Wave binary_sensor.""" + def __init__( + self, + config_entry: ConfigEntry, + client: ZwaveClient, + info: ZwaveDiscoveryInfo, + ) -> None: + """Initialize a ZWaveBooleanBinarySensor entity.""" + super().__init__(config_entry, client, info) + self._name = self.generate_name(include_value_name=True) + @property def is_on(self) -> bool: """Return if the sensor is on or off.""" @@ -294,8 +304,9 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity): super().__init__(config_entry, client, info) self.state_key = state_key self._name = self.generate_name( - self.info.primary_value.property_name, - [self.info.primary_value.metadata.states[self.state_key]], + include_value_name=True, + alternate_value_name=self.info.primary_value.property_name, + additional_info=[self.info.primary_value.metadata.states[self.state_key]], ) # check if we have a custom mapping for this value self._mapping_info = self._get_sensor_mapping() @@ -347,6 +358,7 @@ class ZWavePropertyBinarySensor(ZWaveBaseEntity, BinarySensorEntity): super().__init__(config_entry, client, info) # check if we have a custom mapping for this value self._mapping_info = self._get_sensor_mapping() + self._name = self.generate_name(include_value_name=True) @property def is_on(self) -> bool: diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 08571ad5d8c..5d45cb9511a 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -64,20 +64,22 @@ class ZWaveBaseEntity(Entity): def generate_name( self, + include_value_name: bool = False, alternate_value_name: Optional[str] = None, additional_info: Optional[List[str]] = None, ) -> str: """Generate entity name.""" if additional_info is None: additional_info = [] - node_name = self.info.node.name or self.info.node.device_config.description - value_name = ( - alternate_value_name - or self.info.primary_value.metadata.label - or self.info.primary_value.property_key_name - or self.info.primary_value.property_name - ) - name = f"{node_name}: {value_name}" + name: str = self.info.node.name or self.info.node.device_config.description + if include_value_name: + value_name = ( + alternate_value_name + or self.info.primary_value.metadata.label + or self.info.primary_value.property_key_name + or self.info.primary_value.property_name + ) + name = f"{name}: {value_name}" for item in additional_info: if item: name += f" - {item}" diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 78b536b81f7..8e22323c733 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -64,6 +64,16 @@ async def async_setup_entry( class ZwaveSensorBase(ZWaveBaseEntity): """Basic Representation of a Z-Wave sensor.""" + def __init__( + self, + config_entry: ConfigEntry, + client: ZwaveClient, + info: ZwaveDiscoveryInfo, + ) -> None: + """Initialize a ZWaveSensorBase entity.""" + super().__init__(config_entry, client, info) + self._name = self.generate_name(include_value_name=True) + @property def device_class(self) -> Optional[str]: """Return the device class of the sensor.""" @@ -132,7 +142,10 @@ class ZWaveNumericSensor(ZwaveSensorBase): """Initialize a ZWaveNumericSensor entity.""" super().__init__(config_entry, client, info) if self.info.primary_value.command_class == CommandClass.BASIC: - self._name = self.generate_name(self.info.primary_value.command_class_name) + self._name = self.generate_name( + include_value_name=True, + alternate_value_name=self.info.primary_value.command_class_name, + ) @property def state(self) -> float: @@ -166,8 +179,9 @@ class ZWaveListSensor(ZwaveSensorBase): """Initialize a ZWaveListSensor entity.""" super().__init__(config_entry, client, info) self._name = self.generate_name( - self.info.primary_value.property_name, - [self.info.primary_value.property_key_name], + include_value_name=True, + alternate_value_name=self.info.primary_value.property_name, + additional_info=[self.info.primary_value.property_key_name], ) @property diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index 63ec9013fa3..9c6adb100fa 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -2,7 +2,7 @@ AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature" ENERGY_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_2" POWER_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" -SWITCH_ENTITY = "switch.smart_plug_with_two_usb_ports_current_value" +SWITCH_ENTITY = "switch.smart_plug_with_two_usb_ports" LOW_BATTERY_BINARY_SENSOR = "binary_sensor.multisensor_6_low_battery_level" ENABLED_LEGACY_BINARY_SENSOR = "binary_sensor.z_wave_door_window_sensor_any" DISABLED_LEGACY_BINARY_SENSOR = "binary_sensor.multisensor_6_any" diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index b2455f3cbbd..e11d3b75c47 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -25,9 +25,9 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE -CLIMATE_RADIO_THERMOSTAT_ENTITY = "climate.z_wave_thermostat_thermostat_mode" -CLIMATE_DANFOSS_LC13_ENTITY = "climate.living_connect_z_thermostat_heating" -CLIMATE_FLOOR_THERMOSTAT_ENTITY = "climate.floor_thermostat_thermostat_mode" +CLIMATE_RADIO_THERMOSTAT_ENTITY = "climate.z_wave_thermostat" +CLIMATE_DANFOSS_LC13_ENTITY = "climate.living_connect_z_thermostat" +CLIMATE_FLOOR_THERMOSTAT_ENTITY = "climate.floor_thermostat" async def test_thermostat_v2( diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index 52e0a444ec9..5e50ffb226e 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -3,7 +3,7 @@ from zwave_js_server.event import Event from homeassistant.components.cover import ATTR_CURRENT_POSITION -WINDOW_COVER_ENTITY = "cover.zws_12_current_value" +WINDOW_COVER_ENTITY = "cover.zws_12" async def test_cover(hass, client, chain_actuator_zws12, integration): diff --git a/tests/components/zwave_js/test_fan.py b/tests/components/zwave_js/test_fan.py index a817a551f9b..5bd856c664a 100644 --- a/tests/components/zwave_js/test_fan.py +++ b/tests/components/zwave_js/test_fan.py @@ -4,7 +4,7 @@ from zwave_js_server.event import Event from homeassistant.components.fan import ATTR_SPEED, SPEED_MEDIUM -FAN_ENTITY = "fan.in_wall_smart_fan_control_current_value" +FAN_ENTITY = "fan.in_wall_smart_fan_control" async def test_fan(hass, client, in_wall_smart_fan_control, integration): diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py index b60c7281874..bcd9a2edd9f 100644 --- a/tests/components/zwave_js/test_light.py +++ b/tests/components/zwave_js/test_light.py @@ -12,8 +12,8 @@ from homeassistant.components.light import ( ) from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON -BULB_6_MULTI_COLOR_LIGHT_ENTITY = "light.bulb_6_multi_color_current_value" -EATON_RF9640_ENTITY = "light.allloaddimmer_current_value" +BULB_6_MULTI_COLOR_LIGHT_ENTITY = "light.bulb_6_multi_color" +EATON_RF9640_ENTITY = "light.allloaddimmer" async def test_light(hass, client, bulb_6_multi_color, integration): diff --git a/tests/components/zwave_js/test_lock.py b/tests/components/zwave_js/test_lock.py index 069b3497a55..9ddc7abdd88 100644 --- a/tests/components/zwave_js/test_lock.py +++ b/tests/components/zwave_js/test_lock.py @@ -14,7 +14,7 @@ from homeassistant.components.zwave_js.lock import ( ) from homeassistant.const import ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED -SCHLAGE_BE469_LOCK_ENTITY = "lock.touchscreen_deadbolt_current_lock_mode" +SCHLAGE_BE469_LOCK_ENTITY = "lock.touchscreen_deadbolt" async def test_door_lock(hass, client, lock_schlage_be469, integration): From 369616a6c38f73f8f1c89f83b995e149abc75297 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 6 Feb 2021 12:19:41 +0100 Subject: [PATCH 224/796] Exclude disabled rfxtrx entities from async_entries_for_device (#46102) --- homeassistant/components/rfxtrx/config_flow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 5eeb9b38411..da4d6447e76 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -344,7 +344,9 @@ class OptionsFlow(config_entries.OptionsFlow): new_device_id = "_".join(x for x in new_device_data[CONF_DEVICE_ID]) entity_registry = await async_get_entity_registry(self.hass) - entity_entries = async_entries_for_device(entity_registry, old_device) + entity_entries = async_entries_for_device( + entity_registry, old_device, include_disabled_entities=True + ) entity_migration_map = {} for entry in entity_entries: unique_id = entry.unique_id From a74ae3585a40dd89c38a830dd030e9694094e685 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Feb 2021 01:48:18 -1000 Subject: [PATCH 225/796] Fix backwards compatiblity with fan update to new model (#45951) * Fix backwards compatiblity with fans update to new model There were some non-speeds and devices that report a none speed. These problems were discovered when updating zha tasmota and vesync to the new model in #45407 * Update coverage * fix check --- homeassistant/components/demo/fan.py | 27 ++++++++-- homeassistant/components/fan/__init__.py | 28 ++++++++-- tests/components/demo/test_fan.py | 68 +++++++++++++++++++++++- 3 files changed, 114 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index bd6661b6c2b..cb6036a8938 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -15,6 +15,8 @@ from homeassistant.components.fan import ( PRESET_MODE_AUTO = "auto" PRESET_MODE_SMART = "smart" +PRESET_MODE_SLEEP = "sleep" +PRESET_MODE_ON = "on" FULL_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION LIMITED_SUPPORT = SUPPORT_SET_SPEED @@ -38,6 +40,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= SPEED_HIGH, PRESET_MODE_AUTO, PRESET_MODE_SMART, + PRESET_MODE_SLEEP, + PRESET_MODE_ON, ], ), DemoFan( @@ -54,7 +58,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "fan3", "Percentage Full Fan", FULL_SUPPORT, - [PRESET_MODE_AUTO, PRESET_MODE_SMART], + [ + PRESET_MODE_AUTO, + PRESET_MODE_SMART, + PRESET_MODE_SLEEP, + PRESET_MODE_ON, + ], None, ), DemoPercentageFan( @@ -62,7 +71,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "fan4", "Percentage Limited Fan", LIMITED_SUPPORT, - [PRESET_MODE_AUTO, PRESET_MODE_SMART], + [ + PRESET_MODE_AUTO, + PRESET_MODE_SMART, + PRESET_MODE_SLEEP, + PRESET_MODE_ON, + ], None, ), AsyncDemoPercentageFan( @@ -70,7 +84,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "fan5", "Preset Only Limited Fan", SUPPORT_PRESET_MODE, - [PRESET_MODE_AUTO, PRESET_MODE_SMART], + [ + PRESET_MODE_AUTO, + PRESET_MODE_SMART, + PRESET_MODE_SLEEP, + PRESET_MODE_ON, + ], [], ), ] @@ -99,7 +118,7 @@ class BaseDemoFan(FanEntity): self._unique_id = unique_id self._supported_features = supported_features self._speed = SPEED_OFF - self._percentage = 0 + self._percentage = None self._speed_list = speed_list self._preset_modes = preset_modes self._preset_mode = None diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 7b6b083c964..8d6fcbea2c9 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -64,18 +64,22 @@ ATTR_PRESET_MODES = "preset_modes" # into core integrations at some point so we are temporarily # accommodating them in the transition to percentages. _NOT_SPEED_OFF = "off" +_NOT_SPEED_ON = "on" _NOT_SPEED_AUTO = "auto" _NOT_SPEED_SMART = "smart" _NOT_SPEED_INTERVAL = "interval" _NOT_SPEED_IDLE = "idle" _NOT_SPEED_FAVORITE = "favorite" +_NOT_SPEED_SLEEP = "sleep" _NOT_SPEEDS_FILTER = { _NOT_SPEED_OFF, + _NOT_SPEED_ON, _NOT_SPEED_AUTO, _NOT_SPEED_SMART, _NOT_SPEED_INTERVAL, _NOT_SPEED_IDLE, + _NOT_SPEED_SLEEP, _NOT_SPEED_FAVORITE, } @@ -83,6 +87,8 @@ _FAN_NATIVE = "_fan_native" OFF_SPEED_VALUES = [SPEED_OFF, None] +LEGACY_SPEED_LIST = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + class NoValidSpeedsError(ValueError): """Exception class when there are no valid speeds.""" @@ -386,7 +392,10 @@ class FanEntity(ToggleEntity): if preset_mode: return preset_mode if self._implemented_percentage: - return self.percentage_to_speed(self.percentage) + percentage = self.percentage + if percentage is None: + return None + return self.percentage_to_speed(percentage) return None @property @@ -404,7 +413,7 @@ class FanEntity(ToggleEntity): """Get the list of available speeds.""" speeds = [] if self._implemented_percentage: - speeds += [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + speeds += [SPEED_OFF, *LEGACY_SPEED_LIST] if self._implemented_preset_mode: speeds += self.preset_modes return speeds @@ -434,6 +443,17 @@ class FanEntity(ToggleEntity): return attrs + @property + def _speed_list_without_preset_modes(self) -> list: + """Return the speed list without preset modes. + + This property provides forward and backwards + compatibility for conversion to percentage speeds. + """ + if not self._implemented_speed: + return LEGACY_SPEED_LIST + return speed_list_without_preset_modes(self.speed_list) + def speed_to_percentage(self, speed: str) -> int: """ Map a speed to a percentage. @@ -453,7 +473,7 @@ class FanEntity(ToggleEntity): if speed in OFF_SPEED_VALUES: return 0 - speed_list = speed_list_without_preset_modes(self.speed_list) + speed_list = self._speed_list_without_preset_modes if speed_list and speed not in speed_list: raise NotValidSpeedError(f"The speed {speed} is not a valid speed.") @@ -487,7 +507,7 @@ class FanEntity(ToggleEntity): if percentage == 0: return SPEED_OFF - speed_list = speed_list_without_preset_modes(self.speed_list) + speed_list = self._speed_list_without_preset_modes try: return percentage_to_ordered_list_item(speed_list, percentage) diff --git a/tests/components/demo/test_fan.py b/tests/components/demo/test_fan.py index 5297e64bda9..2439d49685c 100644 --- a/tests/components/demo/test_fan.py +++ b/tests/components/demo/test_fan.py @@ -2,7 +2,12 @@ import pytest from homeassistant.components import fan -from homeassistant.components.demo.fan import PRESET_MODE_AUTO, PRESET_MODE_SMART +from homeassistant.components.demo.fan import ( + PRESET_MODE_AUTO, + PRESET_MODE_ON, + PRESET_MODE_SLEEP, + PRESET_MODE_SMART, +) from homeassistant.const import ( ATTR_ENTITY_ID, ENTITY_MATCH_ALL, @@ -60,6 +65,28 @@ async def test_turn_on_with_speed_and_percentage(hass, fan_entity_id): assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH assert state.attributes[fan.ATTR_PERCENTAGE] == 100 + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_MEDIUM}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM + assert state.attributes[fan.ATTR_PERCENTAGE] == 66 + + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_LOW}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW + assert state.attributes[fan.ATTR_PERCENTAGE] == 33 + await hass.services.async_call( fan.DOMAIN, SERVICE_TURN_ON, @@ -71,6 +98,39 @@ async def test_turn_on_with_speed_and_percentage(hass, fan_entity_id): assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH assert state.attributes[fan.ATTR_PERCENTAGE] == 100 + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 66}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM + assert state.attributes[fan.ATTR_PERCENTAGE] == 66 + + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 33}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW + assert state.attributes[fan.ATTR_PERCENTAGE] == 33 + + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 0}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_OFF + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF + assert state.attributes[fan.ATTR_PERCENTAGE] == 0 + @pytest.mark.parametrize("fan_entity_id", FANS_WITH_PRESET_MODE_ONLY) async def test_turn_on_with_preset_mode_only(hass, fan_entity_id): @@ -89,6 +149,8 @@ async def test_turn_on_with_preset_mode_only(hass, fan_entity_id): assert state.attributes[fan.ATTR_PRESET_MODES] == [ PRESET_MODE_AUTO, PRESET_MODE_SMART, + PRESET_MODE_SLEEP, + PRESET_MODE_ON, ] await hass.services.async_call( @@ -145,10 +207,14 @@ async def test_turn_on_with_preset_mode_and_speed(hass, fan_entity_id): fan.SPEED_HIGH, PRESET_MODE_AUTO, PRESET_MODE_SMART, + PRESET_MODE_SLEEP, + PRESET_MODE_ON, ] assert state.attributes[fan.ATTR_PRESET_MODES] == [ PRESET_MODE_AUTO, PRESET_MODE_SMART, + PRESET_MODE_SLEEP, + PRESET_MODE_ON, ] await hass.services.async_call( From 94ecb792ec2fea66e2f9a65f0767693d1172eb8b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 6 Feb 2021 13:17:52 +0100 Subject: [PATCH 226/796] Use async_update_entry rather than updating config_entry.data directly in Axis (#46078) Don't step version in migrate_entry to support rollbacking --- homeassistant/components/axis/__init__.py | 15 ++++++++++----- tests/components/axis/test_init.py | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index c467359c17e..8722c41c3e0 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -48,8 +48,11 @@ async def async_migrate_entry(hass, config_entry): # Flatten configuration but keep old data if user rollbacks HASS prior to 0.106 if config_entry.version == 1: - config_entry.data = {**config_entry.data, **config_entry.data[CONF_DEVICE]} - config_entry.unique_id = config_entry.data[CONF_MAC] + unique_id = config_entry.data[CONF_MAC] + data = {**config_entry.data, **config_entry.data[CONF_DEVICE]} + hass.config_entries.async_update_entry( + config_entry, unique_id=unique_id, data=data + ) config_entry.version = 2 # Normalise MAC address of device which also affects entity unique IDs @@ -66,10 +69,12 @@ async def async_migrate_entry(hass, config_entry): ) } - await async_migrate_entries(hass, config_entry.entry_id, update_unique_id) + if old_unique_id != new_unique_id: + await async_migrate_entries(hass, config_entry.entry_id, update_unique_id) - config_entry.unique_id = new_unique_id - config_entry.version = 3 + hass.config_entries.async_update_entry( + config_entry, unique_id=new_unique_id + ) _LOGGER.info("Migration to version %s successful", config_entry.version) diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index b7faceaf10d..36a603ea7b3 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -109,7 +109,7 @@ async def test_migrate_entry(hass): CONF_MODEL: "model", CONF_NAME: "name", } - assert entry.version == 3 + assert entry.version == 2 # Keep version to support rollbacking assert entry.unique_id == "00:40:8c:12:34:56" vmd4_entity = registry.async_get("binary_sensor.vmd4") From fb68bf85ae09f97b0f3158839eb88606c302d465 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 6 Feb 2021 13:40:15 +0100 Subject: [PATCH 227/796] Don't defer formatting of log messages (#44873) * Make fast logging optional * Remove fast logging support --- homeassistant/util/logging.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 9b04c2ab007..423685fe9d4 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -30,13 +30,6 @@ class HideSensitiveDataFilter(logging.Filter): class HomeAssistantQueueHandler(logging.handlers.QueueHandler): """Process the log in another thread.""" - def emit(self, record: logging.LogRecord) -> None: - """Emit a log record.""" - try: - self.enqueue(record) - except Exception: # pylint: disable=broad-except - self.handleError(record) - def handle(self, record: logging.LogRecord) -> Any: """ Conditionally emit the specified logging record. From 242ff045b92ed31bc2e3f298b06d77b02f6190cc Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 6 Feb 2021 14:02:03 +0100 Subject: [PATCH 228/796] Handle missing value in all platforms of zwave_js (#46081) --- homeassistant/components/zwave_js/climate.py | 15 +++++++++++++++ homeassistant/components/zwave_js/cover.py | 12 +++++++++--- homeassistant/components/zwave_js/entity.py | 8 +------- homeassistant/components/zwave_js/fan.py | 10 ++++++++-- homeassistant/components/zwave_js/lock.py | 3 +++ homeassistant/components/zwave_js/switch.py | 7 +++++-- 6 files changed, 41 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index b125c8bcd6a..a0b0648932c 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -207,6 +207,9 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): if self._current_mode is None: # Thermostat(valve) with no support for setting a mode is considered heating-only return HVAC_MODE_HEAT + if self._current_mode.value is None: + # guard missing value + return HVAC_MODE_HEAT return ZW_HVAC_MODE_MAP.get(int(self._current_mode.value), HVAC_MODE_HEAT_COOL) @property @@ -219,6 +222,9 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): """Return the current running hvac operation if supported.""" if not self._operating_state: return None + if self._operating_state.value is None: + # guard missing value + return None return HVAC_CURRENT_MAP.get(int(self._operating_state.value)) @property @@ -234,12 +240,18 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): @property def target_temperature(self) -> Optional[float]: """Return the temperature we try to reach.""" + if self._current_mode and self._current_mode.value is None: + # guard missing value + return None temp = self._setpoint_value(self._current_mode_setpoint_enums[0]) return temp.value if temp else None @property def target_temperature_high(self) -> Optional[float]: """Return the highbound target temperature we try to reach.""" + if self._current_mode and self._current_mode.value is None: + # guard missing value + return None temp = self._setpoint_value(self._current_mode_setpoint_enums[1]) return temp.value if temp else None @@ -251,6 +263,9 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): @property def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., home, away, temp.""" + if self._current_mode and self._current_mode.value is None: + # guard missing value + return None if self._current_mode and int(self._current_mode.value) not in THERMOSTAT_MODES: return_val: str = self._current_mode.metadata.states.get( self._current_mode.value diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index b86cbeba944..38c891f7376 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -1,6 +1,6 @@ """Support for Z-Wave cover devices.""" import logging -from typing import Any, Callable, List +from typing import Any, Callable, List, Optional from zwave_js_server.client import Client as ZwaveClient @@ -59,13 +59,19 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity): """Representation of a Z-Wave Cover device.""" @property - def is_closed(self) -> bool: + def is_closed(self) -> Optional[bool]: """Return true if cover is closed.""" + if self.info.primary_value.value is None: + # guard missing value + return None return bool(self.info.primary_value.value == 0) @property - def current_cover_position(self) -> int: + def current_cover_position(self) -> Optional[int]: """Return the current position of cover where 0 means closed and 100 is fully open.""" + if self.info.primary_value.value is None: + # guard missing value + return None return round((self.info.primary_value.value / 99) * 100) async def async_set_cover_position(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 5d45cb9511a..911b0b7b2f8 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -102,13 +102,7 @@ class ZWaveBaseEntity(Entity): @property def available(self) -> bool: """Return entity availability.""" - return ( - self.client.connected - and bool(self.info.node.ready) - # a None value indicates something wrong with the device, - # or the value is simply not yet there (it will arrive later). - and self.info.primary_value.value is not None - ) + return self.client.connected and bool(self.info.node.ready) @callback def _value_changed(self, event_data: dict) -> None: diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index 6e62869f749..58b85eabef1 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -84,13 +84,19 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): await self.info.node.async_set_value(target_value, 0) @property - def is_on(self) -> bool: + def is_on(self) -> Optional[bool]: # type: ignore """Return true if device is on (speed above 0).""" + if self.info.primary_value.value is None: + # guard missing value + return None return bool(self.info.primary_value.value > 0) @property - def percentage(self) -> int: + def percentage(self) -> Optional[int]: """Return the current speed percentage.""" + if self.info.primary_value.value is None: + # guard missing value + return None return ranged_value_to_percentage(SPEED_RANGE, self.info.primary_value.value) @property diff --git a/homeassistant/components/zwave_js/lock.py b/homeassistant/components/zwave_js/lock.py index dedaf9a5e45..6f2a1a72c7d 100644 --- a/homeassistant/components/zwave_js/lock.py +++ b/homeassistant/components/zwave_js/lock.py @@ -90,6 +90,9 @@ class ZWaveLock(ZWaveBaseEntity, LockEntity): @property def is_locked(self) -> Optional[bool]: """Return true if the lock is locked.""" + if self.info.primary_value.value is None: + # guard missing value + return None return int( LOCK_CMD_CLASS_TO_LOCKED_STATE_MAP[ CommandClass(self.info.primary_value.command_class) diff --git a/homeassistant/components/zwave_js/switch.py b/homeassistant/components/zwave_js/switch.py index 2060894684c..8feba5911f8 100644 --- a/homeassistant/components/zwave_js/switch.py +++ b/homeassistant/components/zwave_js/switch.py @@ -1,7 +1,7 @@ """Representation of Z-Wave switches.""" import logging -from typing import Any, Callable, List +from typing import Any, Callable, List, Optional from zwave_js_server.client import Client as ZwaveClient @@ -44,8 +44,11 @@ class ZWaveSwitch(ZWaveBaseEntity, SwitchEntity): """Representation of a Z-Wave switch.""" @property - def is_on(self) -> bool: + def is_on(self) -> Optional[bool]: # type: ignore """Return a boolean for the state of the switch.""" + if self.info.primary_value.value is None: + # guard missing value + return None return bool(self.info.primary_value.value) async def async_turn_on(self, **kwargs: Any) -> None: From 08163a848c3a063f30260d4a24f86310bf197fd6 Mon Sep 17 00:00:00 2001 From: Steven Rollason <2099542+gadgetchnnel@users.noreply.github.com> Date: Sat, 6 Feb 2021 13:05:50 +0000 Subject: [PATCH 229/796] Fix downloader path validation on subdir (#46061) --- homeassistant/components/downloader/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py index 94617ce43aa..3856df696ad 100644 --- a/homeassistant/components/downloader/__init__.py +++ b/homeassistant/components/downloader/__init__.py @@ -70,8 +70,9 @@ def setup(hass, config): overwrite = service.data.get(ATTR_OVERWRITE) - # Check the path - raise_if_invalid_path(subdir) + if subdir: + # Check the path + raise_if_invalid_path(subdir) final_path = None From 60e3fce7dcbed9beba4c43edadc5873f4d413e93 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 6 Feb 2021 14:32:17 +0100 Subject: [PATCH 230/796] Convert old deCONZ groups unique ids (#46093) * Convert old groups unique ids Work around for walrus operator not working properly :/ * Remove CONF_GROUP_ID_BASE once entities unique id are updated * Don't use migrate_entry mechanism to update unique_ids of groups * Remove walrus operator :( * Fix review comments * Walrusify assignment to old_unique_id --- homeassistant/components/deconz/__init__.py | 53 +++++++++---- homeassistant/components/deconz/light.py | 6 +- tests/components/deconz/test_gateway.py | 2 + tests/components/deconz/test_init.py | 85 ++++++++++++++++++++- 4 files changed, 123 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index fec7b82e365..1b8c47d36a2 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -1,11 +1,17 @@ """Support for deCONZ devices.""" import voluptuous as vol -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers.typing import UNDEFINED +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_PORT, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import callback +from homeassistant.helpers.entity_registry import async_migrate_entries from .config_flow import get_master_gateway -from .const import CONF_BRIDGE_ID, CONF_GROUP_ID_BASE, CONF_MASTER_GATEWAY, DOMAIN +from .const import CONF_GROUP_ID_BASE, CONF_MASTER_GATEWAY, DOMAIN from .gateway import DeconzGateway from .services import async_setup_services, async_unload_services @@ -28,6 +34,8 @@ async def async_setup_entry(hass, config_entry): if DOMAIN not in hass.data: hass.data[DOMAIN] = {} + await async_update_group_unique_id(hass, config_entry) + if not config_entry.options: await async_update_master_gateway(hass, config_entry) @@ -36,18 +44,6 @@ async def async_setup_entry(hass, config_entry): if not await gateway.async_setup(): return False - # 0.104 introduced config entry unique id, this makes upgrading possible - if config_entry.unique_id is None: - - 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] - - hass.config_entries.async_update_entry( - config_entry, unique_id=gateway.api.config.bridgeid, data=new_data - ) - hass.data[DOMAIN][config_entry.unique_id] = gateway await gateway.async_update_device_registry() @@ -84,3 +80,30 @@ async def async_update_master_gateway(hass, config_entry): options = {**config_entry.options, CONF_MASTER_GATEWAY: master} hass.config_entries.async_update_entry(config_entry, options=options) + + +async def async_update_group_unique_id(hass, config_entry) -> None: + """Update unique ID entities based on deCONZ groups.""" + if not (old_unique_id := config_entry.data.get(CONF_GROUP_ID_BASE)): + return + + new_unique_id: str = config_entry.unique_id + + @callback + def update_unique_id(entity_entry): + """Update unique ID of entity entry.""" + if f"{old_unique_id}-" not in entity_entry.unique_id: + return None + return { + "new_unique_id": entity_entry.unique_id.replace( + old_unique_id, new_unique_id + ) + } + + await async_migrate_entries(hass, config_entry.entry_id, update_unique_id) + data = { + CONF_API_KEY: config_entry.data[CONF_API_KEY], + CONF_HOST: config_entry.data[CONF_HOST], + CONF_PORT: config_entry.data[CONF_PORT], + } + hass.config_entries.async_update_entry(config_entry, data=data) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 9080160c76f..2da435c5530 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -26,7 +26,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util from .const import ( - CONF_GROUP_ID_BASE, COVER_TYPES, DOMAIN as DECONZ_DOMAIN, LOCK_TYPES, @@ -248,10 +247,7 @@ class DeconzGroup(DeconzBaseLight): def __init__(self, device, gateway): """Set up group and create an unique id.""" - group_id_base = gateway.config_entry.unique_id - if CONF_GROUP_ID_BASE in gateway.config_entry.data: - group_id_base = gateway.config_entry.data[CONF_GROUP_ID_BASE] - self._unique_id = f"{group_id_base}-{device.deconz_id}" + self._unique_id = f"{gateway.bridgeid}-{device.deconz_id}" super().__init__(device, gateway) diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 1790b6ed6e1..f670f2a1d10 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -66,6 +66,7 @@ async def setup_deconz_integration( options=ENTRY_OPTIONS, get_state_response=DECONZ_WEB_REQUEST, entry_id="1", + unique_id=BRIDGEID, source="user", ): """Create the deCONZ gateway.""" @@ -76,6 +77,7 @@ async def setup_deconz_integration( connection_class=CONN_CLASS_LOCAL_PUSH, options=deepcopy(options), entry_id=entry_id, + unique_id=unique_id, ) config_entry.add_to_hass(hass) diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index d408d764d0e..43c0c48440c 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -8,12 +8,21 @@ from homeassistant.components.deconz import ( DeconzGateway, async_setup_entry, async_unload_entry, + async_update_group_unique_id, +) +from homeassistant.components.deconz.const import ( + CONF_GROUP_ID_BASE, + DOMAIN as DECONZ_DOMAIN, ) -from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.gateway import get_gateway_from_config_entry +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT +from homeassistant.helpers import entity_registry from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from tests.common import MockConfigEntry + ENTRY1_HOST = "1.2.3.4" ENTRY1_PORT = 80 ENTRY1_API_KEY = "1234567890ABCDEF" @@ -67,7 +76,7 @@ async def test_setup_entry_multiple_gateways(hass): data = deepcopy(DECONZ_WEB_REQUEST) data["config"]["bridgeid"] = "01234E56789B" config_entry2 = await setup_deconz_integration( - hass, get_state_response=data, entry_id="2" + hass, get_state_response=data, entry_id="2", unique_id="01234E56789B" ) gateway2 = get_gateway_from_config_entry(hass, config_entry2) @@ -92,7 +101,7 @@ async def test_unload_entry_multiple_gateways(hass): data = deepcopy(DECONZ_WEB_REQUEST) data["config"]["bridgeid"] = "01234E56789B" config_entry2 = await setup_deconz_integration( - hass, get_state_response=data, entry_id="2" + hass, get_state_response=data, entry_id="2", unique_id="01234E56789B" ) gateway2 = get_gateway_from_config_entry(hass, config_entry2) @@ -102,3 +111,73 @@ async def test_unload_entry_multiple_gateways(hass): assert len(hass.data[DECONZ_DOMAIN]) == 1 assert hass.data[DECONZ_DOMAIN][gateway2.bridgeid].master + + +async def test_update_group_unique_id(hass): + """Test successful migration of entry data.""" + old_unique_id = "123" + new_unique_id = "1234" + entry = MockConfigEntry( + domain=DECONZ_DOMAIN, + unique_id=new_unique_id, + data={ + CONF_API_KEY: "1", + CONF_HOST: "2", + CONF_GROUP_ID_BASE: old_unique_id, + CONF_PORT: "3", + }, + ) + + registry = await entity_registry.async_get_registry(hass) + # Create entity entry to migrate to new unique ID + registry.async_get_or_create( + LIGHT_DOMAIN, + DECONZ_DOMAIN, + f"{old_unique_id}-OLD", + suggested_object_id="old", + config_entry=entry, + ) + # Create entity entry with new unique ID + registry.async_get_or_create( + LIGHT_DOMAIN, + DECONZ_DOMAIN, + f"{new_unique_id}-NEW", + suggested_object_id="new", + config_entry=entry, + ) + + await async_update_group_unique_id(hass, entry) + + assert entry.data == {CONF_API_KEY: "1", CONF_HOST: "2", CONF_PORT: "3"} + + old_entity = registry.async_get(f"{LIGHT_DOMAIN}.old") + assert old_entity.unique_id == f"{new_unique_id}-OLD" + + new_entity = registry.async_get(f"{LIGHT_DOMAIN}.new") + assert new_entity.unique_id == f"{new_unique_id}-NEW" + + +async def test_update_group_unique_id_no_legacy_group_id(hass): + """Test migration doesn't trigger without old legacy group id in entry data.""" + old_unique_id = "123" + new_unique_id = "1234" + entry = MockConfigEntry( + domain=DECONZ_DOMAIN, + unique_id=new_unique_id, + data={}, + ) + + registry = await entity_registry.async_get_registry(hass) + # Create entity entry to migrate to new unique ID + registry.async_get_or_create( + LIGHT_DOMAIN, + DECONZ_DOMAIN, + f"{old_unique_id}-OLD", + suggested_object_id="old", + config_entry=entry, + ) + + await async_update_group_unique_id(hass, entry) + + old_entity = registry.async_get(f"{LIGHT_DOMAIN}.old") + assert old_entity.unique_id == f"{old_unique_id}-OLD" From cefde8721de1a1831624310e1dc81b688ea03a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Sat, 6 Feb 2021 21:29:48 +0100 Subject: [PATCH 231/796] Add new features to Apple TV media player (#45828) --- homeassistant/components/apple_tv/__init__.py | 15 ++-- .../components/apple_tv/media_player.py | 89 +++++++++++++++---- 2 files changed, 80 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index eca5e91ddeb..b41a107d126 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -180,20 +180,23 @@ class AppleTVManager: This is a callback function from pyatv.interface.DeviceListener. """ - _LOGGER.warning('Connection lost to Apple TV "%s"', self.atv.name) - if self.atv: - self.atv.close() - self.atv = None + _LOGGER.warning( + 'Connection lost to Apple TV "%s"', self.config_entry.data.get(CONF_NAME) + ) self._connection_was_lost = True - self._dispatch_send(SIGNAL_DISCONNECTED) - self._start_connect_loop() + self._handle_disconnect() def connection_closed(self): """Device connection was (intentionally) closed. This is a callback function from pyatv.interface.DeviceListener. """ + self._handle_disconnect() + + def _handle_disconnect(self): + """Handle that the device disconnected and restart connect loop.""" if self.atv: + self.atv.listener = None self.atv.close() self.atv = None self._dispatch_send(SIGNAL_DISCONNECTED) diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 81bb79dc50b..a855fc6b53e 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -1,22 +1,35 @@ """Support for Apple TV media player.""" import logging -from pyatv.const import DeviceState, FeatureName, FeatureState, MediaType +from pyatv.const import ( + DeviceState, + FeatureName, + FeatureState, + MediaType, + RepeatState, + ShuffleState, +) from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, + REPEAT_MODE_ALL, + REPEAT_MODE_OFF, + REPEAT_MODE_ONE, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, + SUPPORT_REPEAT_SET, SUPPORT_SEEK, + SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_STEP, ) from homeassistant.const import ( CONF_NAME, @@ -46,6 +59,9 @@ SUPPORT_APPLE_TV = ( | SUPPORT_STOP | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK + | SUPPORT_VOLUME_STEP + | SUPPORT_REPEAT_SET + | SUPPORT_SHUFFLE_SET ) @@ -110,17 +126,15 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): @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 + if self._is_feature_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 + if self._is_feature_available(FeatureName.App): + return self.atv.metadata.app.name return None @property @@ -198,6 +212,23 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): return self._playing.album return None + @property + def repeat(self): + """Return current repeat mode.""" + if self._is_feature_available(FeatureName.Repeat): + return { + RepeatState.Track: REPEAT_MODE_ONE, + RepeatState.All: REPEAT_MODE_ALL, + }.get(self._playing.repeat, REPEAT_MODE_OFF) + return None + + @property + def shuffle(self): + """Boolean if shuffle is enabled.""" + if self._is_feature_available(FeatureName.Shuffle): + return self._playing.shuffle != ShuffleState.Off + return None + @property def supported_features(self): """Flag media player features that are supported.""" @@ -221,39 +252,61 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): async def async_media_play_pause(self): """Pause media on media player.""" if self._playing: - state = self.state - if state == STATE_PAUSED: - await self.atv.remote_control.play() - elif state == STATE_PLAYING: - await self.atv.remote_control.pause() + await self.atv.remote_control.play_pause() return None async def async_media_play(self): """Play media.""" - if self._playing: + if self.atv: await self.atv.remote_control.play() async def async_media_stop(self): """Stop the media player.""" - if self._playing: + if self.atv: await self.atv.remote_control.stop() async def async_media_pause(self): """Pause the media player.""" - if self._playing: + if self.atv: await self.atv.remote_control.pause() async def async_media_next_track(self): """Send next track command.""" - if self._playing: + if self.atv: await self.atv.remote_control.next() async def async_media_previous_track(self): """Send previous track command.""" - if self._playing: + if self.atv: await self.atv.remote_control.previous() async def async_media_seek(self, position): """Send seek command.""" - if self._playing: + if self.atv: await self.atv.remote_control.set_position(position) + + async def async_volume_up(self): + """Turn volume up for media player.""" + if self.atv: + await self.atv.remote_control.volume_up() + + async def async_volume_down(self): + """Turn volume down for media player.""" + if self.atv: + await self.atv.remote_control.volume_down() + + async def async_set_repeat(self, repeat): + """Set repeat mode.""" + if self.atv: + mode = { + REPEAT_MODE_ONE: RepeatState.Track, + REPEAT_MODE_ALL: RepeatState.All, + }.get(repeat, RepeatState.Off) + await self.atv.remote_control.set_repeat(mode) + + async def async_set_shuffle(self, shuffle): + """Enable/disable shuffle mode.""" + if self.atv: + await self.atv.remote_control.set_shuffle( + ShuffleState.Songs if shuffle else ShuffleState.Off + ) From 618fcda8211a66d0202c1c804839a1a5a12c7400 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 6 Feb 2021 21:32:18 +0100 Subject: [PATCH 232/796] Simplify UniFi entry configuration data (#45759) * Simplify configuration structure by removing the controller key * Fix flake8 * Fix review comments * Don't use migrate_entry mechanism to flatten configuration Keep legacy configuration when creating new entries as well --- homeassistant/components/unifi/__init__.py | 16 ++++++++ homeassistant/components/unifi/config_flow.py | 38 +++++++++++-------- homeassistant/components/unifi/controller.py | 20 +++++++--- tests/components/unifi/test_config_flow.py | 24 ++++++------ tests/components/unifi/test_controller.py | 7 ++-- tests/components/unifi/test_init.py | 28 ++++++++------ 6 files changed, 87 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 5f19a01ce45..8d24a9b642f 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -5,6 +5,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from .const import ( ATTR_MANUFACTURER, + CONF_CONTROLLER, DOMAIN as UNIFI_DOMAIN, LOGGER, UNIFI_WIRELESS_CLIENTS, @@ -28,10 +29,14 @@ async def async_setup_entry(hass, config_entry): """Set up the UniFi component.""" hass.data.setdefault(UNIFI_DOMAIN, {}) + # Flat configuration was introduced with 2021.3 + await async_flatten_entry_data(hass, config_entry) + controller = UniFiController(hass, config_entry) if not await controller.async_setup(): return False + # Unique ID was introduced with 2021.3 if config_entry.unique_id is None: hass.config_entries.async_update_entry( config_entry, unique_id=controller.site_id @@ -64,6 +69,17 @@ async def async_unload_entry(hass, config_entry): return await controller.async_reset() +async def async_flatten_entry_data(hass, config_entry): + """Simpler configuration structure for entry data. + + Keep controller key layer in case user rollbacks. + """ + + data: dict = {**config_entry.data, **config_entry.data[CONF_CONTROLLER]} + if config_entry.data != data: + hass.config_entries.async_update_entry(config_entry, data=data) + + class UnifiWirelessClients: """Class to store clients known to be wireless. diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 1d89215dc89..8e83f53d198 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -73,7 +73,6 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): self.site_ids = {} self.site_names = {} self.reauth_config_entry = None - self.reauth_config = {} self.reauth_schema = {} async def async_step_user(self, user_input=None): @@ -92,7 +91,16 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): } try: - controller = await get_controller(self.hass, **self.config) + controller = await get_controller( + self.hass, + host=self.config[CONF_HOST], + username=self.config[CONF_USERNAME], + password=self.config[CONF_PASSWORD], + port=self.config[CONF_PORT], + site=self.config[CONF_SITE_ID], + verify_ssl=self.config[CONF_VERIFY_SSL], + ) + sites = await controller.sites() except AuthenticationRequired: @@ -143,7 +151,8 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): unique_id = user_input[CONF_SITE_ID] self.config[CONF_SITE_ID] = self.site_ids[unique_id] - data = {CONF_CONTROLLER: self.config} + # Backwards compatible config + self.config[CONF_CONTROLLER] = self.config.copy() config_entry = await self.async_set_unique_id(unique_id) abort_reason = "configuration_updated" @@ -160,12 +169,14 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): if controller and controller.available: return self.async_abort(reason="already_configured") - self.hass.config_entries.async_update_entry(config_entry, data=data) + self.hass.config_entries.async_update_entry( + config_entry, data=self.config + ) await self.hass.config_entries.async_reload(config_entry.entry_id) return self.async_abort(reason=abort_reason) site_nice_name = self.site_names[unique_id] - return self.async_create_entry(title=site_nice_name, data=data) + return self.async_create_entry(title=site_nice_name, data=self.config) if len(self.site_names) == 1: return await self.async_step_site( @@ -183,21 +194,20 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): async def async_step_reauth(self, config_entry: dict): """Trigger a reauthentication flow.""" self.reauth_config_entry = config_entry - self.reauth_config = config_entry.data[CONF_CONTROLLER] # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { - CONF_HOST: self.reauth_config[CONF_HOST], + CONF_HOST: config_entry.data[CONF_HOST], CONF_SITE_ID: config_entry.title, } self.reauth_schema = { - vol.Required(CONF_HOST, default=self.reauth_config[CONF_HOST]): str, - vol.Required(CONF_USERNAME, default=self.reauth_config[CONF_USERNAME]): str, + vol.Required(CONF_HOST, default=config_entry.data[CONF_HOST]): str, + vol.Required(CONF_USERNAME, default=config_entry.data[CONF_USERNAME]): str, vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_PORT, default=self.reauth_config[CONF_PORT]): int, + vol.Required(CONF_PORT, default=config_entry.data[CONF_PORT]): int, vol.Required( - CONF_VERIFY_SSL, default=self.reauth_config[CONF_VERIFY_SSL] + CONF_VERIFY_SSL, default=config_entry.data[CONF_VERIFY_SSL] ): bool, } @@ -217,7 +227,7 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): return self.async_abort(reason="already_configured") await self.async_set_unique_id(mac_address) - self._abort_if_unique_id_configured(updates={CONF_HOST: self.config[CONF_HOST]}) + self._abort_if_unique_id_configured(updates=self.config) # pylint: disable=no-member self.context["title_placeholders"] = { @@ -234,9 +244,7 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): def _host_already_configured(self, host): """See if we already have a UniFi entry matching the host.""" for entry in self._async_current_entries(): - if not entry.data or CONF_CONTROLLER not in entry.data: - continue - if entry.data[CONF_CONTROLLER][CONF_HOST] == host: + if entry.data.get(CONF_HOST) == host: return True return False diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 5d5e679e75e..128f0107984 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -29,7 +29,13 @@ from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import SOURCE_REAUTH -from homeassistant.const import CONF_HOST +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_VERIFY_SSL, +) from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client @@ -41,7 +47,6 @@ from .const import ( CONF_ALLOW_BANDWIDTH_SENSORS, CONF_ALLOW_UPTIME_SENSORS, CONF_BLOCK_CLIENT, - CONF_CONTROLLER, CONF_DETECTION_TIME, CONF_DPI_RESTRICTIONS, CONF_IGNORE_WIRED_BUG, @@ -161,12 +166,12 @@ class UniFiController: @property def host(self): """Return the host of this controller.""" - return self.config_entry.data[CONF_CONTROLLER][CONF_HOST] + return self.config_entry.data[CONF_HOST] @property def site(self): """Return the site of this config entry.""" - return self.config_entry.data[CONF_CONTROLLER][CONF_SITE_ID] + return self.config_entry.data[CONF_SITE_ID] @property def site_name(self): @@ -299,7 +304,12 @@ class UniFiController: try: self.api = await get_controller( self.hass, - **self.config_entry.data[CONF_CONTROLLER], + host=self.config_entry.data[CONF_HOST], + username=self.config_entry.data[CONF_USERNAME], + password=self.config_entry.data[CONF_PASSWORD], + port=self.config_entry.data[CONF_PORT], + site=self.config_entry.data[CONF_SITE_ID], + verify_ssl=self.config_entry.data[CONF_VERIFY_SSL], async_callback=self.async_unifi_signalling_callback, ) await self.api.initialize() diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 096e6ba7791..a28f5f5f7c5 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -134,6 +134,12 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "Site name" assert result["data"] == { + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 1234, + CONF_SITE_ID: "site_id", + CONF_VERIFY_SSL: True, CONF_CONTROLLER: { CONF_HOST: "1.2.3.4", CONF_USERNAME: "username", @@ -141,7 +147,7 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery): CONF_PORT: 1234, CONF_SITE_ID: "site_id", CONF_VERIFY_SSL: True, - } + }, } @@ -241,16 +247,12 @@ async def test_flow_raise_already_configured(hass, aioclient_mock): async def test_flow_aborts_configuration_updated(hass, aioclient_mock): """Test config flow aborts since a connected config entry already exists.""" entry = MockConfigEntry( - domain=UNIFI_DOMAIN, - data={"controller": {"host": "1.2.3.4", "site": "office"}}, - unique_id="2", + domain=UNIFI_DOMAIN, data={"host": "1.2.3.4", "site": "office"}, unique_id="2" ) entry.add_to_hass(hass) entry = MockConfigEntry( - domain=UNIFI_DOMAIN, - data={"controller": {"host": "1.2.3.4", "site": "site_id"}}, - unique_id="1", + domain=UNIFI_DOMAIN, data={"host": "1.2.3.4", "site": "site_id"}, unique_id="1" ) entry.add_to_hass(hass) @@ -399,9 +401,9 @@ async def test_reauth_flow_update_configuration(hass, aioclient_mock): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "reauth_successful" - assert config_entry.data[CONF_CONTROLLER][CONF_HOST] == "1.2.3.4" - assert config_entry.data[CONF_CONTROLLER][CONF_USERNAME] == "new_name" - assert config_entry.data[CONF_CONTROLLER][CONF_PASSWORD] == "new_pass" + assert config_entry.data[CONF_HOST] == "1.2.3.4" + assert config_entry.data[CONF_USERNAME] == "new_name" + assert config_entry.data[CONF_PASSWORD] == "new_pass" async def test_advanced_option_flow(hass, aioclient_mock): @@ -544,7 +546,7 @@ async def test_form_ssdp_aborts_if_host_already_exists(hass): await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=UNIFI_DOMAIN, - data={"controller": {"host": "192.168.208.1", "site": "site_id"}}, + data={"host": "192.168.208.1", "site": "site_id"}, ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 3ecd44b3db7..00865b4e910 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -66,7 +66,7 @@ CONTROLLER_DATA = { CONF_VERIFY_SSL: False, } -ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} +ENTRY_CONFIG = {**CONTROLLER_DATA, CONF_CONTROLLER: CONTROLLER_DATA} ENTRY_OPTIONS = {} CONFIGURATION = [] @@ -167,6 +167,7 @@ async def setup_unifi_integration( options=deepcopy(options), entry_id=1, unique_id="1", + version=1, ) config_entry.add_to_hass(hass) @@ -178,8 +179,8 @@ async def setup_unifi_integration( if aioclient_mock: mock_default_unifi_requests( aioclient_mock, - host=config_entry.data[CONF_CONTROLLER][CONF_HOST], - site_id=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID], + host=config_entry.data[CONF_HOST], + site_id=config_entry.data[CONF_SITE_ID], sites=sites, description=site_description, clients_response=clients_response, diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index 9de8b0a0990..6d8b894fc34 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -2,10 +2,11 @@ from unittest.mock import AsyncMock, Mock, patch from homeassistant.components import unifi -from homeassistant.components.unifi.const import DOMAIN as UNIFI_DOMAIN +from homeassistant.components.unifi import async_flatten_entry_data +from homeassistant.components.unifi.const import CONF_CONTROLLER, DOMAIN as UNIFI_DOMAIN from homeassistant.setup import async_setup_component -from .test_controller import setup_unifi_integration +from .test_controller import CONTROLLER_DATA, ENTRY_CONFIG, setup_unifi_integration from tests.common import MockConfigEntry, mock_coro @@ -35,17 +36,9 @@ async def test_controller_no_mac(hass): """Test that configured options for a host are loaded via config entry.""" entry = MockConfigEntry( domain=UNIFI_DOMAIN, - data={ - "controller": { - "host": "0.0.0.0", - "username": "user", - "password": "pass", - "port": 80, - "site": "default", - "verify_ssl": True, - }, - }, + data=ENTRY_CONFIG, unique_id="1", + version=1, ) entry.add_to_hass(hass) mock_registry = Mock() @@ -64,6 +57,17 @@ async def test_controller_no_mac(hass): assert len(mock_registry.mock_calls) == 0 +async def test_flatten_entry_data(hass): + """Verify entry data can be flattened.""" + entry = MockConfigEntry( + domain=UNIFI_DOMAIN, + data={CONF_CONTROLLER: CONTROLLER_DATA}, + ) + await async_flatten_entry_data(hass, entry) + + assert entry.data == ENTRY_CONFIG + + async def test_unload_entry(hass, aioclient_mock): """Test being able to unload an entry.""" config_entry = await setup_unifi_integration(hass, aioclient_mock) From 818501216ea6496d09580a6bde972e03ca92be8b Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 7 Feb 2021 00:06:57 +0000 Subject: [PATCH 233/796] [ci skip] Translation update --- .../components/airvisual/translations/es.json | 18 ++++- .../components/foscam/translations/ca.json | 2 + .../components/foscam/translations/et.json | 2 + .../components/foscam/translations/it.json | 2 + .../components/foscam/translations/ru.json | 2 + .../foscam/translations/zh-Hant.json | 2 + .../components/fritzbox/translations/es.json | 9 ++- .../fritzbox_callmonitor/translations/es.json | 27 +++++++ .../components/homekit/translations/es.json | 14 +++- .../huisbaasje/translations/es.json | 21 +++++ .../lutron_caseta/translations/es.json | 6 ++ .../components/lyric/translations/es.json | 16 ++++ .../components/mysensors/translations/ca.json | 79 +++++++++++++++++++ .../components/mysensors/translations/es.json | 32 ++++++++ .../components/mysensors/translations/et.json | 79 +++++++++++++++++++ .../components/mysensors/translations/it.json | 79 +++++++++++++++++++ .../components/mysensors/translations/ru.json | 79 +++++++++++++++++++ .../mysensors/translations/zh-Hant.json | 77 ++++++++++++++++++ .../components/nuki/translations/es.json | 18 +++++ .../components/number/translations/es.json | 3 + .../components/plaato/translations/es.json | 27 +++++++ .../components/zwave_js/translations/es.json | 17 ++++ 22 files changed, 608 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/fritzbox_callmonitor/translations/es.json create mode 100644 homeassistant/components/huisbaasje/translations/es.json create mode 100644 homeassistant/components/lyric/translations/es.json create mode 100644 homeassistant/components/mysensors/translations/ca.json create mode 100644 homeassistant/components/mysensors/translations/es.json create mode 100644 homeassistant/components/mysensors/translations/et.json create mode 100644 homeassistant/components/mysensors/translations/it.json create mode 100644 homeassistant/components/mysensors/translations/ru.json create mode 100644 homeassistant/components/mysensors/translations/zh-Hant.json create mode 100644 homeassistant/components/nuki/translations/es.json create mode 100644 homeassistant/components/number/translations/es.json diff --git a/homeassistant/components/airvisual/translations/es.json b/homeassistant/components/airvisual/translations/es.json index 4325f1561b9..9b3ac467b84 100644 --- a/homeassistant/components/airvisual/translations/es.json +++ b/homeassistant/components/airvisual/translations/es.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "No se pudo conectar", "general_error": "Se ha producido un error desconocido.", - "invalid_api_key": "Se proporciona una clave API no v\u00e1lida." + "invalid_api_key": "Se proporciona una clave API no v\u00e1lida.", + "location_not_found": "Ubicaci\u00f3n no encontrada" }, "step": { "geography": { @@ -19,6 +20,21 @@ "description": "Utilizar la API en la nube de AirVisual para monitorizar una ubicaci\u00f3n geogr\u00e1fica.", "title": "Configurar una Geograf\u00eda" }, + "geography_by_coords": { + "data": { + "api_key": "Clave API", + "latitude": "Latitud", + "longitude": "Longitud" + } + }, + "geography_by_name": { + "data": { + "api_key": "Clave API", + "city": "Ciudad", + "country": "Pa\u00eds", + "state": "estado" + } + }, "node_pro": { "data": { "ip_address": "Direcci\u00f3n IP/Nombre de host de la Unidad", diff --git a/homeassistant/components/foscam/translations/ca.json b/homeassistant/components/foscam/translations/ca.json index 5a6c84f400e..b7f71c8c922 100644 --- a/homeassistant/components/foscam/translations/ca.json +++ b/homeassistant/components/foscam/translations/ca.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_response": "Resposta del dispositiu inv\u00e0lida", "unknown": "Error inesperat" }, "step": { @@ -14,6 +15,7 @@ "host": "Amfitri\u00f3", "password": "Contrasenya", "port": "Port", + "rtsp_port": "Port RTSP", "stream": "Flux de v\u00eddeo", "username": "Nom d'usuari" } diff --git a/homeassistant/components/foscam/translations/et.json b/homeassistant/components/foscam/translations/et.json index b20a33aec1d..c21ffa0cdd1 100644 --- a/homeassistant/components/foscam/translations/et.json +++ b/homeassistant/components/foscam/translations/et.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Vigane autentimine", + "invalid_response": "Seadme vastus on vigane", "unknown": "Ootamatu t\u00f5rge" }, "step": { @@ -14,6 +15,7 @@ "host": "Host", "password": "Salas\u00f5na", "port": "Port", + "rtsp_port": "RTSP port", "stream": "Voog", "username": "Kasutajanimi" } diff --git a/homeassistant/components/foscam/translations/it.json b/homeassistant/components/foscam/translations/it.json index 0562012b1fa..63868a0f07f 100644 --- a/homeassistant/components/foscam/translations/it.json +++ b/homeassistant/components/foscam/translations/it.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", + "invalid_response": "Risposta non valida dal dispositivo", "unknown": "Errore imprevisto" }, "step": { @@ -14,6 +15,7 @@ "host": "Host", "password": "Password", "port": "Porta", + "rtsp_port": "Porta RTSP", "stream": "Flusso", "username": "Nome utente" } diff --git a/homeassistant/components/foscam/translations/ru.json b/homeassistant/components/foscam/translations/ru.json index ad8b7961ca3..01e0494a07e 100644 --- a/homeassistant/components/foscam/translations/ru.json +++ b/homeassistant/components/foscam/translations/ru.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_response": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043e\u0442\u0432\u0435\u0442 \u043e\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { @@ -14,6 +15,7 @@ "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", + "rtsp_port": "\u041f\u043e\u0440\u0442 RTSP", "stream": "\u041f\u043e\u0442\u043e\u043a", "username": "\u041b\u043e\u0433\u0438\u043d" } diff --git a/homeassistant/components/foscam/translations/zh-Hant.json b/homeassistant/components/foscam/translations/zh-Hant.json index 2cc6303c17a..a0920c93548 100644 --- a/homeassistant/components/foscam/translations/zh-Hant.json +++ b/homeassistant/components/foscam/translations/zh-Hant.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_response": "\u4f86\u81ea\u88dd\u7f6e\u56de\u61c9\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { @@ -14,6 +15,7 @@ "host": "\u4e3b\u6a5f\u7aef", "password": "\u5bc6\u78bc", "port": "\u901a\u8a0a\u57e0", + "rtsp_port": "RTSP \u57e0", "stream": "\u4e32\u6d41", "username": "\u4f7f\u7528\u8005\u540d\u7a31" } diff --git a/homeassistant/components/fritzbox/translations/es.json b/homeassistant/components/fritzbox/translations/es.json index 5a9544df4e5..c9b67ca59f1 100644 --- a/homeassistant/components/fritzbox/translations/es.json +++ b/homeassistant/components/fritzbox/translations/es.json @@ -4,7 +4,8 @@ "already_configured": "Este AVM FRITZ!Box ya est\u00e1 configurado.", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", "no_devices_found": "No se encontraron dispositivos en la red", - "not_supported": "Conectado a AVM FRITZ!Box pero no es capaz de controlar dispositivos Smart Home." + "not_supported": "Conectado a AVM FRITZ!Box pero no es capaz de controlar dispositivos Smart Home.", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" @@ -18,6 +19,12 @@ }, "description": "\u00bfQuieres configurar {name}?" }, + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + } + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/fritzbox_callmonitor/translations/es.json b/homeassistant/components/fritzbox_callmonitor/translations/es.json new file mode 100644 index 00000000000..899a5050755 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/es.json @@ -0,0 +1,27 @@ +{ + "config": { + "error": { + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Usuario" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "Los prefijos tienen un formato incorrecto, comprueba el formato." + }, + "step": { + "init": { + "title": "Configurar prefijos" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json index aeb75f838c1..8ceee3f3016 100644 --- a/homeassistant/components/homekit/translations/es.json +++ b/homeassistant/components/homekit/translations/es.json @@ -4,6 +4,17 @@ "port_name_in_use": "Ya est\u00e1 configurada una pasarela con el mismo nombre o puerto." }, "step": { + "accessory_mode": { + "data": { + "entity_id": "Entidad" + } + }, + "bridge_mode": { + "data": { + "include_domains": "Dominios a incluir" + }, + "title": "Selecciona los dominios a incluir" + }, "pairing": { "description": "Tan pronto como la pasarela {name} est\u00e9 lista, la vinculaci\u00f3n estar\u00e1 disponible en \"Notificaciones\" como \"configuraci\u00f3n de pasarela Homekit\"", "title": "Vincular pasarela Homekit" @@ -11,7 +22,8 @@ "user": { "data": { "auto_start": "Arranque autom\u00e1tico (desactivado si se utiliza Z-Wave u otro sistema de arranque retardado)", - "include_domains": "Dominios para incluir" + "include_domains": "Dominios para incluir", + "mode": "Modo" }, "description": "Una pasarela Homekit permitir\u00e1 a Homekit acceder a sus entidades de Home Assistant. La pasarela Homekit est\u00e1 limitada a 150 accesorios por instancia incluyendo la propia pasarela. Si desea enlazar m\u00e1s del m\u00e1ximo n\u00famero de accesorios, se recomienda que use multiples pasarelas Homekit para diferentes dominios. Configuraci\u00f3n detallada de la entidad solo est\u00e1 disponible via YAML para la pasarela primaria.", "title": "Activar pasarela Homekit" diff --git a/homeassistant/components/huisbaasje/translations/es.json b/homeassistant/components/huisbaasje/translations/es.json new file mode 100644 index 00000000000..def06b0941d --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "connection_exception": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unauthenticated_exception": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/es.json b/homeassistant/components/lutron_caseta/translations/es.json index 37b1a0d9072..460d4fc5e69 100644 --- a/homeassistant/components/lutron_caseta/translations/es.json +++ b/homeassistant/components/lutron_caseta/translations/es.json @@ -29,6 +29,12 @@ }, "device_automation": { "trigger_subtype": { + "button_1": "Primer bot\u00f3n", + "button_3": "Tercer bot\u00f3n", + "close_1": "Cerrar 1", + "close_2": "Cerrar 2", + "off": "Apagado", + "on": "Encendido", "open_1": "Abrir 1", "open_2": "Abrir 2", "open_3": "Abrir 3", diff --git a/homeassistant/components/lyric/translations/es.json b/homeassistant/components/lyric/translations/es.json new file mode 100644 index 00000000000..db8d744d176 --- /dev/null +++ b/homeassistant/components/lyric/translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "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." + }, + "create_entry": { + "default": "Autenticado correctamente" + }, + "step": { + "pick_implementation": { + "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/ca.json b/homeassistant/components/mysensors/translations/ca.json new file mode 100644 index 00000000000..844d9e51da1 --- /dev/null +++ b/homeassistant/components/mysensors/translations/ca.json @@ -0,0 +1,79 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", + "duplicate_persistence_file": "Fitxer de persist\u00e8ncia ja en \u00fas", + "duplicate_topic": "Topic ja en \u00fas", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_device": "Dispositiu no v\u00e0lid", + "invalid_ip": "Adre\u00e7a IP inv\u00e0lida", + "invalid_persistence_file": "Fitxer de persist\u00e8ncia inv\u00e0lid", + "invalid_port": "N\u00famero de port inv\u00e0lid", + "invalid_publish_topic": "Topic de publicaci\u00f3 inv\u00e0lid", + "invalid_serial": "Port s\u00e8rie inv\u00e0lid", + "invalid_subscribe_topic": "Topic de subscripci\u00f3 inv\u00e0lid", + "invalid_version": "Versi\u00f3 de MySensors inv\u00e0lida", + "not_a_number": "Introdueix un n\u00famero", + "port_out_of_range": "El n\u00famero de port ha d'estar entre 1 i 65535", + "same_topic": "Els topics de publicaci\u00f3 i subscripci\u00f3 son els mateixos", + "unknown": "Error inesperat" + }, + "error": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", + "duplicate_persistence_file": "Fitxer de persist\u00e8ncia ja en \u00fas", + "duplicate_topic": "Topic ja en \u00fas", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_device": "Dispositiu no v\u00e0lid", + "invalid_ip": "Adre\u00e7a IP inv\u00e0lida", + "invalid_persistence_file": "Fitxer de persist\u00e8ncia inv\u00e0lid", + "invalid_port": "N\u00famero de port inv\u00e0lid", + "invalid_publish_topic": "Topic de publicaci\u00f3 inv\u00e0lid", + "invalid_serial": "Port s\u00e8rie inv\u00e0lid", + "invalid_subscribe_topic": "Topic de subscripci\u00f3 inv\u00e0lid", + "invalid_version": "Versi\u00f3 de MySensors inv\u00e0lida", + "not_a_number": "Introdueix un n\u00famero", + "port_out_of_range": "El n\u00famero de port ha d'estar entre 1 i 65535", + "same_topic": "Els topics de publicaci\u00f3 i subscripci\u00f3 son els mateixos", + "unknown": "Error inesperat" + }, + "step": { + "gw_mqtt": { + "data": { + "persistence_file": "fitxer de persist\u00e8ncia (deixa-ho buit per generar-lo autom\u00e0ticament)", + "retain": "retenci\u00f3 mqtt", + "topic_in_prefix": "prefix per als topics d'entrada (topic_in_prefix)", + "topic_out_prefix": "prefix per als topics de sortida (topic_out_prefix)", + "version": "Versi\u00f3 de MySensors" + }, + "description": "Configuraci\u00f3 de passarel\u00b7la MQTT" + }, + "gw_serial": { + "data": { + "baud_rate": "Velocitat, en baudis", + "device": "Port s\u00e8rie", + "persistence_file": "fitxer de persist\u00e8ncia (deixa-ho buit per generar-lo autom\u00e0ticament)", + "version": "Versi\u00f3 de MySensors" + }, + "description": "Configuraci\u00f3 de passarel\u00b7la s\u00e8rie" + }, + "gw_tcp": { + "data": { + "device": "Adre\u00e7a IP de la passarel\u00b7la", + "persistence_file": "fitxer de persist\u00e8ncia (deixa-ho buit per generar-lo autom\u00e0ticament)", + "tcp_port": "port", + "version": "Versi\u00f3 de MySensors" + }, + "description": "Configuraci\u00f3 de passarel\u00b7la Ethernet" + }, + "user": { + "data": { + "gateway_type": "Tipus de passarel\u00b7la" + }, + "description": "Tria el m\u00e8tode de connexi\u00f3 a la passarel\u00b7la" + } + } + }, + "title": "MySensors" +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/es.json b/homeassistant/components/mysensors/translations/es.json new file mode 100644 index 00000000000..f43762b9701 --- /dev/null +++ b/homeassistant/components/mysensors/translations/es.json @@ -0,0 +1,32 @@ +{ + "config": { + "error": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "invalid_device": "Dispositivo no v\u00e1lido", + "invalid_ip": "Direcci\u00f3n IP no v\u00e1lida", + "invalid_port": "N\u00famero de puerto no v\u00e1lido", + "invalid_serial": "Puerto serie no v\u00e1lido", + "invalid_version": "Versi\u00f3n no v\u00e1lida de MySensors", + "not_a_number": "Por favor, introduce un n\u00famero", + "port_out_of_range": "El n\u00famero de puerto debe ser como m\u00ednimo 1 y como m\u00e1ximo 65535", + "unknown": "Error inesperado" + }, + "step": { + "gw_mqtt": { + "data": { + "version": "Versi\u00f3n de MySensors" + }, + "description": "Configuraci\u00f3n del gateway MQTT" + }, + "gw_serial": { + "data": { + "baud_rate": "tasa de baudios", + "device": "Puerto serie", + "version": "Versi\u00f3n de MySensors" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/et.json b/homeassistant/components/mysensors/translations/et.json new file mode 100644 index 00000000000..0682610be97 --- /dev/null +++ b/homeassistant/components/mysensors/translations/et.json @@ -0,0 +1,79 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "cannot_connect": "\u00dchendamine nurjus", + "duplicate_persistence_file": "P\u00fcsivusfail on juba kasutusel", + "duplicate_topic": "Teema on juba kasutusel", + "invalid_auth": "Vigane autentimine", + "invalid_device": "Sobimatu seade", + "invalid_ip": "Sobimatu IP-aadress", + "invalid_persistence_file": "Sobimatu p\u00fcsivusfail", + "invalid_port": "Lubamatu pordinumber", + "invalid_publish_topic": "Kehtetu avaldamisteema", + "invalid_serial": "Sobimatu jadaport", + "invalid_subscribe_topic": "Kehtetu tellimisteema", + "invalid_version": "Sobimatu MySensors versioon", + "not_a_number": "Sisesta number", + "port_out_of_range": "Pordi number peab olema v\u00e4hemalt 1 ja k\u00f5ige rohkem 65535", + "same_topic": "Tellimise ja avaldamise teemad kattuvad", + "unknown": "Ootamatu t\u00f5rge" + }, + "error": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "cannot_connect": "\u00dchendamine nurjus", + "duplicate_persistence_file": "P\u00fcsivusfail on juba kasutusel", + "duplicate_topic": "Teema on juba kasutusel", + "invalid_auth": "Vigane autentimine", + "invalid_device": "Sobimatu seade", + "invalid_ip": "Sobimatu IP-aadress", + "invalid_persistence_file": "Sobimatu p\u00fcsivusfail", + "invalid_port": "Lubamatu pordinumber", + "invalid_publish_topic": "Kehtetu avaldamisteema", + "invalid_serial": "Sobimatu jadaport", + "invalid_subscribe_topic": "Kehtetu tellimisteema", + "invalid_version": "Sobimatu MySensors versioon", + "not_a_number": "Sisesta number", + "port_out_of_range": "Pordi number peab olema v\u00e4hemalt 1 ja k\u00f5ige rohkem 65535", + "same_topic": "Tellimise ja avaldamise teemad kattuvad", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "gw_mqtt": { + "data": { + "persistence_file": "p\u00fcsivusfail (j\u00e4ta automaatse genereerimise jaoks t\u00fchjaks)", + "retain": "mqtt oleku s\u00e4ilitamine", + "topic_in_prefix": "sisendteemade eesliide (topic_in_prefix)", + "topic_out_prefix": "v\u00e4ljunditeemade eesliide (topic_out_prefix)", + "version": "MySensors versioon" + }, + "description": "MQTT-l\u00fc\u00fcsi seadistamine" + }, + "gw_serial": { + "data": { + "baud_rate": "andmeedastuskiirus", + "device": "Jadaport", + "persistence_file": "p\u00fcsivusfail (j\u00e4ta automaatse genereerimise jaoks t\u00fchjaks)", + "version": "MySensors versioon" + }, + "description": "Jadal\u00fc\u00fcsi h\u00e4\u00e4lestus" + }, + "gw_tcp": { + "data": { + "device": "L\u00fc\u00fcsi IP-aadress", + "persistence_file": "p\u00fcsivusfail (j\u00e4ta automaatse genereerimise jaoks t\u00fchjaks)", + "tcp_port": "port", + "version": "MySensors versioon" + }, + "description": "Etherneti l\u00fc\u00fcsi seadistamine" + }, + "user": { + "data": { + "gateway_type": "L\u00fc\u00fcsi t\u00fc\u00fcp" + }, + "description": "Vali l\u00fc\u00fcsi \u00fchendusviis" + } + } + }, + "title": "MySensors" +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/it.json b/homeassistant/components/mysensors/translations/it.json new file mode 100644 index 00000000000..f256ddb95eb --- /dev/null +++ b/homeassistant/components/mysensors/translations/it.json @@ -0,0 +1,79 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi", + "duplicate_persistence_file": "File di persistenza gi\u00e0 in uso", + "duplicate_topic": "Argomento gi\u00e0 in uso", + "invalid_auth": "Autenticazione non valida", + "invalid_device": "Dispositivo non valido", + "invalid_ip": "Indirizzo IP non valido", + "invalid_persistence_file": "File di persistenza non valido", + "invalid_port": "Numero di porta non valido", + "invalid_publish_topic": "Argomento di pubblicazione non valido", + "invalid_serial": "Porta seriale non valida", + "invalid_subscribe_topic": "Argomento di sottoscrizione non valido", + "invalid_version": "Versione di MySensors non valida", + "not_a_number": "Per favore inserisci un numero", + "port_out_of_range": "Il numero di porta deve essere almeno 1 e al massimo 65535", + "same_topic": "Gli argomenti di sottoscrizione e pubblicazione sono gli stessi", + "unknown": "Errore imprevisto" + }, + "error": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi", + "duplicate_persistence_file": "File di persistenza gi\u00e0 in uso", + "duplicate_topic": "Argomento gi\u00e0 in uso", + "invalid_auth": "Autenticazione non valida", + "invalid_device": "Dispositivo non valido", + "invalid_ip": "Indirizzo IP non valido", + "invalid_persistence_file": "File di persistenza non valido", + "invalid_port": "Numero di porta non valido", + "invalid_publish_topic": "Argomento di pubblicazione non valido", + "invalid_serial": "Porta seriale non valida", + "invalid_subscribe_topic": "Argomento di sottoscrizione non valido", + "invalid_version": "Versione di MySensors non valida", + "not_a_number": "Per favore inserisci un numero", + "port_out_of_range": "Il numero di porta deve essere almeno 1 e al massimo 65535", + "same_topic": "Gli argomenti di sottoscrizione e pubblicazione sono gli stessi", + "unknown": "Errore imprevisto" + }, + "step": { + "gw_mqtt": { + "data": { + "persistence_file": "file di persistenza (lasciare vuoto per generare automaticamente)", + "retain": "mqtt conserva", + "topic_in_prefix": "prefisso per argomenti di input (topic_in_prefix)", + "topic_out_prefix": "prefisso per argomenti di output (topic_out_prefix)", + "version": "Versione MySensors" + }, + "description": "Configurazione del gateway MQTT" + }, + "gw_serial": { + "data": { + "baud_rate": "velocit\u00e0 di trasmissione", + "device": "Porta seriale", + "persistence_file": "file di persistenza (lasciare vuoto per generare automaticamente)", + "version": "Versione MySensors" + }, + "description": "Configurazione del gateway seriale" + }, + "gw_tcp": { + "data": { + "device": "Indirizzo IP del gateway", + "persistence_file": "file di persistenza (lasciare vuoto per generare automaticamente)", + "tcp_port": "porta", + "version": "Versione MySensors" + }, + "description": "Configurazione del gateway Ethernet" + }, + "user": { + "data": { + "gateway_type": "Tipo di gateway" + }, + "description": "Scegli il metodo di connessione al gateway" + } + } + }, + "title": "MySensors" +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/ru.json b/homeassistant/components/mysensors/translations/ru.json new file mode 100644 index 00000000000..e78685e3f6b --- /dev/null +++ b/homeassistant/components/mysensors/translations/ru.json @@ -0,0 +1,79 @@ +{ + "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.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "duplicate_persistence_file": "\u042d\u0442\u043e\u0442 \u0444\u0430\u0439\u043b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0439 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", + "duplicate_topic": "\u042d\u0442\u043e\u0442 \u0442\u043e\u043f\u0438\u043a \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_device": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e.", + "invalid_ip": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441.", + "invalid_persistence_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0439.", + "invalid_port": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u043f\u043e\u0440\u0442\u0430.", + "invalid_publish_topic": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u0434\u043b\u044f \u043f\u0443\u0431\u043b\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_serial": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442.", + "invalid_subscribe_topic": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u0434\u043b\u044f \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438.", + "invalid_version": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f MySensors.", + "not_a_number": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0447\u0438\u0441\u043b\u043e.", + "port_out_of_range": "\u041d\u043e\u043c\u0435\u0440 \u043f\u043e\u0440\u0442\u0430 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043e\u0442 1 \u0434\u043e 65535.", + "same_topic": "\u0422\u043e\u043f\u0438\u043a\u0438 \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438 \u0438 \u043f\u0443\u0431\u043b\u0438\u043a\u0430\u0446\u0438\u0438 \u0441\u043e\u0432\u043f\u0430\u0434\u0430\u044e\u0442.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "error": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "duplicate_persistence_file": "\u042d\u0442\u043e\u0442 \u0444\u0430\u0439\u043b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0439 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", + "duplicate_topic": "\u042d\u0442\u043e\u0442 \u0442\u043e\u043f\u0438\u043a \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_device": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e.", + "invalid_ip": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441.", + "invalid_persistence_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0439.", + "invalid_port": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u043f\u043e\u0440\u0442\u0430.", + "invalid_publish_topic": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u0434\u043b\u044f \u043f\u0443\u0431\u043b\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_serial": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442.", + "invalid_subscribe_topic": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u0434\u043b\u044f \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438.", + "invalid_version": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f MySensors.", + "not_a_number": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0447\u0438\u0441\u043b\u043e.", + "port_out_of_range": "\u041d\u043e\u043c\u0435\u0440 \u043f\u043e\u0440\u0442\u0430 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043e\u0442 1 \u0434\u043e 65535.", + "same_topic": "\u0422\u043e\u043f\u0438\u043a\u0438 \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438 \u0438 \u043f\u0443\u0431\u043b\u0438\u043a\u0430\u0446\u0438\u0438 \u0441\u043e\u0432\u043f\u0430\u0434\u0430\u044e\u0442.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "gw_mqtt": { + "data": { + "persistence_file": "\u0424\u0430\u0439\u043b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0439 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f)", + "retain": "\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f MQTT", + "topic_in_prefix": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0442\u043e\u043f\u0438\u043a\u043e\u0432 (topic_in_prefix)", + "topic_out_prefix": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441 \u0434\u043b\u044f \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0442\u043e\u043f\u0438\u043a\u043e\u0432 (topic_out_prefix)", + "version": "\u0412\u0435\u0440\u0441\u0438\u044f MySensors" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 MQTT" + }, + "gw_serial": { + "data": { + "baud_rate": "\u0421\u043a\u043e\u0440\u043e\u0441\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0434\u0430\u043d\u043d\u044b\u0445", + "device": "\u041f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442", + "persistence_file": "\u0424\u0430\u0439\u043b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0439 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f)", + "version": "\u0412\u0435\u0440\u0441\u0438\u044f MySensors" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0448\u043b\u044e\u0437\u0430" + }, + "gw_tcp": { + "data": { + "device": "IP-\u0430\u0434\u0440\u0435\u0441 \u0448\u043b\u044e\u0437\u0430", + "persistence_file": "\u0424\u0430\u0439\u043b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0439 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f)", + "tcp_port": "\u041f\u043e\u0440\u0442", + "version": "\u0412\u0435\u0440\u0441\u0438\u044f MySensors" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 Ethernet" + }, + "user": { + "data": { + "gateway_type": "\u0422\u0438\u043f \u0448\u043b\u044e\u0437\u0430" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0448\u043b\u044e\u0437\u0443" + } + } + }, + "title": "MySensors" +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/zh-Hant.json b/homeassistant/components/mysensors/translations/zh-Hant.json new file mode 100644 index 00000000000..0d4db4502e5 --- /dev/null +++ b/homeassistant/components/mysensors/translations/zh-Hant.json @@ -0,0 +1,77 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "duplicate_topic": "\u4e3b\u984c\u5df2\u4f7f\u7528\u4e2d", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_device": "\u88dd\u7f6e\u7121\u6548", + "invalid_ip": "IP \u4f4d\u5740\u7121\u6548", + "invalid_port": "\u901a\u8a0a\u57e0\u865f\u78bc\u7121\u6548", + "invalid_publish_topic": "\u767c\u5e03\u4e3b\u984c\u7121\u6548", + "invalid_serial": "\u5e8f\u5217\u57e0\u7121\u6548", + "invalid_subscribe_topic": "\u8a02\u95b1\u4e3b\u984c\u7121\u6548", + "invalid_version": "MySensors \u7248\u672c\u7121\u6548", + "not_a_number": "\u8acb\u8f38\u5165\u865f\u78bc", + "port_out_of_range": "\u8acb\u8f38\u5165\u4ecb\u65bc 1 \u81f3 65535 \u4e4b\u9593\u7684\u865f\u78bc", + "same_topic": "\u8a02\u95b1\u8207\u767c\u4f48\u4e3b\u984c\u76f8\u540c", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "error": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "duplicate_persistence_file": "Persistence \u6a94\u6848\u5df2\u4f7f\u7528\u4e2d", + "duplicate_topic": "\u4e3b\u984c\u5df2\u4f7f\u7528\u4e2d", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_device": "\u88dd\u7f6e\u7121\u6548", + "invalid_ip": "IP \u4f4d\u5740\u7121\u6548", + "invalid_persistence_file": "Persistence \u6a94\u6848\u7121\u6548", + "invalid_port": "\u901a\u8a0a\u57e0\u865f\u78bc\u7121\u6548", + "invalid_publish_topic": "\u767c\u5e03\u4e3b\u984c\u7121\u6548", + "invalid_serial": "\u5e8f\u5217\u57e0\u7121\u6548", + "invalid_subscribe_topic": "\u8a02\u95b1\u4e3b\u984c\u7121\u6548", + "invalid_version": "MySensors \u7248\u672c\u7121\u6548", + "not_a_number": "\u8acb\u8f38\u5165\u865f\u78bc", + "port_out_of_range": "\u8acb\u8f38\u5165\u4ecb\u65bc 1 \u81f3 65535 \u4e4b\u9593\u7684\u865f\u78bc", + "same_topic": "\u8a02\u95b1\u8207\u767c\u4f48\u4e3b\u984c\u76f8\u540c", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "gw_mqtt": { + "data": { + "persistence_file": "Persistence \u6a94\u6848\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u81ea\u52d5\u7522\u751f\uff09", + "retain": "mqtt retain", + "topic_in_prefix": "\u8f38\u5165\u4e3b\u984c\u524d\u7db4\uff08topic_in_prefix\uff09", + "topic_out_prefix": "\u8f38\u51fa\u4e3b\u984c\u524d\u7db4\uff08topic_out_prefix\uff09", + "version": "MySensors \u7248\u672c" + }, + "description": "MQTT \u9598\u9053\u5668\u8a2d\u5b9a" + }, + "gw_serial": { + "data": { + "baud_rate": "\u50b3\u8f38\u7387", + "device": "\u5e8f\u5217\u57e0", + "persistence_file": "Persistence \u6a94\u6848\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u81ea\u52d5\u7522\u751f\uff09", + "version": "MySensors \u7248\u672c" + }, + "description": "\u9598\u9053\u5668\u8a0a\u5217\u57e0\u8a2d\u5b9a" + }, + "gw_tcp": { + "data": { + "device": "\u7db2\u95dc IP \u4f4d\u5740", + "persistence_file": "Persistence \u6a94\u6848\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u81ea\u52d5\u7522\u751f\uff09", + "tcp_port": "\u901a\u8a0a\u57e0", + "version": "MySensors \u7248\u672c" + }, + "description": "\u9598\u9053\u5668\u4e59\u592a\u7db2\u8def\u8a2d\u5b9a" + }, + "user": { + "data": { + "gateway_type": "\u9598\u9053\u5668\u985e\u578b" + }, + "description": "\u9078\u64c7\u9598\u9053\u5668\u9023\u7dda\u65b9\u5f0f" + } + } + }, + "title": "MySensors" +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/es.json b/homeassistant/components/nuki/translations/es.json new file mode 100644 index 00000000000..8def4e2780d --- /dev/null +++ b/homeassistant/components/nuki/translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Puerto", + "token": "Token de acceso" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/es.json b/homeassistant/components/number/translations/es.json new file mode 100644 index 00000000000..e77258e777d --- /dev/null +++ b/homeassistant/components/number/translations/es.json @@ -0,0 +1,3 @@ +{ + "title": "N\u00famero" +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/es.json b/homeassistant/components/plaato/translations/es.json index 0f030e56b4e..6ff49fc707c 100644 --- a/homeassistant/components/plaato/translations/es.json +++ b/homeassistant/components/plaato/translations/es.json @@ -1,16 +1,43 @@ { "config": { "abort": { + "already_configured": "La cuenta ya ha sido configurada", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, "create_entry": { "default": "Para enviar eventos a Home Assistant, necesitar\u00e1s configurar la funci\u00f3n de webhook en Plaato Airlock.\n\nCompleta la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- M\u00e9todo: POST\n\nEcha un vistazo a [la documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." }, + "error": { + "invalid_webhook_device": "Has seleccionado un dispositivo que no admite el env\u00edo de datos a un webhook. Solo est\u00e1 disponible para Airlock", + "no_api_method": "Necesitas a\u00f1adir un token de autenticaci\u00f3n o seleccionar un webhook", + "no_auth_token": "Es necesario a\u00f1adir un token de autenticaci\u00f3n" + }, "step": { + "api_method": { + "data": { + "token": "Pega el token de autenticaci\u00f3n aqu\u00ed", + "use_webhook": "Usar webhook" + }, + "title": "Selecciona el m\u00e9todo API" + }, "user": { "description": "\u00bfEst\u00e1s seguro de que quieres configurar el Airlock de Plaato?", "title": "Configurar el webhook de Plaato" + }, + "webhook": { + "title": "Webhook a utilizar" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "Intervalo de actualizaci\u00f3n (minutos)" + }, + "description": "Intervalo de actualizaci\u00f3n (minutos)", + "title": "Opciones de Plaato" } } } diff --git a/homeassistant/components/zwave_js/translations/es.json b/homeassistant/components/zwave_js/translations/es.json index e5ee009c0d1..257d26eefd6 100644 --- a/homeassistant/components/zwave_js/translations/es.json +++ b/homeassistant/components/zwave_js/translations/es.json @@ -1,22 +1,39 @@ { "config": { "abort": { + "addon_info_failed": "No se pudo obtener la informaci\u00f3n del complemento Z-Wave JS.", + "addon_install_failed": "No se ha podido instalar el complemento Z-Wave JS.", + "addon_set_config_failed": "Fallo en la configuraci\u00f3n de Z-Wave JS.", "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", "cannot_connect": "No se pudo conectar" }, "error": { + "addon_start_failed": "No se pudo iniciar el complemento Z-Wave JS. Comprueba la configuraci\u00f3n.", "cannot_connect": "No se pudo conectar", "invalid_ws_url": "URL de websocket no v\u00e1lida", "unknown": "Error inesperado" }, + "progress": { + "install_addon": "Espera mientras termina la instalaci\u00f3n del complemento Z-Wave JS. Puede tardar varios minutos." + }, "step": { + "hassio_confirm": { + "title": "Configurar la integraci\u00f3n de Z-Wave JS con el complemento Z-Wave JS" + }, + "install_addon": { + "title": "La instalaci\u00f3n del complemento Z-Wave JS ha comenzado" + }, "manual": { "data": { "url": "URL" } }, "on_supervisor": { + "data": { + "use_addon": "Usar el complemento Z-Wave JS Supervisor" + }, + "description": "\u00bfQuieres utilizar el complemento Z-Wave JS Supervisor?", "title": "Selecciona el m\u00e9todo de conexi\u00f3n" }, "start_addon": { From 94eb31025c57a4745163146c3d12d235980ebca3 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 7 Feb 2021 01:27:58 +0100 Subject: [PATCH 234/796] xknx 0.16.3 (#46128) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 93daee0e348..60fb097128c 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.16.2"], + "requirements": ["xknx==0.16.3"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver" } diff --git a/requirements_all.txt b/requirements_all.txt index 1962b4d393e..1f5704aa3b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2323,7 +2323,7 @@ xboxapi==2.0.1 xfinity-gateway==0.0.4 # homeassistant.components.knx -xknx==0.16.2 +xknx==0.16.3 # homeassistant.components.bluesound # homeassistant.components.rest From 74053b5f2d953349a13e497c0e0c9fb610a632b2 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sun, 7 Feb 2021 13:28:40 -0500 Subject: [PATCH 235/796] Use core constants for envisalink (#46136) --- homeassistant/components/envisalink/__init__.py | 9 ++++++--- .../components/envisalink/alarm_control_panel.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/envisalink/__init__.py b/homeassistant/components/envisalink/__init__.py index 636cf0c19df..73e20eea92c 100644 --- a/homeassistant/components/envisalink/__init__.py +++ b/homeassistant/components/envisalink/__init__.py @@ -5,7 +5,12 @@ import logging from pyenvisalink import EnvisalinkAlarmPanel import voluptuous as vol -from homeassistant.const import CONF_HOST, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + CONF_CODE, + CONF_HOST, + CONF_TIMEOUT, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform @@ -18,7 +23,6 @@ DOMAIN = "envisalink" DATA_EVL = "envisalink" -CONF_CODE = "code" CONF_EVL_KEEPALIVE = "keepalive_interval" CONF_EVL_PORT = "port" CONF_EVL_VERSION = "evl_version" @@ -99,7 +103,6 @@ SERVICE_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up for Envisalink devices.""" - conf = config.get(DOMAIN) host = conf.get(CONF_HOST) diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py index 670dc78392f..dff434a68ee 100644 --- a/homeassistant/components/envisalink/alarm_control_panel.py +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -15,6 +15,7 @@ from homeassistant.components.alarm_control_panel.const import ( ) from homeassistant.const import ( ATTR_ENTITY_ID, + CONF_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, @@ -28,7 +29,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import ( - CONF_CODE, CONF_PANIC, CONF_PARTITIONNAME, DATA_EVL, From 19e9515bec9e76603deada69a87c4e0f4db833ee Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sun, 7 Feb 2021 13:47:16 -0500 Subject: [PATCH 236/796] Use core constants for ffmpeg_motion (#46137) --- homeassistant/components/ffmpeg_motion/binary_sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/ffmpeg_motion/binary_sensor.py b/homeassistant/components/ffmpeg_motion/binary_sensor.py index 314fbbd2210..ecbf6f3b1ae 100644 --- a/homeassistant/components/ffmpeg_motion/binary_sensor.py +++ b/homeassistant/components/ffmpeg_motion/binary_sensor.py @@ -14,13 +14,12 @@ from homeassistant.components.ffmpeg import ( DATA_FFMPEG, FFmpegBase, ) -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, CONF_REPEAT from homeassistant.core import callback import homeassistant.helpers.config_validation as cv CONF_RESET = "reset" CONF_CHANGES = "changes" -CONF_REPEAT = "repeat" CONF_REPEAT_TIME = "repeat_time" DEFAULT_NAME = "FFmpeg Motion" @@ -88,7 +87,6 @@ class FFmpegMotion(FFmpegBinarySensor): def __init__(self, hass, manager, config): """Initialize FFmpeg motion binary sensor.""" - super().__init__(config) self.ffmpeg = ffmpeg_sensor.SensorMotion(manager.binary, self._async_callback) From 8e06fa017d7e472ce54866486b9373999d0785e8 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sun, 7 Feb 2021 13:52:48 -0500 Subject: [PATCH 237/796] Use core constants for emulated_hue (#46092) --- homeassistant/components/emulated_hue/__init__.py | 9 ++++++--- homeassistant/components/emulated_hue/upnp.py | 1 - 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index b4a49c7efcd..11ad80688a3 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -5,7 +5,12 @@ from aiohttp import web import voluptuous as vol from homeassistant import util -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + CONF_ENTITIES, + CONF_TYPE, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json @@ -31,7 +36,6 @@ NUMBERS_FILE = "emulated_hue_ids.json" CONF_ADVERTISE_IP = "advertise_ip" CONF_ADVERTISE_PORT = "advertise_port" -CONF_ENTITIES = "entities" CONF_ENTITY_HIDDEN = "hidden" CONF_ENTITY_NAME = "name" CONF_EXPOSE_BY_DEFAULT = "expose_by_default" @@ -40,7 +44,6 @@ CONF_HOST_IP = "host_ip" CONF_LIGHTS_ALL_DIMMABLE = "lights_all_dimmable" CONF_LISTEN_PORT = "listen_port" CONF_OFF_MAPS_TO_ON_DOMAINS = "off_maps_to_on_domains" -CONF_TYPE = "type" CONF_UPNP_BIND_MULTICAST = "upnp_bind_multicast" TYPE_ALEXA = "alexa" diff --git a/homeassistant/components/emulated_hue/upnp.py b/homeassistant/components/emulated_hue/upnp.py index 58f964d4984..8ff7eb85b39 100644 --- a/homeassistant/components/emulated_hue/upnp.py +++ b/homeassistant/components/emulated_hue/upnp.py @@ -63,7 +63,6 @@ def create_upnp_datagram_endpoint( advertise_port, ): """Create the UPNP socket and protocol.""" - # Listen for UDP port 1900 packets sent to SSDP multicast address ssdp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) ssdp_socket.setblocking(False) From aa00c6230262151ef01d0552ccae10333ca97242 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 8 Feb 2021 00:07:01 +0000 Subject: [PATCH 238/796] [ci skip] Translation update --- .../components/binary_sensor/translations/pl.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/binary_sensor/translations/pl.json b/homeassistant/components/binary_sensor/translations/pl.json index 59fd573d5b3..726765aea02 100644 --- a/homeassistant/components/binary_sensor/translations/pl.json +++ b/homeassistant/components/binary_sensor/translations/pl.json @@ -95,8 +95,8 @@ "on": "w\u0142." }, "battery": { - "off": "Normalna", - "on": "Niska" + "off": "na\u0142adowana", + "on": "roz\u0142adowana" }, "battery_charging": { "off": "roz\u0142adowywanie", @@ -107,8 +107,8 @@ "on": "zimno" }, "connectivity": { - "off": "Roz\u0142\u0105czony", - "on": "Po\u0142\u0105czony" + "off": "offline", + "on": "online" }, "door": { "off": "zamkni\u0119te", @@ -135,7 +135,7 @@ "on": "otwarty" }, "moisture": { - "off": "Sucho", + "off": "brak wilgoci", "on": "wilgo\u0107" }, "motion": { From d02b78c634f6eb7198e42d40e0024abc6d216eb4 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Sun, 7 Feb 2021 22:05:10 -0800 Subject: [PATCH 239/796] Revert "Convert ozw climate values to correct units (#45369)" (#46163) This reverts commit 1b6ee8301a5c076f93d0799b9f7fcb82cc6eb902. --- homeassistant/components/ozw/climate.py | 51 ++++--------------------- tests/components/ozw/test_climate.py | 20 ++++------ 2 files changed, 15 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/ozw/climate.py b/homeassistant/components/ozw/climate.py index 67bbe5cdc4d..a74fd869f0f 100644 --- a/homeassistant/components/ozw/climate.py +++ b/homeassistant/components/ozw/climate.py @@ -28,7 +28,6 @@ from homeassistant.components.climate.const import ( from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.util.temperature import convert as convert_temperature from .const import DATA_UNSUBSCRIBE, DOMAIN from .entity import ZWaveDeviceEntity @@ -155,13 +154,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -def convert_units(units): - """Return units as a farenheit or celsius constant.""" - if units == "F": - return TEMP_FAHRENHEIT - return TEMP_CELSIUS - - class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): """Representation of a Z-Wave Climate device.""" @@ -207,18 +199,16 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): @property def temperature_unit(self): """Return the unit of measurement.""" - return convert_units(self._current_mode_setpoint_values[0].units) + if self.values.temperature is not None and self.values.temperature.units == "F": + return TEMP_FAHRENHEIT + return TEMP_CELSIUS @property def current_temperature(self): """Return the current temperature.""" if not self.values.temperature: return None - return convert_temperature( - self.values.temperature.value, - convert_units(self._current_mode_setpoint_values[0].units), - self.temperature_unit, - ) + return self.values.temperature.value @property def hvac_action(self): @@ -246,29 +236,17 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): @property def target_temperature(self): """Return the temperature we try to reach.""" - return convert_temperature( - self._current_mode_setpoint_values[0].value, - convert_units(self._current_mode_setpoint_values[0].units), - self.temperature_unit, - ) + return self._current_mode_setpoint_values[0].value @property def target_temperature_low(self) -> Optional[float]: """Return the lowbound target temperature we try to reach.""" - return convert_temperature( - self._current_mode_setpoint_values[0].value, - convert_units(self._current_mode_setpoint_values[0].units), - self.temperature_unit, - ) + return self._current_mode_setpoint_values[0].value @property def target_temperature_high(self) -> Optional[float]: """Return the highbound target temperature we try to reach.""" - return convert_temperature( - self._current_mode_setpoint_values[1].value, - convert_units(self._current_mode_setpoint_values[1].units), - self.temperature_unit, - ) + return self._current_mode_setpoint_values[1].value async def async_set_temperature(self, **kwargs): """Set new target temperature. @@ -284,29 +262,14 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): setpoint = self._current_mode_setpoint_values[0] target_temp = kwargs.get(ATTR_TEMPERATURE) if setpoint is not None and target_temp is not None: - target_temp = convert_temperature( - target_temp, - self.temperature_unit, - convert_units(setpoint.units), - ) setpoint.send_value(target_temp) elif len(self._current_mode_setpoint_values) == 2: (setpoint_low, setpoint_high) = self._current_mode_setpoint_values target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) if setpoint_low is not None and target_temp_low is not None: - target_temp_low = convert_temperature( - target_temp_low, - self.temperature_unit, - convert_units(setpoint_low.units), - ) setpoint_low.send_value(target_temp_low) if setpoint_high is not None and target_temp_high is not None: - target_temp_high = convert_temperature( - target_temp_high, - self.temperature_unit, - convert_units(setpoint_high.units), - ) setpoint_high.send_value(target_temp_high) async def async_set_fan_mode(self, fan_mode): diff --git a/tests/components/ozw/test_climate.py b/tests/components/ozw/test_climate.py index e251a93c115..3414e6c4832 100644 --- a/tests/components/ozw/test_climate.py +++ b/tests/components/ozw/test_climate.py @@ -16,8 +16,6 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, ) -from homeassistant.components.ozw.climate import convert_units -from homeassistant.const import TEMP_FAHRENHEIT from .common import setup_ozw @@ -38,8 +36,8 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): HVAC_MODE_HEAT_COOL, ] assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE - assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 73.5 - assert state.attributes[ATTR_TEMPERATURE] == 70.0 + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 23.1 + assert state.attributes[ATTR_TEMPERATURE] == 21.1 assert state.attributes.get(ATTR_TARGET_TEMP_LOW) is None assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) is None assert state.attributes[ATTR_FAN_MODE] == "Auto Low" @@ -56,7 +54,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): msg = sent_messages[-1] assert msg["topic"] == "OpenZWave/1/command/setvalue/" # Celsius is converted to Fahrenheit here! - assert round(msg["payload"]["Value"], 2) == 26.1 + assert round(msg["payload"]["Value"], 2) == 78.98 assert msg["payload"]["ValueIDKey"] == 281475099443218 # Test hvac_mode with set_temperature @@ -74,7 +72,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): msg = sent_messages[-1] assert msg["topic"] == "OpenZWave/1/command/setvalue/" # Celsius is converted to Fahrenheit here! - assert round(msg["payload"]["Value"], 2) == 24.1 + assert round(msg["payload"]["Value"], 2) == 75.38 assert msg["payload"]["ValueIDKey"] == 281475099443218 # Test set mode @@ -129,8 +127,8 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): assert state is not None assert state.state == HVAC_MODE_HEAT_COOL assert state.attributes.get(ATTR_TEMPERATURE) is None - assert state.attributes[ATTR_TARGET_TEMP_LOW] == 70.0 - assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 78.0 + assert state.attributes[ATTR_TARGET_TEMP_LOW] == 21.1 + assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 25.6 # Test setting high/low temp on multiple setpoints await hass.services.async_call( @@ -146,11 +144,11 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): assert len(sent_messages) == 7 # 2 messages ! msg = sent_messages[-2] # low setpoint assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert round(msg["payload"]["Value"], 2) == 20.0 + assert round(msg["payload"]["Value"], 2) == 68.0 assert msg["payload"]["ValueIDKey"] == 281475099443218 msg = sent_messages[-1] # high setpoint assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert round(msg["payload"]["Value"], 2) == 25.0 + assert round(msg["payload"]["Value"], 2) == 77.0 assert msg["payload"]["ValueIDKey"] == 562950076153874 # Test basic/single-setpoint thermostat (node 16 in dump) @@ -327,5 +325,3 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): ) assert len(sent_messages) == 12 assert "does not support setting a mode" in caplog.text - - assert convert_units("F") == TEMP_FAHRENHEIT From 66ecd2e0f2e9f67b88404684f8d99ab026363c5b Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 8 Feb 2021 02:32:24 -0500 Subject: [PATCH 240/796] Remove unused config_flows (#46188) --- homeassistant/components/aws/__init__.py | 1 - homeassistant/components/daikin/__init__.py | 1 - homeassistant/components/mqtt/__init__.py | 1 - homeassistant/components/tellduslive/__init__.py | 1 - homeassistant/components/tradfri/__init__.py | 1 - homeassistant/components/zwave/__init__.py | 1 - 6 files changed, 6 deletions(-) diff --git a/homeassistant/components/aws/__init__.py b/homeassistant/components/aws/__init__.py index 19563efa144..da8c27d7445 100644 --- a/homeassistant/components/aws/__init__.py +++ b/homeassistant/components/aws/__init__.py @@ -16,7 +16,6 @@ from homeassistant.const import ( from homeassistant.helpers import config_validation as cv, discovery # Loading the config flow file will register the flow -from . import config_flow # noqa: F401 from .const import ( CONF_ACCESS_KEY_ID, CONF_CONTEXT, diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index b4950b8b05b..16fd2b2ff56 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -16,7 +16,6 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle -from . import config_flow # noqa: F401 from .const import CONF_UUID, KEY_MAC, TIMEOUT _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 788f8d1957e..098099f0a03 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -40,7 +40,6 @@ from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.logging import catch_log_exception # Loading the config flow file will register the flow -from . import config_flow # noqa: F401 pylint: disable=unused-import from . import debug_info, discovery from .const import ( ATTR_PAYLOAD, diff --git a/homeassistant/components/tellduslive/__init__.py b/homeassistant/components/tellduslive/__init__.py index ae98a5d8504..5d4721e60e6 100644 --- a/homeassistant/components/tellduslive/__init__.py +++ b/homeassistant/components/tellduslive/__init__.py @@ -12,7 +12,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later -from . import config_flow # noqa: F401 from .const import ( CONF_HOST, DOMAIN, diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index 8d82df07bbb..4c984067ada 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -16,7 +16,6 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.json import load_json -from . import config_flow # noqa: F401 from .const import ( ATTR_TRADFRI_GATEWAY, ATTR_TRADFRI_GATEWAY_MODEL, diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 27f6c0a4801..3acf361dd52 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -36,7 +36,6 @@ from homeassistant.helpers.event import async_track_time_change from homeassistant.util import convert import homeassistant.util.dt as dt_util -from . import config_flow # noqa: F401 pylint: disable=unused-import from . import const, websocket_api as wsapi, workaround from .const import ( CONF_AUTOHEAL, From 5755598c95c772da108e48d2c945acb0570104de Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 8 Feb 2021 03:31:02 -0500 Subject: [PATCH 241/796] Use core constants for fixer (#46173) --- homeassistant/components/fixer/sensor.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/fixer/sensor.py b/homeassistant/components/fixer/sensor.py index e3dfd432a41..99ebbdd6bb6 100644 --- a/homeassistant/components/fixer/sensor.py +++ b/homeassistant/components/fixer/sensor.py @@ -7,7 +7,7 @@ from fixerio.exceptions import FixerioException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME, CONF_TARGET import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -17,8 +17,6 @@ ATTR_EXCHANGE_RATE = "Exchange rate" ATTR_TARGET = "Target currency" ATTRIBUTION = "Data provided by the European Central Bank (ECB)" -CONF_TARGET = "target" - DEFAULT_BASE = "USD" DEFAULT_NAME = "Exchange rate" @@ -37,7 +35,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Fixer.io sensor.""" - api_key = config.get(CONF_API_KEY) name = config.get(CONF_NAME) target = config.get(CONF_TARGET) @@ -103,7 +100,6 @@ class ExchangeData: def __init__(self, target_currency, api_key): """Initialize the data object.""" - self.api_key = api_key self.rate = None self.target_currency = target_currency From 04c1578f15a55aedfa3e853a2f2b0da8364bd1bc Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 8 Feb 2021 03:32:01 -0500 Subject: [PATCH 242/796] Use core constants for file integration (#46171) --- homeassistant/components/file/sensor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/file/sensor.py b/homeassistant/components/file/sensor.py index e928541a724..3368bd878d5 100644 --- a/homeassistant/components/file/sensor.py +++ b/homeassistant/components/file/sensor.py @@ -5,14 +5,17 @@ import os import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE +from homeassistant.const import ( + CONF_FILE_PATH, + CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -CONF_FILE_PATH = "file_path" - DEFAULT_NAME = "File" ICON = "mdi:file" From ca87bf49b6b5ed7ee3fdee35492c2119143be1a0 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 8 Feb 2021 09:34:12 +0100 Subject: [PATCH 243/796] Upgrade praw to 7.1.3 (#46073) --- homeassistant/components/reddit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reddit/manifest.json b/homeassistant/components/reddit/manifest.json index e19ae570d0f..d270f994159 100644 --- a/homeassistant/components/reddit/manifest.json +++ b/homeassistant/components/reddit/manifest.json @@ -2,6 +2,6 @@ "domain": "reddit", "name": "Reddit", "documentation": "https://www.home-assistant.io/integrations/reddit", - "requirements": ["praw==7.1.2"], + "requirements": ["praw==7.1.3"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 1f5704aa3b7..d21c11fb466 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1159,7 +1159,7 @@ pmsensor==0.4 poolsense==0.0.8 # homeassistant.components.reddit -praw==7.1.2 +praw==7.1.3 # homeassistant.components.islamic_prayer_times prayer_times_calculator==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 65b19d7aabb..d0febd47326 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -596,7 +596,7 @@ pmsensor==0.4 poolsense==0.0.8 # homeassistant.components.reddit -praw==7.1.2 +praw==7.1.3 # homeassistant.components.islamic_prayer_times prayer_times_calculator==0.0.3 From 28ef3f68f3d8e913598122fd096ed963c1fc0468 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 8 Feb 2021 09:36:14 +0100 Subject: [PATCH 244/796] Add media_player device triggers (#45430) * Add media player device triggers * Update tests --- .../components/media_player/device_trigger.py | 90 +++++++++++ .../components/media_player/strings.json | 7 + .../arcam_fmj/test_device_trigger.py | 9 +- .../specific_devices/test_lg_tv.py | 5 +- tests/components/kodi/test_device_trigger.py | 8 +- .../media_player/test_device_trigger.py | 147 ++++++++++++++++++ 6 files changed, 260 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/media_player/device_trigger.py create mode 100644 tests/components/media_player/test_device_trigger.py diff --git a/homeassistant/components/media_player/device_trigger.py b/homeassistant/components/media_player/device_trigger.py new file mode 100644 index 00000000000..6db5f16cf01 --- /dev/null +++ b/homeassistant/components/media_player/device_trigger.py @@ -0,0 +1,90 @@ +"""Provides device automations for Media player.""" +from typing import List + +import voluptuous as vol + +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.homeassistant.triggers import state as state_trigger +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, + STATE_IDLE, + STATE_OFF, + STATE_ON, + STATE_PAUSED, + STATE_PLAYING, +) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType + +from . import DOMAIN + +TRIGGER_TYPES = {"turned_on", "turned_off", "idle", "paused", "playing"} + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + } +) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for Media player entities.""" + registry = await entity_registry.async_get_registry(hass) + triggers = [] + + # Get all the integration entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add triggers for each entity that belongs to this integration + triggers += [ + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: trigger, + } + for trigger in TRIGGER_TYPES + ] + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + + if config[CONF_TYPE] == "turned_on": + to_state = STATE_ON + elif config[CONF_TYPE] == "turned_off": + to_state = STATE_OFF + elif config[CONF_TYPE] == "idle": + to_state = STATE_IDLE + elif config[CONF_TYPE] == "paused": + to_state = STATE_PAUSED + else: + to_state = STATE_PLAYING + + state_config = { + CONF_PLATFORM: "state", + CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state_trigger.CONF_TO: to_state, + } + state_config = state_trigger.TRIGGER_SCHEMA(state_config) + return await state_trigger.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) diff --git a/homeassistant/components/media_player/strings.json b/homeassistant/components/media_player/strings.json index 14f1eea131c..64841413f12 100644 --- a/homeassistant/components/media_player/strings.json +++ b/homeassistant/components/media_player/strings.json @@ -7,6 +7,13 @@ "is_idle": "{entity_name} is idle", "is_paused": "{entity_name} is paused", "is_playing": "{entity_name} is playing" + }, + "trigger_type": { + "turned_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off", + "idle": "{entity_name} becomes idle", + "paused": "{entity_name} is paused", + "playing": "{entity_name} starts playing" } }, "state": { diff --git a/tests/components/arcam_fmj/test_device_trigger.py b/tests/components/arcam_fmj/test_device_trigger.py index 0f2cfaf2893..0cae565f7bb 100644 --- a/tests/components/arcam_fmj/test_device_trigger.py +++ b/tests/components/arcam_fmj/test_device_trigger.py @@ -7,7 +7,6 @@ from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, - assert_lists_same, async_get_device_automations, async_mock_service, mock_device_registry, @@ -55,7 +54,13 @@ async def test_get_triggers(hass, device_reg, entity_reg): }, ] triggers = await async_get_device_automations(hass, "trigger", device_entry.id) - assert_lists_same(triggers, expected_triggers) + + # Test triggers are either arcam_fmj specific or media_player entity triggers + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + for expected_trigger in expected_triggers: + assert expected_trigger in triggers + for trigger in triggers: + assert trigger in expected_triggers or trigger["domain"] == "media_player" async def test_if_fires_on_turn_on_request(hass, calls, player_setup, state): diff --git a/tests/components/homekit_controller/specific_devices/test_lg_tv.py b/tests/components/homekit_controller/specific_devices/test_lg_tv.py index cd3f57137bf..ebc50fda8bc 100644 --- a/tests/components/homekit_controller/specific_devices/test_lg_tv.py +++ b/tests/components/homekit_controller/specific_devices/test_lg_tv.py @@ -63,6 +63,7 @@ async def test_lg_tv(hass): assert device.sw_version == "04.71.04" assert device.via_device_id is None - # A TV doesn't have any triggers + # A TV has media player device triggers triggers = await async_get_device_automations(hass, "trigger", device.id) - assert triggers == [] + for trigger in triggers: + assert trigger["domain"] == "media_player" diff --git a/tests/components/kodi/test_device_trigger.py b/tests/components/kodi/test_device_trigger.py index 8cf6c635393..0dd75b9c357 100644 --- a/tests/components/kodi/test_device_trigger.py +++ b/tests/components/kodi/test_device_trigger.py @@ -10,7 +10,6 @@ from . import init_integration from tests.common import ( MockConfigEntry, - assert_lists_same, async_get_device_automations, async_mock_service, mock_device_registry, @@ -69,8 +68,13 @@ async def test_get_triggers(hass, device_reg, entity_reg): "entity_id": f"{MP_DOMAIN}.kodi_5678", }, ] + + # Test triggers are either kodi specific triggers or media_player entity triggers triggers = await async_get_device_automations(hass, "trigger", device_entry.id) - assert_lists_same(triggers, expected_triggers) + for expected_trigger in expected_triggers: + assert expected_trigger in triggers + for trigger in triggers: + assert trigger in expected_triggers or trigger["domain"] == "media_player" async def test_if_fires_on_state_change(hass, calls, kodi_media_player): diff --git a/tests/components/media_player/test_device_trigger.py b/tests/components/media_player/test_device_trigger.py new file mode 100644 index 00000000000..93d9127f8b8 --- /dev/null +++ b/tests/components/media_player/test_device_trigger.py @@ -0,0 +1,147 @@ +"""The tests for Media player device triggers.""" +import pytest + +import homeassistant.components.automation as automation +from homeassistant.components.media_player import DOMAIN +from homeassistant.const import ( + STATE_IDLE, + STATE_OFF, + STATE_ON, + STATE_PAUSED, + STATE_PLAYING, +) +from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_get_device_automations, + async_mock_service, + mock_device_registry, + mock_registry, +) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a media player.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + + trigger_types = {"turned_on", "turned_off", "idle", "paused", "playing"} + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": trigger, + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + } + for trigger in trigger_types + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_if_fires_on_state_change(hass, calls): + """Test triggers firing.""" + hass.states.async_set("media_player.entity", STATE_OFF) + + data_template = ( + "{label} - {{{{ trigger.platform}}}} - " + "{{{{ trigger.entity_id}}}} - {{{{ trigger.from_state.state}}}} - " + "{{{{ trigger.to_state.state}}}} - {{{{ trigger.for }}}}" + ) + trigger_types = {"turned_on", "turned_off", "idle", "paused", "playing"} + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "media_player.entity", + "type": trigger, + }, + "action": { + "service": "test.automation", + "data_template": {"some": data_template.format(label=trigger)}, + }, + } + for trigger in trigger_types + ] + }, + ) + + # Fake that the entity is turning on. + hass.states.async_set("media_player.entity", STATE_ON) + await hass.async_block_till_done() + assert len(calls) == 1 + assert ( + calls[0].data["some"] + == "turned_on - device - media_player.entity - off - on - None" + ) + + # Fake that the entity is turning off. + hass.states.async_set("media_player.entity", STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 2 + assert ( + calls[1].data["some"] + == "turned_off - device - media_player.entity - on - off - None" + ) + + # Fake that the entity becomes idle. + hass.states.async_set("media_player.entity", STATE_IDLE) + await hass.async_block_till_done() + assert len(calls) == 3 + assert ( + calls[2].data["some"] + == "idle - device - media_player.entity - off - idle - None" + ) + + # Fake that the entity starts playing. + hass.states.async_set("media_player.entity", STATE_PLAYING) + await hass.async_block_till_done() + assert len(calls) == 4 + assert ( + calls[3].data["some"] + == "playing - device - media_player.entity - idle - playing - None" + ) + + # Fake that the entity is paused. + hass.states.async_set("media_player.entity", STATE_PAUSED) + await hass.async_block_till_done() + assert len(calls) == 5 + assert ( + calls[4].data["some"] + == "paused - device - media_player.entity - playing - paused - None" + ) From 840891e4f4f92a9660d0f7f708d052c23f627ce5 Mon Sep 17 00:00:00 2001 From: Adam Outler Date: Mon, 8 Feb 2021 03:37:23 -0500 Subject: [PATCH 245/796] Increase skybell scan time to reduce timeouts (#46169) --- homeassistant/components/skybell/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/skybell/binary_sensor.py b/homeassistant/components/skybell/binary_sensor.py index 512731ab355..8949a58fa01 100644 --- a/homeassistant/components/skybell/binary_sensor.py +++ b/homeassistant/components/skybell/binary_sensor.py @@ -14,7 +14,7 @@ import homeassistant.helpers.config_validation as cv from . import DEFAULT_ENTITY_NAMESPACE, DOMAIN as SKYBELL_DOMAIN, SkybellDevice -SCAN_INTERVAL = timedelta(seconds=5) +SCAN_INTERVAL = timedelta(seconds=10) # Sensor types: Name, device_class, event SENSOR_TYPES = { From 352bba1f15b62eb6d2088699911d6f6363279422 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 8 Feb 2021 03:45:05 -0500 Subject: [PATCH 246/796] Use core constants for efergy (#46090) --- homeassistant/components/efergy/sensor.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index 8c16317beda..02bc8fa0ccb 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -5,7 +5,13 @@ import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_CURRENCY, ENERGY_KILO_WATT_HOUR, POWER_WATT +from homeassistant.const import ( + CONF_CURRENCY, + CONF_MONITORED_VARIABLES, + CONF_TYPE, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -14,8 +20,6 @@ _RESOURCE = "https://engage.efergy.com/mobile_proxy/" CONF_APPTOKEN = "app_token" CONF_UTC_OFFSET = "utc_offset" -CONF_MONITORED_VARIABLES = "monitored_variables" -CONF_SENSOR_TYPE = "type" CONF_PERIOD = "period" @@ -40,7 +44,7 @@ TYPES_SCHEMA = vol.In(SENSOR_TYPES) SENSORS_SCHEMA = vol.Schema( { - vol.Required(CONF_SENSOR_TYPE): TYPES_SCHEMA, + vol.Required(CONF_TYPE): TYPES_SCHEMA, vol.Optional(CONF_CURRENCY, default=""): cv.string, vol.Optional(CONF_PERIOD, default=DEFAULT_PERIOD): cv.string, } @@ -62,14 +66,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): dev = [] for variable in config[CONF_MONITORED_VARIABLES]: - if variable[CONF_SENSOR_TYPE] == CONF_CURRENT_VALUES: + if variable[CONF_TYPE] == CONF_CURRENT_VALUES: url_string = f"{_RESOURCE}getCurrentValuesSummary?token={app_token}" response = requests.get(url_string, timeout=10) for sensor in response.json(): sid = sensor["sid"] dev.append( EfergySensor( - variable[CONF_SENSOR_TYPE], + variable[CONF_TYPE], app_token, utc_offset, variable[CONF_PERIOD], @@ -79,7 +83,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) dev.append( EfergySensor( - variable[CONF_SENSOR_TYPE], + variable[CONF_TYPE], app_token, utc_offset, variable[CONF_PERIOD], From 75519d2d6cb3ab530adc6994d3f2c4502d719203 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Feb 2021 09:59:07 +0100 Subject: [PATCH 247/796] Bump actions/cache from v2 to v2.1.4 (#46197) Bumps [actions/cache](https://github.com/actions/cache) from v2 to v2.1.4. - [Release notes](https://github.com/actions/cache/releases) - [Commits](https://github.com/actions/cache/compare/v2...26968a09c0ea4f3e233fdddbafd1166051a095f6) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 54 +++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6356803cbef..28410123914 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -30,7 +30,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: venv key: >- @@ -52,7 +52,7 @@ jobs: pip install -r requirements.txt -r requirements_test.txt - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: ${{ env.PRE_COMMIT_HOME }} key: | @@ -79,7 +79,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: venv key: >- @@ -95,7 +95,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: ${{ env.PRE_COMMIT_HOME }} key: | @@ -124,7 +124,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: venv key: >- @@ -140,7 +140,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: ${{ env.PRE_COMMIT_HOME }} key: | @@ -169,7 +169,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: venv key: >- @@ -185,7 +185,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: ${{ env.PRE_COMMIT_HOME }} key: | @@ -236,7 +236,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: venv key: >- @@ -252,7 +252,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: ${{ env.PRE_COMMIT_HOME }} key: | @@ -284,7 +284,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: venv key: >- @@ -300,7 +300,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: ${{ env.PRE_COMMIT_HOME }} key: | @@ -332,7 +332,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: venv key: >- @@ -348,7 +348,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: ${{ env.PRE_COMMIT_HOME }} key: | @@ -377,7 +377,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: venv key: >- @@ -393,7 +393,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: ${{ env.PRE_COMMIT_HOME }} key: | @@ -425,7 +425,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: venv key: >- @@ -441,7 +441,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: ${{ env.PRE_COMMIT_HOME }} key: | @@ -481,7 +481,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: venv key: >- @@ -497,7 +497,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: ${{ env.PRE_COMMIT_HOME }} key: | @@ -528,7 +528,7 @@ jobs: uses: actions/checkout@v2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: venv key: >- @@ -560,7 +560,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: venv key: >- @@ -591,7 +591,7 @@ jobs: uses: actions/checkout@v2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: venv key: >- @@ -630,7 +630,7 @@ jobs: uses: actions/checkout@v2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: venv key: >- @@ -664,7 +664,7 @@ jobs: uses: actions/checkout@v2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: venv key: >- @@ -700,7 +700,7 @@ jobs: uses: actions/checkout@v2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: venv key: >- @@ -760,7 +760,7 @@ jobs: uses: actions/checkout@v2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: venv key: >- From c7a957192034129337ea0b58a927a2db54398408 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Feb 2021 10:06:38 +0100 Subject: [PATCH 248/796] Bump actions/stale from v3.0.15 to v3.0.16 (#46196) --- .github/workflows/stale.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 6daeccc4aca..fd8ca4eb477 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -16,7 +16,7 @@ jobs: # - No PRs marked as no-stale # - No issues marked as no-stale or help-wanted - name: 90 days stale issues & PRs policy - uses: actions/stale@v3.0.15 + uses: actions/stale@v3.0.16 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 90 @@ -53,7 +53,7 @@ jobs: # - No PRs marked as no-stale or new-integrations # - No issues (-1) - name: 30 days stale PRs policy - uses: actions/stale@v3.0.15 + uses: actions/stale@v3.0.16 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 30 @@ -78,7 +78,7 @@ jobs: # - No Issues marked as no-stale or help-wanted # - No PRs (-1) - name: Needs more information stale issues policy - uses: actions/stale@v3.0.15 + uses: actions/stale@v3.0.16 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-labels: "needs-more-information" From aa005af2665078f4ff26153734abb1c07cf1ac0f Mon Sep 17 00:00:00 2001 From: Aaron Godfrey Date: Mon, 8 Feb 2021 01:39:33 -0800 Subject: [PATCH 249/796] Fix dyson service name in services.yaml (#46176) --- homeassistant/components/dyson/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/dyson/services.yaml b/homeassistant/components/dyson/services.yaml index 73f7bc75874..f96aa9315c1 100644 --- a/homeassistant/components/dyson/services.yaml +++ b/homeassistant/components/dyson/services.yaml @@ -33,7 +33,7 @@ set_angle: description: The angle at which the oscillation should end example: 255 -flow_direction_front: +set_flow_direction_front: description: Set the fan flow direction. fields: entity_id: From 9e07910ab06b909c8410e9c785da3c76bff681fc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Feb 2021 10:45:46 +0100 Subject: [PATCH 250/796] Mark entities as unavailable when they are removed but are still registered (#45528) * Mark entities as unavailable when they are removed but are still registered * Add sync_entity_lifecycle to collection helper * Remove debug print * Lint * Fix tests * Fix tests * Update zha * Update zone * Fix tests * Update hyperion * Update rfxtrx * Fix tests * Pass force_remove=True from integrations Co-authored-by: Erik --- homeassistant/components/acmeda/base.py | 2 +- homeassistant/components/counter/__init__.py | 11 ++-- homeassistant/components/esphome/__init__.py | 3 +- .../components/gdacs/geo_location.py | 2 +- .../geo_json_events/geo_location.py | 2 +- .../geonetnz_quakes/geo_location.py | 2 +- .../homematicip_cloud/generic_entity.py | 2 +- homeassistant/components/hue/helpers.py | 2 +- homeassistant/components/hyperion/light.py | 3 +- homeassistant/components/hyperion/switch.py | 3 +- .../components/ign_sismologia/geo_location.py | 2 +- .../components/input_boolean/__init__.py | 28 +++++----- .../components/input_datetime/__init__.py | 11 ++-- .../components/input_number/__init__.py | 11 ++-- .../components/input_select/__init__.py | 11 ++-- .../components/input_text/__init__.py | 11 ++-- .../components/insteon/insteon_entity.py | 7 ++- homeassistant/components/mqtt/mixins.py | 2 +- .../geo_location.py | 2 +- .../components/owntracks/messages.py | 3 +- homeassistant/components/ozw/entity.py | 2 +- homeassistant/components/person/__init__.py | 23 ++++---- .../components/qld_bushfire/geo_location.py | 2 +- homeassistant/components/rfxtrx/__init__.py | 4 +- .../components/seventeentrack/sensor.py | 2 +- homeassistant/components/timer/__init__.py | 11 ++-- homeassistant/components/tuya/__init__.py | 2 +- .../components/unifi/unifi_entity_base.py | 2 +- .../usgs_earthquakes_feed/geo_location.py | 2 +- homeassistant/components/wled/light.py | 2 +- homeassistant/components/zha/entity.py | 5 +- homeassistant/components/zone/__init__.py | 40 ++++++-------- homeassistant/components/zwave/node_entity.py | 4 +- homeassistant/helpers/collection.py | 36 ++++--------- homeassistant/helpers/entity.py | 28 ++++++++-- homeassistant/helpers/entity_platform.py | 2 +- homeassistant/helpers/entity_registry.py | 54 ++++++++++--------- tests/components/cert_expiry/test_init.py | 12 ++++- tests/components/deconz/test_binary_sensor.py | 6 ++- tests/components/deconz/test_climate.py | 14 ++++- tests/components/deconz/test_cover.py | 14 ++++- tests/components/deconz/test_deconz_event.py | 9 ++++ tests/components/deconz/test_fan.py | 9 +++- tests/components/deconz/test_light.py | 8 +++ tests/components/deconz/test_lock.py | 14 ++++- tests/components/deconz/test_sensor.py | 8 +++ tests/components/deconz/test_switch.py | 16 +++++- tests/components/dynalite/test_light.py | 20 +++++-- tests/components/eafm/test_sensor.py | 9 ++-- .../forked_daapd/test_media_player.py | 6 +-- tests/components/fritzbox/test_init.py | 20 +++++-- tests/components/heos/test_media_player.py | 4 +- .../homekit_controller/test_light.py | 15 ++++-- tests/components/huisbaasje/test_init.py | 10 +++- tests/components/input_boolean/test_init.py | 2 +- tests/components/met/test_weather.py | 2 + tests/components/nest/camera_sdm_test.py | 1 + tests/components/nws/test_init.py | 10 +++- tests/components/ozw/test_init.py | 12 ++++- tests/components/panasonic_viera/test_init.py | 8 +-- tests/components/plex/test_media_players.py | 2 +- .../smartthings/test_binary_sensor.py | 7 ++- tests/components/smartthings/test_cover.py | 4 +- tests/components/smartthings/test_fan.py | 8 ++- tests/components/smartthings/test_light.py | 8 ++- tests/components/smartthings/test_lock.py | 3 +- tests/components/smartthings/test_scene.py | 4 +- tests/components/smartthings/test_sensor.py | 3 +- tests/components/smartthings/test_switch.py | 3 +- tests/components/vizio/test_init.py | 11 +++- tests/components/yeelight/test_init.py | 8 ++- tests/helpers/test_collection.py | 2 +- tests/helpers/test_entity.py | 28 +++++++++- 73 files changed, 439 insertions(+), 222 deletions(-) diff --git a/homeassistant/components/acmeda/base.py b/homeassistant/components/acmeda/base.py index b325e2c944a..15f9716db47 100644 --- a/homeassistant/components/acmeda/base.py +++ b/homeassistant/components/acmeda/base.py @@ -32,7 +32,7 @@ class AcmedaBase(entity.Entity): device.id, remove_config_entry_id=self.registry_entry.config_entry_id ) - await self.async_remove() + await self.async_remove(force_remove=True) async def async_added_to_hass(self): """Entity has been added to hass.""" diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index ad5e4000116..d23c90bcb93 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -108,8 +108,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: yaml_collection = collection.YamlCollection( logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) - collection.attach_entity_component_collection( - component, yaml_collection, Counter.from_yaml + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, yaml_collection, Counter.from_yaml ) storage_collection = CounterStorageCollection( @@ -117,8 +117,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.storage_collection"), id_manager, ) - collection.attach_entity_component_collection( - component, storage_collection, Counter + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, storage_collection, Counter ) await yaml_collection.async_load( @@ -130,9 +130,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) - component.async_register_entity_service(SERVICE_INCREMENT, {}, "async_increment") component.async_register_entity_service(SERVICE_DECREMENT, {}, "async_decrement") component.async_register_entity_service(SERVICE_RESET, {}, "async_reset") diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index c0c3d02ec56..0b3a7522845 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -1,5 +1,6 @@ """Support for esphome devices.""" import asyncio +import functools import logging import math from typing import Any, Callable, Dict, List, Optional @@ -520,7 +521,7 @@ class EsphomeBaseEntity(Entity): f"esphome_{self._entry_id}_remove_" f"{self._component_key}_{self._key}" ), - self.async_remove, + functools.partial(self.async_remove, force_remove=True), ) ) diff --git a/homeassistant/components/gdacs/geo_location.py b/homeassistant/components/gdacs/geo_location.py index c45d6e56425..890c9f8e050 100644 --- a/homeassistant/components/gdacs/geo_location.py +++ b/homeassistant/components/gdacs/geo_location.py @@ -116,7 +116,7 @@ class GdacsEvent(GeolocationEvent): @callback def _delete_callback(self): """Remove this entity.""" - self.hass.async_create_task(self.async_remove()) + self.hass.async_create_task(self.async_remove(force_remove=True)) @callback def _update_callback(self): diff --git a/homeassistant/components/geo_json_events/geo_location.py b/homeassistant/components/geo_json_events/geo_location.py index bb2d86539e9..40386648138 100644 --- a/homeassistant/components/geo_json_events/geo_location.py +++ b/homeassistant/components/geo_json_events/geo_location.py @@ -144,7 +144,7 @@ class GeoJsonLocationEvent(GeolocationEvent): """Remove this entity.""" self._remove_signal_delete() self._remove_signal_update() - self.hass.async_create_task(self.async_remove()) + self.hass.async_create_task(self.async_remove(force_remove=True)) @callback def _update_callback(self): diff --git a/homeassistant/components/geonetnz_quakes/geo_location.py b/homeassistant/components/geonetnz_quakes/geo_location.py index ed0b9f9f714..718b4c06b9c 100644 --- a/homeassistant/components/geonetnz_quakes/geo_location.py +++ b/homeassistant/components/geonetnz_quakes/geo_location.py @@ -102,7 +102,7 @@ class GeonetnzQuakesEvent(GeolocationEvent): @callback def _delete_callback(self): """Remove this entity.""" - self.hass.async_create_task(self.async_remove()) + self.hass.async_create_task(self.async_remove(force_remove=True)) @callback def _update_callback(self): diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index a8df0107eeb..65e5ade7d1d 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -172,7 +172,7 @@ class HomematicipGenericEntity(Entity): """Handle hmip device removal.""" # Set marker showing that the HmIP device hase been removed. self.hmip_device_removed = True - self.hass.async_create_task(self.async_remove()) + self.hass.async_create_task(self.async_remove(force_remove=True)) @property def name(self) -> str: diff --git a/homeassistant/components/hue/helpers.py b/homeassistant/components/hue/helpers.py index 1760c59a69d..739e27d3360 100644 --- a/homeassistant/components/hue/helpers.py +++ b/homeassistant/components/hue/helpers.py @@ -17,7 +17,7 @@ async def remove_devices(bridge, api_ids, current): # Device is removed from Hue, so we remove it from Home Assistant entity = current[item_id] removed_items.append(item_id) - await entity.async_remove() + await entity.async_remove(force_remove=True) ent_registry = await get_ent_reg(bridge.hass) if entity.entity_id in ent_registry.entities: ent_registry.async_remove(entity.entity_id) diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index ce672194b9a..7bb8a75dfc7 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -1,6 +1,7 @@ """Support for Hyperion-NG remotes.""" from __future__ import annotations +import functools import logging from types import MappingProxyType from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple @@ -401,7 +402,7 @@ class HyperionBaseLight(LightEntity): async_dispatcher_connect( self.hass, SIGNAL_ENTITY_REMOVE.format(self._unique_id), - self.async_remove, + functools.partial(self.async_remove, force_remove=True), ) ) diff --git a/homeassistant/components/hyperion/switch.py b/homeassistant/components/hyperion/switch.py index 372e9876c35..9d90e1e12ef 100644 --- a/homeassistant/components/hyperion/switch.py +++ b/homeassistant/components/hyperion/switch.py @@ -1,5 +1,6 @@ """Switch platform for Hyperion.""" +import functools from typing import Any, Callable, Dict, Optional from hyperion import client @@ -199,7 +200,7 @@ class HyperionComponentSwitch(SwitchEntity): async_dispatcher_connect( self.hass, SIGNAL_ENTITY_REMOVE.format(self._unique_id), - self.async_remove, + functools.partial(self.async_remove, force_remove=True), ) ) diff --git a/homeassistant/components/ign_sismologia/geo_location.py b/homeassistant/components/ign_sismologia/geo_location.py index cc06110c111..0db580701d0 100644 --- a/homeassistant/components/ign_sismologia/geo_location.py +++ b/homeassistant/components/ign_sismologia/geo_location.py @@ -165,7 +165,7 @@ class IgnSismologiaLocationEvent(GeolocationEvent): """Remove this entity.""" self._remove_signal_delete() self._remove_signal_update() - self.hass.async_create_task(self.async_remove()) + self.hass.async_create_task(self.async_remove(force_remove=True)) @callback def _update_callback(self): diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index f123d6d3297..1b996722c01 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -89,8 +89,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: yaml_collection = collection.YamlCollection( logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) - collection.attach_entity_component_collection( - component, yaml_collection, lambda conf: InputBoolean(conf, from_yaml=True) + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, yaml_collection, InputBoolean.from_yaml ) storage_collection = InputBooleanStorageCollection( @@ -98,8 +98,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.storage_collection"), id_manager, ) - collection.attach_entity_component_collection( - component, storage_collection, InputBoolean + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, storage_collection, InputBoolean ) await yaml_collection.async_load( @@ -111,9 +111,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) - async def reload_service_handler(service_call: ServiceCallType) -> None: """Remove all input booleans and load new ones from config.""" conf = await component.async_prepare_reload(skip_reset=True) @@ -146,14 +143,19 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: class InputBoolean(ToggleEntity, RestoreEntity): """Representation of a boolean input.""" - def __init__(self, config: typing.Optional[dict], from_yaml: bool = False): + def __init__(self, config: typing.Optional[dict]): """Initialize a boolean input.""" self._config = config - self._editable = True + self.editable = True self._state = config.get(CONF_INITIAL) - if from_yaml: - self._editable = False - self.entity_id = f"{DOMAIN}.{self.unique_id}" + + @classmethod + def from_yaml(cls, config: typing.Dict) -> "InputBoolean": + """Return entity instance initialized from yaml storage.""" + input_bool = cls(config) + input_bool.entity_id = f"{DOMAIN}.{config[CONF_ID]}" + input_bool.editable = False + return input_bool @property def should_poll(self): @@ -168,7 +170,7 @@ class InputBoolean(ToggleEntity, RestoreEntity): @property def state_attributes(self): """Return the state attributes of the entity.""" - return {ATTR_EDITABLE: self._editable} + return {ATTR_EDITABLE: self.editable} @property def icon(self): diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 0eab810245d..9589fe9a7ea 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -108,8 +108,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: yaml_collection = collection.YamlCollection( logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) - collection.attach_entity_component_collection( - component, yaml_collection, InputDatetime.from_yaml + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, yaml_collection, InputDatetime.from_yaml ) storage_collection = DateTimeStorageCollection( @@ -117,8 +117,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.storage_collection"), id_manager, ) - collection.attach_entity_component_collection( - component, storage_collection, InputDatetime + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, storage_collection, InputDatetime ) await yaml_collection.async_load( @@ -130,9 +130,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) - async def reload_service_handler(service_call: ServiceCallType) -> None: """Reload yaml entities.""" conf = await component.async_prepare_reload(skip_reset=True) diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index 1f979cad7a9..5cad0f49c88 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -119,8 +119,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: yaml_collection = collection.YamlCollection( logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) - collection.attach_entity_component_collection( - component, yaml_collection, InputNumber.from_yaml + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, yaml_collection, InputNumber.from_yaml ) storage_collection = NumberStorageCollection( @@ -128,8 +128,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.storage_collection"), id_manager, ) - collection.attach_entity_component_collection( - component, storage_collection, InputNumber + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, storage_collection, InputNumber ) await yaml_collection.async_load( @@ -141,9 +141,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) - async def reload_service_handler(service_call: ServiceCallType) -> None: """Reload yaml entities.""" conf = await component.async_prepare_reload(skip_reset=True) diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 6272992f243..a390d8e1901 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -94,8 +94,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: yaml_collection = collection.YamlCollection( logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) - collection.attach_entity_component_collection( - component, yaml_collection, InputSelect.from_yaml + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, yaml_collection, InputSelect.from_yaml ) storage_collection = InputSelectStorageCollection( @@ -103,8 +103,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.storage_collection"), id_manager, ) - collection.attach_entity_component_collection( - component, storage_collection, InputSelect + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, storage_collection, InputSelect ) await yaml_collection.async_load( @@ -116,9 +116,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) - async def reload_service_handler(service_call: ServiceCallType) -> None: """Reload yaml entities.""" conf = await component.async_prepare_reload(skip_reset=True) diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index c512bc221db..76eb51eedd5 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -119,8 +119,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: yaml_collection = collection.YamlCollection( logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) - collection.attach_entity_component_collection( - component, yaml_collection, InputText.from_yaml + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, yaml_collection, InputText.from_yaml ) storage_collection = InputTextStorageCollection( @@ -128,8 +128,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.storage_collection"), id_manager, ) - collection.attach_entity_component_collection( - component, storage_collection, InputText + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, storage_collection, InputText ) await yaml_collection.async_load( @@ -141,9 +141,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) - async def reload_service_handler(service_call: ServiceCallType) -> None: """Reload yaml entities.""" conf = await component.async_prepare_reload(skip_reset=True) diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index e2b9dd39f34..2234eb4750c 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -1,4 +1,5 @@ """Insteon base entity.""" +import functools import logging from pyinsteon import devices @@ -122,7 +123,11 @@ class InsteonEntity(Entity): ) remove_signal = f"{self._insteon_device.address.id}_{SIGNAL_REMOVE_ENTITY}" self.async_on_remove( - async_dispatcher_connect(self.hass, remove_signal, self.async_remove) + async_dispatcher_connect( + self.hass, + remove_signal, + functools.partial(self.async_remove, force_remove=True), + ) ) async def async_will_remove_from_hass(self): diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 1ab2054b355..8d9c9533ed3 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -387,7 +387,7 @@ class MqttDiscoveryUpdate(Entity): entity_registry.async_remove(self.entity_id) await cleanup_device_registry(self.hass, entity_entry.device_id) else: - await self.async_remove() + await self.async_remove(force_remove=True) async def discovery_callback(payload): """Handle discovery update.""" diff --git a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py index c8eda3690ef..12ae9d8990a 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py +++ b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py @@ -210,7 +210,7 @@ class NswRuralFireServiceLocationEvent(GeolocationEvent): @callback def _delete_callback(self): """Remove this entity.""" - self.hass.async_create_task(self.async_remove()) + self.hass.async_create_task(self.async_remove(force_remove=True)) @callback def _update_callback(self): diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index 3a4aac6bfd1..bd01284329b 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -304,7 +304,7 @@ async def async_handle_waypoint(hass, name_base, waypoint): if hass.states.get(entity_id) is not None: return - zone = zone_comp.Zone( + zone = zone_comp.Zone.from_yaml( { zone_comp.CONF_NAME: pretty_name, zone_comp.CONF_LATITUDE: lat, @@ -313,7 +313,6 @@ async def async_handle_waypoint(hass, name_base, waypoint): zone_comp.CONF_ICON: zone_comp.ICON_IMPORT, zone_comp.CONF_PASSIVE: False, }, - False, ) zone.hass = hass zone.entity_id = entity_id diff --git a/homeassistant/components/ozw/entity.py b/homeassistant/components/ozw/entity.py index 9c494a514e0..c1cb9617a5c 100644 --- a/homeassistant/components/ozw/entity.py +++ b/homeassistant/components/ozw/entity.py @@ -268,7 +268,7 @@ class ZWaveDeviceEntity(Entity): if not self.values: return # race condition: delete already requested if values_id == self.values.values_id: - await self.async_remove() + await self.async_remove(force_remove=True) def create_device_name(node: OZWNode): diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index d0c0e9eccc8..d3e17d904ea 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -306,14 +306,12 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): yaml_collection, ) - collection.attach_entity_component_collection( - entity_component, yaml_collection, lambda conf: Person(conf, False) + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, entity_component, yaml_collection, Person ) - collection.attach_entity_component_collection( - entity_component, storage_collection, lambda conf: Person(conf, True) + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, entity_component, storage_collection, Person.from_yaml ) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) await yaml_collection.async_load( await filter_yaml_data(hass, config.get(DOMAIN, [])) @@ -358,10 +356,10 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): class Person(RestoreEntity): """Represent a tracked person.""" - def __init__(self, config, editable): + def __init__(self, config): """Set up person.""" self._config = config - self._editable = editable + self.editable = True self._latitude = None self._longitude = None self._gps_accuracy = None @@ -369,6 +367,13 @@ class Person(RestoreEntity): self._state = None self._unsub_track_device = None + @classmethod + def from_yaml(cls, config): + """Return entity instance initialized from yaml storage.""" + person = cls(config) + person.editable = False + return person + @property def name(self): """Return the name of the entity.""" @@ -395,7 +400,7 @@ class Person(RestoreEntity): @property def state_attributes(self): """Return the state attributes of the person.""" - data = {ATTR_EDITABLE: self._editable, ATTR_ID: self.unique_id} + data = {ATTR_EDITABLE: self.editable, ATTR_ID: self.unique_id} if self._latitude is not None: data[ATTR_LATITUDE] = self._latitude if self._longitude is not None: diff --git a/homeassistant/components/qld_bushfire/geo_location.py b/homeassistant/components/qld_bushfire/geo_location.py index 8efb1a32705..f608f6e12ae 100644 --- a/homeassistant/components/qld_bushfire/geo_location.py +++ b/homeassistant/components/qld_bushfire/geo_location.py @@ -167,7 +167,7 @@ class QldBushfireLocationEvent(GeolocationEvent): """Remove this entity.""" self._remove_signal_delete() self._remove_signal_update() - self.hass.async_create_task(self.async_remove()) + self.hass.async_create_task(self.async_remove(force_remove=True)) @callback def _update_callback(self): diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 067ffeb5313..5952cb62a71 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -3,6 +3,7 @@ import asyncio import binascii from collections import OrderedDict import copy +import functools import logging import RFXtrx as rfxtrxmod @@ -488,7 +489,8 @@ class RfxtrxEntity(RestoreEntity): self.async_on_remove( self.hass.helpers.dispatcher.async_dispatcher_connect( - f"{DOMAIN}_{CONF_REMOVE_DEVICE}_{self._device_id}", self.async_remove + f"{DOMAIN}_{CONF_REMOVE_DEVICE}_{self._device_id}", + functools.partial(self.async_remove, force_remove=True), ) ) diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index 94efe9b98c7..fa94ca4e384 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -244,7 +244,7 @@ class SeventeenTrackPackageSensor(Entity): async def _remove(self, *_): """Remove entity itself.""" - await self.async_remove() + await self.async_remove(force_remove=True) reg = await self.hass.helpers.entity_registry.async_get_registry() entity_id = reg.async_get_entity_id( diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 64d651b4cd8..b123bbadf7d 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -107,8 +107,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: yaml_collection = collection.YamlCollection( logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) - collection.attach_entity_component_collection( - component, yaml_collection, Timer.from_yaml + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, yaml_collection, Timer.from_yaml ) storage_collection = TimerStorageCollection( @@ -116,7 +116,9 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.storage_collection"), id_manager, ) - collection.attach_entity_component_collection(component, storage_collection, Timer) + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, storage_collection, Timer + ) await yaml_collection.async_load( [{CONF_ID: id_, **cfg} for id_, cfg in config.get(DOMAIN, {}).items()] @@ -127,9 +129,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) - async def reload_service_handler(service_call: ServiceCallType) -> None: """Reload yaml entities.""" conf = await component.async_prepare_reload(skip_reset=True) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 5876331ea97..7f6ba6b26fd 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -392,7 +392,7 @@ class TuyaDevice(Entity): entity_registry.async_remove(self.entity_id) await cleanup_device_registry(self.hass, entity_entry.device_id) else: - await self.async_remove() + await self.async_remove(force_remove=True) @callback def _update_callback(self): diff --git a/homeassistant/components/unifi/unifi_entity_base.py b/homeassistant/components/unifi/unifi_entity_base.py index 904348f6324..03c63ce4e84 100644 --- a/homeassistant/components/unifi/unifi_entity_base.py +++ b/homeassistant/components/unifi/unifi_entity_base.py @@ -91,7 +91,7 @@ class UniFiBase(Entity): entity_registry = await self.hass.helpers.entity_registry.async_get_registry() entity_entry = entity_registry.async_get(self.entity_id) if not entity_entry: - await self.async_remove() + await self.async_remove(force_remove=True) return device_registry = await self.hass.helpers.device_registry.async_get_registry() diff --git a/homeassistant/components/usgs_earthquakes_feed/geo_location.py b/homeassistant/components/usgs_earthquakes_feed/geo_location.py index 40a544a2e21..2b149fcac26 100644 --- a/homeassistant/components/usgs_earthquakes_feed/geo_location.py +++ b/homeassistant/components/usgs_earthquakes_feed/geo_location.py @@ -210,7 +210,7 @@ class UsgsEarthquakesEvent(GeolocationEvent): """Remove this entity.""" self._remove_signal_delete() self._remove_signal_update() - self.hass.async_create_task(self.async_remove()) + self.hass.async_create_task(self.async_remove(force_remove=True)) @callback def _update_callback(self): diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 527d985a47b..f89cf06a44c 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -442,7 +442,7 @@ async def async_remove_entity( ) -> None: """Remove WLED segment light from Home Assistant.""" entity = current[index] - await entity.async_remove() + await entity.async_remove(force_remove=True) registry = await async_get_entity_registry(coordinator.hass) if entity.entity_id in registry.entities: registry.async_remove(entity.entity_id) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 96f005ba288..db30e9e178c 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -1,6 +1,7 @@ """Entity for Zigbee Home Automation.""" import asyncio +import functools import logging from typing import Any, Awaitable, Dict, List, Optional @@ -165,7 +166,7 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): self.async_accept_signal( None, f"{SIGNAL_REMOVE}_{self.zha_device.ieee}", - self.async_remove, + functools.partial(self.async_remove, force_remove=True), signal_override=True, ) @@ -239,7 +240,7 @@ class ZhaGroupEntity(BaseZhaEntity): return self._handled_group_membership = True - await self.async_remove() + await self.async_remove(force_remove=True) async def async_added_to_hass(self) -> None: """Register callbacks.""" diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 01a8b9aa0f4..1eef9636e36 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -25,7 +25,6 @@ from homeassistant.helpers import ( config_validation as cv, entity, entity_component, - entity_registry, service, storage, ) @@ -183,8 +182,8 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool: yaml_collection = collection.IDLessCollection( logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) - collection.attach_entity_component_collection( - component, yaml_collection, lambda conf: Zone(conf, False) + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, yaml_collection, Zone.from_yaml ) storage_collection = ZoneStorageCollection( @@ -192,8 +191,8 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool: logging.getLogger(f"{__name__}.storage_collection"), id_manager, ) - collection.attach_entity_component_collection( - component, storage_collection, lambda conf: Zone(conf, True) + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, storage_collection, Zone ) if config[DOMAIN]: @@ -205,18 +204,6 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool: storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) - async def _collection_changed(change_type: str, item_id: str, config: Dict) -> None: - """Handle a collection change: clean up entity registry on removals.""" - if change_type != collection.CHANGE_REMOVED: - return - - ent_reg = await entity_registry.async_get_registry(hass) - ent_reg.async_remove( - cast(str, ent_reg.async_get_entity_id(DOMAIN, DOMAIN, item_id)) - ) - - storage_collection.async_add_listener(_collection_changed) - async def reload_service_handler(service_call: ServiceCall) -> None: """Remove all zones and load new ones from config.""" conf = await component.async_prepare_reload(skip_reset=True) @@ -235,10 +222,7 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool: if component.get_entity("zone.home"): return True - home_zone = Zone( - _home_conf(hass), - True, - ) + home_zone = Zone(_home_conf(hass)) home_zone.entity_id = ENTITY_ID_HOME await component.async_add_entities([home_zone]) @@ -293,13 +277,21 @@ async def async_unload_entry( class Zone(entity.Entity): """Representation of a Zone.""" - def __init__(self, config: Dict, editable: bool): + def __init__(self, config: Dict): """Initialize the zone.""" self._config = config - self._editable = editable + self.editable = True self._attrs: Optional[Dict] = None self._generate_attrs() + @classmethod + def from_yaml(cls, config: Dict) -> "Zone": + """Return entity instance initialized from yaml storage.""" + zone = cls(config) + zone.editable = False + zone._generate_attrs() # pylint:disable=protected-access + return zone + @property def state(self) -> str: """Return the state property really does nothing for a zone.""" @@ -346,5 +338,5 @@ class Zone(entity.Entity): ATTR_LONGITUDE: self._config[CONF_LONGITUDE], ATTR_RADIUS: self._config[CONF_RADIUS], ATTR_PASSIVE: self._config[CONF_PASSIVE], - ATTR_EDITABLE: self._editable, + ATTR_EDITABLE: self.editable, } diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index 56dea1639a3..faaea30e0ee 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -95,7 +95,7 @@ class ZWaveBaseEntity(Entity): """Remove this entity and add it back.""" async def _async_remove_and_add(): - await self.async_remove() + await self.async_remove(force_remove=True) self.entity_id = None await self.platform.async_add_entities([self]) @@ -104,7 +104,7 @@ class ZWaveBaseEntity(Entity): async def node_removed(self): """Call when a node is removed from the Z-Wave network.""" - await self.async_remove() + await self.async_remove(force_remove=True) registry = await async_get_registry(self.hass) if self.entity_id not in registry.entities: diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 6733b1d3dbd..4af524bbbc9 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -301,7 +301,10 @@ class IDLessCollection(ObservableCollection): @callback -def attach_entity_component_collection( +def sync_entity_lifecycle( + hass: HomeAssistantType, + domain: str, + platform: str, entity_component: EntityComponent, collection: ObservableCollection, create_entity: Callable[[dict], Entity], @@ -318,8 +321,13 @@ def attach_entity_component_collection( return if change_type == CHANGE_REMOVED: - entity = entities.pop(item_id) - await entity.async_remove() + ent_reg = await entity_registry.async_get_registry(hass) + ent_to_remove = ent_reg.async_get_entity_id(domain, platform, item_id) + if ent_to_remove is not None: + ent_reg.async_remove(ent_to_remove) + else: + await entities[item_id].async_remove(force_remove=True) + entities.pop(item_id) return # CHANGE_UPDATED @@ -328,28 +336,6 @@ def attach_entity_component_collection( collection.async_add_listener(_collection_changed) -@callback -def attach_entity_registry_cleaner( - hass: HomeAssistantType, - domain: str, - platform: str, - collection: ObservableCollection, -) -> None: - """Attach a listener to clean up entity registry on collection changes.""" - - async def _collection_changed(change_type: str, item_id: str, config: Dict) -> None: - """Handle a collection change: clean up entity registry on removals.""" - if change_type != CHANGE_REMOVED: - return - - ent_reg = await entity_registry.async_get_registry(hass) - ent_to_remove = ent_reg.async_get_entity_id(domain, platform, item_id) - if ent_to_remove is not None: - ent_reg.async_remove(ent_to_remove) - - collection.async_add_listener(_collection_changed) - - class StorageCollectionWebsocket: """Class to expose storage collection management over websocket.""" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 03342a9f235..04c07ef0f36 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -530,8 +530,16 @@ class Entity(ABC): await self.async_added_to_hass() self.async_write_ha_state() - async def async_remove(self) -> None: - """Remove entity from Home Assistant.""" + async def async_remove(self, *, force_remove: bool = False) -> None: + """Remove entity from Home Assistant. + + If the entity has a non disabled entry in the entity registry, + the entity's state will be set to unavailable, in the same way + as when the entity registry is loaded. + + If the entity doesn't have a non disabled entry in the entity registry, + or if force_remove=True, its state will be removed. + """ assert self.hass is not None if self.platform and not self._added: @@ -548,7 +556,16 @@ class Entity(ABC): await self.async_internal_will_remove_from_hass() await self.async_will_remove_from_hass() - self.hass.states.async_remove(self.entity_id, context=self._context) + # Check if entry still exists in entity registry (e.g. unloading config entry) + if ( + not force_remove + and self.registry_entry + and not self.registry_entry.disabled + ): + # Set the entity's state will to unavailable + ATTR_RESTORED: True + self.registry_entry.write_unavailable_state(self.hass) + else: + self.hass.states.async_remove(self.entity_id, context=self._context) async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass. @@ -606,6 +623,7 @@ class Entity(ABC): data = event.data if data["action"] == "remove": await self.async_removed_from_registry() + self.registry_entry = None await self.async_remove() if data["action"] != "update": @@ -617,7 +635,7 @@ class Entity(ABC): self.registry_entry = ent_reg.async_get(data["entity_id"]) assert self.registry_entry is not None - if self.registry_entry.disabled_by is not None: + if self.registry_entry.disabled: await self.async_remove() return @@ -626,7 +644,7 @@ class Entity(ABC): self.async_write_ha_state() return - await self.async_remove() + await self.async_remove(force_remove=True) assert self.platform is not None self.entity_id = self.registry_entry.entity_id diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index bd687ab7ce8..26fec28c047 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -517,7 +517,7 @@ class EntityPlatform: if not self.entities: return - tasks = [self.async_remove_entity(entity_id) for entity_id in self.entities] + tasks = [entity.async_remove() for entity in self.entities.values()] await asyncio.gather(*tasks) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 0628c1e0eb5..15218afc227 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -115,6 +115,33 @@ class RegistryEntry: """Return if entry is disabled.""" return self.disabled_by is not None + @callback + def write_unavailable_state(self, hass: HomeAssistantType) -> None: + """Write the unavailable state to the state machine.""" + attrs: Dict[str, Any] = {ATTR_RESTORED: True} + + if self.capabilities is not None: + attrs.update(self.capabilities) + + if self.supported_features is not None: + attrs[ATTR_SUPPORTED_FEATURES] = self.supported_features + + if self.device_class is not None: + attrs[ATTR_DEVICE_CLASS] = self.device_class + + if self.unit_of_measurement is not None: + attrs[ATTR_UNIT_OF_MEASUREMENT] = self.unit_of_measurement + + name = self.name or self.original_name + if name is not None: + attrs[ATTR_FRIENDLY_NAME] = name + + icon = self.icon or self.original_icon + if icon is not None: + attrs[ATTR_ICON] = icon + + hass.states.async_set(self.entity_id, STATE_UNAVAILABLE, attrs) + class EntityRegistry: """Class to hold a registry of entities.""" @@ -616,36 +643,13 @@ def async_setup_entity_restore( @callback def _write_unavailable_states(_: Event) -> None: """Make sure state machine contains entry for each registered entity.""" - states = hass.states - existing = set(states.async_entity_ids()) + existing = set(hass.states.async_entity_ids()) for entry in registry.entities.values(): if entry.entity_id in existing or entry.disabled: continue - attrs: Dict[str, Any] = {ATTR_RESTORED: True} - - if entry.capabilities is not None: - attrs.update(entry.capabilities) - - if entry.supported_features is not None: - attrs[ATTR_SUPPORTED_FEATURES] = entry.supported_features - - if entry.device_class is not None: - attrs[ATTR_DEVICE_CLASS] = entry.device_class - - if entry.unit_of_measurement is not None: - attrs[ATTR_UNIT_OF_MEASUREMENT] = entry.unit_of_measurement - - name = entry.name or entry.original_name - if name is not None: - attrs[ATTR_FRIENDLY_NAME] = name - - icon = entry.icon or entry.original_icon - if icon is not None: - attrs[ATTR_ICON] = icon - - states.async_set(entry.entity_id, STATE_UNAVAILABLE, attrs) + entry.write_unavailable_state(hass) hass.bus.async_listen(EVENT_HOMEASSISTANT_START, _write_unavailable_states) diff --git a/tests/components/cert_expiry/test_init.py b/tests/components/cert_expiry/test_init.py index ea31ba50ea0..1c62782107b 100644 --- a/tests/components/cert_expiry/test_init.py +++ b/tests/components/cert_expiry/test_init.py @@ -5,7 +5,12 @@ from unittest.mock import patch from homeassistant.components.cert_expiry.const import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED -from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_START +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + EVENT_HOMEASSISTANT_START, + STATE_UNAVAILABLE, +) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -94,4 +99,9 @@ async def test_unload_config_entry(mock_now, hass): assert entry.state == ENTRY_STATE_NOT_LOADED state = hass.states.get("sensor.cert_expiry_timestamp_example_com") + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() + state = hass.states.get("sensor.cert_expiry_timestamp_example_com") assert state is None diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 3611e30f665..f64a4c4c259 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -16,7 +16,7 @@ from homeassistant.components.deconz.const import ( ) 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.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.helpers.entity_registry import async_entries_for_config_entry from homeassistant.setup import async_setup_component @@ -111,6 +111,10 @@ async def test_binary_sensors(hass): await hass.config_entries.async_unload(config_entry.entry_id) + assert hass.states.get("binary_sensor.presence_sensor").state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 4d68ba2a6a7..fcb6e16f07f 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -39,7 +39,12 @@ from homeassistant.components.deconz.const import ( DOMAIN as DECONZ_DOMAIN, ) from homeassistant.components.deconz.gateway import get_gateway_from_config_entry -from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_OFF +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_TEMPERATURE, + STATE_OFF, + STATE_UNAVAILABLE, +) from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration @@ -361,6 +366,13 @@ async def test_climate_device_without_cooling_support(hass): await hass.config_entries.async_unload(config_entry.entry_id) + states = hass.states.async_all() + assert len(hass.states.async_all()) == 2 + for state in states: + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 5314a41b315..43364208f4f 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -19,7 +19,12 @@ from homeassistant.components.cover import ( ) from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.gateway import get_gateway_from_config_entry -from homeassistant.const import ATTR_ENTITY_ID, STATE_CLOSED, STATE_OPEN +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_CLOSED, + STATE_OPEN, + STATE_UNAVAILABLE, +) from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration @@ -251,6 +256,13 @@ async def test_cover(hass): await hass.config_entries.async_unload(config_entry.entry_id) + states = hass.states.async_all() + assert len(hass.states.async_all()) == 5 + for state in states: + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index 14faf1a938c..232de5eacd2 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -4,6 +4,7 @@ from copy import deepcopy from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT from homeassistant.components.deconz.gateway import get_gateway_from_config_entry +from homeassistant.const import STATE_UNAVAILABLE from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration @@ -121,5 +122,13 @@ async def test_deconz_events(hass): await hass.config_entries.async_unload(config_entry.entry_id) + states = hass.states.async_all() + assert len(hass.states.async_all()) == 3 + for state in states: + assert state.state == STATE_UNAVAILABLE + assert len(gateway.events) == 0 + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 assert len(gateway.events) == 0 diff --git a/tests/components/deconz/test_fan.py b/tests/components/deconz/test_fan.py index b9c154a2791..7f225196744 100644 --- a/tests/components/deconz/test_fan.py +++ b/tests/components/deconz/test_fan.py @@ -18,7 +18,7 @@ from homeassistant.components.fan import ( SPEED_MEDIUM, SPEED_OFF, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration @@ -207,4 +207,11 @@ async def test_fans(hass): await hass.config_entries.async_unload(config_entry.entry_id) + states = hass.states.async_all() + assert len(hass.states.async_all()) == 2 + for state in states: + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 20fb50247ee..bdb7fbb8aef 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -31,6 +31,7 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, ) from homeassistant.setup import async_setup_component @@ -296,6 +297,13 @@ async def test_lights_and_groups(hass): await hass.config_entries.async_unload(config_entry.entry_id) + states = hass.states.async_all() + assert len(hass.states.async_all()) == 6 + for state in states: + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_lock.py b/tests/components/deconz/test_lock.py index 7e9b8233778..d53da74dfdd 100644 --- a/tests/components/deconz/test_lock.py +++ b/tests/components/deconz/test_lock.py @@ -10,7 +10,12 @@ from homeassistant.components.lock import ( SERVICE_LOCK, SERVICE_UNLOCK, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_LOCKED, + STATE_UNAVAILABLE, + STATE_UNLOCKED, +) from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration @@ -104,4 +109,11 @@ async def test_locks(hass): await hass.config_entries.async_unload(config_entry.entry_id) + states = hass.states.async_all() + assert len(hass.states.async_all()) == 1 + for state in states: + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index def2a1412e5..426a88b8bb6 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, + STATE_UNAVAILABLE, ) from homeassistant.setup import async_setup_component @@ -165,6 +166,13 @@ async def test_sensors(hass): await hass.config_entries.async_unload(config_entry.entry_id) + states = hass.states.async_all() + assert len(hass.states.async_all()) == 5 + for state in states: + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index e42e89d903e..22ce182cb62 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -10,7 +10,7 @@ from homeassistant.components.switch import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration @@ -139,6 +139,13 @@ async def test_power_plugs(hass): await hass.config_entries.async_unload(config_entry.entry_id) + states = hass.states.async_all() + assert len(hass.states.async_all()) == 4 + for state in states: + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 @@ -202,4 +209,11 @@ async def test_sirens(hass): await hass.config_entries.async_unload(config_entry.entry_id) + states = hass.states.async_all() + assert len(hass.states.async_all()) == 2 + for state in states: + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/dynalite/test_light.py b/tests/components/dynalite/test_light.py index 7df10fb08e8..230e7584d70 100644 --- a/tests/components/dynalite/test_light.py +++ b/tests/components/dynalite/test_light.py @@ -4,7 +4,11 @@ from dynalite_devices_lib.light import DynaliteChannelLightDevice import pytest from homeassistant.components.light import SUPPORT_BRIGHTNESS -from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + ATTR_SUPPORTED_FEATURES, + STATE_UNAVAILABLE, +) from .common import ( ATTR_METHOD, @@ -40,11 +44,21 @@ async def test_light_setup(hass, mock_device): ) -async def test_remove_entity(hass, mock_device): - """Test when an entity is removed from HA.""" +async def test_unload_config_entry(hass, mock_device): + """Test when a config entry is unloaded from HA.""" await create_entity_from_device(hass, mock_device) assert hass.states.get("light.name") entry_id = await get_entry_id_from_hass(hass) assert await hass.config_entries.async_unload(entry_id) await hass.async_block_till_done() + assert hass.states.get("light.name").state == STATE_UNAVAILABLE + + +async def test_remove_config_entry(hass, mock_device): + """Test when a config entry is removed from HA.""" + await create_entity_from_device(hass, mock_device) + assert hass.states.get("light.name") + entry_id = await get_entry_id_from_hass(hass) + assert await hass.config_entries.async_remove(entry_id) + await hass.async_block_till_done() assert not hass.states.get("light.name") diff --git a/tests/components/eafm/test_sensor.py b/tests/components/eafm/test_sensor.py index a7ee0403c7c..3f2eb72a8e3 100644 --- a/tests/components/eafm/test_sensor.py +++ b/tests/components/eafm/test_sensor.py @@ -5,7 +5,7 @@ import aiohttp import pytest from homeassistant import config_entries -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_UNAVAILABLE from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -428,5 +428,8 @@ async def test_unload_entry(hass, mock_get_station): assert await entry.async_unload(hass) - # And the entity should be gone - assert not hass.states.get("sensor.my_station_water_level_stage") + # And the entity should be unavailable + assert ( + hass.states.get("sensor.my_station_water_level_stage").state + == STATE_UNAVAILABLE + ) diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index 149cbdae4e2..ffbf7a569f9 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -345,12 +345,12 @@ async def mock_api_object_fixture(hass, config_entry, get_request_return_values) async def test_unload_config_entry(hass, config_entry, mock_api_object): - """Test the player is removed when the config entry is unloaded.""" + """Test the player is set unavailable when the config entry is unloaded.""" assert hass.states.get(TEST_MASTER_ENTITY_NAME) assert hass.states.get(TEST_ZONE_ENTITY_NAMES[0]) await config_entry.async_unload(hass) - assert not hass.states.get(TEST_MASTER_ENTITY_NAME) - assert not hass.states.get(TEST_ZONE_ENTITY_NAMES[0]) + assert hass.states.get(TEST_MASTER_ENTITY_NAME).state == STATE_UNAVAILABLE + assert hass.states.get(TEST_ZONE_ENTITY_NAMES[0]).state == STATE_UNAVAILABLE def test_master_state(hass, mock_api_object): diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index 11067c1aa51..08655033f4d 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -4,7 +4,13 @@ from unittest.mock import Mock, call from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED -from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_DEVICES, + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, + STATE_UNAVAILABLE, +) from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component @@ -45,8 +51,8 @@ async def test_setup_duplicate_config(hass: HomeAssistantType, fritz: Mock, capl assert "duplicate host entries found" in caplog.text -async def test_unload(hass: HomeAssistantType, fritz: Mock): - """Test unload of integration.""" +async def test_unload_remove(hass: HomeAssistantType, fritz: Mock): + """Test unload and remove of integration.""" fritz().get_devices.return_value = [FritzDeviceSwitchMock()] entity_id = f"{SWITCH_DOMAIN}.fake_name" @@ -70,6 +76,14 @@ async def test_unload(hass: HomeAssistantType, fritz: Mock): await hass.config_entries.async_unload(entry.entry_id) + assert fritz().logout.call_count == 1 + assert entry.state == ENTRY_STATE_NOT_LOADED + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() + assert fritz().logout.call_count == 1 assert entry.state == ENTRY_STATE_NOT_LOADED state = hass.states.get(entity_id) diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py index ef7285ab185..4d979f8e556 100644 --- a/tests/components/heos/test_media_player.py +++ b/tests/components/heos/test_media_player.py @@ -587,10 +587,10 @@ async def test_select_input_command_error( async def test_unload_config_entry(hass, config_entry, config, controller): - """Test the player is removed when the config entry is unloaded.""" + """Test the player is set unavailable when the config entry is unloaded.""" await setup_platform(hass, config_entry, config) await config_entry.async_unload(hass) - assert not hass.states.get("media_player.test_player") + assert hass.states.get("media_player.test_player").state == STATE_UNAVAILABLE async def test_play_media_url(hass, config_entry, config, controller, caplog): diff --git a/tests/components/homekit_controller/test_light.py b/tests/components/homekit_controller/test_light.py index e443e36b910..f4950512063 100644 --- a/tests/components/homekit_controller/test_light.py +++ b/tests/components/homekit_controller/test_light.py @@ -3,6 +3,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes from homeassistant.components.homekit_controller.const import KNOWN_DEVICES +from homeassistant.const import STATE_UNAVAILABLE from tests.components.homekit_controller.common import setup_test_component @@ -209,8 +210,8 @@ async def test_light_becomes_unavailable_but_recovers(hass, utcnow): assert state.attributes["color_temp"] == 400 -async def test_light_unloaded(hass, utcnow): - """Test entity and HKDevice are correctly unloaded.""" +async def test_light_unloaded_removed(hass, utcnow): + """Test entity and HKDevice are correctly unloaded and removed.""" helper = await setup_test_component(hass, create_lightbulb_service_with_color_temp) # Initial state is that the light is off @@ -220,9 +221,15 @@ async def test_light_unloaded(hass, utcnow): unload_result = await helper.config_entry.async_unload(hass) assert unload_result is True - # Make sure entity is unloaded - assert hass.states.get(helper.entity_id) is None + # Make sure entity is set to unavailable state + assert hass.states.get(helper.entity_id).state == STATE_UNAVAILABLE # Make sure HKDevice is no longer set to poll this accessory conn = hass.data[KNOWN_DEVICES]["00:00:00:00:00:00"] assert not conn.pollable_characteristics + + await helper.config_entry.async_remove(hass) + await hass.async_block_till_done() + + # Make sure entity is removed + assert hass.states.get(helper.entity_id).state == STATE_UNAVAILABLE diff --git a/tests/components/huisbaasje/test_init.py b/tests/components/huisbaasje/test_init.py index 96be450f7e4..3de6af83e46 100644 --- a/tests/components/huisbaasje/test_init.py +++ b/tests/components/huisbaasje/test_init.py @@ -11,7 +11,7 @@ from homeassistant.config_entries import ( ENTRY_STATE_SETUP_ERROR, ConfigEntry, ) -from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -145,6 +145,14 @@ async def test_unload_entry(hass: HomeAssistant): await hass.config_entries.async_unload(config_entry.entry_id) assert config_entry.state == ENTRY_STATE_NOT_LOADED entities = hass.states.async_entity_ids("sensor") + assert len(entities) == 14 + for entity in entities: + assert hass.states.get(entity).state == STATE_UNAVAILABLE + + # Remove config entry + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + entities = hass.states.async_entity_ids("sensor") assert len(entities) == 0 # Assert mocks are called diff --git a/tests/components/input_boolean/test_init.py b/tests/components/input_boolean/test_init.py index 88562678436..c5d4d40e0d5 100644 --- a/tests/components/input_boolean/test_init.py +++ b/tests/components/input_boolean/test_init.py @@ -264,7 +264,7 @@ async def test_reload(hass, hass_admin_user): assert "mdi:work_reloaded" == state_2.attributes.get(ATTR_ICON) -async def test_load_person_storage(hass, storage_setup): +async def test_load_from_storage(hass, storage_setup): """Test set up from storage.""" assert await storage_setup() state = hass.states.get(f"{DOMAIN}.from_storage") diff --git a/tests/components/met/test_weather.py b/tests/components/met/test_weather.py index 242352c2498..24a81be3896 100644 --- a/tests/components/met/test_weather.py +++ b/tests/components/met/test_weather.py @@ -30,6 +30,7 @@ async def test_tracking_home(hass, mock_weather): entry = hass.config_entries.async_entries()[0] await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_entity_ids("weather")) == 0 @@ -63,4 +64,5 @@ async def test_not_tracking_home(hass, mock_weather): entry = hass.config_entries.async_entries()[0] await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_entity_ids("weather")) == 0 diff --git a/tests/components/nest/camera_sdm_test.py b/tests/components/nest/camera_sdm_test.py index 84deef92d62..117c8e97884 100644 --- a/tests/components/nest/camera_sdm_test.py +++ b/tests/components/nest/camera_sdm_test.py @@ -339,6 +339,7 @@ async def test_camera_removed(hass, auth): for config_entry in hass.config_entries.async_entries(DOMAIN): await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/nws/test_init.py b/tests/components/nws/test_init.py index 44b5193d79c..01a203aa07b 100644 --- a/tests/components/nws/test_init.py +++ b/tests/components/nws/test_init.py @@ -1,6 +1,7 @@ """Tests for init module.""" from homeassistant.components.nws.const import DOMAIN from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN +from homeassistant.const import STATE_UNAVAILABLE from tests.common import MockConfigEntry from tests.components.nws.const import NWS_CONFIG @@ -25,5 +26,12 @@ async def test_unload_entry(hass, mock_simple_nws): assert len(entries) == 1 assert await hass.config_entries.async_unload(entries[0].entry_id) - assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 0 + entities = hass.states.async_entity_ids(WEATHER_DOMAIN) + assert len(entities) == 1 + for entity in entities: + assert hass.states.get(entity).state == STATE_UNAVAILABLE assert DOMAIN not in hass.data + + assert await hass.config_entries.async_remove(entries[0].entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 0 diff --git a/tests/components/ozw/test_init.py b/tests/components/ozw/test_init.py index 2e57c4c01f3..339b690f4e4 100644 --- a/tests/components/ozw/test_init.py +++ b/tests/components/ozw/test_init.py @@ -4,6 +4,7 @@ from unittest.mock import patch from homeassistant import config_entries from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.ozw import DOMAIN, PLATFORMS, const +from homeassistant.const import ATTR_RESTORED, STATE_UNAVAILABLE from .common import setup_ozw @@ -76,14 +77,21 @@ async def test_unload_entry(hass, generic_data, switch_msg, caplog): await hass.config_entries.async_unload(entry.entry_id) assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED - assert len(hass.states.async_entity_ids("switch")) == 0 + entities = hass.states.async_entity_ids("switch") + assert len(entities) == 1 + for entity in entities: + assert hass.states.get(entity).state == STATE_UNAVAILABLE + assert hass.states.get(entity).attributes.get(ATTR_RESTORED) # Send a message for a switch from the broker to check that # all entity topic subscribers are unsubscribed. receive_message(switch_msg) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("switch")) == 0 + assert len(hass.states.async_entity_ids("switch")) == 1 + for entity in entities: + assert hass.states.get(entity).state == STATE_UNAVAILABLE + assert hass.states.get(entity).attributes.get(ATTR_RESTORED) # Load the integration again and check that there are no errors when # adding the entities. diff --git a/tests/components/panasonic_viera/test_init.py b/tests/components/panasonic_viera/test_init.py index 8f95043f4fa..5c9bf183c6f 100644 --- a/tests/components/panasonic_viera/test_init.py +++ b/tests/components/panasonic_viera/test_init.py @@ -17,7 +17,7 @@ from homeassistant.components.panasonic_viera.const import ( DOMAIN, ) from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_UNAVAILABLE from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -253,9 +253,11 @@ async def test_setup_unload_entry(hass): await hass.async_block_till_done() await hass.config_entries.async_unload(mock_entry.entry_id) - assert mock_entry.state == ENTRY_STATE_NOT_LOADED - state = hass.states.get("media_player.panasonic_viera_tv") + assert state.state == STATE_UNAVAILABLE + await hass.config_entries.async_remove(mock_entry.entry_id) + await hass.async_block_till_done() + state = hass.states.get("media_player.panasonic_viera_tv") assert state is None diff --git a/tests/components/plex/test_media_players.py b/tests/components/plex/test_media_players.py index 092d7e09008..fbd1205b2ef 100644 --- a/tests/components/plex/test_media_players.py +++ b/tests/components/plex/test_media_players.py @@ -22,7 +22,7 @@ async def test_plex_tv_clients( media_players_after = len(hass.states.async_entity_ids("media_player")) assert media_players_after == media_players_before + 1 - await hass.config_entries.async_unload(entry.entry_id) + await hass.config_entries.async_remove(entry.entry_id) # Ensure only plex.tv resource client is found with patch("plexapi.server.PlexServer.sessions", return_value=[]): diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index 6931b3dfbb5..e10d63a2e07 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -12,7 +12,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.components.smartthings import binary_sensor from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE -from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE from homeassistant.helpers.dispatcher import async_dispatcher_send from .conftest import setup_platform @@ -93,4 +93,7 @@ async def test_unload_config_entry(hass, device_factory): # Act await hass.config_entries.async_forward_entry_unload(config_entry, "binary_sensor") # Assert - assert not hass.states.get("binary_sensor.motion_sensor_1_motion") + assert ( + hass.states.get("binary_sensor.motion_sensor_1_motion").state + == STATE_UNAVAILABLE + ) diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index 0483480cb8a..178c905208e 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -19,7 +19,7 @@ from homeassistant.components.cover import ( STATE_OPENING, ) from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE -from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID +from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.helpers.dispatcher import async_dispatcher_send from .conftest import setup_platform @@ -193,4 +193,4 @@ async def test_unload_config_entry(hass, device_factory): # Act await hass.config_entries.async_forward_entry_unload(config_entry, COVER_DOMAIN) # Assert - assert not hass.states.get("cover.garage") + assert hass.states.get("cover.garage").state == STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index 0ebef7e7323..1f837d58bf8 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -17,7 +17,11 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, ) from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE -from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + STATE_UNAVAILABLE, +) from homeassistant.helpers.dispatcher import async_dispatcher_send from .conftest import setup_platform @@ -184,4 +188,4 @@ async def test_unload_config_entry(hass, device_factory): # Act await hass.config_entries.async_forward_entry_unload(config_entry, "fan") # Assert - assert not hass.states.get("fan.fan_1") + assert hass.states.get("fan.fan_1").state == STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index bd9557c6b97..f6d7d8dd9f4 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -19,7 +19,11 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, ) from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE -from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + STATE_UNAVAILABLE, +) from homeassistant.helpers.dispatcher import async_dispatcher_send from .conftest import setup_platform @@ -304,4 +308,4 @@ async def test_unload_config_entry(hass, device_factory): # Act await hass.config_entries.async_forward_entry_unload(config_entry, "light") # Assert - assert not hass.states.get("light.color_dimmer_2") + assert hass.states.get("light.color_dimmer_2").state == STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py index 0492f2281ce..185eae22ccf 100644 --- a/tests/components/smartthings/test_lock.py +++ b/tests/components/smartthings/test_lock.py @@ -9,6 +9,7 @@ from pysmartthings.device import Status from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers.dispatcher import async_dispatcher_send from .conftest import setup_platform @@ -104,4 +105,4 @@ async def test_unload_config_entry(hass, device_factory): # Act await hass.config_entries.async_forward_entry_unload(config_entry, "lock") # Assert - assert not hass.states.get("lock.lock_1") + assert hass.states.get("lock.lock_1").state == STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_scene.py b/tests/components/smartthings/test_scene.py index a9e6443d2bf..6ab4bc08080 100644 --- a/tests/components/smartthings/test_scene.py +++ b/tests/components/smartthings/test_scene.py @@ -5,7 +5,7 @@ The only mocking required is of the underlying SmartThings API object so real HTTP calls are not initiated during testing. """ from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, STATE_UNAVAILABLE from .conftest import setup_platform @@ -46,4 +46,4 @@ async def test_unload_config_entry(hass, scene): # Act await hass.config_entries.async_forward_entry_unload(config_entry, SCENE_DOMAIN) # Assert - assert not hass.states.get("scene.test_scene") + assert hass.states.get("scene.test_scene").state == STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index 3faf0f621a3..53f4b2c7244 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -13,6 +13,7 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, + STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -117,4 +118,4 @@ async def test_unload_config_entry(hass, device_factory): # Act await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") # Assert - assert not hass.states.get("sensor.sensor_1_battery") + assert hass.states.get("sensor.sensor_1_battery").state == STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 3ac86426eeb..27ed5050bee 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -12,6 +12,7 @@ from homeassistant.components.switch import ( ATTR_TODAY_ENERGY_KWH, DOMAIN as SWITCH_DOMAIN, ) +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers.dispatcher import async_dispatcher_send from .conftest import setup_platform @@ -96,4 +97,4 @@ async def test_unload_config_entry(hass, device_factory): # Act await hass.config_entries.async_forward_entry_unload(config_entry, "switch") # Assert - assert not hass.states.get("switch.switch_1") + assert hass.states.get("switch.switch_1").state == STATE_UNAVAILABLE diff --git a/tests/components/vizio/test_init.py b/tests/components/vizio/test_init.py index cd611662597..b223202d5b1 100644 --- a/tests/components/vizio/test_init.py +++ b/tests/components/vizio/test_init.py @@ -3,6 +3,7 @@ import pytest from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN from homeassistant.components.vizio.const import DOMAIN +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component @@ -41,7 +42,10 @@ async def test_tv_load_and_unload( assert await config_entry.async_unload(hass) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0 + entities = hass.states.async_entity_ids(MP_DOMAIN) + assert len(entities) == 1 + for entity in entities: + assert hass.states.get(entity).state == STATE_UNAVAILABLE assert DOMAIN not in hass.data @@ -62,5 +66,8 @@ async def test_speaker_load_and_unload( assert await config_entry.async_unload(hass) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0 + entities = hass.states.async_entity_ids(MP_DOMAIN) + assert len(entities) == 1 + for entity in entities: + assert hass.states.get(entity).state == STATE_UNAVAILABLE assert DOMAIN not in hass.data diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index c91ae33d986..05a0bd0d8d4 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -11,7 +11,7 @@ from homeassistant.components.yeelight import ( DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, ) -from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_NAME +from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_NAME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component @@ -50,6 +50,12 @@ async def test_setup_discovery(hass: HomeAssistant): # Unload assert await hass.config_entries.async_unload(config_entry.entry_id) + assert hass.states.get(ENTITY_BINARY_SENSOR).state == STATE_UNAVAILABLE + assert hass.states.get(ENTITY_LIGHT).state == STATE_UNAVAILABLE + + # Remove + assert await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert hass.states.get(ENTITY_BINARY_SENSOR) is None assert hass.states.get(ENTITY_LIGHT) is None diff --git a/tests/helpers/test_collection.py b/tests/helpers/test_collection.py index d5a8526b6da..11ab0f46ce4 100644 --- a/tests/helpers/test_collection.py +++ b/tests/helpers/test_collection.py @@ -226,7 +226,7 @@ async def test_attach_entity_component_collection(hass): """Test attaching collection to entity component.""" ent_comp = entity_component.EntityComponent(_LOGGER, "test", hass) coll = collection.ObservableCollection(_LOGGER) - collection.attach_entity_component_collection(ent_comp, coll, MockEntity) + collection.sync_entity_lifecycle(hass, "test", "test", ent_comp, coll, MockEntity) await coll.notify_changes( [ diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 52149b060e4..b8d0fc7dc9c 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock, PropertyMock, patch import pytest -from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE +from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import Context from homeassistant.helpers import entity, entity_registry @@ -718,3 +718,29 @@ async def test_setup_source(hass): await platform.async_reset() assert entity.entity_sources(hass) == {} + + +async def test_removing_entity_unavailable(hass): + """Test removing an entity that is still registered creates an unavailable state.""" + entry = entity_registry.RegistryEntry( + entity_id="hello.world", + unique_id="test-unique-id", + platform="test-platform", + disabled_by=None, + ) + + ent = entity.Entity() + ent.hass = hass + ent.entity_id = "hello.world" + ent.registry_entry = entry + ent.async_write_ha_state() + + state = hass.states.get("hello.world") + assert state is not None + assert state.state == STATE_UNKNOWN + + await ent.async_remove() + + state = hass.states.get("hello.world") + assert state is not None + assert state.state == STATE_UNAVAILABLE From b9b1caf4d7e26208aa4abfa54b4ab4f6bc66d488 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 8 Feb 2021 10:47:57 +0100 Subject: [PATCH 251/796] Raise ConditionError for numeric_state errors (#45923) --- .../components/automation/__init__.py | 8 +- .../components/bayesian/binary_sensor.py | 21 +-- .../homeassistant/triggers/numeric_state.py | 17 ++- homeassistant/exceptions.py | 4 + homeassistant/helpers/condition.py | 51 ++++--- homeassistant/helpers/script.py | 39 ++++-- tests/components/automation/test_init.py | 12 +- .../triggers/test_numeric_state.py | 26 ++++ tests/helpers/test_condition.py | 128 ++++++++++++++++-- tests/helpers/test_script.py | 109 +++++++++++++++ 10 files changed, 352 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 295450e8211..94f2cedd58e 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -32,7 +32,7 @@ from homeassistant.core import ( callback, split_entity_id, ) -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import ConditionError, HomeAssistantError from homeassistant.helpers import condition, extract_domain_configs, template import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity @@ -588,7 +588,11 @@ async def _async_process_if(hass, config, p_config): def if_action(variables=None): """AND all conditions.""" - return all(check(hass, variables) for check in checks) + try: + return all(check(hass, variables) for check in checks) + except ConditionError as ex: + LOGGER.warning("Error in 'condition' evaluation: %s", ex) + return False if_action.config = if_configs diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 4768b3f4fe6..15176d45349 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -17,7 +17,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import callback -from homeassistant.exceptions import TemplateError +from homeassistant.exceptions import ConditionError, TemplateError from homeassistant.helpers import condition import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( @@ -340,14 +340,17 @@ class BayesianBinarySensor(BinarySensorEntity): """Return True if numeric condition is met.""" entity = entity_observation["entity_id"] - return condition.async_numeric_state( - self.hass, - entity, - entity_observation.get("below"), - entity_observation.get("above"), - None, - entity_observation, - ) + try: + return condition.async_numeric_state( + self.hass, + entity, + entity_observation.get("below"), + entity_observation.get("above"), + None, + entity_observation, + ) + except ConditionError: + return False def _process_state(self, entity_observation): """Return True if state conditions are met.""" diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index 7cfee8fad93..55e875c90de 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -96,13 +96,20 @@ async def async_attach_trigger( @callback def check_numeric_state(entity_id, from_s, to_s): """Return True if criteria are now met.""" - if to_s is None: + try: + return condition.async_numeric_state( + hass, + to_s, + below, + above, + value_template, + variables(entity_id), + attribute, + ) + except exceptions.ConditionError as err: + _LOGGER.warning("%s", err) return False - return condition.async_numeric_state( - hass, to_s, below, above, value_template, variables(entity_id), attribute - ) - @callback def state_automation_listener(event): """Listen for state changes and calls action.""" diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index e37f68a07bf..852795ebb4a 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -25,6 +25,10 @@ class TemplateError(HomeAssistantError): super().__init__(f"{exception.__class__.__name__}: {exception}") +class ConditionError(HomeAssistantError): + """Error during condition evaluation.""" + + class PlatformNotReady(HomeAssistantError): """Error to indicate that platform is not ready.""" diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 5ace4c91bcf..e47374a9d17 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -36,7 +36,7 @@ from homeassistant.const import ( WEEKDAYS, ) from homeassistant.core import HomeAssistant, State, callback -from homeassistant.exceptions import HomeAssistantError, TemplateError +from homeassistant.exceptions import ConditionError, HomeAssistantError, TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.sun import get_astral_event_date from homeassistant.helpers.template import Template @@ -204,11 +204,22 @@ def async_numeric_state( attribute: Optional[str] = None, ) -> bool: """Test a numeric state condition.""" + if entity is None: + raise ConditionError("No entity specified") + if isinstance(entity, str): + entity_id = entity entity = hass.states.get(entity) - if entity is None or (attribute is not None and attribute not in entity.attributes): - return False + if entity is None: + raise ConditionError(f"Unknown entity {entity_id}") + else: + entity_id = entity.entity_id + + if attribute is not None and attribute not in entity.attributes: + raise ConditionError( + f"Attribute '{attribute}' (of entity {entity_id}) does not exist" + ) value: Any = None if value_template is None: @@ -222,30 +233,27 @@ def async_numeric_state( try: value = value_template.async_render(variables) except TemplateError as ex: - _LOGGER.error("Template error: %s", ex) - return False + raise ConditionError(f"Template error: {ex}") from ex if value in (STATE_UNAVAILABLE, STATE_UNKNOWN): - return False + raise ConditionError("State is not available") try: fvalue = float(value) - except ValueError: - _LOGGER.warning( - "Value cannot be processed as a number: %s (Offending entity: %s)", - entity, - value, - ) - return False + except ValueError as ex: + raise ConditionError( + f"Entity {entity_id} state '{value}' cannot be processed as a number" + ) from ex if below is not None: if isinstance(below, str): below_entity = hass.states.get(below) - if ( - not below_entity - or below_entity.state in (STATE_UNAVAILABLE, STATE_UNKNOWN) - or fvalue >= float(below_entity.state) + if not below_entity or below_entity.state in ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, ): + raise ConditionError(f"The below entity {below} is not available") + if fvalue >= float(below_entity.state): return False elif fvalue >= below: return False @@ -253,11 +261,12 @@ def async_numeric_state( if above is not None: if isinstance(above, str): above_entity = hass.states.get(above) - if ( - not above_entity - or above_entity.state in (STATE_UNAVAILABLE, STATE_UNKNOWN) - or fvalue <= float(above_entity.state) + if not above_entity or above_entity.state in ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, ): + raise ConditionError(f"The above entity {above} is not available") + if fvalue <= float(above_entity.state): return False elif fvalue <= above: return False diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 60a1be6103a..8706f765b50 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -519,7 +519,12 @@ class _ScriptRun: CONF_ALIAS, self._action[CONF_CONDITION] ) cond = await self._async_get_condition(self._action) - check = cond(self._hass, self._variables) + try: + check = cond(self._hass, self._variables) + except exceptions.ConditionError as ex: + _LOGGER.warning("Error in 'condition' evaluation: %s", ex) + check = False + self._log("Test condition %s: %s", self._script.last_action, check) if not check: raise _StopScript @@ -570,10 +575,15 @@ class _ScriptRun: ] for iteration in itertools.count(1): set_repeat_var(iteration) - if self._stop.is_set() or not all( - cond(self._hass, self._variables) for cond in conditions - ): + try: + if self._stop.is_set() or not all( + cond(self._hass, self._variables) for cond in conditions + ): + break + except exceptions.ConditionError as ex: + _LOGGER.warning("Error in 'while' evaluation: %s", ex) break + await async_run_sequence(iteration) elif CONF_UNTIL in repeat: @@ -583,9 +593,13 @@ class _ScriptRun: for iteration in itertools.count(1): set_repeat_var(iteration) await async_run_sequence(iteration) - if self._stop.is_set() or all( - cond(self._hass, self._variables) for cond in conditions - ): + try: + if self._stop.is_set() or all( + cond(self._hass, self._variables) for cond in conditions + ): + break + except exceptions.ConditionError as ex: + _LOGGER.warning("Error in 'until' evaluation: %s", ex) break if saved_repeat_vars: @@ -599,9 +613,14 @@ class _ScriptRun: choose_data = await self._script._async_get_choose_data(self._step) for conditions, script in choose_data["choices"]: - if all(condition(self._hass, self._variables) for condition in conditions): - await self._async_run_script(script) - return + try: + if all( + condition(self._hass, self._variables) for condition in conditions + ): + await self._async_run_script(script) + return + except exceptions.ConditionError as ex: + _LOGGER.warning("Error in 'choose' evaluation: %s", ex) if choose_data["default"]: await self._async_run_script(choose_data["default"]) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index c31af555e32..0dbc4b2cc69 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -162,18 +162,20 @@ async def test_trigger_service_ignoring_condition(hass, calls): "alias": "test", "trigger": [{"platform": "event", "event_type": "test_event"}], "condition": { - "condition": "state", + "condition": "numeric_state", "entity_id": "non.existing", - "state": "beer", + "above": "1", }, "action": {"service": "test.automation"}, } }, ) - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 0 + with patch("homeassistant.components.automation.LOGGER.warning") as logwarn: + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 0 + assert len(logwarn.mock_calls) == 1 await hass.services.async_call( "automation", "trigger", {"entity_id": "automation.test"}, blocking=True diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index b9696fffe06..979c5bad5d7 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -572,6 +572,32 @@ async def test_if_not_fires_if_entity_not_match(hass, calls, below): assert len(calls) == 0 +async def test_if_not_fires_and_warns_if_below_entity_unknown(hass, calls): + """Test if warns with unknown below entity.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "numeric_state", + "entity_id": "test.entity", + "below": "input_number.unknown", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + with patch( + "homeassistant.components.homeassistant.triggers.numeric_state._LOGGER.warning" + ) as logwarn: + hass.states.async_set("test.entity", 1) + await hass.async_block_till_done() + assert len(calls) == 0 + assert len(logwarn.mock_calls) == 1 + + @pytest.mark.parametrize("below", (10, "input_number.value_10")) async def test_if_fires_on_entity_change_below_with_attribute(hass, calls, below): """Test attributes change.""" diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index fe2a9aa4406..5c388aa6db4 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import ConditionError, HomeAssistantError from homeassistant.helpers import condition from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component @@ -338,8 +338,8 @@ async def test_time_using_input_datetime(hass): assert not condition.time(hass, before="input_datetime.not_existing") -async def test_if_numeric_state_not_raise_on_unavailable(hass): - """Test numeric_state doesn't raise on unavailable/unknown state.""" +async def test_if_numeric_state_raises_on_unavailable(hass): + """Test numeric_state raises on unavailable/unknown state.""" test = await condition.async_from_config( hass, {"condition": "numeric_state", "entity_id": "sensor.temperature", "below": 42}, @@ -347,11 +347,13 @@ async def test_if_numeric_state_not_raise_on_unavailable(hass): with patch("homeassistant.helpers.condition._LOGGER.warning") as logwarn: hass.states.async_set("sensor.temperature", "unavailable") - assert not test(hass) + with pytest.raises(ConditionError): + test(hass) assert len(logwarn.mock_calls) == 0 hass.states.async_set("sensor.temperature", "unknown") - assert not test(hass) + with pytest.raises(ConditionError): + test(hass) assert len(logwarn.mock_calls) == 0 @@ -550,6 +552,108 @@ async def test_state_using_input_entities(hass): assert test(hass) +async def test_numeric_state_raises(hass): + """Test that numeric_state raises ConditionError on errors.""" + # Unknown entity_id + with pytest.raises(ConditionError, match="Unknown entity"): + test = await condition.async_from_config( + hass, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature_unknown", + "above": 0, + }, + ) + + assert test(hass) + + # Unknown attribute + with pytest.raises(ConditionError, match=r"Attribute .* does not exist"): + test = await condition.async_from_config( + hass, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "attribute": "temperature", + "above": 0, + }, + ) + + hass.states.async_set("sensor.temperature", 50) + test(hass) + + # Template error + with pytest.raises(ConditionError, match="ZeroDivisionError"): + test = await condition.async_from_config( + hass, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "value_template": "{{ 1 / 0 }}", + "above": 0, + }, + ) + + hass.states.async_set("sensor.temperature", 50) + test(hass) + + # Unavailable state + with pytest.raises(ConditionError, match="State is not available"): + test = await condition.async_from_config( + hass, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "above": 0, + }, + ) + + hass.states.async_set("sensor.temperature", "unavailable") + test(hass) + + # Bad number + with pytest.raises(ConditionError, match="cannot be processed as a number"): + test = await condition.async_from_config( + hass, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "above": 0, + }, + ) + + hass.states.async_set("sensor.temperature", "fifty") + test(hass) + + # Below entity missing + with pytest.raises(ConditionError, match="below entity"): + test = await condition.async_from_config( + hass, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "below": "input_number.missing", + }, + ) + + hass.states.async_set("sensor.temperature", 50) + test(hass) + + # Above entity missing + with pytest.raises(ConditionError, match="above entity"): + test = await condition.async_from_config( + hass, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "above": "input_number.missing", + }, + ) + + hass.states.async_set("sensor.temperature", 50) + test(hass) + + async def test_numeric_state_multiple_entities(hass): """Test with multiple entities in condition.""" test = await condition.async_from_config( @@ -660,12 +764,14 @@ async def test_numeric_state_using_input_number(hass): ) assert test(hass) - assert not condition.async_numeric_state( - hass, entity="sensor.temperature", below="input_number.not_exist" - ) - assert not condition.async_numeric_state( - hass, entity="sensor.temperature", above="input_number.not_exist" - ) + with pytest.raises(ConditionError): + condition.async_numeric_state( + hass, entity="sensor.temperature", below="input_number.not_exist" + ) + with pytest.raises(ConditionError): + condition.async_numeric_state( + hass, entity="sensor.temperature", above="input_number.not_exist" + ) async def test_zone_multiple_entities(hass): diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 65d8b442bf0..003e903de14 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -990,6 +990,32 @@ async def test_wait_for_trigger_generated_exception(hass, caplog): assert "something bad" in caplog.text +async def test_condition_warning(hass): + """Test warning on condition.""" + event = "test_event" + events = async_capture_events(hass, event) + sequence = cv.SCRIPT_SCHEMA( + [ + {"event": event}, + { + "condition": "numeric_state", + "entity_id": "test.entity", + "above": 0, + }, + {"event": event}, + ] + ) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + hass.states.async_set("test.entity", "string") + with patch("homeassistant.helpers.script._LOGGER.warning") as logwarn: + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + assert len(logwarn.mock_calls) == 1 + + assert len(events) == 1 + + async def test_condition_basic(hass): """Test if we can use conditions in a script.""" event = "test_event" @@ -1100,6 +1126,44 @@ async def test_repeat_count(hass): assert event.data.get("last") == (index == count - 1) +@pytest.mark.parametrize("condition", ["while", "until"]) +async def test_repeat_condition_warning(hass, condition): + """Test warning on repeat conditions.""" + event = "test_event" + events = async_capture_events(hass, event) + count = 0 if condition == "while" else 1 + + sequence = { + "repeat": { + "sequence": [ + { + "event": event, + }, + ], + } + } + sequence["repeat"][condition] = { + "condition": "numeric_state", + "entity_id": "sensor.test", + "value_template": "{{ unassigned_variable }}", + "above": "0", + } + + script_obj = script.Script( + hass, cv.SCRIPT_SCHEMA(sequence), f"Test {condition}", "test_domain" + ) + + # wait_started = async_watch_for_action(script_obj, "wait") + hass.states.async_set("sensor.test", "1") + + with patch("homeassistant.helpers.script._LOGGER.warning") as logwarn: + hass.async_create_task(script_obj.async_run(context=Context())) + await asyncio.wait_for(hass.async_block_till_done(), 1) + assert len(logwarn.mock_calls) == 1 + + assert len(events) == count + + @pytest.mark.parametrize("condition", ["while", "until"]) @pytest.mark.parametrize("direct_template", [False, True]) async def test_repeat_conditional(hass, condition, direct_template): @@ -1305,6 +1369,51 @@ async def test_repeat_nested(hass, variables, first_last, inside_x): } +async def test_choose_warning(hass): + """Test warning on choose.""" + event = "test_event" + events = async_capture_events(hass, event) + + sequence = cv.SCRIPT_SCHEMA( + { + "choose": [ + { + "conditions": { + "condition": "numeric_state", + "entity_id": "test.entity", + "value_template": "{{ undefined_a + undefined_b }}", + "above": 1, + }, + "sequence": {"event": event, "event_data": {"choice": "first"}}, + }, + { + "conditions": { + "condition": "numeric_state", + "entity_id": "test.entity", + "value_template": "{{ 'string' }}", + "above": 2, + }, + "sequence": {"event": event, "event_data": {"choice": "second"}}, + }, + ], + "default": {"event": event, "event_data": {"choice": "default"}}, + } + ) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + hass.states.async_set("test.entity", "9") + await hass.async_block_till_done() + + with patch("homeassistant.helpers.script._LOGGER.warning") as logwarn: + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + print(logwarn.mock_calls) + assert len(logwarn.mock_calls) == 2 + + assert len(events) == 1 + assert events[0].data["choice"] == "default" + + @pytest.mark.parametrize("var,result", [(1, "first"), (2, "second"), (3, "default")]) async def test_choose(hass, var, result): """Test choose action.""" From 047f16772f7369aaf55a96f98e64b6011449b5ab Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 8 Feb 2021 10:50:38 +0100 Subject: [PATCH 252/796] Support templating MQTT triggers (#45614) * Add support for limited templates (no HASS access) * Pass variables to automation triggers * Support templates in MQTT triggers * Spelling * Handle trigger referenced by variables * Raise on unsupported function in limited templates * Validate MQTT trigger schema in MQTT device trigger * Add trigger_variables to automation config schema * Don't print stacktrace when setting up trigger throws * Make pylint happy * Add trigger_variables to variables * Add debug prints, document limited template * Add tests * Validate MQTT trigger topic early when possible * Improve valid_subscribe_topic_template --- .../components/automation/__init__.py | 29 +++++- homeassistant/components/automation/config.py | 2 + homeassistant/components/automation/const.py | 1 + .../components/mqtt/device_trigger.py | 2 + homeassistant/components/mqtt/trigger.py | 25 +++++- homeassistant/components/mqtt/util.py | 12 ++- homeassistant/helpers/config_validation.py | 2 +- homeassistant/helpers/script_variables.py | 5 +- homeassistant/helpers/template.py | 54 ++++++++++-- homeassistant/helpers/trigger.py | 5 +- tests/components/automation/test_init.py | 88 +++++++++++++++++++ tests/components/mqtt/test_trigger.py | 52 +++++++++++ 12 files changed, 262 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 94f2cedd58e..ae8c71b4fb8 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -60,6 +60,7 @@ from .const import ( CONF_ACTION, CONF_INITIAL_STATE, CONF_TRIGGER, + CONF_TRIGGER_VARIABLES, DEFAULT_INITIAL_STATE, DOMAIN, LOGGER, @@ -221,6 +222,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): action_script, initial_state, variables, + trigger_variables, ): """Initialize an automation entity.""" self._id = automation_id @@ -236,6 +238,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._referenced_devices: Optional[Set[str]] = None self._logger = LOGGER self._variables: ScriptVariables = variables + self._trigger_variables: ScriptVariables = trigger_variables @property def name(self): @@ -471,6 +474,16 @@ class AutomationEntity(ToggleEntity, RestoreEntity): def log_cb(level, msg, **kwargs): self._logger.log(level, "%s %s", msg, self._name, **kwargs) + variables = None + if self._trigger_variables: + try: + variables = self._trigger_variables.async_render( + cast(HomeAssistant, self.hass), None, limited=True + ) + except template.TemplateError as err: + self._logger.error("Error rendering trigger variables: %s", err) + return None + return await async_initialize_triggers( cast(HomeAssistant, self.hass), self._trigger_config, @@ -479,6 +492,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._name, log_cb, home_assistant_start, + variables, ) @property @@ -556,6 +570,18 @@ async def _async_process_config( else: cond_func = None + # Add trigger variables to variables + variables = None + if CONF_TRIGGER_VARIABLES in config_block: + variables = ScriptVariables( + dict(config_block[CONF_TRIGGER_VARIABLES].as_dict()) + ) + if CONF_VARIABLES in config_block: + if variables: + variables.variables.update(config_block[CONF_VARIABLES].as_dict()) + else: + variables = config_block[CONF_VARIABLES] + entity = AutomationEntity( automation_id, name, @@ -563,7 +589,8 @@ async def _async_process_config( cond_func, action_script, initial_state, - config_block.get(CONF_VARIABLES), + variables, + config_block.get(CONF_TRIGGER_VARIABLES), ) entities.append(entity) diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 89d5e184748..32ad92cb86e 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -21,6 +21,7 @@ from .const import ( CONF_HIDE_ENTITY, CONF_INITIAL_STATE, CONF_TRIGGER, + CONF_TRIGGER_VARIABLES, DOMAIN, ) from .helpers import async_get_blueprints @@ -43,6 +44,7 @@ PLATFORM_SCHEMA = vol.All( vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA, vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA, vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA, + vol.Optional(CONF_TRIGGER_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA, vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA, }, script.SCRIPT_MODE_SINGLE, diff --git a/homeassistant/components/automation/const.py b/homeassistant/components/automation/const.py index ffb89ba0907..829f78590e0 100644 --- a/homeassistant/components/automation/const.py +++ b/homeassistant/components/automation/const.py @@ -3,6 +3,7 @@ import logging CONF_ACTION = "action" CONF_TRIGGER = "trigger" +CONF_TRIGGER_VARIABLES = "trigger_variables" DOMAIN = "automation" CONF_DESCRIPTION = "description" diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 6a04fd48049..8969072553c 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -89,12 +89,14 @@ class TriggerInstance: async def async_attach_trigger(self): """Attach MQTT trigger.""" mqtt_config = { + mqtt_trigger.CONF_PLATFORM: mqtt.DOMAIN, mqtt_trigger.CONF_TOPIC: self.trigger.topic, mqtt_trigger.CONF_ENCODING: DEFAULT_ENCODING, mqtt_trigger.CONF_QOS: self.trigger.qos, } if self.trigger.payload: mqtt_config[CONF_PAYLOAD] = self.trigger.payload + mqtt_config = mqtt_trigger.TRIGGER_SCHEMA(mqtt_config) if self.remove: self.remove() diff --git a/homeassistant/components/mqtt/trigger.py b/homeassistant/components/mqtt/trigger.py index 1c96b3de266..a82ea355343 100644 --- a/homeassistant/components/mqtt/trigger.py +++ b/homeassistant/components/mqtt/trigger.py @@ -1,11 +1,12 @@ """Offer MQTT listening automation rules.""" import json +import logging import voluptuous as vol from homeassistant.const import CONF_PAYLOAD, CONF_PLATFORM from homeassistant.core import HassJob, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, template from .. import mqtt @@ -20,8 +21,8 @@ DEFAULT_QOS = 0 TRIGGER_SCHEMA = vol.Schema( { vol.Required(CONF_PLATFORM): mqtt.DOMAIN, - vol.Required(CONF_TOPIC): mqtt.util.valid_subscribe_topic, - vol.Optional(CONF_PAYLOAD): cv.string, + vol.Required(CONF_TOPIC): mqtt.util.valid_subscribe_topic_template, + vol.Optional(CONF_PAYLOAD): cv.template, vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, vol.Optional(CONF_QOS, default=DEFAULT_QOS): vol.All( vol.Coerce(int), vol.In([0, 1, 2]) @@ -29,6 +30,8 @@ TRIGGER_SCHEMA = vol.Schema( } ) +_LOGGER = logging.getLogger(__name__) + async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" @@ -37,6 +40,18 @@ async def async_attach_trigger(hass, config, action, automation_info): encoding = config[CONF_ENCODING] or None qos = config[CONF_QOS] job = HassJob(action) + variables = None + if automation_info: + variables = automation_info.get("variables") + + template.attach(hass, payload) + if payload: + payload = payload.async_render(variables, limited=True) + + template.attach(hass, topic) + if isinstance(topic, template.Template): + topic = topic.async_render(variables, limited=True) + topic = mqtt.util.valid_subscribe_topic(topic) @callback def mqtt_automation_listener(mqttmsg): @@ -57,6 +72,10 @@ async def async_attach_trigger(hass, config, action, automation_info): hass.async_run_hass_job(job, {"trigger": data}) + _LOGGER.debug( + "Attaching MQTT trigger for topic: '%s', payload: '%s'", topic, payload + ) + remove = await mqtt.async_subscribe( hass, topic, mqtt_automation_listener, encoding=encoding, qos=qos ) diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index 651fe48fe3d..b8fca50a153 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -4,7 +4,7 @@ from typing import Any import voluptuous as vol from homeassistant.const import CONF_PAYLOAD -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, template from .const import ( ATTR_PAYLOAD, @@ -61,6 +61,16 @@ def valid_subscribe_topic(value: Any) -> str: return value +def valid_subscribe_topic_template(value: Any) -> template.Template: + """Validate either a jinja2 template or a valid MQTT subscription topic.""" + tpl = template.Template(value) + + if tpl.is_static: + valid_subscribe_topic(value) + + return tpl + + def valid_publish_topic(value: Any) -> str: """Validate that we can publish using this MQTT topic.""" value = valid_topic(value) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index d47ba30c114..4af4744e509 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -572,7 +572,7 @@ def dynamic_template(value: Optional[Any]) -> template_helper.Template: if isinstance(value, (list, dict, template_helper.Template)): raise vol.Invalid("template value should be a string") if not template_helper.is_template_string(str(value)): - raise vol.Invalid("template value does not contain a dynmamic template") + raise vol.Invalid("template value does not contain a dynamic template") template_value = template_helper.Template(str(value)) # type: ignore try: diff --git a/homeassistant/helpers/script_variables.py b/homeassistant/helpers/script_variables.py index 3140fc4dced..818263c9dd5 100644 --- a/homeassistant/helpers/script_variables.py +++ b/homeassistant/helpers/script_variables.py @@ -21,6 +21,7 @@ class ScriptVariables: run_variables: Optional[Mapping[str, Any]], *, render_as_defaults: bool = True, + limited: bool = False, ) -> Dict[str, Any]: """Render script variables. @@ -55,7 +56,9 @@ class ScriptVariables: if render_as_defaults and key in rendered_variables: continue - rendered_variables[key] = template.render_complex(value, rendered_variables) + rendered_variables[key] = template.render_complex( + value, rendered_variables, limited + ) return rendered_variables diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 5f506c02eef..af63cab10eb 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -84,7 +84,9 @@ def attach(hass: HomeAssistantType, obj: Any) -> None: obj.hass = hass -def render_complex(value: Any, variables: TemplateVarsType = None) -> Any: +def render_complex( + value: Any, variables: TemplateVarsType = None, limited: bool = False +) -> Any: """Recursive template creator helper function.""" if isinstance(value, list): return [render_complex(item, variables) for item in value] @@ -94,7 +96,7 @@ def render_complex(value: Any, variables: TemplateVarsType = None) -> Any: for key, item in value.items() } if isinstance(value, Template): - return value.async_render(variables) + return value.async_render(variables, limited=limited) return value @@ -279,6 +281,7 @@ class Template: "is_static", "_compiled_code", "_compiled", + "_limited", ) def __init__(self, template, hass=None): @@ -291,10 +294,11 @@ class Template: self._compiled: Optional[Template] = None self.hass = hass self.is_static = not is_template_string(template) + self._limited = None @property def _env(self) -> "TemplateEnvironment": - if self.hass is None: + if self.hass is None or self._limited: return _NO_HASS_ENV ret: Optional[TemplateEnvironment] = self.hass.data.get(_ENVIRONMENT) if ret is None: @@ -315,9 +319,13 @@ class Template: self, variables: TemplateVarsType = None, parse_result: bool = True, + limited: bool = False, **kwargs: Any, ) -> Any: - """Render given template.""" + """Render given template. + + If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine. + """ if self.is_static: if self.hass.config.legacy_templates or not parse_result: return self.template @@ -325,7 +333,7 @@ class Template: return run_callback_threadsafe( self.hass.loop, - partial(self.async_render, variables, parse_result, **kwargs), + partial(self.async_render, variables, parse_result, limited, **kwargs), ).result() @callback @@ -333,18 +341,21 @@ class Template: self, variables: TemplateVarsType = None, parse_result: bool = True, + limited: bool = False, **kwargs: Any, ) -> Any: """Render given template. This method must be run in the event loop. + + If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine. """ if self.is_static: if self.hass.config.legacy_templates or not parse_result: return self.template return self._parse_result(self.template) - compiled = self._compiled or self._ensure_compiled() + compiled = self._compiled or self._ensure_compiled(limited) if variables is not None: kwargs.update(variables) @@ -519,12 +530,16 @@ class Template: ) return value if error_value is _SENTINEL else error_value - def _ensure_compiled(self) -> "Template": + def _ensure_compiled(self, limited: bool = False) -> "Template": """Bind a template to a specific hass instance.""" self.ensure_valid() assert self.hass is not None, "hass variable not set on template" + assert ( + self._limited is None or self._limited == limited + ), "can't change between limited and non limited template" + self._limited = limited env = self._env self._compiled = cast( @@ -1352,6 +1367,31 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["strptime"] = strptime self.globals["urlencode"] = urlencode if hass is None: + + def unsupported(name): + def warn_unsupported(*args, **kwargs): + raise TemplateError( + f"Use of '{name}' is not supported in limited templates" + ) + + return warn_unsupported + + hass_globals = [ + "closest", + "distance", + "expand", + "is_state", + "is_state_attr", + "state_attr", + "states", + "utcnow", + "now", + ] + hass_filters = ["closest", "expand"] + for glob in hass_globals: + self.globals[glob] = unsupported(glob) + for filt in hass_filters: + self.filters[filt] = unsupported(filt) return # We mark these as a context functions to ensure they get diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index 2c7275a9cc3..58ac71a515e 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant.const import CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.loader import IntegrationNotFound, async_get_integration @@ -79,7 +80,9 @@ async def async_initialize_triggers( removes = [] for result in attach_results: - if isinstance(result, Exception): + if isinstance(result, HomeAssistantError): + log_cb(logging.ERROR, f"Got error '{result}' when setting up triggers for") + elif isinstance(result, Exception): log_cb(logging.ERROR, "Error setting up trigger", exc_info=result) elif result is None: log_cb( diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 0dbc4b2cc69..16d56c84cb0 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1237,6 +1237,94 @@ async def test_automation_variables(hass, caplog): assert len(calls) == 3 +async def test_automation_trigger_variables(hass, caplog): + """Test automation trigger variables.""" + calls = async_mock_service(hass, "test", "automation") + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "variables": { + "event_type": "{{ trigger.event.event_type }}", + }, + "trigger_variables": { + "test_var": "defined_in_config", + }, + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": { + "service": "test.automation", + "data": { + "value": "{{ test_var }}", + "event_type": "{{ event_type }}", + }, + }, + }, + { + "variables": { + "event_type": "{{ trigger.event.event_type }}", + "test_var": "overridden_in_config", + }, + "trigger_variables": { + "test_var": "defined_in_config", + }, + "trigger": {"platform": "event", "event_type": "test_event_2"}, + "action": { + "service": "test.automation", + "data": { + "value": "{{ test_var }}", + "event_type": "{{ event_type }}", + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["value"] == "defined_in_config" + assert calls[0].data["event_type"] == "test_event" + + hass.bus.async_fire("test_event_2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["value"] == "overridden_in_config" + assert calls[1].data["event_type"] == "test_event_2" + + assert "Error rendering variables" not in caplog.text + + +async def test_automation_bad_trigger_variables(hass, caplog): + """Test automation trigger variables accessing hass is rejected.""" + calls = async_mock_service(hass, "test", "automation") + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger_variables": { + "test_var": "{{ states('foo.bar') }}", + }, + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": { + "service": "test.automation", + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event") + assert "Use of 'states' is not supported in limited templates" in caplog.text + + await hass.async_block_till_done() + assert len(calls) == 0 + + async def test_blueprint_automation(hass, calls): """Test blueprint automation.""" assert await async_setup_component( diff --git a/tests/components/mqtt/test_trigger.py b/tests/components/mqtt/test_trigger.py index b27af2b9bd0..537a4f8dc64 100644 --- a/tests/components/mqtt/test_trigger.py +++ b/tests/components/mqtt/test_trigger.py @@ -81,6 +81,58 @@ async def test_if_fires_on_topic_and_payload_match(hass, calls): assert len(calls) == 1 +async def test_if_fires_on_templated_topic_and_payload_match(hass, calls): + """Test if message is fired on templated topic and payload match.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "mqtt", + "topic": "test-topic-{{ sqrt(16)|round }}", + "payload": '{{ "foo"|regex_replace("foo", "bar") }}', + }, + "action": {"service": "test.automation"}, + } + }, + ) + + async_fire_mqtt_message(hass, "test-topic-", "foo") + await hass.async_block_till_done() + assert len(calls) == 0 + + async_fire_mqtt_message(hass, "test-topic-4", "foo") + await hass.async_block_till_done() + assert len(calls) == 0 + + async_fire_mqtt_message(hass, "test-topic-4", "bar") + await hass.async_block_till_done() + assert len(calls) == 1 + + +async def test_non_allowed_templates(hass, calls, caplog): + """Test non allowed function in template.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "mqtt", + "topic": "test-topic-{{ states() }}", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + assert ( + "Got error 'TemplateError: str: Use of 'states' is not supported in limited templates' when setting up triggers" + in caplog.text + ) + + async def test_if_not_fires_on_topic_but_no_payload_match(hass, calls): """Test if message is not fired on topic but no payload.""" assert await async_setup_component( From 8efb5eea4ddf9b8440ff8700f708ddbe4fed1e1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Mon, 8 Feb 2021 11:00:23 +0100 Subject: [PATCH 253/796] Bump python-verisure to version 1.7.2 (#46177) --- homeassistant/components/verisure/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json index 22c5e0c2362..814b5f148fa 100644 --- a/homeassistant/components/verisure/manifest.json +++ b/homeassistant/components/verisure/manifest.json @@ -2,6 +2,6 @@ "domain": "verisure", "name": "Verisure", "documentation": "https://www.home-assistant.io/integrations/verisure", - "requirements": ["jsonpath==0.82", "vsure==1.6.1"], + "requirements": ["jsonpath==0.82", "vsure==1.7.2"], "codeowners": ["@frenck"] } diff --git a/requirements_all.txt b/requirements_all.txt index d21c11fb466..66c8bef2101 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2269,7 +2269,7 @@ volkszaehler==0.2.1 volvooncall==0.8.12 # homeassistant.components.verisure -vsure==1.6.1 +vsure==1.7.2 # homeassistant.components.vasttrafik vtjp==0.1.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d0febd47326..38ed8b427b6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1145,7 +1145,7 @@ uvcclient==0.11.0 vilfo-api-client==0.3.2 # homeassistant.components.verisure -vsure==1.6.1 +vsure==1.7.2 # homeassistant.components.vultr vultr==0.1.2 From f99c27c6d4159f3b27852bb7af8e024d959b7091 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 8 Feb 2021 11:09:45 +0100 Subject: [PATCH 254/796] Remove unneeded from_state from device triggers (#45152) --- .../components/alarm_control_panel/device_trigger.py | 4 ---- homeassistant/components/binary_sensor/device_trigger.py | 3 --- homeassistant/components/fan/device_trigger.py | 3 --- homeassistant/components/lock/device_trigger.py | 3 --- homeassistant/components/vacuum/device_trigger.py | 5 +---- .../device_trigger/integration/device_trigger.py | 3 --- .../alarm_control_panel/test_device_trigger.py | 9 +++------ 7 files changed, 4 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py index bb5d82c52b1..5669340c2ce 100644 --- a/homeassistant/components/alarm_control_panel/device_trigger.py +++ b/homeassistant/components/alarm_control_panel/device_trigger.py @@ -132,14 +132,12 @@ async def async_attach_trigger( ) -> CALLBACK_TYPE: """Attach a trigger.""" config = TRIGGER_SCHEMA(config) - from_state = None if config[CONF_TYPE] == "triggered": to_state = STATE_ALARM_TRIGGERED elif config[CONF_TYPE] == "disarmed": to_state = STATE_ALARM_DISARMED elif config[CONF_TYPE] == "arming": - from_state = STATE_ALARM_DISARMED to_state = STATE_ALARM_ARMING elif config[CONF_TYPE] == "armed_home": to_state = STATE_ALARM_ARMED_HOME @@ -153,8 +151,6 @@ async def async_attach_trigger( CONF_ENTITY_ID: config[CONF_ENTITY_ID], state_trigger.CONF_TO: to_state, } - if from_state: - state_config[state_trigger.CONF_FROM] = from_state state_config = state_trigger.TRIGGER_SCHEMA(state_config) return await state_trigger.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index f7f0c53a698..b87a761a7a1 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -190,16 +190,13 @@ async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" trigger_type = config[CONF_TYPE] if trigger_type in TURNED_ON: - from_state = "off" to_state = "on" else: - from_state = "on" to_state = "off" state_config = { state_trigger.CONF_PLATFORM: "state", state_trigger.CONF_ENTITY_ID: config[CONF_ENTITY_ID], - state_trigger.CONF_FROM: from_state, state_trigger.CONF_TO: to_state, } if CONF_FOR in config: diff --git a/homeassistant/components/fan/device_trigger.py b/homeassistant/components/fan/device_trigger.py index c78ebcfffe4..95f4b429a24 100644 --- a/homeassistant/components/fan/device_trigger.py +++ b/homeassistant/components/fan/device_trigger.py @@ -74,16 +74,13 @@ async def async_attach_trigger( config = TRIGGER_SCHEMA(config) if config[CONF_TYPE] == "turned_on": - from_state = STATE_OFF to_state = STATE_ON else: - from_state = STATE_ON to_state = STATE_OFF state_config = { state_trigger.CONF_PLATFORM: "state", CONF_ENTITY_ID: config[CONF_ENTITY_ID], - state_trigger.CONF_FROM: from_state, state_trigger.CONF_TO: to_state, } state_config = state_trigger.TRIGGER_SCHEMA(state_config) diff --git a/homeassistant/components/lock/device_trigger.py b/homeassistant/components/lock/device_trigger.py index 091811446b5..05d5041ca65 100644 --- a/homeassistant/components/lock/device_trigger.py +++ b/homeassistant/components/lock/device_trigger.py @@ -74,16 +74,13 @@ async def async_attach_trigger( config = TRIGGER_SCHEMA(config) if config[CONF_TYPE] == "locked": - from_state = STATE_UNLOCKED to_state = STATE_LOCKED else: - from_state = STATE_LOCKED to_state = STATE_UNLOCKED state_config = { CONF_PLATFORM: "state", CONF_ENTITY_ID: config[CONF_ENTITY_ID], - state_trigger.CONF_FROM: from_state, state_trigger.CONF_TO: to_state, } state_config = state_trigger.TRIGGER_SCHEMA(state_config) diff --git a/homeassistant/components/vacuum/device_trigger.py b/homeassistant/components/vacuum/device_trigger.py index 29fc5628b22..21a2ae5e8c2 100644 --- a/homeassistant/components/vacuum/device_trigger.py +++ b/homeassistant/components/vacuum/device_trigger.py @@ -17,7 +17,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.typing import ConfigType -from . import DOMAIN, STATE_CLEANING, STATE_DOCKED, STATES +from . import DOMAIN, STATE_CLEANING, STATE_DOCKED TRIGGER_TYPES = {"cleaning", "docked"} @@ -71,16 +71,13 @@ async def async_attach_trigger( config = TRIGGER_SCHEMA(config) if config[CONF_TYPE] == "cleaning": - from_state = [state for state in STATES if state != STATE_CLEANING] to_state = STATE_CLEANING else: - from_state = [state for state in STATES if state != STATE_DOCKED] to_state = STATE_DOCKED state_config = { CONF_PLATFORM: "state", CONF_ENTITY_ID: config[CONF_ENTITY_ID], - state_trigger.CONF_FROM: from_state, state_trigger.CONF_TO: to_state, } state_config = state_trigger.TRIGGER_SCHEMA(state_config) diff --git a/script/scaffold/templates/device_trigger/integration/device_trigger.py b/script/scaffold/templates/device_trigger/integration/device_trigger.py index 7709813957e..2fa59d4eac8 100644 --- a/script/scaffold/templates/device_trigger/integration/device_trigger.py +++ b/script/scaffold/templates/device_trigger/integration/device_trigger.py @@ -84,16 +84,13 @@ async def async_attach_trigger( # Use the existing state or event triggers from the automation integration. if config[CONF_TYPE] == "turned_on": - from_state = STATE_OFF to_state = STATE_ON else: - from_state = STATE_ON to_state = STATE_OFF state_config = { state.CONF_PLATFORM: "state", CONF_ENTITY_ID: config[CONF_ENTITY_ID], - state.CONF_FROM: from_state, state.CONF_TO: to_state, } state_config = state.TRIGGER_SCHEMA(state_config) diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py index 82432bc37ab..56316026c9a 100644 --- a/tests/components/alarm_control_panel/test_device_trigger.py +++ b/tests/components/alarm_control_panel/test_device_trigger.py @@ -230,31 +230,28 @@ async def test_if_fires_on_state_change(hass, calls): ) # Fake that the entity is armed home. - hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_PENDING) hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_ARMED_HOME) await hass.async_block_till_done() assert len(calls) == 3 assert ( calls[2].data["some"] - == "armed_home - device - alarm_control_panel.entity - pending - armed_home - None" + == "armed_home - device - alarm_control_panel.entity - disarmed - armed_home - None" ) # Fake that the entity is armed away. - hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_PENDING) hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_ARMED_AWAY) await hass.async_block_till_done() assert len(calls) == 4 assert ( calls[3].data["some"] - == "armed_away - device - alarm_control_panel.entity - pending - armed_away - None" + == "armed_away - device - alarm_control_panel.entity - armed_home - armed_away - None" ) # Fake that the entity is armed night. - hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_PENDING) hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_ARMED_NIGHT) await hass.async_block_till_done() assert len(calls) == 5 assert ( calls[4].data["some"] - == "armed_night - device - alarm_control_panel.entity - pending - armed_night - None" + == "armed_night - device - alarm_control_panel.entity - armed_away - armed_night - None" ) From e7ca0ff71a96066d95bbd6f2c49a0f3849bc2902 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 8 Feb 2021 11:23:50 +0100 Subject: [PATCH 255/796] Enable KNX auto_reconnect for auto-discovered connections (#46178) --- homeassistant/components/knx/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 7dbeb513e09..1492e5df7b7 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -290,7 +290,7 @@ class KNXModule: if CONF_KNX_ROUTING in self.config[DOMAIN]: return self.connection_config_routing() # config from xknx.yaml always has priority later on - return ConnectionConfig() + return ConnectionConfig(auto_reconnect=True) def connection_config_routing(self): """Return the connection_config if routing is configured.""" From 9b0955b67e18dc15da2d4d2c4916792180e6214f Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 8 Feb 2021 05:26:57 -0500 Subject: [PATCH 256/796] Use core constants for flux (#46201) --- homeassistant/components/flux/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/flux/switch.py b/homeassistant/components/flux/switch.py index 4d45f217a59..ab0d296928f 100644 --- a/homeassistant/components/flux/switch.py +++ b/homeassistant/components/flux/switch.py @@ -22,6 +22,7 @@ from homeassistant.components.light import ( from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.const import ( ATTR_ENTITY_ID, + CONF_BRIGHTNESS, CONF_LIGHTS, CONF_MODE, CONF_NAME, @@ -49,7 +50,6 @@ CONF_STOP_TIME = "stop_time" CONF_START_CT = "start_colortemp" CONF_SUNSET_CT = "sunset_colortemp" CONF_STOP_CT = "stop_colortemp" -CONF_BRIGHTNESS = "brightness" CONF_DISABLE_BRIGHTNESS_ADJUST = "disable_brightness_adjust" CONF_INTERVAL = "interval" From 5faf463205a0d70e4b5689b2b87d223c51995a5a Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 8 Feb 2021 05:36:45 -0500 Subject: [PATCH 257/796] Use core constants for frontend component (#46203) --- homeassistant/components/frontend/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index cdf25d22fe8..bb503ee8673 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -14,7 +14,7 @@ from yarl import URL from homeassistant.components import websocket_api from homeassistant.components.http.view import HomeAssistantView from homeassistant.config import async_hass_config_yaml -from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED +from homeassistant.const import CONF_MODE, CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback from homeassistant.helpers import service import homeassistant.helpers.config_validation as cv @@ -113,7 +113,6 @@ CONFIG_SCHEMA = vol.Schema( SERVICE_SET_THEME = "set_theme" SERVICE_RELOAD_THEMES = "reload_themes" -CONF_MODE = "mode" class Panel: From 87c36d6b6b8e6d68a1777eeaf97588ff696f9fb4 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 8 Feb 2021 05:36:59 -0500 Subject: [PATCH 258/796] Use core constants for google_assistant (#46204) --- homeassistant/components/google_assistant/__init__.py | 3 +-- homeassistant/components/google_assistant/trait.py | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 8f4ee3b51c4..00c09242517 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -5,7 +5,7 @@ from typing import Any, Dict import voluptuous as vol # Typing imports -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv @@ -35,7 +35,6 @@ 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( { diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index f2a2274d8a8..9e0da39b58a 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1938,12 +1938,10 @@ class TransportControlTrait(_Trait): def query_attributes(self): """Return the attributes of this trait for this entity.""" - return {} async def execute(self, command, data, params, challenge): """Execute a media command.""" - service_attrs = {ATTR_ENTITY_ID: self.state.entity_id} if command == COMMAND_MEDIA_SEEK_RELATIVE: From a23e05d1f68d70ac2dda9a91c6ef30f538b3952b Mon Sep 17 00:00:00 2001 From: Hmmbob <33529490+hmmbob@users.noreply.github.com> Date: Mon, 8 Feb 2021 11:43:30 +0100 Subject: [PATCH 259/796] Fix Google translate TTS by bumping gTTS from 2.2.1 to 2.2.2 (#46110) --- homeassistant/components/google_translate/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_translate/manifest.json b/homeassistant/components/google_translate/manifest.json index c5b3edc8798..64d19bed277 100644 --- a/homeassistant/components/google_translate/manifest.json +++ b/homeassistant/components/google_translate/manifest.json @@ -2,6 +2,6 @@ "domain": "google_translate", "name": "Google Translate Text-to-Speech", "documentation": "https://www.home-assistant.io/integrations/google_translate", - "requirements": ["gTTS==2.2.1"], + "requirements": ["gTTS==2.2.2"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 66c8bef2101..1cfd894c9ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -622,7 +622,7 @@ freesms==0.1.2 fritzconnection==1.4.0 # homeassistant.components.google_translate -gTTS==2.2.1 +gTTS==2.2.2 # homeassistant.components.garmin_connect garminconnect==0.1.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 38ed8b427b6..befb0187c0c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -319,7 +319,7 @@ freebox-api==0.0.9 fritzconnection==1.4.0 # homeassistant.components.google_translate -gTTS==2.2.1 +gTTS==2.2.2 # homeassistant.components.garmin_connect garminconnect==0.1.16 From 5a4e1eeb0e87b86cc1fdea367e4f7356b1867e58 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 8 Feb 2021 11:46:58 +0100 Subject: [PATCH 260/796] Upgrade praw to 7.1.4 (#46202) --- homeassistant/components/reddit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reddit/manifest.json b/homeassistant/components/reddit/manifest.json index d270f994159..252052ac5c2 100644 --- a/homeassistant/components/reddit/manifest.json +++ b/homeassistant/components/reddit/manifest.json @@ -2,6 +2,6 @@ "domain": "reddit", "name": "Reddit", "documentation": "https://www.home-assistant.io/integrations/reddit", - "requirements": ["praw==7.1.3"], + "requirements": ["praw==7.1.4"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 1cfd894c9ad..04dbcdf9d1a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1159,7 +1159,7 @@ pmsensor==0.4 poolsense==0.0.8 # homeassistant.components.reddit -praw==7.1.3 +praw==7.1.4 # homeassistant.components.islamic_prayer_times prayer_times_calculator==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index befb0187c0c..548d6ba1117 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -596,7 +596,7 @@ pmsensor==0.4 poolsense==0.0.8 # homeassistant.components.reddit -praw==7.1.3 +praw==7.1.4 # homeassistant.components.islamic_prayer_times prayer_times_calculator==0.0.3 From 54dce1c50545e49fea8896c9adb8f1156f5917c4 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 8 Feb 2021 05:47:30 -0500 Subject: [PATCH 261/796] Use core constants for fleetgo (#46200) --- homeassistant/components/fleetgo/device_tracker.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/fleetgo/device_tracker.py b/homeassistant/components/fleetgo/device_tracker.py index d46bbc9c85b..1f4c0d0ddfc 100644 --- a/homeassistant/components/fleetgo/device_tracker.py +++ b/homeassistant/components/fleetgo/device_tracker.py @@ -9,6 +9,7 @@ from homeassistant.components.device_tracker import PLATFORM_SCHEMA from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, + CONF_INCLUDE, CONF_PASSWORD, CONF_USERNAME, ) @@ -17,8 +18,6 @@ from homeassistant.helpers.event import track_utc_time_change _LOGGER = logging.getLogger(__name__) -CONF_INCLUDE = "include" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_USERNAME): cv.string, @@ -44,7 +43,6 @@ class FleetGoDeviceScanner: def __init__(self, config, see): """Initialize FleetGoDeviceScanner.""" - self._include = config.get(CONF_INCLUDE) self._see = see From 82607977efaa385e6097509ada5a1153f95214b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 8 Feb 2021 12:59:46 +0200 Subject: [PATCH 262/796] Various type hint improvements (#46144) --- homeassistant/config.py | 5 +++-- homeassistant/helpers/area_registry.py | 4 +++- homeassistant/helpers/discovery.py | 13 ++++++++++--- homeassistant/helpers/entity_component.py | 4 +++- homeassistant/helpers/reload.py | 8 ++++---- homeassistant/helpers/script.py | 2 +- homeassistant/helpers/service.py | 8 ++++++-- homeassistant/helpers/update_coordinator.py | 6 ++++-- 8 files changed, 34 insertions(+), 16 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 2da9b0331c9..5cd4a0700e5 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -51,6 +51,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, extract_domain_configs import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_values import EntityValues +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import Integration, IntegrationNotFound from homeassistant.requirements import ( RequirementsNotFound, @@ -734,8 +735,8 @@ async def merge_packages_config( async def async_process_component_config( - hass: HomeAssistant, config: Dict, integration: Integration -) -> Optional[Dict]: + hass: HomeAssistant, config: ConfigType, integration: Integration +) -> Optional[ConfigType]: """Check component configuration and return processed configuration. Returns None on error. diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index 1a919996f86..bdd231686e2 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -11,6 +11,8 @@ from homeassistant.util import slugify from .typing import HomeAssistantType +# mypy: disallow-any-generics + DATA_REGISTRY = "area_registry" EVENT_AREA_REGISTRY_UPDATED = "area_registry_updated" STORAGE_KEY = "core.area_registry" @@ -25,7 +27,7 @@ class AreaEntry: name: str = attr.ib() id: Optional[str] = attr.ib(default=None) - def generate_id(self, existing_ids: Container) -> None: + def generate_id(self, existing_ids: Container[str]) -> None: """Initialize ID.""" suggestion = suggestion_base = slugify(self.name) tries = 1 diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index acde8d73a50..0770e6798f1 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -9,6 +9,7 @@ from typing import Any, Callable, Collection, Dict, Optional, Union from homeassistant import core, setup from homeassistant.const import ATTR_DISCOVERED, ATTR_SERVICE, EVENT_PLATFORM_DISCOVERED +from homeassistant.core import CALLBACK_TYPE from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.loader import bind_hass from homeassistant.util.async_ import run_callback_threadsafe @@ -16,10 +17,14 @@ from homeassistant.util.async_ import run_callback_threadsafe EVENT_LOAD_PLATFORM = "load_platform.{}" ATTR_PLATFORM = "platform" +# mypy: disallow-any-generics + @bind_hass def listen( - hass: core.HomeAssistant, service: Union[str, Collection[str]], callback: Callable + hass: core.HomeAssistant, + service: Union[str, Collection[str]], + callback: CALLBACK_TYPE, ) -> None: """Set up listener for discovery of specific service. @@ -31,7 +36,9 @@ def listen( @core.callback @bind_hass def async_listen( - hass: core.HomeAssistant, service: Union[str, Collection[str]], callback: Callable + hass: core.HomeAssistant, + service: Union[str, Collection[str]], + callback: CALLBACK_TYPE, ) -> None: """Set up listener for discovery of specific service. @@ -94,7 +101,7 @@ async def async_discover( @bind_hass def listen_platform( - hass: core.HomeAssistant, component: str, callback: Callable + hass: core.HomeAssistant, component: str, callback: CALLBACK_TYPE ) -> None: """Register a platform loader listener.""" run_callback_threadsafe( diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 0f1f04e3aec..6fb8696d845 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -272,7 +272,9 @@ class EntityComponent: if found: await found.async_remove_entity(entity_id) - async def async_prepare_reload(self, *, skip_reset: bool = False) -> Optional[dict]: + async def async_prepare_reload( + self, *, skip_reset: bool = False + ) -> Optional[ConfigType]: """Prepare reloading this entity component. This method must be run in the event loop. diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index e596027b7e1..8ff454eab6f 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -2,7 +2,7 @@ import asyncio import logging -from typing import Any, Dict, Iterable, List, Optional +from typing import Dict, Iterable, List, Optional from homeassistant import config as conf_util from homeassistant.const import SERVICE_RELOAD @@ -10,7 +10,7 @@ from homeassistant.core import Event, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform from homeassistant.helpers.entity_platform import EntityPlatform, async_get_platforms -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component @@ -49,7 +49,7 @@ async def _resetup_platform( hass: HomeAssistantType, integration_name: str, integration_platform: str, - unprocessed_conf: Dict, + unprocessed_conf: ConfigType, ) -> None: """Resetup a platform.""" integration = await async_get_integration(hass, integration_platform) @@ -129,7 +129,7 @@ async def _async_reconfig_platform( async def async_integration_yaml_config( hass: HomeAssistantType, integration_name: str -) -> Optional[Dict[Any, Any]]: +) -> Optional[ConfigType]: """Fetch the latest yaml configuration for an integration.""" integration = await async_get_integration(hass, integration_name) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 8706f765b50..56accf9cf49 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -779,7 +779,7 @@ 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: +def _referenced_extract_ids(data: Dict[str, Any], key: str, found: Set[str]) -> None: """Extract referenced IDs.""" if not data: return diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index c95f942c6dc..c83fa4a7763 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -669,10 +669,14 @@ def async_register_admin_service( @bind_hass @ha.callback -def verify_domain_control(hass: HomeAssistantType, domain: str) -> Callable: +def verify_domain_control( + hass: HomeAssistantType, domain: str +) -> Callable[[Callable[[ha.ServiceCall], Any]], Callable[[ha.ServiceCall], Any]]: """Ensure permission to access any entity under domain in service call.""" - def decorator(service_handler: Callable[[ha.ServiceCall], Any]) -> Callable: + def decorator( + service_handler: Callable[[ha.ServiceCall], Any] + ) -> Callable[[ha.ServiceCall], Any]: """Decorate.""" if not asyncio.iscoroutinefunction(service_handler): raise HomeAssistantError("Can only decorate async functions.") diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 8df2c57b1e7..b2424a06927 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -3,7 +3,7 @@ import asyncio from datetime import datetime, timedelta import logging from time import monotonic -from typing import Awaitable, Callable, Generic, List, Optional, TypeVar +from typing import Any, Awaitable, Callable, Generic, List, Optional, TypeVar import urllib.error import aiohttp @@ -21,6 +21,8 @@ REQUEST_REFRESH_DEFAULT_IMMEDIATE = True T = TypeVar("T") +# mypy: disallow-any-generics + class UpdateFailed(Exception): """Raised when an update has failed.""" @@ -231,7 +233,7 @@ class DataUpdateCoordinator(Generic[T]): class CoordinatorEntity(entity.Entity): """A class for entities using DataUpdateCoordinator.""" - def __init__(self, coordinator: DataUpdateCoordinator) -> None: + def __init__(self, coordinator: DataUpdateCoordinator[Any]) -> None: """Create the entity with a DataUpdateCoordinator.""" self.coordinator = coordinator From 92e5bf978660c02aa79e3a3bda9431564d6c300d Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 8 Feb 2021 06:24:48 -0500 Subject: [PATCH 263/796] Use core constants for google (#46210) --- homeassistant/components/google/__init__.py | 13 ++++++++----- homeassistant/components/google/calendar.py | 5 +---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 78ea1616f99..b46d48848da 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -15,7 +15,14 @@ import voluptuous as vol from voluptuous.error import Error as VoluptuousError import yaml -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_DEVICE_ID, + CONF_ENTITIES, + CONF_NAME, + CONF_OFFSET, +) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import generate_entity_id @@ -30,12 +37,8 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}" CONF_TRACK_NEW = "track_new_calendar" CONF_CAL_ID = "cal_id" -CONF_DEVICE_ID = "device_id" -CONF_NAME = "name" -CONF_ENTITIES = "entities" CONF_TRACK = "track" CONF_SEARCH = "search" -CONF_OFFSET = "offset" CONF_IGNORE_AVAILABILITY = "ignore_availability" CONF_MAX_RESULTS = "max_results" diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 6448e035171..2fcde78354b 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -11,17 +11,14 @@ from homeassistant.components.calendar import ( calculate_offset, is_offset_reached, ) +from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_OFFSET from homeassistant.helpers.entity import generate_entity_id from homeassistant.util import Throttle, dt from . import ( CONF_CAL_ID, - CONF_DEVICE_ID, - CONF_ENTITIES, CONF_IGNORE_AVAILABILITY, CONF_MAX_RESULTS, - CONF_NAME, - CONF_OFFSET, CONF_SEARCH, CONF_TRACK, DEFAULT_CONF_OFFSET, From 9d9c4b47ee308ea0b770255f4d2b8b09be6c18ae Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 8 Feb 2021 13:21:31 +0100 Subject: [PATCH 264/796] Pass variables to numeric state trigger templates (#46209) --- .../homeassistant/triggers/numeric_state.py | 7 +- .../triggers/test_numeric_state.py | 90 +++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index 55e875c90de..4f406de23ca 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -78,12 +78,16 @@ async def async_attach_trigger( attribute = config.get(CONF_ATTRIBUTE) job = HassJob(action) + _variables = {} + if automation_info: + _variables = automation_info.get("variables") or {} + if value_template is not None: value_template.hass = hass def variables(entity_id): """Return a dict with trigger variables.""" - return { + trigger_info = { "trigger": { "platform": "numeric_state", "entity_id": entity_id, @@ -92,6 +96,7 @@ async def async_attach_trigger( "attribute": attribute, } } + return {**_variables, **trigger_info} @callback def check_numeric_state(entity_id, from_s, to_s): diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index 979c5bad5d7..e58bccb0bcc 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -1647,3 +1647,93 @@ async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop( async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() assert len(calls) == 1 + + +@pytest.mark.parametrize( + "above, below", + ((8, 12),), +) +async def test_variables_priority(hass, calls, above, below): + """Test an externally defined trigger variable is overridden.""" + 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, + { + automation.DOMAIN: { + "trigger_variables": {"trigger": "illegal"}, + "trigger": { + "platform": "numeric_state", + "entity_id": ["test.entity_1", "test.entity_2"], + "above": above, + "below": below, + "for": '{{ 5 if trigger.entity_id == "test.entity_1"' + " else 10 }}", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "{{ trigger.entity_id }} - {{ trigger.for }}" + }, + }, + } + }, + ) + await hass.async_block_till_done() + + utcnow = dt_util.utcnow() + with patch("homeassistant.util.dt.utcnow") as mock_utcnow: + mock_utcnow.return_value = utcnow + hass.states.async_set("test.entity_1", 9) + await hass.async_block_till_done() + mock_utcnow.return_value += timedelta(seconds=1) + async_fire_time_changed(hass, mock_utcnow.return_value) + hass.states.async_set("test.entity_2", 9) + await hass.async_block_till_done() + mock_utcnow.return_value += timedelta(seconds=1) + async_fire_time_changed(hass, mock_utcnow.return_value) + hass.states.async_set("test.entity_2", 15) + await hass.async_block_till_done() + mock_utcnow.return_value += timedelta(seconds=1) + async_fire_time_changed(hass, mock_utcnow.return_value) + hass.states.async_set("test.entity_2", 9) + await hass.async_block_till_done() + assert len(calls) == 0 + mock_utcnow.return_value += timedelta(seconds=3) + async_fire_time_changed(hass, mock_utcnow.return_value) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "test.entity_1 - 0:00:05" + + +@pytest.mark.parametrize("multiplier", (1, 5)) +async def test_template_variable(hass, calls, multiplier): + """Test template variable.""" + 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, + { + automation.DOMAIN: { + "trigger_variables": {"multiplier": multiplier}, + "trigger": { + "platform": "numeric_state", + "entity_id": "test.entity", + "value_template": "{{ state.attributes.test_attribute[2] * multiplier}}", + "below": 10, + }, + "action": {"service": "test.automation"}, + } + }, + ) + # 3 is below 10 + hass.states.async_set("test.entity", "entity", {"test_attribute": [11, 15, 3]}) + await hass.async_block_till_done() + if multiplier * 3 < 10: + assert len(calls) == 1 + else: + assert len(calls) == 0 From 2744d64a3ed8c2c1337fd6d4cca87cb903f3a8f5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 8 Feb 2021 13:22:01 +0100 Subject: [PATCH 265/796] Pass variables to state trigger templates (#46208) * Pass variables to state trigger templates * Remove non working test --- .../homeassistant/triggers/state.py | 7 +- .../homeassistant/triggers/test_state.py | 88 +++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index 5dd7335f56e..8a03905d98d 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -85,6 +85,10 @@ async def async_attach_trigger( attribute = config.get(CONF_ATTRIBUTE) job = HassJob(action) + _variables = {} + if automation_info: + _variables = automation_info.get("variables") or {} + @callback def state_automation_listener(event: Event): """Listen for state changes and calls action.""" @@ -143,7 +147,7 @@ async def async_attach_trigger( call_action() return - variables = { + trigger_info = { "trigger": { "platform": "state", "entity_id": entity, @@ -151,6 +155,7 @@ async def async_attach_trigger( "to_state": to_s, } } + variables = {**_variables, **trigger_info} try: period[entity] = cv.positive_time_period( diff --git a/tests/components/homeassistant/triggers/test_state.py b/tests/components/homeassistant/triggers/test_state.py index dd98dbc429c..2cf2081f018 100644 --- a/tests/components/homeassistant/triggers/test_state.py +++ b/tests/components/homeassistant/triggers/test_state.py @@ -984,6 +984,33 @@ async def test_if_fires_on_change_with_for_template_3(hass, calls): assert len(calls) == 1 +async def test_if_fires_on_change_with_for_template_4(hass, calls): + """Test for firing on change with for template.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger_variables": {"seconds": 5}, + "trigger": { + "platform": "state", + "entity_id": "test.entity", + "to": "world", + "for": {"seconds": "{{ seconds }}"}, + }, + "action": {"service": "test.automation"}, + } + }, + ) + + hass.states.async_set("test.entity", "world") + 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 + + async def test_if_fires_on_change_from_with_for(hass, calls): """Test for firing on change with from/for.""" assert await async_setup_component( @@ -1269,3 +1296,64 @@ async def test_attribute_if_fires_on_entity_change_with_both_filters_boolean( hass.states.async_set("test.entity", "bla", {"happening": True}) await hass.async_block_till_done() assert len(calls) == 1 + + +async def test_variables_priority(hass, calls): + """Test an externally defined trigger variable is overridden.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger_variables": {"trigger": "illegal"}, + "trigger": { + "platform": "state", + "entity_id": ["test.entity_1", "test.entity_2"], + "to": "world", + "for": '{{ 5 if trigger.entity_id == "test.entity_1"' + " else 10 }}", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "{{ trigger.entity_id }} - {{ trigger.for }}" + }, + }, + } + }, + ) + await hass.async_block_till_done() + + utcnow = dt_util.utcnow() + with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: + mock_utcnow.return_value = utcnow + hass.states.async_set("test.entity_1", "world") + await hass.async_block_till_done() + mock_utcnow.return_value += timedelta(seconds=1) + async_fire_time_changed(hass, mock_utcnow.return_value) + hass.states.async_set("test.entity_2", "world") + await hass.async_block_till_done() + mock_utcnow.return_value += timedelta(seconds=1) + async_fire_time_changed(hass, mock_utcnow.return_value) + hass.states.async_set("test.entity_2", "hello") + await hass.async_block_till_done() + mock_utcnow.return_value += timedelta(seconds=1) + async_fire_time_changed(hass, mock_utcnow.return_value) + hass.states.async_set("test.entity_2", "world") + await hass.async_block_till_done() + assert len(calls) == 0 + mock_utcnow.return_value += timedelta(seconds=3) + async_fire_time_changed(hass, mock_utcnow.return_value) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "test.entity_1 - 0:00:05" + + mock_utcnow.return_value += timedelta(seconds=3) + async_fire_time_changed(hass, mock_utcnow.return_value) + await hass.async_block_till_done() + assert len(calls) == 1 + mock_utcnow.return_value += timedelta(seconds=5) + async_fire_time_changed(hass, mock_utcnow.return_value) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "test.entity_2 - 0:00:10" From eaa2d371a7b5e36f5a55660c1ae1c2b41bf72e95 Mon Sep 17 00:00:00 2001 From: Matteo Agnoletto Date: Mon, 8 Feb 2021 14:03:26 +0100 Subject: [PATCH 266/796] Add select selector for blueprints (#45803) Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- homeassistant/helpers/selector.py | 9 +++++++++ tests/helpers/test_selector.py | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index b48ffb6e964..68511a771d3 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -176,3 +176,12 @@ class StringSelector(Selector): """Selector for a multi-line text string.""" CONFIG_SCHEMA = vol.Schema({vol.Optional("multiline", default=False): bool}) + + +@SELECTORS.register("select") +class SelectSelector(Selector): + """Selector for an single-choice input select.""" + + CONFIG_SCHEMA = vol.Schema( + {vol.Required("options"): vol.All([str], vol.Length(min=1))} + ) diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 2916d616703..c43ed4097e0 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -187,3 +187,26 @@ def test_object_selector_schema(schema): def test_text_selector_schema(schema): """Test text selector.""" selector.validate_selector({"text": schema}) + + +@pytest.mark.parametrize( + "schema", + ({"options": ["red", "green", "blue"]},), +) +def test_select_selector_schema(schema): + """Test select selector.""" + selector.validate_selector({"select": schema}) + + +@pytest.mark.parametrize( + "schema", + ( + {}, + {"options": {"hello": "World"}}, + {"options": []}, + ), +) +def test_select_selector_schema_error(schema): + """Test select selector.""" + with pytest.raises(vol.Invalid): + selector.validate_selector({"select": schema}) From 0780e52ca455b394e54a92b441a266d74a237266 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 8 Feb 2021 14:06:27 +0100 Subject: [PATCH 267/796] Support templates in event triggers (#46207) * Support templates in event triggers * Don't validate trigger schemas twice --- .../homeassistant/triggers/event.py | 42 ++++++--- .../lutron_caseta/device_trigger.py | 20 ++--- .../components/shelly/device_trigger.py | 20 ++--- homeassistant/helpers/event.py | 2 +- .../homeassistant/triggers/test_event.py | 87 ++++++++++++++++++- 5 files changed, 135 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index b7ab081d266..7665ee1b4d7 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -3,7 +3,7 @@ import voluptuous as vol from homeassistant.const import CONF_PLATFORM from homeassistant.core import HassJob, callback -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, template # mypy: allow-untyped-defs @@ -14,9 +14,9 @@ CONF_EVENT_CONTEXT = "context" TRIGGER_SCHEMA = vol.Schema( { vol.Required(CONF_PLATFORM): "event", - vol.Required(CONF_EVENT_TYPE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_EVENT_DATA): dict, - vol.Optional(CONF_EVENT_CONTEXT): dict, + vol.Required(CONF_EVENT_TYPE): vol.All(cv.ensure_list, [cv.template]), + vol.Optional(CONF_EVENT_DATA): vol.All(dict, cv.template_complex), + vol.Optional(CONF_EVENT_CONTEXT): vol.All(dict, cv.template_complex), } ) @@ -32,25 +32,43 @@ async def async_attach_trigger( hass, config, action, automation_info, *, platform_type="event" ): """Listen for events based on configuration.""" - event_types = config.get(CONF_EVENT_TYPE) + variables = None + if automation_info: + variables = automation_info.get("variables") + + template.attach(hass, config[CONF_EVENT_TYPE]) + event_types = template.render_complex( + config[CONF_EVENT_TYPE], variables, limited=True + ) removes = [] event_data_schema = None - if config.get(CONF_EVENT_DATA): + if CONF_EVENT_DATA in config: + # Render the schema input + template.attach(hass, config[CONF_EVENT_DATA]) + event_data = {} + event_data.update( + template.render_complex(config[CONF_EVENT_DATA], variables, limited=True) + ) + # Build the schema event_data_schema = vol.Schema( - { - vol.Required(key): value - for key, value in config.get(CONF_EVENT_DATA).items() - }, + {vol.Required(key): value for key, value in event_data.items()}, extra=vol.ALLOW_EXTRA, ) event_context_schema = None - if config.get(CONF_EVENT_CONTEXT): + if CONF_EVENT_CONTEXT in config: + # Render the schema input + template.attach(hass, config[CONF_EVENT_CONTEXT]) + event_context = {} + event_context.update( + template.render_complex(config[CONF_EVENT_CONTEXT], variables, limited=True) + ) + # Build the schema event_context_schema = vol.Schema( { vol.Required(key): _schema_value(value) - for key, value in config.get(CONF_EVENT_CONTEXT).items() + for key, value in event_context.items() }, extra=vol.ALLOW_EXTRA, ) diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index 402db7286af..80d147191e6 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -265,17 +265,15 @@ async def async_attach_trigger( schema = DEVICE_TYPE_SCHEMA_MAP.get(device["type"]) valid_buttons = DEVICE_TYPE_SUBTYPE_MAP.get(device["type"]) config = schema(config) - event_config = event_trigger.TRIGGER_SCHEMA( - { - event_trigger.CONF_PLATFORM: CONF_EVENT, - event_trigger.CONF_EVENT_TYPE: LUTRON_CASETA_BUTTON_EVENT, - event_trigger.CONF_EVENT_DATA: { - ATTR_SERIAL: device["serial"], - ATTR_BUTTON_NUMBER: valid_buttons[config[CONF_SUBTYPE]], - ATTR_ACTION: config[CONF_TYPE], - }, - } - ) + event_config = { + event_trigger.CONF_PLATFORM: CONF_EVENT, + event_trigger.CONF_EVENT_TYPE: LUTRON_CASETA_BUTTON_EVENT, + event_trigger.CONF_EVENT_DATA: { + ATTR_SERIAL: device["serial"], + ATTR_BUTTON_NUMBER: valid_buttons[config[CONF_SUBTYPE]], + ATTR_ACTION: config[CONF_TYPE], + }, + } event_config = event_trigger.TRIGGER_SCHEMA(event_config) return await event_trigger.async_attach_trigger( hass, event_config, action, automation_info, platform_type="device" diff --git a/homeassistant/components/shelly/device_trigger.py b/homeassistant/components/shelly/device_trigger.py index f6cdfaee19f..9d4851c92a4 100644 --- a/homeassistant/components/shelly/device_trigger.py +++ b/homeassistant/components/shelly/device_trigger.py @@ -93,17 +93,15 @@ async def async_attach_trigger( ) -> CALLBACK_TYPE: """Attach a trigger.""" config = TRIGGER_SCHEMA(config) - event_config = event_trigger.TRIGGER_SCHEMA( - { - event_trigger.CONF_PLATFORM: CONF_EVENT, - event_trigger.CONF_EVENT_TYPE: EVENT_SHELLY_CLICK, - event_trigger.CONF_EVENT_DATA: { - ATTR_DEVICE_ID: config[CONF_DEVICE_ID], - ATTR_CHANNEL: INPUTS_EVENTS_SUBTYPES[config[CONF_SUBTYPE]], - ATTR_CLICK_TYPE: config[CONF_TYPE], - }, - } - ) + event_config = { + event_trigger.CONF_PLATFORM: CONF_EVENT, + event_trigger.CONF_EVENT_TYPE: EVENT_SHELLY_CLICK, + event_trigger.CONF_EVENT_DATA: { + ATTR_DEVICE_ID: config[CONF_DEVICE_ID], + ATTR_CHANNEL: INPUTS_EVENTS_SUBTYPES[config[CONF_SUBTYPE]], + ATTR_CLICK_TYPE: config[CONF_TYPE], + }, + } event_config = event_trigger.TRIGGER_SCHEMA(event_config) return await event_trigger.async_attach_trigger( hass, event_config, action, automation_info, platform_type="device" diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index da7f6cd52e8..44cbd89fde7 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -259,7 +259,7 @@ def async_track_state_change_event( hass.async_run_hass_job(job, event) except Exception: # pylint: disable=broad-except _LOGGER.exception( - "Error while processing state changed for %s", entity_id + "Error while processing state change for %s", entity_id ) hass.data[TRACK_STATE_CHANGE_LISTENER] = hass.bus.async_listen( diff --git a/tests/components/homeassistant/triggers/test_event.py b/tests/components/homeassistant/triggers/test_event.py index 8fedaac3815..f1ff3564065 100644 --- a/tests/components/homeassistant/triggers/test_event.py +++ b/tests/components/homeassistant/triggers/test_event.py @@ -17,7 +17,7 @@ def calls(hass): @pytest.fixture def context_with_user(): - """Track calls to a mock service.""" + """Create a context with default user_id.""" return Context(user_id="test_user_id") @@ -59,6 +59,39 @@ async def test_if_fires_on_event(hass, calls): assert len(calls) == 1 +async def test_if_fires_on_templated_event(hass, calls): + """Test the firing of events.""" + context = Context() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger_variables": {"event_type": "test_event"}, + "trigger": {"platform": "event", "event_type": "{{event_type}}"}, + "action": {"service": "test.automation"}, + } + }, + ) + + hass.bus.async_fire("test_event", context=context) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].context.parent_id == context.id + + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, + blocking=True, + ) + + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + + async def test_if_fires_on_multiple_events(hass, calls): """Test the firing of events.""" context = Context() @@ -161,6 +194,58 @@ async def test_if_fires_on_event_with_data_and_context(hass, calls, context_with assert len(calls) == 1 +async def test_if_fires_on_event_with_templated_data_and_context( + hass, calls, context_with_user +): + """Test the firing of events with templated data and context.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger_variables": { + "attr_1_val": "milk", + "attr_2_val": "beer", + "user_id": context_with_user.user_id, + }, + "trigger": { + "platform": "event", + "event_type": "test_event", + "event_data": { + "attr_1": "{{attr_1_val}}", + "attr_2": "{{attr_2_val}}", + }, + "context": {"user_id": "{{user_id}}"}, + }, + "action": {"service": "test.automation"}, + } + }, + ) + + hass.bus.async_fire( + "test_event", + {"attr_1": "milk", "another": "value", "attr_2": "beer"}, + context=context_with_user, + ) + await hass.async_block_till_done() + assert len(calls) == 1 + + hass.bus.async_fire( + "test_event", + {"attr_1": "milk", "another": "value"}, + context=context_with_user, + ) + await hass.async_block_till_done() + assert len(calls) == 1 # No new call + + hass.bus.async_fire( + "test_event", + {"attr_1": "milk", "another": "value", "attr_2": "beer"}, + ) + await hass.async_block_till_done() + assert len(calls) == 1 + + async def test_if_fires_on_event_with_empty_data_and_context_config( hass, calls, context_with_user ): From 48002f47f4bc3b41d2b73ab0f69ca12015e79bb2 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 8 Feb 2021 14:33:57 +0100 Subject: [PATCH 268/796] Use caplog fixture for log capturing (#46214) --- tests/components/automation/test_init.py | 17 ++++--- .../triggers/test_numeric_state.py | 21 +++++---- tests/helpers/test_condition.py | 24 +++++----- tests/helpers/test_script.py | 44 ++++++++++++------- 4 files changed, 64 insertions(+), 42 deletions(-) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 16d56c84cb0..3e498b52a08 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1,5 +1,6 @@ """The tests for the automation component.""" import asyncio +import logging from unittest.mock import Mock, patch import pytest @@ -152,7 +153,7 @@ async def test_two_triggers(hass, calls): assert len(calls) == 2 -async def test_trigger_service_ignoring_condition(hass, calls): +async def test_trigger_service_ignoring_condition(hass, caplog, calls): """Test triggers.""" assert await async_setup_component( hass, @@ -171,11 +172,15 @@ async def test_trigger_service_ignoring_condition(hass, calls): }, ) - with patch("homeassistant.components.automation.LOGGER.warning") as logwarn: - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(calls) == 0 - assert len(logwarn.mock_calls) == 1 + caplog.clear() + caplog.set_level(logging.WARNING) + + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 0 + + assert len(caplog.record_tuples) == 1 + assert caplog.record_tuples[0][1] == logging.WARNING await hass.services.async_call( "automation", "trigger", {"entity_id": "automation.test"}, blocking=True diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index e58bccb0bcc..85dc68c770d 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -1,5 +1,6 @@ """The tests for numeric state automation.""" from datetime import timedelta +import logging from unittest.mock import patch import pytest @@ -572,7 +573,7 @@ async def test_if_not_fires_if_entity_not_match(hass, calls, below): assert len(calls) == 0 -async def test_if_not_fires_and_warns_if_below_entity_unknown(hass, calls): +async def test_if_not_fires_and_warns_if_below_entity_unknown(hass, caplog, calls): """Test if warns with unknown below entity.""" assert await async_setup_component( hass, @@ -589,13 +590,15 @@ async def test_if_not_fires_and_warns_if_below_entity_unknown(hass, calls): }, ) - with patch( - "homeassistant.components.homeassistant.triggers.numeric_state._LOGGER.warning" - ) as logwarn: - hass.states.async_set("test.entity", 1) - await hass.async_block_till_done() - assert len(calls) == 0 - assert len(logwarn.mock_calls) == 1 + caplog.clear() + caplog.set_level(logging.WARNING) + + hass.states.async_set("test.entity", 1) + await hass.async_block_till_done() + assert len(calls) == 0 + + assert len(caplog.record_tuples) == 1 + assert caplog.record_tuples[0][1] == logging.WARNING @pytest.mark.parametrize("below", (10, "input_number.value_10")) @@ -1203,7 +1206,7 @@ async def test_wait_template_with_trigger(hass, calls, above): hass.states.async_set("test.entity", "8") await hass.async_block_till_done() assert len(calls) == 1 - assert "numeric_state - test.entity - 12" == calls[0].data["some"] + assert calls[0].data["some"] == "numeric_state - test.entity - 12" @pytest.mark.parametrize( diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 5c388aa6db4..cd4039f5262 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -1,5 +1,5 @@ """Test the condition helper.""" -from logging import ERROR +from logging import ERROR, WARNING from unittest.mock import patch import pytest @@ -338,23 +338,25 @@ async def test_time_using_input_datetime(hass): assert not condition.time(hass, before="input_datetime.not_existing") -async def test_if_numeric_state_raises_on_unavailable(hass): +async def test_if_numeric_state_raises_on_unavailable(hass, caplog): """Test numeric_state raises on unavailable/unknown state.""" test = await condition.async_from_config( hass, {"condition": "numeric_state", "entity_id": "sensor.temperature", "below": 42}, ) - with patch("homeassistant.helpers.condition._LOGGER.warning") as logwarn: - hass.states.async_set("sensor.temperature", "unavailable") - with pytest.raises(ConditionError): - test(hass) - assert len(logwarn.mock_calls) == 0 + caplog.clear() + caplog.set_level(WARNING) - hass.states.async_set("sensor.temperature", "unknown") - with pytest.raises(ConditionError): - test(hass) - assert len(logwarn.mock_calls) == 0 + hass.states.async_set("sensor.temperature", "unavailable") + with pytest.raises(ConditionError): + test(hass) + assert len(caplog.record_tuples) == 0 + + hass.states.async_set("sensor.temperature", "unknown") + with pytest.raises(ConditionError): + test(hass) + assert len(caplog.record_tuples) == 0 async def test_state_multiple_entities(hass): diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 003e903de14..5cd9a9d2449 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -990,7 +990,7 @@ async def test_wait_for_trigger_generated_exception(hass, caplog): assert "something bad" in caplog.text -async def test_condition_warning(hass): +async def test_condition_warning(hass, caplog): """Test warning on condition.""" event = "test_event" events = async_capture_events(hass, event) @@ -1007,11 +1007,15 @@ async def test_condition_warning(hass): ) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + caplog.clear() + caplog.set_level(logging.WARNING) + hass.states.async_set("test.entity", "string") - with patch("homeassistant.helpers.script._LOGGER.warning") as logwarn: - await script_obj.async_run(context=Context()) - await hass.async_block_till_done() - assert len(logwarn.mock_calls) == 1 + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + assert len(caplog.record_tuples) == 1 + assert caplog.record_tuples[0][1] == logging.WARNING assert len(events) == 1 @@ -1127,7 +1131,7 @@ async def test_repeat_count(hass): @pytest.mark.parametrize("condition", ["while", "until"]) -async def test_repeat_condition_warning(hass, condition): +async def test_repeat_condition_warning(hass, caplog, condition): """Test warning on repeat conditions.""" event = "test_event" events = async_capture_events(hass, event) @@ -1156,10 +1160,14 @@ async def test_repeat_condition_warning(hass, condition): # wait_started = async_watch_for_action(script_obj, "wait") hass.states.async_set("sensor.test", "1") - with patch("homeassistant.helpers.script._LOGGER.warning") as logwarn: - hass.async_create_task(script_obj.async_run(context=Context())) - await asyncio.wait_for(hass.async_block_till_done(), 1) - assert len(logwarn.mock_calls) == 1 + caplog.clear() + caplog.set_level(logging.WARNING) + + hass.async_create_task(script_obj.async_run(context=Context())) + await asyncio.wait_for(hass.async_block_till_done(), 1) + + assert len(caplog.record_tuples) == 1 + assert caplog.record_tuples[0][1] == logging.WARNING assert len(events) == count @@ -1369,7 +1377,7 @@ async def test_repeat_nested(hass, variables, first_last, inside_x): } -async def test_choose_warning(hass): +async def test_choose_warning(hass, caplog): """Test warning on choose.""" event = "test_event" events = async_capture_events(hass, event) @@ -1404,11 +1412,15 @@ async def test_choose_warning(hass): hass.states.async_set("test.entity", "9") await hass.async_block_till_done() - with patch("homeassistant.helpers.script._LOGGER.warning") as logwarn: - await script_obj.async_run(context=Context()) - await hass.async_block_till_done() - print(logwarn.mock_calls) - assert len(logwarn.mock_calls) == 2 + caplog.clear() + caplog.set_level(logging.WARNING) + + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + assert len(caplog.record_tuples) == 2 + assert caplog.record_tuples[0][1] == logging.WARNING + assert caplog.record_tuples[1][1] == logging.WARNING assert len(events) == 1 assert events[0].data["choice"] == "default" From 6f446cf627263fb9cb0d1a5be73f1986ce50dbc4 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 8 Feb 2021 14:44:46 +0100 Subject: [PATCH 269/796] Add my component (#46058) Co-authored-by: Franck Nijhof Co-authored-by: Martin Hjelmare --- CODEOWNERS | 1 + .../components/default_config/manifest.json | 1 + homeassistant/components/http/__init__.py | 7 ++++--- homeassistant/components/my/__init__.py | 12 ++++++++++++ homeassistant/components/my/manifest.json | 7 +++++++ tests/components/http/test_init.py | 5 ++++- tests/components/my/__init__.py | 1 + tests/components/my/test_init.py | 17 +++++++++++++++++ 8 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/my/__init__.py create mode 100644 homeassistant/components/my/manifest.json create mode 100644 tests/components/my/__init__.py create mode 100644 tests/components/my/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 8785ce382cb..7c45a66e166 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -287,6 +287,7 @@ homeassistant/components/motion_blinds/* @starkillerOG homeassistant/components/mpd/* @fabaff homeassistant/components/mqtt/* @home-assistant/core @emontnemery homeassistant/components/msteams/* @peroyvind +homeassistant/components/my/* @home-assistant/core homeassistant/components/myq/* @bdraco homeassistant/components/mysensors/* @MartinHjelmare @functionpointer homeassistant/components/mystrom/* @fabaff diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index f8be3c9fe2a..0f4b940cc36 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -18,6 +18,7 @@ "map", "media_source", "mobile_app", + "my", "person", "scene", "script", diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 7f70d49f686..63c88427a5b 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -58,8 +58,9 @@ SSL_INTERMEDIATE = "intermediate" _LOGGER = logging.getLogger(__name__) DEFAULT_DEVELOPMENT = "0" -# To be able to load custom cards. -DEFAULT_CORS = "https://cast.home-assistant.io" +# Cast to be able to load custom cards. +# My to be able to check url and version info. +DEFAULT_CORS = ["https://cast.home-assistant.io", "https://my.home-assistant.io"] NO_LOGIN_ATTEMPT_THRESHOLD = -1 MAX_CLIENT_SIZE: int = 1024 ** 2 * 16 @@ -80,7 +81,7 @@ HTTP_SCHEMA = vol.All( vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile, vol.Optional(CONF_SSL_PEER_CERTIFICATE): cv.isfile, vol.Optional(CONF_SSL_KEY): cv.isfile, - vol.Optional(CONF_CORS_ORIGINS, default=[DEFAULT_CORS]): vol.All( + vol.Optional(CONF_CORS_ORIGINS, default=DEFAULT_CORS): vol.All( cv.ensure_list, [cv.string] ), vol.Inclusive(CONF_USE_X_FORWARDED_FOR, "proxy"): cv.boolean, diff --git a/homeassistant/components/my/__init__.py b/homeassistant/components/my/__init__.py new file mode 100644 index 00000000000..8cc725cb9a5 --- /dev/null +++ b/homeassistant/components/my/__init__.py @@ -0,0 +1,12 @@ +"""Support for my.home-assistant.io redirect service.""" + +DOMAIN = "my" +URL_PATH = "_my_redirect" + + +async def async_setup(hass, config): + """Register hidden _my_redirect panel.""" + hass.components.frontend.async_register_built_in_panel( + DOMAIN, frontend_url_path=URL_PATH + ) + return True diff --git a/homeassistant/components/my/manifest.json b/homeassistant/components/my/manifest.json new file mode 100644 index 00000000000..3b9e253f353 --- /dev/null +++ b/homeassistant/components/my/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "my", + "name": "My Home Assistant", + "documentation": "https://www.home-assistant.io/integrations/my", + "dependencies": ["frontend"], + "codeowners": ["@home-assistant/core"] +} diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 3dd587cd7a4..e7a3884c481 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -242,7 +242,10 @@ async def test_cors_defaults(hass): assert await async_setup_component(hass, "http", {}) assert len(mock_setup.mock_calls) == 1 - assert mock_setup.mock_calls[0][1][1] == ["https://cast.home-assistant.io"] + assert mock_setup.mock_calls[0][1][1] == [ + "https://cast.home-assistant.io", + "https://my.home-assistant.io", + ] async def test_storing_config(hass, aiohttp_client, aiohttp_unused_port): diff --git a/tests/components/my/__init__.py b/tests/components/my/__init__.py new file mode 100644 index 00000000000..82953c8dac2 --- /dev/null +++ b/tests/components/my/__init__.py @@ -0,0 +1 @@ +"""Tests for the my component.""" diff --git a/tests/components/my/test_init.py b/tests/components/my/test_init.py new file mode 100644 index 00000000000..86929271be9 --- /dev/null +++ b/tests/components/my/test_init.py @@ -0,0 +1,17 @@ +"""Test the my init.""" + +from unittest import mock + +from homeassistant.components.my import URL_PATH +from homeassistant.setup import async_setup_component + + +async def test_setup(hass): + """Test setup.""" + with mock.patch( + "homeassistant.components.frontend.async_register_built_in_panel" + ) as mock_register_panel: + assert await async_setup_component(hass, "my", {"foo": "bar"}) + assert mock_register_panel.call_args == mock.call( + hass, "my", frontend_url_path=URL_PATH + ) From 568180632edfca977395d8a33f81ececa614250b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 8 Feb 2021 15:00:17 +0100 Subject: [PATCH 270/796] Fix sync oath2 scaffold template (#46219) --- .../config_flow_oauth2/integration/__init__.py | 2 +- .../templates/config_flow_oauth2/integration/api.py | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py index 20b5a03206e..c51061b57fe 100644 --- a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py +++ b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) # If using a requests-based API lib - hass.data[DOMAIN][entry.entry_id] = api.ConfigEntryAuth(hass, entry, session) + hass.data[DOMAIN][entry.entry_id] = api.ConfigEntryAuth(hass, session) # If using an aiohttp-based API lib hass.data[DOMAIN][entry.entry_id] = api.AsyncConfigEntryAuth( diff --git a/script/scaffold/templates/config_flow_oauth2/integration/api.py b/script/scaffold/templates/config_flow_oauth2/integration/api.py index 710c76600fb..50b54399579 100644 --- a/script/scaffold/templates/config_flow_oauth2/integration/api.py +++ b/script/scaffold/templates/config_flow_oauth2/integration/api.py @@ -4,7 +4,7 @@ from asyncio import run_coroutine_threadsafe from aiohttp import ClientSession import my_pypi_package -from homeassistant import config_entries, core +from homeassistant import core from homeassistant.helpers import config_entry_oauth2_flow # TODO the following two API examples are based on our suggested best practices @@ -18,15 +18,11 @@ class ConfigEntryAuth(my_pypi_package.AbstractAuth): def __init__( self, hass: core.HomeAssistant, - config_entry: config_entries.ConfigEntry, - implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, + oauth_session: config_entry_oauth2_flow.OAuth2Session, ): """Initialize NEW_NAME Auth.""" self.hass = hass - self.config_entry = config_entry - self.session = config_entry_oauth2_flow.OAuth2Session( - hass, config_entry, implementation - ) + self.session = oauth_session super().__init__(self.session.token) def refresh_tokens(self) -> str: From 48808978c422216250f8cfb0aa0fc6b91fe5884b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 8 Feb 2021 15:05:11 +0100 Subject: [PATCH 271/796] Upgrade pre-commit to 2.10.1 (#46211) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 077c895293f..4683c927085 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,7 +9,7 @@ coverage==5.4 jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.800 -pre-commit==2.10.0 +pre-commit==2.10.1 pylint==2.6.0 astroid==2.4.2 pipdeptree==1.0.0 From 2811e39c5cdfb210275b04f489d187cc9b2168a6 Mon Sep 17 00:00:00 2001 From: Joeri <2417500+yurnih@users.noreply.github.com> Date: Mon, 8 Feb 2021 15:18:36 +0100 Subject: [PATCH 272/796] Add entity specific force_update for DSMR (#46111) --- homeassistant/components/dsmr/sensor.py | 97 ++++++++++++++++--------- tests/components/dsmr/test_sensor.py | 2 +- 2 files changed, 65 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 78cd317bb3e..897fcd4e77b 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -80,35 +80,59 @@ async def async_setup_entry( dsmr_version = config[CONF_DSMR_VERSION] - # Define list of name,obis mappings to generate entities + # Define list of name,obis,force_update mappings to generate entities obis_mapping = [ - ["Power Consumption", obis_ref.CURRENT_ELECTRICITY_USAGE], - ["Power Production", obis_ref.CURRENT_ELECTRICITY_DELIVERY], - ["Power Tariff", obis_ref.ELECTRICITY_ACTIVE_TARIFF], - ["Energy Consumption (tarif 1)", obis_ref.ELECTRICITY_USED_TARIFF_1], - ["Energy Consumption (tarif 2)", obis_ref.ELECTRICITY_USED_TARIFF_2], - ["Energy Production (tarif 1)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_1], - ["Energy Production (tarif 2)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_2], - ["Power Consumption Phase L1", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE], - ["Power Consumption Phase L2", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE], - ["Power Consumption Phase L3", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE], - ["Power Production Phase L1", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE], - ["Power Production Phase L2", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE], - ["Power Production Phase L3", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE], - ["Short Power Failure Count", obis_ref.SHORT_POWER_FAILURE_COUNT], - ["Long Power Failure Count", obis_ref.LONG_POWER_FAILURE_COUNT], - ["Voltage Sags Phase L1", obis_ref.VOLTAGE_SAG_L1_COUNT], - ["Voltage Sags Phase L2", obis_ref.VOLTAGE_SAG_L2_COUNT], - ["Voltage Sags Phase L3", obis_ref.VOLTAGE_SAG_L3_COUNT], - ["Voltage Swells Phase L1", obis_ref.VOLTAGE_SWELL_L1_COUNT], - ["Voltage Swells Phase L2", obis_ref.VOLTAGE_SWELL_L2_COUNT], - ["Voltage Swells Phase L3", obis_ref.VOLTAGE_SWELL_L3_COUNT], - ["Voltage Phase L1", obis_ref.INSTANTANEOUS_VOLTAGE_L1], - ["Voltage Phase L2", obis_ref.INSTANTANEOUS_VOLTAGE_L2], - ["Voltage Phase L3", obis_ref.INSTANTANEOUS_VOLTAGE_L3], - ["Current Phase L1", obis_ref.INSTANTANEOUS_CURRENT_L1], - ["Current Phase L2", obis_ref.INSTANTANEOUS_CURRENT_L2], - ["Current Phase L3", obis_ref.INSTANTANEOUS_CURRENT_L3], + ["Power Consumption", obis_ref.CURRENT_ELECTRICITY_USAGE, True], + ["Power Production", obis_ref.CURRENT_ELECTRICITY_DELIVERY, True], + ["Power Tariff", obis_ref.ELECTRICITY_ACTIVE_TARIFF, False], + ["Energy Consumption (tarif 1)", obis_ref.ELECTRICITY_USED_TARIFF_1, True], + ["Energy Consumption (tarif 2)", obis_ref.ELECTRICITY_USED_TARIFF_2, True], + ["Energy Production (tarif 1)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_1, True], + ["Energy Production (tarif 2)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_2, True], + [ + "Power Consumption Phase L1", + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, + False, + ], + [ + "Power Consumption Phase L2", + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, + False, + ], + [ + "Power Consumption Phase L3", + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, + False, + ], + [ + "Power Production Phase L1", + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, + False, + ], + [ + "Power Production Phase L2", + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, + False, + ], + [ + "Power Production Phase L3", + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, + False, + ], + ["Short Power Failure Count", obis_ref.SHORT_POWER_FAILURE_COUNT, False], + ["Long Power Failure Count", obis_ref.LONG_POWER_FAILURE_COUNT, False], + ["Voltage Sags Phase L1", obis_ref.VOLTAGE_SAG_L1_COUNT, False], + ["Voltage Sags Phase L2", obis_ref.VOLTAGE_SAG_L2_COUNT, False], + ["Voltage Sags Phase L3", obis_ref.VOLTAGE_SAG_L3_COUNT, False], + ["Voltage Swells Phase L1", obis_ref.VOLTAGE_SWELL_L1_COUNT, False], + ["Voltage Swells Phase L2", obis_ref.VOLTAGE_SWELL_L2_COUNT, False], + ["Voltage Swells Phase L3", obis_ref.VOLTAGE_SWELL_L3_COUNT, False], + ["Voltage Phase L1", obis_ref.INSTANTANEOUS_VOLTAGE_L1, False], + ["Voltage Phase L2", obis_ref.INSTANTANEOUS_VOLTAGE_L2, False], + ["Voltage Phase L3", obis_ref.INSTANTANEOUS_VOLTAGE_L3, False], + ["Current Phase L1", obis_ref.INSTANTANEOUS_CURRENT_L1, False], + ["Current Phase L2", obis_ref.INSTANTANEOUS_CURRENT_L2, False], + ["Current Phase L3", obis_ref.INSTANTANEOUS_CURRENT_L3, False], ] if dsmr_version == "5L": @@ -117,22 +141,26 @@ async def async_setup_entry( [ "Energy Consumption (total)", obis_ref.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL, + True, ], [ "Energy Production (total)", obis_ref.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL, + True, ], ] ) else: obis_mapping.extend( - [["Energy Consumption (total)", obis_ref.ELECTRICITY_IMPORTED_TOTAL]] + [["Energy Consumption (total)", obis_ref.ELECTRICITY_IMPORTED_TOTAL, True]] ) # Generate device entities devices = [ - DSMREntity(name, DEVICE_NAME_ENERGY, config[CONF_SERIAL_ID], obis, config) - for name, obis in obis_mapping + DSMREntity( + name, DEVICE_NAME_ENERGY, config[CONF_SERIAL_ID], obis, config, force_update + ) + for name, obis, force_update in obis_mapping ] # Protocol version specific obis @@ -152,6 +180,7 @@ async def async_setup_entry( config[CONF_SERIAL_ID_GAS], gas_obis, config, + True, ), DerivativeDSMREntity( "Hourly Gas Consumption", @@ -159,6 +188,7 @@ async def async_setup_entry( config[CONF_SERIAL_ID_GAS], gas_obis, config, + False, ), ] @@ -257,7 +287,7 @@ async def async_setup_entry( class DSMREntity(Entity): """Entity reading values from DSMR telegram.""" - def __init__(self, name, device_name, device_serial, obis, config): + def __init__(self, name, device_name, device_serial, obis, config, force_update): """Initialize entity.""" self._name = name self._obis = obis @@ -266,6 +296,7 @@ class DSMREntity(Entity): self._device_name = device_name self._device_serial = device_serial + self._force_update = force_update self._unique_id = f"{device_serial}_{name}".replace(" ", "_") @callback @@ -341,7 +372,7 @@ class DSMREntity(Entity): @property def force_update(self): """Force update.""" - return True + return self._force_update @property def should_poll(self): diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index dde66c6bfb7..31c4f2be8db 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -183,7 +183,7 @@ async def test_derivative(): config = {"platform": "dsmr"} - entity = DerivativeDSMREntity("test", "test_device", "5678", "1.0.0", config) + entity = DerivativeDSMREntity("test", "test_device", "5678", "1.0.0", config, False) await entity.async_update() assert entity.state is None, "initial state not unknown" From b1ffe429cdda8613f5fa204642ac4f9a9d1ed319 Mon Sep 17 00:00:00 2001 From: Henco Appel Date: Mon, 8 Feb 2021 14:24:18 +0000 Subject: [PATCH 273/796] Fix BT Smarthub device tracker (#44813) --- .../components/bt_smarthub/device_tracker.py | 46 +++++++++---------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/bt_smarthub/device_tracker.py b/homeassistant/components/bt_smarthub/device_tracker.py index 383f724decd..107eb5598d9 100644 --- a/homeassistant/components/bt_smarthub/device_tracker.py +++ b/homeassistant/components/bt_smarthub/device_tracker.py @@ -1,4 +1,5 @@ """Support for BT Smart Hub (Sometimes referred to as BT Home Hub 6).""" +from collections import namedtuple import logging from btsmarthub_devicelist import BTSmartHub @@ -31,19 +32,30 @@ def get_scanner(hass, config): smarthub_client = BTSmartHub( router_ip=info[CONF_HOST], smarthub_model=info.get(CONF_SMARTHUB_MODEL) ) - scanner = BTSmartHubScanner(smarthub_client) - return scanner if scanner.success_init else None +def _create_device(data): + """Create new device from the dict.""" + ip_address = data.get("IPAddress") + mac = data.get("PhysAddress") + host = data.get("UserHostName") + status = data.get("Active") + name = data.get("name") + return _Device(ip_address, mac, host, status, name) + + +_Device = namedtuple("_Device", ["ip_address", "mac", "host", "status", "name"]) + + class BTSmartHubScanner(DeviceScanner): """This class queries a BT Smart Hub.""" def __init__(self, smarthub_client): """Initialise the scanner.""" self.smarthub = smarthub_client - self.last_results = {} + self.last_results = [] self.success_init = False # Test the router is accessible @@ -56,15 +68,15 @@ class BTSmartHubScanner(DeviceScanner): def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" self._update_info() - return [client["mac"] for client in self.last_results] + return [device.mac for device in self.last_results] def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" if not self.last_results: return None - for client in self.last_results: - if client["mac"] == device: - return client["host"] + for result_device in self.last_results: + if result_device.mac == device: + return result_device.name or result_device.host return None def _update_info(self): @@ -77,26 +89,10 @@ class BTSmartHubScanner(DeviceScanner): if not data: _LOGGER.warning("Error scanning devices") return - - clients = list(data.values()) - self.last_results = clients + self.last_results = data def get_bt_smarthub_data(self): """Retrieve data from BT Smart Hub and return parsed result.""" - # Request data from bt smarthub into a list of dicts. data = self.smarthub.get_devicelist(only_active_devices=True) - - # Renaming keys from parsed result. - devices = {} - for device in data: - try: - devices[device["UserHostName"]] = { - "ip": device["IPAddress"], - "mac": device["PhysAddress"], - "host": device["UserHostName"], - "status": device["Active"], - } - except KeyError: - pass - return devices + return [_create_device(d) for d in data if d.get("PhysAddress")] From 8f4ea3818d7f4ec843dcf7af9b97f30bd068171f Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Mon, 8 Feb 2021 14:25:54 +0000 Subject: [PATCH 274/796] Add unavailable to Vera (#46064) --- homeassistant/components/vera/__init__.py | 10 ++++++++++ homeassistant/components/vera/binary_sensor.py | 1 + homeassistant/components/vera/light.py | 1 + homeassistant/components/vera/manifest.json | 2 +- homeassistant/components/vera/sensor.py | 2 +- homeassistant/components/vera/switch.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/vera/test_binary_sensor.py | 1 + tests/components/vera/test_climate.py | 2 ++ tests/components/vera/test_cover.py | 1 + tests/components/vera/test_light.py | 1 + tests/components/vera/test_lock.py | 1 + tests/components/vera/test_sensor.py | 2 ++ tests/components/vera/test_switch.py | 1 + 15 files changed, 26 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 3fd1c189b63..4bfa72b5eb6 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -232,6 +232,11 @@ class VeraDevice(Generic[DeviceType], Entity): """Update the state.""" self.schedule_update_ha_state(True) + def update(self): + """Force a refresh from the device if the device is unavailable.""" + if not self.available: + self.vera_device.refresh() + @property def name(self) -> str: """Return the name of the device.""" @@ -276,6 +281,11 @@ class VeraDevice(Generic[DeviceType], Entity): return attr + @property + def available(self): + """If device communications have failed return false.""" + return not self.vera_device.comm_failure + @property def unique_id(self) -> str: """Return a unique ID. diff --git a/homeassistant/components/vera/binary_sensor.py b/homeassistant/components/vera/binary_sensor.py index 7932fa14f4c..00d4fb3a758 100644 --- a/homeassistant/components/vera/binary_sensor.py +++ b/homeassistant/components/vera/binary_sensor.py @@ -50,4 +50,5 @@ class VeraBinarySensor(VeraDevice[veraApi.VeraBinarySensor], BinarySensorEntity) def update(self) -> None: """Get the latest data and update the state.""" + super().update() self._state = self.vera_device.is_tripped diff --git a/homeassistant/components/vera/light.py b/homeassistant/components/vera/light.py index c52627d340f..30c4e93a2ba 100644 --- a/homeassistant/components/vera/light.py +++ b/homeassistant/components/vera/light.py @@ -93,6 +93,7 @@ class VeraLight(VeraDevice[veraApi.VeraDimmer], LightEntity): def update(self) -> None: """Call to update state.""" + super().update() self._state = self.vera_device.is_switched_on() if self.vera_device.is_dimmable: # If it is dimmable, both functions exist. In case color diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index 264f44782f5..1f180b39750 100644 --- a/homeassistant/components/vera/manifest.json +++ b/homeassistant/components/vera/manifest.json @@ -3,6 +3,6 @@ "name": "Vera", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/vera", - "requirements": ["pyvera==0.3.11"], + "requirements": ["pyvera==0.3.13"], "codeowners": ["@vangorra"] } diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py index ea7dbf0ae30..007290807e6 100644 --- a/homeassistant/components/vera/sensor.py +++ b/homeassistant/components/vera/sensor.py @@ -68,7 +68,7 @@ class VeraSensor(VeraDevice[veraApi.VeraSensor], Entity): def update(self) -> None: """Update the state.""" - + super().update() if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR: self.current_value = self.vera_device.temperature diff --git a/homeassistant/components/vera/switch.py b/homeassistant/components/vera/switch.py index 5dfeba6f5b2..f567893e5b0 100644 --- a/homeassistant/components/vera/switch.py +++ b/homeassistant/components/vera/switch.py @@ -70,4 +70,5 @@ class VeraSwitch(VeraDevice[veraApi.VeraSwitch], SwitchEntity): def update(self) -> None: """Update device state.""" + super().update() self._state = self.vera_device.is_switched_on() diff --git a/requirements_all.txt b/requirements_all.txt index 04dbcdf9d1a..6a8b5f63fa2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1880,7 +1880,7 @@ pyuptimerobot==0.0.5 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.3.11 +pyvera==0.3.13 # homeassistant.components.versasense pyversasense==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 548d6ba1117..54f6651ad0c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -953,7 +953,7 @@ pytraccar==0.9.0 pytradfri[async]==7.0.6 # homeassistant.components.vera -pyvera==0.3.11 +pyvera==0.3.13 # homeassistant.components.vesync pyvesync==1.2.0 diff --git a/tests/components/vera/test_binary_sensor.py b/tests/components/vera/test_binary_sensor.py index 1bcb8d1a183..b3b8d2d6ae1 100644 --- a/tests/components/vera/test_binary_sensor.py +++ b/tests/components/vera/test_binary_sensor.py @@ -14,6 +14,7 @@ async def test_binary_sensor( """Test function.""" vera_device = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor vera_device.device_id = 1 + vera_device.comm_failure = False vera_device.vera_device_id = vera_device.device_id vera_device.name = "dev1" vera_device.is_tripped = False diff --git a/tests/components/vera/test_climate.py b/tests/components/vera/test_climate.py index 076b51997a0..5ec39f07953 100644 --- a/tests/components/vera/test_climate.py +++ b/tests/components/vera/test_climate.py @@ -23,6 +23,7 @@ async def test_climate( vera_device = MagicMock(spec=pv.VeraThermostat) # type: pv.VeraThermostat vera_device.device_id = 1 vera_device.vera_device_id = vera_device.device_id + vera_device.comm_failure = False vera_device.name = "dev1" vera_device.category = pv.CATEGORY_THERMOSTAT vera_device.power = 10 @@ -133,6 +134,7 @@ async def test_climate_f( vera_device = MagicMock(spec=pv.VeraThermostat) # type: pv.VeraThermostat vera_device.device_id = 1 vera_device.vera_device_id = vera_device.device_id + vera_device.comm_failure = False vera_device.name = "dev1" vera_device.category = pv.CATEGORY_THERMOSTAT vera_device.power = 10 diff --git a/tests/components/vera/test_cover.py b/tests/components/vera/test_cover.py index 0c05d84e2db..cfc33fb2dcf 100644 --- a/tests/components/vera/test_cover.py +++ b/tests/components/vera/test_cover.py @@ -15,6 +15,7 @@ async def test_cover( vera_device = MagicMock(spec=pv.VeraCurtain) # type: pv.VeraCurtain vera_device.device_id = 1 vera_device.vera_device_id = vera_device.device_id + vera_device.comm_failure = False vera_device.name = "dev1" vera_device.category = pv.CATEGORY_CURTAIN vera_device.is_closed = False diff --git a/tests/components/vera/test_light.py b/tests/components/vera/test_light.py index 3b14aba7429..ad5ad7e0259 100644 --- a/tests/components/vera/test_light.py +++ b/tests/components/vera/test_light.py @@ -16,6 +16,7 @@ async def test_light( vera_device = MagicMock(spec=pv.VeraDimmer) # type: pv.VeraDimmer vera_device.device_id = 1 vera_device.vera_device_id = vera_device.device_id + vera_device.comm_failure = False vera_device.name = "dev1" vera_device.category = pv.CATEGORY_DIMMER vera_device.is_switched_on = MagicMock(return_value=False) diff --git a/tests/components/vera/test_lock.py b/tests/components/vera/test_lock.py index c288ac8709e..171f799f87b 100644 --- a/tests/components/vera/test_lock.py +++ b/tests/components/vera/test_lock.py @@ -16,6 +16,7 @@ async def test_lock( vera_device = MagicMock(spec=pv.VeraLock) # type: pv.VeraLock vera_device.device_id = 1 vera_device.vera_device_id = vera_device.device_id + vera_device.comm_failure = False vera_device.name = "dev1" vera_device.category = pv.CATEGORY_LOCK vera_device.is_locked = MagicMock(return_value=False) diff --git a/tests/components/vera/test_sensor.py b/tests/components/vera/test_sensor.py index 43777642816..62639df3a35 100644 --- a/tests/components/vera/test_sensor.py +++ b/tests/components/vera/test_sensor.py @@ -23,6 +23,7 @@ async def run_sensor_test( vera_device = MagicMock(spec=pv.VeraSensor) # type: pv.VeraSensor vera_device.device_id = 1 vera_device.vera_device_id = vera_device.device_id + vera_device.comm_failure = False vera_device.name = "dev1" vera_device.category = category setattr(vera_device, class_property, "33") @@ -178,6 +179,7 @@ async def test_scene_controller_sensor( vera_device = MagicMock(spec=pv.VeraSensor) # type: pv.VeraSensor vera_device.device_id = 1 vera_device.vera_device_id = vera_device.device_id + vera_device.comm_failure = False vera_device.name = "dev1" vera_device.category = pv.CATEGORY_SCENE_CONTROLLER vera_device.get_last_scene_id = MagicMock(return_value="id0") diff --git a/tests/components/vera/test_switch.py b/tests/components/vera/test_switch.py index b61564c56bc..ac90edc9ded 100644 --- a/tests/components/vera/test_switch.py +++ b/tests/components/vera/test_switch.py @@ -15,6 +15,7 @@ async def test_switch( vera_device = MagicMock(spec=pv.VeraSwitch) # type: pv.VeraSwitch vera_device.device_id = 1 vera_device.vera_device_id = vera_device.device_id + vera_device.comm_failure = False vera_device.name = "dev1" vera_device.category = pv.CATEGORY_SWITCH vera_device.is_switched_on = MagicMock(return_value=False) From 81c88cd63914bd47b403e4e170f53dce3f573c67 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 8 Feb 2021 17:02:12 +0200 Subject: [PATCH 275/796] Enhance MQTT cover platform (#46059) * Enhance MQTT cover platform Allow combining of position and state of MQTT cover Add template and fix optimistic in set tilt position Add tests * Add abbreviations * Add tests and stopped state * Cleanup & fix range for templates * Apply suggestions from code review Co-authored-by: Erik Montnemery --- .../components/mqtt/abbreviations.py | 3 + homeassistant/components/mqtt/cover.py | 108 ++++-- tests/components/mqtt/test_cover.py | 347 ++++++++++++++++++ 3 files changed, 434 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 4b209f6f364..8868d487f93 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -136,6 +136,7 @@ ABBREVIATIONS = { "set_pos_tpl": "set_position_template", "set_pos_t": "set_position_topic", "pos_t": "position_topic", + "pos_tpl": "position_template", "spd_cmd_t": "speed_command_topic", "spd_stat_t": "speed_state_topic", "spd_val_tpl": "speed_value_template", @@ -147,6 +148,7 @@ ABBREVIATIONS = { "stat_on": "state_on", "stat_open": "state_open", "stat_opening": "state_opening", + "stat_stopped": "state_stopped", "stat_locked": "state_locked", "stat_unlocked": "state_unlocked", "stat_t": "state_topic", @@ -173,6 +175,7 @@ ABBREVIATIONS = { "temp_unit": "temperature_unit", "tilt_clsd_val": "tilt_closed_value", "tilt_cmd_t": "tilt_command_topic", + "tilt_cmd_tpl": "tilt_command_template", "tilt_inv_stat": "tilt_invert_state", "tilt_max": "tilt_max", "tilt_min": "tilt_min", diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index dc2cba0efab..4b428027a4d 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -59,9 +59,11 @@ from .mixins import ( _LOGGER = logging.getLogger(__name__) CONF_GET_POSITION_TOPIC = "position_topic" -CONF_SET_POSITION_TEMPLATE = "set_position_template" +CONF_GET_POSITION_TEMPLATE = "position_template" CONF_SET_POSITION_TOPIC = "set_position_topic" +CONF_SET_POSITION_TEMPLATE = "set_position_template" CONF_TILT_COMMAND_TOPIC = "tilt_command_topic" +CONF_TILT_COMMAND_TEMPLATE = "tilt_command_template" CONF_TILT_STATUS_TOPIC = "tilt_status_topic" CONF_TILT_STATUS_TEMPLATE = "tilt_status_template" @@ -74,6 +76,7 @@ CONF_STATE_CLOSED = "state_closed" CONF_STATE_CLOSING = "state_closing" CONF_STATE_OPEN = "state_open" CONF_STATE_OPENING = "state_opening" +CONF_STATE_STOPPED = "state_stopped" CONF_TILT_CLOSED_POSITION = "tilt_closed_value" CONF_TILT_INVERT_STATE = "tilt_invert_state" CONF_TILT_MAX = "tilt_max" @@ -92,6 +95,7 @@ DEFAULT_PAYLOAD_STOP = "STOP" DEFAULT_POSITION_CLOSED = 0 DEFAULT_POSITION_OPEN = 100 DEFAULT_RETAIN = False +DEFAULT_STATE_STOPPED = "stopped" DEFAULT_TILT_CLOSED_POSITION = 0 DEFAULT_TILT_INVERT_STATE = False DEFAULT_TILT_MAX = 100 @@ -115,8 +119,27 @@ def validate_options(value): """ if CONF_SET_POSITION_TOPIC in value and CONF_GET_POSITION_TOPIC not in value: raise vol.Invalid( - "set_position_topic must be set together with position_topic." + "'set_position_topic' must be set together with 'position_topic'." ) + + if ( + CONF_GET_POSITION_TOPIC in value + and CONF_STATE_TOPIC not in value + and CONF_VALUE_TEMPLATE in value + ): + _LOGGER.warning( + "using 'value_template' for 'position_topic' is deprecated " + "and will be removed from Home Assistant in version 2021.6" + "please replace it with 'position_template'" + ) + + if CONF_TILT_INVERT_STATE in value: + _LOGGER.warning( + "'tilt_invert_state' is deprecated " + "and will be removed from Home Assistant in version 2021.6" + "please invert tilt using 'tilt_min' & 'tilt_max'" + ) + return value @@ -143,6 +166,7 @@ PLATFORM_SCHEMA = vol.All( vol.Optional(CONF_STATE_CLOSING, default=STATE_CLOSING): cv.string, vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string, vol.Optional(CONF_STATE_OPENING, default=STATE_OPENING): cv.string, + vol.Optional(CONF_STATE_STOPPED, default=DEFAULT_STATE_STOPPED): cv.string, vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional( CONF_TILT_CLOSED_POSITION, default=DEFAULT_TILT_CLOSED_POSITION @@ -163,6 +187,8 @@ PLATFORM_SCHEMA = vol.All( vol.Optional(CONF_TILT_STATUS_TEMPLATE): cv.template, vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_GET_POSITION_TEMPLATE): cv.template, + vol.Optional(CONF_TILT_COMMAND_TEMPLATE): cv.template, } ) .extend(MQTT_AVAILABILITY_SCHEMA.schema) @@ -228,6 +254,9 @@ class MqttCover(MqttEntity, CoverEntity): set_position_template = self._config.get(CONF_SET_POSITION_TEMPLATE) if set_position_template is not None: set_position_template.hass = self.hass + set_tilt_template = self._config.get(CONF_TILT_COMMAND_TEMPLATE) + if set_tilt_template is not None: + set_tilt_template.hass = self.hass tilt_status_template = self._config.get(CONF_TILT_STATUS_TEMPLATE) if tilt_status_template is not None: tilt_status_template.hass = self.hass @@ -266,17 +295,31 @@ class MqttCover(MqttEntity, CoverEntity): if template is not None: payload = template.async_render_with_possible_json_value(payload) - if payload == self._config[CONF_STATE_OPEN]: - self._state = STATE_OPEN + if payload == self._config[CONF_STATE_STOPPED]: + if ( + self._optimistic + or self._config.get(CONF_GET_POSITION_TOPIC) is None + ): + self._state = ( + STATE_CLOSED if self._state == STATE_CLOSING else STATE_OPEN + ) + else: + self._state = ( + STATE_CLOSED + if self._position == DEFAULT_POSITION_CLOSED + else STATE_OPEN + ) elif payload == self._config[CONF_STATE_OPENING]: self._state = STATE_OPENING - elif payload == self._config[CONF_STATE_CLOSED]: - self._state = STATE_CLOSED elif payload == self._config[CONF_STATE_CLOSING]: self._state = STATE_CLOSING + elif payload == self._config[CONF_STATE_OPEN]: + self._state = STATE_OPEN + elif payload == self._config[CONF_STATE_CLOSED]: + self._state = STATE_CLOSED else: _LOGGER.warning( - "Payload is not supported (e.g. open, closed, opening, closing): %s", + "Payload is not supported (e.g. open, closed, opening, closing, stopped): %s", payload, ) return @@ -286,9 +329,16 @@ class MqttCover(MqttEntity, CoverEntity): @callback @log_messages(self.hass, self.entity_id) def position_message_received(msg): - """Handle new MQTT state messages.""" + """Handle new MQTT position messages.""" payload = msg.payload - template = self._config.get(CONF_VALUE_TEMPLATE) + + template = self._config.get(CONF_GET_POSITION_TEMPLATE) + + # To be removed in 2021.6: + # allow using `value_template` as position template if no `state_topic` + if template is None and self._config.get(CONF_STATE_TOPIC) is None: + template = self._config.get(CONF_VALUE_TEMPLATE) + if template is not None: payload = template.async_render_with_possible_json_value(payload) @@ -297,13 +347,14 @@ class MqttCover(MqttEntity, CoverEntity): float(payload), COVER_PAYLOAD ) self._position = percentage_payload - self._state = ( - STATE_CLOSED - if percentage_payload == DEFAULT_POSITION_CLOSED - else STATE_OPEN - ) + if self._config.get(CONF_STATE_TOPIC) is None: + self._state = ( + STATE_CLOSED + if percentage_payload == DEFAULT_POSITION_CLOSED + else STATE_OPEN + ) else: - _LOGGER.warning("Payload is not integer within range: %s", payload) + _LOGGER.warning("Payload '%s' is not numeric", payload) return self.async_write_ha_state() @@ -313,13 +364,18 @@ class MqttCover(MqttEntity, CoverEntity): "msg_callback": position_message_received, "qos": self._config[CONF_QOS], } - elif self._config.get(CONF_STATE_TOPIC): + + if self._config.get(CONF_STATE_TOPIC): topics["state_topic"] = { "topic": self._config.get(CONF_STATE_TOPIC), "msg_callback": state_message_received, "qos": self._config[CONF_QOS], } - else: + + if ( + self._config.get(CONF_GET_POSITION_TOPIC) is None + and self._config.get(CONF_STATE_TOPIC) is None + ): # Force into optimistic mode. self._optimistic = True @@ -488,28 +544,32 @@ class MqttCover(MqttEntity, CoverEntity): async def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" - position = kwargs[ATTR_TILT_POSITION] - - # The position needs to be between min and max - level = self.find_in_range_from_percent(position) + set_tilt_template = self._config.get(CONF_TILT_COMMAND_TEMPLATE) + tilt = kwargs[ATTR_TILT_POSITION] + percentage_tilt = tilt + tilt = self.find_in_range_from_percent(tilt) + if set_tilt_template is not None: + tilt = set_tilt_template.async_render(parse_result=False, **kwargs) mqtt.async_publish( self.hass, self._config.get(CONF_TILT_COMMAND_TOPIC), - level, + tilt, self._config[CONF_QOS], self._config[CONF_RETAIN], ) + if self._tilt_optimistic: + self._tilt_value = percentage_tilt + self.async_write_ha_state() async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" set_position_template = self._config.get(CONF_SET_POSITION_TEMPLATE) position = kwargs[ATTR_POSITION] percentage_position = position + position = self.find_in_range_from_percent(position, COVER_PAYLOAD) if set_position_template is not None: position = set_position_template.async_render(parse_result=False, **kwargs) - else: - position = self.find_in_range_from_percent(position, COVER_PAYLOAD) mqtt.async_publish( self.hass, diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 019f0e19911..87b016e2d59 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -1082,6 +1082,23 @@ async def test_tilt_given_value_optimistic(hass, mqtt_mock): ) mqtt_mock.async_publish.reset_mock() + await hass.services.async_call( + cover.DOMAIN, + SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: "cover.test", ATTR_TILT_POSITION: 50}, + blocking=True, + ) + + current_cover_tilt_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_TILT_POSITION + ] + assert current_cover_tilt_position == 50 + + mqtt_mock.async_publish.assert_called_once_with( + "tilt-command-topic", "50", 0, False + ) + mqtt_mock.async_publish.reset_mock() + await hass.services.async_call( cover.DOMAIN, SERVICE_CLOSE_COVER_TILT, @@ -1381,6 +1398,41 @@ async def test_tilt_position(hass, mqtt_mock): ) +async def test_tilt_position_templated(hass, mqtt_mock): + """Test tilt position via template.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_command_template": "{{100-32}}", + } + }, + ) + await hass.async_block_till_done() + + await hass.services.async_call( + cover.DOMAIN, + SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: "cover.test", ATTR_TILT_POSITION: 100}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "tilt-command-topic", "68", 0, False + ) + + async def test_tilt_position_altered_range(hass, mqtt_mock): """Test tilt via method invocation with altered range.""" assert await async_setup_component( @@ -1978,3 +2030,298 @@ async def test_entity_debug_info_message(hass, mqtt_mock): await help_test_entity_debug_info_message( hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG ) + + +async def test_deprecated_value_template_for_position_topic_warnning( + hass, caplog, mqtt_mock +): + """Test warnning when value_template is used for position_topic.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "position-topic", + "value_template": "{{100-62}}", + } + }, + ) + await hass.async_block_till_done() + + assert ( + "using 'value_template' for 'position_topic' is deprecated " + "and will be removed from Home Assistant in version 2021.6" + "please replace it with 'position_template'" + ) in caplog.text + + +async def test_deprecated_tilt_invert_state_warnning(hass, caplog, mqtt_mock): + """Test warnning when tilt_invert_state is used.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "tilt_invert_state": True, + } + }, + ) + await hass.async_block_till_done() + + assert ( + "'tilt_invert_state' is deprecated " + "and will be removed from Home Assistant in version 2021.6" + "please invert tilt using 'tilt_min' & 'tilt_max'" + ) in caplog.text + + +async def test_no_deprecated_warning_for_position_topic_using_position_template( + hass, caplog, mqtt_mock +): + """Test no warning when position_template is used for position_topic.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "position-topic", + "position_template": "{{100-62}}", + } + }, + ) + await hass.async_block_till_done() + + assert ( + "using 'value_template' for 'position_topic' is deprecated " + "and will be removed from Home Assistant in version 2021.6" + "please replace it with 'position_template'" + ) not in caplog.text + + +async def test_state_and_position_topics_state_not_set_via_position_topic( + hass, mqtt_mock +): + """Test state is not set via position topic when both state and position topics are set.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "position_topic": "get-position-topic", + "position_open": 100, + "position_closed": 0, + "state_open": "OPEN", + "state_closed": "CLOSE", + "command_topic": "command-topic", + "qos": 0, + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("cover.test") + assert state.state == STATE_UNKNOWN + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "state-topic", "OPEN") + + state = hass.states.get("cover.test") + assert state.state == STATE_OPEN + + async_fire_mqtt_message(hass, "get-position-topic", "0") + + state = hass.states.get("cover.test") + assert state.state == STATE_OPEN + + async_fire_mqtt_message(hass, "get-position-topic", "100") + + state = hass.states.get("cover.test") + assert state.state == STATE_OPEN + + async_fire_mqtt_message(hass, "state-topic", "CLOSE") + + state = hass.states.get("cover.test") + assert state.state == STATE_CLOSED + + async_fire_mqtt_message(hass, "get-position-topic", "0") + + state = hass.states.get("cover.test") + assert state.state == STATE_CLOSED + + async_fire_mqtt_message(hass, "get-position-topic", "100") + + state = hass.states.get("cover.test") + assert state.state == STATE_CLOSED + + +async def test_set_state_via_position_using_stopped_state(hass, mqtt_mock): + """Test the controlling state via position topic using stopped state.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "position_topic": "get-position-topic", + "position_open": 100, + "position_closed": 0, + "state_open": "OPEN", + "state_closed": "CLOSE", + "state_stopped": "STOPPED", + "command_topic": "command-topic", + "qos": 0, + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("cover.test") + assert state.state == STATE_UNKNOWN + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "state-topic", "OPEN") + + state = hass.states.get("cover.test") + assert state.state == STATE_OPEN + + async_fire_mqtt_message(hass, "get-position-topic", "0") + + state = hass.states.get("cover.test") + assert state.state == STATE_OPEN + + async_fire_mqtt_message(hass, "state-topic", "STOPPED") + + state = hass.states.get("cover.test") + assert state.state == STATE_CLOSED + + async_fire_mqtt_message(hass, "get-position-topic", "100") + + state = hass.states.get("cover.test") + assert state.state == STATE_CLOSED + + async_fire_mqtt_message(hass, "state-topic", "STOPPED") + + state = hass.states.get("cover.test") + assert state.state == STATE_OPEN + + +async def test_set_state_via_stopped_state_optimistic(hass, mqtt_mock): + """Test the controlling state via stopped state in optimistic mode.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "position_topic": "get-position-topic", + "position_open": 100, + "position_closed": 0, + "state_open": "OPEN", + "state_closed": "CLOSE", + "state_stopped": "STOPPED", + "state_opening": "OPENING", + "state_closing": "CLOSING", + "command_topic": "command-topic", + "qos": 0, + "optimistic": True, + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "state-topic", "OPEN") + + state = hass.states.get("cover.test") + assert state.state == STATE_OPEN + + async_fire_mqtt_message(hass, "get-position-topic", "50") + + state = hass.states.get("cover.test") + assert state.state == STATE_OPEN + + async_fire_mqtt_message(hass, "state-topic", "OPENING") + + state = hass.states.get("cover.test") + assert state.state == STATE_OPENING + + async_fire_mqtt_message(hass, "state-topic", "STOPPED") + + state = hass.states.get("cover.test") + assert state.state == STATE_OPEN + + async_fire_mqtt_message(hass, "state-topic", "CLOSING") + + state = hass.states.get("cover.test") + assert state.state == STATE_CLOSING + + async_fire_mqtt_message(hass, "state-topic", "STOPPED") + + state = hass.states.get("cover.test") + assert state.state == STATE_CLOSED + + +async def test_set_state_via_stopped_state_no_position_topic(hass, mqtt_mock): + """Test the controlling state via stopped state when no position topic.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "state_open": "OPEN", + "state_closed": "CLOSE", + "state_stopped": "STOPPED", + "state_opening": "OPENING", + "state_closing": "CLOSING", + "command_topic": "command-topic", + "qos": 0, + "optimistic": False, + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "state-topic", "OPEN") + + state = hass.states.get("cover.test") + assert state.state == STATE_OPEN + + async_fire_mqtt_message(hass, "state-topic", "OPENING") + + state = hass.states.get("cover.test") + assert state.state == STATE_OPENING + + async_fire_mqtt_message(hass, "state-topic", "STOPPED") + + state = hass.states.get("cover.test") + assert state.state == STATE_OPEN + + async_fire_mqtt_message(hass, "state-topic", "CLOSING") + + state = hass.states.get("cover.test") + assert state.state == STATE_CLOSING + + async_fire_mqtt_message(hass, "state-topic", "STOPPED") + + state = hass.states.get("cover.test") + assert state.state == STATE_CLOSED From e20a814926e6fecc252688aaca6a83e8cd62d00c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Feb 2021 16:16:40 +0100 Subject: [PATCH 276/796] Call setup during devcontainer create (#46224) --- .devcontainer/devcontainer.json | 3 ++- script/bootstrap | 17 ----------------- script/setup | 14 ++++++++++++-- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 94d3c284f1a..efcc0380748 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,7 +2,8 @@ "name": "Home Assistant Dev", "context": "..", "dockerFile": "../Dockerfile.dev", - "postCreateCommand": "script/bootstrap", + "postCreateCommand": "script/setup", + "postStartCommand": "script/bootstrap", "containerEnv": { "DEVCONTAINER": "1" }, "appPort": 8123, "runArgs": ["-e", "GIT_EDITOR=code --wait"], diff --git a/script/bootstrap b/script/bootstrap index 12a2e5da707..3ffac11852b 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -6,23 +6,6 @@ set -e cd "$(dirname "$0")/.." -# Add default vscode settings if not existing -SETTINGS_FILE=./.vscode/settings.json -SETTINGS_TEMPLATE_FILE=./.vscode/settings.default.json -if [ ! -f "$SETTINGS_FILE" ]; then - echo "Copy $SETTINGS_TEMPLATE_FILE to $SETTINGS_FILE." - cp "$SETTINGS_TEMPLATE_FILE" "$SETTINGS_FILE" -fi - echo "Installing development dependencies..." python3 -m pip install wheel --constraint homeassistant/package_constraints.txt python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) $(grep stdlib-list requirements_test.txt) $(grep tqdm requirements_test.txt) $(grep pipdeptree requirements_test.txt) $(grep awesomeversion requirements.txt) --constraint homeassistant/package_constraints.txt - -if [ -n "$DEVCONTAINER" ];then - pre-commit install - pre-commit install-hooks - python3 -m pip install -e . --constraint homeassistant/package_constraints.txt - - mkdir -p config - hass --script ensure_config -c config -fi \ No newline at end of file diff --git a/script/setup b/script/setup index 83c2d24f038..46865ecfcb6 100755 --- a/script/setup +++ b/script/setup @@ -6,10 +6,20 @@ set -e cd "$(dirname "$0")/.." +# Add default vscode settings if not existing +SETTINGS_FILE=./.vscode/settings.json +SETTINGS_TEMPLATE_FILE=./.vscode/settings.default.json +if [ ! -f "$SETTINGS_FILE" ]; then + echo "Copy $SETTINGS_TEMPLATE_FILE to $SETTINGS_FILE." + cp "$SETTINGS_TEMPLATE_FILE" "$SETTINGS_FILE" +fi + mkdir -p config -python3 -m venv venv -source venv/bin/activate +if [ ! -n "$DEVCONTAINER" ];then + python3 -m venv venv + source venv/bin/activate +fi script/bootstrap From dca6a9389854380a27baea3b694320786fc34c68 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 8 Feb 2021 07:19:41 -0800 Subject: [PATCH 277/796] Centralize keepalive logic in Stream class (#45850) * Remove dependencies on keepalive from StremaOutput and stream_worker Pull logic from StreamOutput and stream_worker into the Stream class, unifying keepalive and idle timeout logic. This prepares for future changes to preserve hls state across stream url changes. --- homeassistant/components/stream/__init__.py | 68 ++++++++++++--- homeassistant/components/stream/const.py | 2 + homeassistant/components/stream/core.py | 92 +++++++++++++-------- homeassistant/components/stream/recorder.py | 16 ++-- homeassistant/components/stream/worker.py | 38 --------- tests/components/stream/conftest.py | 28 +++---- tests/components/stream/test_hls.py | 5 +- tests/components/stream/test_init.py | 4 +- tests/components/stream/test_recorder.py | 8 +- tests/components/stream/test_worker.py | 20 +---- 10 files changed, 142 insertions(+), 139 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index c7d1dad4835..6980f7ead8f 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -2,6 +2,7 @@ import logging import secrets import threading +import time from types import MappingProxyType import voluptuous as vol @@ -20,9 +21,12 @@ from .const import ( CONF_STREAM_SOURCE, DOMAIN, MAX_SEGMENTS, + OUTPUT_IDLE_TIMEOUT, SERVICE_RECORD, + STREAM_RESTART_INCREMENT, + STREAM_RESTART_RESET_TIME, ) -from .core import PROVIDERS +from .core import PROVIDERS, IdleTimer from .hls import async_setup_hls _LOGGER = logging.getLogger(__name__) @@ -142,18 +146,27 @@ class Stream: # without concern about self._outputs being modified from another thread. return MappingProxyType(self._outputs.copy()) - def add_provider(self, fmt): + def add_provider(self, fmt, timeout=OUTPUT_IDLE_TIMEOUT): """Add provider output stream.""" if not self._outputs.get(fmt): - provider = PROVIDERS[fmt](self) + + @callback + def idle_callback(): + if not self.keepalive and fmt in self._outputs: + self.remove_provider(self._outputs[fmt]) + self.check_idle() + + provider = PROVIDERS[fmt]( + self.hass, IdleTimer(self.hass, timeout, idle_callback) + ) self._outputs[fmt] = provider return self._outputs[fmt] def remove_provider(self, provider): """Remove provider output stream.""" if provider.name in self._outputs: + self._outputs[provider.name].cleanup() del self._outputs[provider.name] - self.check_idle() if not self._outputs: self.stop() @@ -165,10 +178,6 @@ class Stream: def start(self): """Start a stream.""" - # Keep import here so that we can import stream integration without installing reqs - # pylint: disable=import-outside-toplevel - from .worker import stream_worker - if self._thread is None or not self._thread.is_alive(): if self._thread is not None: # The thread must have crashed/exited. Join to clean up the @@ -177,12 +186,48 @@ class Stream: self._thread_quit = threading.Event() self._thread = threading.Thread( name="stream_worker", - target=stream_worker, - args=(self.hass, self, self._thread_quit), + target=self._run_worker, ) self._thread.start() _LOGGER.info("Started stream: %s", self.source) + def _run_worker(self): + """Handle consuming streams and restart keepalive streams.""" + # Keep import here so that we can import stream integration without installing reqs + # pylint: disable=import-outside-toplevel + from .worker import stream_worker + + wait_timeout = 0 + while not self._thread_quit.wait(timeout=wait_timeout): + start_time = time.time() + stream_worker(self.hass, self, self._thread_quit) + if not self.keepalive or self._thread_quit.is_set(): + break + + # To avoid excessive restarts, wait before restarting + # As the required recovery time may be different for different setups, start + # with trying a short wait_timeout and increase it on each reconnection attempt. + # Reset the wait_timeout after the worker has been up for several minutes + if time.time() - start_time > STREAM_RESTART_RESET_TIME: + wait_timeout = 0 + wait_timeout += STREAM_RESTART_INCREMENT + _LOGGER.debug( + "Restarting stream worker in %d seconds: %s", + wait_timeout, + self.source, + ) + self._worker_finished() + + def _worker_finished(self): + """Schedule cleanup of all outputs.""" + + @callback + def remove_outputs(): + for provider in self.outputs.values(): + self.remove_provider(provider) + + self.hass.loop.call_soon_threadsafe(remove_outputs) + def stop(self): """Remove outputs and access token.""" self._outputs = {} @@ -223,9 +268,8 @@ async def async_handle_record_service(hass, call): if recorder: raise HomeAssistantError(f"Stream already recording to {recorder.video_path}!") - recorder = stream.add_provider("recorder") + recorder = stream.add_provider("recorder", timeout=duration) recorder.video_path = video_path - recorder.timeout = duration stream.start() diff --git a/homeassistant/components/stream/const.py b/homeassistant/components/stream/const.py index 181808e549e..45fa3d9e76a 100644 --- a/homeassistant/components/stream/const.py +++ b/homeassistant/components/stream/const.py @@ -15,6 +15,8 @@ OUTPUT_FORMATS = ["hls"] FORMAT_CONTENT_TYPE = {"hls": "application/vnd.apple.mpegurl"} +OUTPUT_IDLE_TIMEOUT = 300 # Idle timeout due to inactivity + MAX_SEGMENTS = 3 # Max number of segments to keep around MIN_SEGMENT_DURATION = 1.5 # Each segment is at least this many seconds diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 5158ba185b1..5427172a55c 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -8,7 +8,7 @@ from aiohttp import web import attr from homeassistant.components.http import HomeAssistantView -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.event import async_call_later from homeassistant.util.decorator import Registry @@ -36,24 +36,69 @@ class Segment: duration: float = attr.ib() +class IdleTimer: + """Invoke a callback after an inactivity timeout. + + The IdleTimer invokes the callback after some timeout has passed. The awake() method + resets the internal alarm, extending the inactivity time. + """ + + def __init__( + self, hass: HomeAssistant, timeout: int, idle_callback: Callable[[], None] + ): + """Initialize IdleTimer.""" + self._hass = hass + self._timeout = timeout + self._callback = idle_callback + self._unsub = None + self.idle = False + + def start(self): + """Start the idle timer if not already started.""" + self.idle = False + if self._unsub is None: + self._unsub = async_call_later(self._hass, self._timeout, self.fire) + + def awake(self): + """Keep the idle time alive by resetting the timeout.""" + self.idle = False + # Reset idle timeout + self.clear() + self._unsub = async_call_later(self._hass, self._timeout, self.fire) + + def clear(self): + """Clear and disable the timer.""" + if self._unsub is not None: + self._unsub() + + def fire(self, _now=None): + """Invoke the idle timeout callback, called when the alarm fires.""" + self.idle = True + self._unsub = None + self._callback() + + class StreamOutput: """Represents a stream output.""" - def __init__(self, stream, timeout: int = 300) -> None: + def __init__(self, hass: HomeAssistant, idle_timer: IdleTimer) -> None: """Initialize a stream output.""" - self.idle = False - self.timeout = timeout - self._stream = stream + self._hass = hass + self._idle_timer = idle_timer self._cursor = None self._event = asyncio.Event() self._segments = deque(maxlen=MAX_SEGMENTS) - self._unsub = None @property def name(self) -> str: """Return provider name.""" return None + @property + def idle(self) -> bool: + """Return True if the output is idle.""" + return self._idle_timer.idle + @property def format(self) -> str: """Return container format.""" @@ -90,11 +135,7 @@ class StreamOutput: def get_segment(self, sequence: int = None) -> Any: """Retrieve a specific segment, or the whole list.""" - self.idle = False - # Reset idle timeout - if self._unsub is not None: - self._unsub() - self._unsub = async_call_later(self._stream.hass, self.timeout, self._timeout) + self._idle_timer.awake() if not sequence: return self._segments @@ -119,43 +160,22 @@ class StreamOutput: def put(self, segment: Segment) -> None: """Store output.""" - self._stream.hass.loop.call_soon_threadsafe(self._async_put, segment) + self._hass.loop.call_soon_threadsafe(self._async_put, segment) @callback def _async_put(self, segment: Segment) -> None: """Store output from event loop.""" # Start idle timeout when we start receiving data - if self._unsub is None: - self._unsub = async_call_later( - self._stream.hass, self.timeout, self._timeout - ) - - if segment is None: - self._event.set() - # Cleanup provider - if self._unsub is not None: - self._unsub() - self.cleanup() - return - + self._idle_timer.start() self._segments.append(segment) self._event.set() self._event.clear() - @callback - def _timeout(self, _now=None): - """Handle stream timeout.""" - self._unsub = None - if self._stream.keepalive: - self.idle = True - self._stream.check_idle() - else: - self.cleanup() - def cleanup(self): """Handle cleanup.""" + self._event.set() + self._idle_timer.clear() self._segments = deque(maxlen=MAX_SEGMENTS) - self._stream.remove_provider(self) class StreamView(HomeAssistantView): diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index cf923de85c2..7db9997f870 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -6,9 +6,9 @@ from typing import List import av -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback -from .core import PROVIDERS, Segment, StreamOutput +from .core import PROVIDERS, IdleTimer, Segment, StreamOutput _LOGGER = logging.getLogger(__name__) @@ -72,9 +72,9 @@ def recorder_save_worker(file_out: str, segments: List[Segment], container_forma class RecorderOutput(StreamOutput): """Represents HLS Output formats.""" - def __init__(self, stream, timeout: int = 30) -> None: + def __init__(self, hass: HomeAssistant, idle_timer: IdleTimer) -> None: """Initialize recorder output.""" - super().__init__(stream, timeout) + super().__init__(hass, idle_timer) self.video_path = None self._segments = [] @@ -104,12 +104,6 @@ class RecorderOutput(StreamOutput): segments = [s for s in segments if s.sequence not in own_segments] self._segments = segments + self._segments - @callback - def _timeout(self, _now=None): - """Handle recorder timeout.""" - self._unsub = None - self.cleanup() - def cleanup(self): """Write recording and clean up.""" _LOGGER.debug("Starting recorder worker thread") @@ -120,5 +114,5 @@ class RecorderOutput(StreamOutput): ) thread.start() + super().cleanup() self._segments = [] - self._stream.remove_provider(self) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index cccbfd1b48b..510d0ebd460 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -2,7 +2,6 @@ from collections import deque import io import logging -import time import av @@ -11,8 +10,6 @@ from .const import ( MAX_TIMESTAMP_GAP, MIN_SEGMENT_DURATION, PACKETS_TO_WAIT_FOR_AUDIO, - STREAM_RESTART_INCREMENT, - STREAM_RESTART_RESET_TIME, STREAM_TIMEOUT, ) from .core import Segment, StreamBuffer @@ -47,32 +44,6 @@ def create_stream_buffer(stream_output, video_stream, audio_stream, sequence): def stream_worker(hass, stream, quit_event): - """Handle consuming streams and restart keepalive streams.""" - - wait_timeout = 0 - while not quit_event.wait(timeout=wait_timeout): - start_time = time.time() - try: - _stream_worker_internal(hass, stream, quit_event) - except av.error.FFmpegError: # pylint: disable=c-extension-no-member - _LOGGER.exception("Stream connection failed: %s", stream.source) - if not stream.keepalive or quit_event.is_set(): - break - # To avoid excessive restarts, wait before restarting - # As the required recovery time may be different for different setups, start - # with trying a short wait_timeout and increase it on each reconnection attempt. - # Reset the wait_timeout after the worker has been up for several minutes - if time.time() - start_time > STREAM_RESTART_RESET_TIME: - wait_timeout = 0 - wait_timeout += STREAM_RESTART_INCREMENT - _LOGGER.debug( - "Restarting stream worker in %d seconds: %s", - wait_timeout, - stream.source, - ) - - -def _stream_worker_internal(hass, stream, quit_event): """Handle consuming streams.""" try: @@ -183,7 +154,6 @@ def _stream_worker_internal(hass, stream, quit_event): _LOGGER.error( "Error demuxing stream while finding first packet: %s", str(ex) ) - finalize_stream() return False return True @@ -220,12 +190,6 @@ def _stream_worker_internal(hass, stream, quit_event): packet.stream = output_streams[audio_stream] buffer.output.mux(packet) - def finalize_stream(): - if not stream.keepalive: - # End of stream, clear listeners and stop thread - for fmt in stream.outputs: - stream.outputs[fmt].put(None) - if not peek_first_pts(): container.close() return @@ -249,7 +213,6 @@ def _stream_worker_internal(hass, stream, quit_event): missing_dts = 0 except (av.AVError, StopIteration) as ex: _LOGGER.error("Error demuxing stream: %s", str(ex)) - finalize_stream() break # Discard packet if dts is not monotonic @@ -263,7 +226,6 @@ def _stream_worker_internal(hass, stream, quit_event): last_dts[packet.stream], packet.dts, ) - finalize_stream() break continue diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py index 1b2f0645f9b..75ac9377b7c 100644 --- a/tests/components/stream/conftest.py +++ b/tests/components/stream/conftest.py @@ -9,14 +9,13 @@ nothing for the test to verify. The solution is the WorkerSync class that allows the tests to pause the worker thread before finalizing the stream so that it can inspect the output. """ - import logging import threading from unittest.mock import patch import pytest -from homeassistant.components.stream.core import Segment, StreamOutput +from homeassistant.components.stream import Stream class WorkerSync: @@ -25,7 +24,7 @@ class WorkerSync: def __init__(self): """Initialize WorkerSync.""" self._event = None - self._put_original = StreamOutput.put + self._original = Stream._worker_finished def pause(self): """Pause the worker before it finalizes the stream.""" @@ -35,17 +34,16 @@ class WorkerSync: """Allow the worker thread to finalize the stream.""" self._event.set() - def blocking_put(self, stream_output: StreamOutput, segment: Segment): - """Proxy StreamOutput.put, intercepted for test to pause worker.""" - if segment is None and self._event: - # Worker is ending the stream, which clears all output buffers. - # Block the worker thread until the test has a chance to verify - # the segments under test. - logging.error("blocking worker") - self._event.wait() + def blocking_finish(self, stream: Stream): + """Intercept call to pause stream worker.""" + # Worker is ending the stream, which clears all output buffers. + # Block the worker thread until the test has a chance to verify + # the segments under test. + logging.debug("blocking worker") + self._event.wait() - # Forward to actual StreamOutput.put - self._put_original(stream_output, segment) + # Forward to actual implementation + self._original(stream) @pytest.fixture() @@ -53,8 +51,8 @@ def stream_worker_sync(hass): """Patch StreamOutput to allow test to synchronize worker stream end.""" sync = WorkerSync() with patch( - "homeassistant.components.stream.core.StreamOutput.put", - side_effect=sync.blocking_put, + "homeassistant.components.stream.Stream._worker_finished", + side_effect=sync.blocking_finish, autospec=True, ): yield sync diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 790222b1630..ab49a56ca02 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -98,6 +98,7 @@ async def test_stream_timeout(hass, hass_client, stream_worker_sync): # Wait 5 minutes future = dt_util.utcnow() + timedelta(minutes=5) async_fire_time_changed(hass, future) + await hass.async_block_till_done() # Ensure playlist not accessible fail_response = await http_client.get(parsed_url.path) @@ -155,9 +156,9 @@ async def test_stream_keepalive(hass): return cur_time with patch("av.open") as av_open, patch( - "homeassistant.components.stream.worker.time" + "homeassistant.components.stream.time" ) as mock_time, patch( - "homeassistant.components.stream.worker.STREAM_RESTART_INCREMENT", 0 + "homeassistant.components.stream.STREAM_RESTART_INCREMENT", 0 ): av_open.side_effect = av.error.InvalidDataError(-2, "error") mock_time.time.side_effect = time_side_effect diff --git a/tests/components/stream/test_init.py b/tests/components/stream/test_init.py index 1515ff1a490..2e13493b641 100644 --- a/tests/components/stream/test_init.py +++ b/tests/components/stream/test_init.py @@ -80,5 +80,7 @@ async def test_record_service_lookback(hass): await hass.services.async_call(DOMAIN, SERVICE_RECORD, data, blocking=True) assert stream_mock.called - stream_mock.return_value.add_provider.assert_called_once_with("recorder") + stream_mock.return_value.add_provider.assert_called_once_with( + "recorder", timeout=30 + ) assert hls_mock.recv.called diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index 1b46738c8f2..bda53a9cc17 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -106,13 +106,11 @@ async def test_recorder_timeout(hass, hass_client, stream_worker_sync): stream_worker_sync.pause() - with patch( - "homeassistant.components.stream.recorder.RecorderOutput.cleanup" - ) as mock_cleanup: + with patch("homeassistant.components.stream.IdleTimer.fire") as mock_timeout: # Setup demo track source = generate_h264_video() stream = preload_stream(hass, source) - recorder = stream.add_provider("recorder") + recorder = stream.add_provider("recorder", timeout=30) stream.start() await recorder.recv() @@ -122,7 +120,7 @@ async def test_recorder_timeout(hass, hass_client, stream_worker_sync): async_fire_time_changed(hass, future) await hass.async_block_till_done() - assert mock_cleanup.called + assert mock_timeout.called stream_worker_sync.resume() stream.stop() diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 8196899dcf9..91d02664d74 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -132,7 +132,6 @@ class FakePyAvBuffer: self.segments = [] self.audio_packets = [] self.video_packets = [] - self.finished = False def add_stream(self, template=None): """Create an output buffer that captures packets for test to examine.""" @@ -162,11 +161,7 @@ class FakePyAvBuffer: def capture_output_segment(self, segment): """Capture the output segment for tests to inspect.""" - assert not self.finished - if segment is None: - self.finished = True - else: - self.segments.append(segment) + self.segments.append(segment) class MockPyAv: @@ -223,7 +218,6 @@ async def test_stream_worker_success(hass): decoded_stream = await async_decode_stream( hass, PacketSequence(TEST_SEQUENCE_LENGTH) ) - assert decoded_stream.finished segments = decoded_stream.segments # Check number of segments. A segment is only formed when a packet from the next # segment arrives, hence the subtraction of one from the sequence length. @@ -243,7 +237,6 @@ async def test_skip_out_of_order_packet(hass): packets[OUT_OF_ORDER_PACKET_INDEX].dts = -9090 decoded_stream = await async_decode_stream(hass, iter(packets)) - assert decoded_stream.finished segments = decoded_stream.segments # Check sequence numbers assert all([segments[i].sequence == i + 1 for i in range(len(segments))]) @@ -279,7 +272,6 @@ async def test_discard_old_packets(hass): packets[OUT_OF_ORDER_PACKET_INDEX - 1].dts = 9090 decoded_stream = await async_decode_stream(hass, iter(packets)) - assert decoded_stream.finished segments = decoded_stream.segments # Check number of segments assert len(segments) == int((OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET) @@ -299,7 +291,6 @@ async def test_packet_overflow(hass): packets[OUT_OF_ORDER_PACKET_INDEX].dts = -9000000 decoded_stream = await async_decode_stream(hass, iter(packets)) - assert decoded_stream.finished segments = decoded_stream.segments # Check number of segments assert len(segments) == int((OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET) @@ -321,7 +312,6 @@ async def test_skip_initial_bad_packets(hass): packets[i].dts = None decoded_stream = await async_decode_stream(hass, iter(packets)) - assert decoded_stream.finished segments = decoded_stream.segments # Check number of segments assert len(segments) == int( @@ -345,7 +335,6 @@ async def test_too_many_initial_bad_packets_fails(hass): packets[i].dts = None decoded_stream = await async_decode_stream(hass, iter(packets)) - assert decoded_stream.finished segments = decoded_stream.segments assert len(segments) == 0 assert len(decoded_stream.video_packets) == 0 @@ -363,7 +352,6 @@ async def test_skip_missing_dts(hass): packets[i].dts = None decoded_stream = await async_decode_stream(hass, iter(packets)) - assert decoded_stream.finished segments = decoded_stream.segments # Check sequence numbers assert all([segments[i].sequence == i + 1 for i in range(len(segments))]) @@ -387,7 +375,6 @@ async def test_too_many_bad_packets(hass): packets[i].dts = None decoded_stream = await async_decode_stream(hass, iter(packets)) - assert decoded_stream.finished segments = decoded_stream.segments assert len(segments) == int((bad_packet_start - 1) * SEGMENTS_PER_PACKET) assert len(decoded_stream.video_packets) == bad_packet_start @@ -402,7 +389,6 @@ async def test_no_video_stream(hass): hass, PacketSequence(TEST_SEQUENCE_LENGTH), py_av=py_av ) # Note: This failure scenario does not output an end of stream - assert not decoded_stream.finished segments = decoded_stream.segments assert len(segments) == 0 assert len(decoded_stream.video_packets) == 0 @@ -417,7 +403,6 @@ async def test_audio_packets_not_found(hass): packets = PacketSequence(num_packets) # Contains only video packets decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av) - assert decoded_stream.finished segments = decoded_stream.segments assert len(segments) == int((num_packets - 1) * SEGMENTS_PER_PACKET) assert len(decoded_stream.video_packets) == num_packets @@ -439,7 +424,6 @@ async def test_audio_is_first_packet(hass): packets[2].pts = packets[3].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av) - assert decoded_stream.finished segments = decoded_stream.segments # The audio packets are segmented with the video packets assert len(segments) == int((num_packets - 2 - 1) * SEGMENTS_PER_PACKET) @@ -458,7 +442,6 @@ async def test_audio_packets_found(hass): packets[1].pts = packets[0].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av) - assert decoded_stream.finished segments = decoded_stream.segments # The audio packet above is buffered with the video packet assert len(segments) == int((num_packets - 1 - 1) * SEGMENTS_PER_PACKET) @@ -477,7 +460,6 @@ async def test_pts_out_of_order(hass): packets[i].is_keyframe = False decoded_stream = await async_decode_stream(hass, iter(packets)) - assert decoded_stream.finished segments = decoded_stream.segments # Check number of segments assert len(segments) == int((TEST_SEQUENCE_LENGTH - 1) * SEGMENTS_PER_PACKET) From 86fe5d0561373c3a55028be93e8307f242cc2b9e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 8 Feb 2021 16:42:33 +0100 Subject: [PATCH 278/796] Update frontend to 20210208.0 (#46225) --- homeassistant/components/frontend/manifest.json | 10 +++++++--- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 65a5497d1f9..7faac6c99cb 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,9 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20210127.7"], + "requirements": [ + "home-assistant-frontend==20210208.0" + ], "dependencies": [ "api", "auth", @@ -15,6 +17,8 @@ "system_log", "websocket_api" ], - "codeowners": ["@home-assistant/frontend"], + "codeowners": [ + "@home-assistant/frontend" + ], "quality_scale": "internal" -} +} \ No newline at end of file diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 45a871081d1..d926471660e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,7 +14,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.41.0 -home-assistant-frontend==20210127.7 +home-assistant-frontend==20210208.0 httpx==0.16.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 6a8b5f63fa2..a28eee2096e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210127.7 +home-assistant-frontend==20210208.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 54f6651ad0c..f2f022e9ffe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -405,7 +405,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210127.7 +home-assistant-frontend==20210208.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 1b194e3b2f12aac2c3260dd7b20729b9e225478f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 8 Feb 2021 17:08:13 +0100 Subject: [PATCH 279/796] Add noltari to Tado code owners (#46216) --- CODEOWNERS | 2 +- homeassistant/components/tado/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 7c45a66e166..0e33076105f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -458,7 +458,7 @@ homeassistant/components/syncthru/* @nielstron homeassistant/components/synology_dsm/* @hacf-fr @Quentame @mib1185 homeassistant/components/synology_srm/* @aerialls homeassistant/components/syslog/* @fabaff -homeassistant/components/tado/* @michaelarnauts @bdraco +homeassistant/components/tado/* @michaelarnauts @bdraco @noltari homeassistant/components/tag/* @balloob @dmulcahey homeassistant/components/tahoma/* @philklei homeassistant/components/tankerkoenig/* @guillempages diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 9b166027df3..27c7ecff411 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -3,7 +3,7 @@ "name": "Tado", "documentation": "https://www.home-assistant.io/integrations/tado", "requirements": ["python-tado==0.10.0"], - "codeowners": ["@michaelarnauts", "@bdraco"], + "codeowners": ["@michaelarnauts", "@bdraco", "@noltari"], "config_flow": true, "homekit": { "models": ["tado", "AC02"] From e27619fe5041e088e7dd413a79ded103fe68fd12 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Feb 2021 17:19:55 +0100 Subject: [PATCH 280/796] Allow discovery info accessible from CORS enabled domains (#46226) --- homeassistant/components/api/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index f383f982abc..e7bac8532ee 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -178,6 +178,7 @@ class APIDiscoveryView(HomeAssistantView): requires_auth = False url = URL_API_DISCOVERY_INFO name = "api:discovery" + cors_allowed = True async def get(self, request): """Get discovery information.""" From be779d8712bf8ffbaeb1b786c7649381be69f1e5 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 8 Feb 2021 17:56:19 +0100 Subject: [PATCH 281/796] update discovery scheme for zwave_js light platform (#46082) --- homeassistant/components/zwave_js/discovery.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index d741946a1c9..03593ab79ad 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -130,6 +130,7 @@ DISCOVERY_SCHEMAS = [ "Multilevel Remote Switch", "Multilevel Power Switch", "Multilevel Scene Switch", + "Unused", }, command_class={CommandClass.SWITCH_MULTILEVEL}, property={"currentValue"}, From 829131fe51bd09ff9032766c57bb293c89db7614 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 8 Feb 2021 17:57:22 +0100 Subject: [PATCH 282/796] Update zwave_js discovery scheme for boolean sensors in the Alarm CC (#46085) --- homeassistant/components/zwave_js/discovery.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 03593ab79ad..be6d9b698d4 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -143,6 +143,7 @@ DISCOVERY_SCHEMAS = [ command_class={ CommandClass.SENSOR_BINARY, CommandClass.BATTERY, + CommandClass.SENSOR_ALARM, }, type={"boolean"}, ), From 71d7ae5992c8a2241c6ddbb73492264cb6688f00 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Feb 2021 11:48:02 -1000 Subject: [PATCH 283/796] Downgrade and improve lutron caseta LIP error message (#46236) --- homeassistant/components/lutron_caseta/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 73eb0b83fa6..220096fe0bf 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -158,7 +158,11 @@ async def async_setup_lip(hass, config_entry, lip_devices): try: await lip.async_connect(host) except asyncio.TimeoutError: - _LOGGER.error("Failed to connect to via LIP at %s:23", host) + _LOGGER.warning( + "Failed to connect to via LIP at %s:23, Pico and Shade remotes will not be available; " + "Enable Telnet Support in the Lutron app under Settings >> Advanced >> Integration", + host, + ) return _LOGGER.debug("Connected to Lutron Caseta bridge via LIP at %s:23", host) From fcae8406418694566167dbf25de513366f7d6356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 8 Feb 2021 22:49:46 +0100 Subject: [PATCH 284/796] Fix Tado Power and Link binary sensors (#46235) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Power and Link aren't converted from strings to booleans by python-tado, so we need to properly parse before assigning the string value to binary sensors. Fixes: 067f2d0098d1 ("Add tado zone binary sensors (#44576)") Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/tado/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tado/binary_sensor.py b/homeassistant/components/tado/binary_sensor.py index 1acefdb4c16..71b52931013 100644 --- a/homeassistant/components/tado/binary_sensor.py +++ b/homeassistant/components/tado/binary_sensor.py @@ -244,10 +244,10 @@ class TadoZoneBinarySensor(TadoZoneEntity, BinarySensorEntity): return if self.zone_variable == "power": - self._state = self._tado_zone_data.power + self._state = self._tado_zone_data.power == "ON" elif self.zone_variable == "link": - self._state = self._tado_zone_data.link + self._state = self._tado_zone_data.link == "ONLINE" elif self.zone_variable == "overlay": self._state = self._tado_zone_data.overlay_active From c0a1fc2916207190024146f958612aba0f91c2b9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Feb 2021 11:51:46 -1000 Subject: [PATCH 285/796] Handle empty mylink response at startup (#46241) --- homeassistant/components/somfy_mylink/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py index d15ea029530..d371fd96310 100644 --- a/homeassistant/components/somfy_mylink/__init__.py +++ b/homeassistant/components/somfy_mylink/__init__.py @@ -108,6 +108,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) return False + if "result" not in mylink_status: + raise ConfigEntryNotReady("The Somfy MyLink device returned an empty result") + _async_migrate_entity_config(hass, entry, mylink_status) undo_listener = entry.add_update_listener(_async_update_listener) From 00bbf8c3a2c566dfde58639e8710fda14e6b5111 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 8 Feb 2021 16:52:28 -0500 Subject: [PATCH 286/796] Use core constants for group component (#46239) --- homeassistant/components/group/__init__.py | 3 +-- homeassistant/components/group/reproduce_state.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 32a9bd41014..f185601ce87 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -13,6 +13,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ICON, ATTR_NAME, + CONF_ENTITIES, CONF_ICON, CONF_NAME, ENTITY_MATCH_ALL, @@ -41,7 +42,6 @@ GROUP_ORDER = "group_order" ENTITY_ID_FORMAT = DOMAIN + ".{}" -CONF_ENTITIES = "entities" CONF_ALL = "all" ATTR_ADD_ENTITIES = "add_entities" @@ -345,7 +345,6 @@ async def async_setup(hass, config): async def _process_group_platform(hass, domain, platform): """Process a group platform.""" - current_domain.set(domain) platform.async_describe_on_off_states(hass, hass.data[REG_KEY]) diff --git a/homeassistant/components/group/reproduce_state.py b/homeassistant/components/group/reproduce_state.py index 95915412e4f..adeb0cfee0a 100644 --- a/homeassistant/components/group/reproduce_state.py +++ b/homeassistant/components/group/reproduce_state.py @@ -16,7 +16,6 @@ async def async_reproduce_states( reproduce_options: Optional[Dict[str, Any]] = None, ) -> None: """Reproduce component states.""" - states_copy = [] for state in states: members = get_entity_ids(hass, state.entity_id) From c2302784c27d824b48bdb997a9e47438da704fd0 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 8 Feb 2021 16:53:17 -0500 Subject: [PATCH 287/796] Use core constants for helpers (#46240) --- homeassistant/helpers/collection.py | 1 - homeassistant/helpers/config_validation.py | 2 -- homeassistant/helpers/entity.py | 1 - homeassistant/helpers/event.py | 4 ---- homeassistant/helpers/frame.py | 1 - homeassistant/helpers/httpx_client.py | 1 - homeassistant/helpers/reload.py | 3 --- homeassistant/helpers/service.py | 2 +- homeassistant/helpers/storage.py | 1 - homeassistant/helpers/sun.py | 1 - homeassistant/helpers/template.py | 1 - homeassistant/helpers/update_coordinator.py | 1 - 12 files changed, 1 insertion(+), 18 deletions(-) diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 4af524bbbc9..abeef0f0d68 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -136,7 +136,6 @@ class YamlCollection(ObservableCollection): async def async_load(self, data: List[dict]) -> None: """Load the YAML collection. Overrides existing data.""" - old_ids = set(self.data) change_sets = [] diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 4af4744e509..6f1c6f46599 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -549,7 +549,6 @@ unit_system = vol.All( def template(value: Optional[Any]) -> template_helper.Template: """Validate a jinja2 template.""" - if value is None: raise vol.Invalid("template value is None") if isinstance(value, (list, dict, template_helper.Template)): @@ -566,7 +565,6 @@ def template(value: Optional[Any]) -> template_helper.Template: def dynamic_template(value: Optional[Any]) -> template_helper.Template: """Validate a dynamic (non static) jinja2 template.""" - if value is None: raise vol.Invalid("template value is None") if isinstance(value, (list, dict, template_helper.Template)): diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 04c07ef0f36..7d0e38ab119 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -65,7 +65,6 @@ def async_generate_entity_id( hass: Optional[HomeAssistant] = None, ) -> str: """Generate a unique entity ID based on given entity IDs or used IDs.""" - name = (name or DEVICE_DEFAULT_NAME).lower() preferred_string = entity_id_format.format(slugify(name)) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 44cbd89fde7..102a84863bd 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -299,7 +299,6 @@ def _async_remove_indexed_listeners( job: HassJob, ) -> None: """Remove a listener.""" - callbacks = hass.data[data_key] for storage_key in storage_keys: @@ -686,7 +685,6 @@ def async_track_template( Callable to unregister the listener. """ - job = HassJob(action) @callback @@ -1105,7 +1103,6 @@ def async_track_point_in_time( point_in_time: datetime, ) -> CALLBACK_TYPE: """Add a listener that fires once after a specific point in time.""" - job = action if isinstance(action, HassJob) else HassJob(action) @callback @@ -1329,7 +1326,6 @@ def async_track_utc_time_change( local: bool = False, ) -> CALLBACK_TYPE: """Add a listener that will fire if time matches a pattern.""" - job = HassJob(action) # We do not have to wrap the function with time pattern matching logic # if no pattern given diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index def2508ff92..a0517338ec8 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -70,7 +70,6 @@ def report_integration( Async friendly. """ - found_frame, integration, path = integration_frame index = found_frame.filename.index(path) diff --git a/homeassistant/helpers/httpx_client.py b/homeassistant/helpers/httpx_client.py index 0f1719b388d..b86223964b3 100644 --- a/homeassistant/helpers/httpx_client.py +++ b/homeassistant/helpers/httpx_client.py @@ -51,7 +51,6 @@ def create_async_httpx_client( This method must be run in the event loop. """ - client = httpx.AsyncClient( verify=verify_ssl, headers={USER_AGENT: SERVER_SOFTWARE}, diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index 8ff454eab6f..4a768a79320 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -157,13 +157,11 @@ async def async_setup_reload_service( hass: HomeAssistantType, domain: str, platforms: Iterable ) -> None: """Create the reload service for the domain.""" - if hass.services.has_service(domain, SERVICE_RELOAD): return async def _reload_config(call: Event) -> None: """Reload the platforms.""" - await async_reload_integration_platforms(hass, domain, platforms) hass.bus.async_fire(f"event_{domain}_reloaded", context=call.context) @@ -176,7 +174,6 @@ def setup_reload_service( hass: HomeAssistantType, domain: str, platforms: Iterable ) -> None: """Sync version of async_setup_reload_service.""" - asyncio.run_coroutine_threadsafe( async_setup_reload_service(hass, domain, platforms), hass.loop, diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index c83fa4a7763..13dcd779b25 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -26,6 +26,7 @@ from homeassistant.const import ( ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_SERVICE, + CONF_SERVICE_DATA, CONF_SERVICE_TEMPLATE, CONF_TARGET, ENTITY_MATCH_ALL, @@ -62,7 +63,6 @@ if TYPE_CHECKING: CONF_SERVICE_ENTITY_ID = "entity_id" -CONF_SERVICE_DATA = "data" CONF_SERVICE_DATA_TEMPLATE = "data_template" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index a969b2cad9a..2bc13fbdf44 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -202,7 +202,6 @@ class Store: async def _async_handle_write_data(self, *_args): """Handle writing the config.""" - async with self._write_lock: self._async_cleanup_delay_listener() self._async_cleanup_final_write_listener() diff --git a/homeassistant/helpers/sun.py b/homeassistant/helpers/sun.py index 818010c3410..a2385ba397c 100644 --- a/homeassistant/helpers/sun.py +++ b/homeassistant/helpers/sun.py @@ -19,7 +19,6 @@ DATA_LOCATION_CACHE = "astral_location_cache" @bind_hass def get_astral_location(hass: HomeAssistantType) -> "astral.Location": """Get an astral location for the current Home Assistant configuration.""" - from astral import Location # pylint: disable=import-outside-toplevel latitude = hass.config.latitude diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index af63cab10eb..9da0cbc09eb 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1292,7 +1292,6 @@ def relative_time(value): If the input are not a datetime object the input will be returned unmodified. """ - if not isinstance(value, datetime): return value if not value.tzinfo: diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index b2424a06927..8ba355e6489 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -264,7 +264,6 @@ class CoordinatorEntity(entity.Entity): Only used by the generic entity update service. """ - # Ignore manual update requests if the entity is disabled if not self.enabled: return From 6b340415b2b6b3585d835f6e17aec7914fa29cd2 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 8 Feb 2021 16:53:46 -0500 Subject: [PATCH 288/796] Use core constants for greeneye_monitor (#46238) --- homeassistant/components/greeneye_monitor/__init__.py | 5 ++--- homeassistant/components/greeneye_monitor/sensor.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/greeneye_monitor/__init__.py b/homeassistant/components/greeneye_monitor/__init__.py index 697a96649ab..51471739e98 100644 --- a/homeassistant/components/greeneye_monitor/__init__.py +++ b/homeassistant/components/greeneye_monitor/__init__.py @@ -7,6 +7,8 @@ import voluptuous as vol from homeassistant.const import ( CONF_NAME, CONF_PORT, + CONF_SENSOR_TYPE, + CONF_SENSORS, CONF_TEMPERATURE_UNIT, EVENT_HOMEASSISTANT_STOP, TIME_HOURS, @@ -27,8 +29,6 @@ CONF_NET_METERING = "net_metering" CONF_NUMBER = "number" CONF_PULSE_COUNTERS = "pulse_counters" CONF_SERIAL_NUMBER = "serial_number" -CONF_SENSORS = "sensors" -CONF_SENSOR_TYPE = "sensor_type" CONF_TEMPERATURE_SENSORS = "temperature_sensors" CONF_TIME_UNIT = "time_unit" CONF_VOLTAGE_SENSORS = "voltage" @@ -119,7 +119,6 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: COMPONENT_SCHEMA}, extra=vol.ALLOW_EXTRA) async def async_setup(hass, config): """Set up the GreenEye Monitor component.""" - monitors = Monitors() hass.data[DATA_GREENEYE_MONITOR] = monitors diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index c8cd1669f05..f026bdfe3a4 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -1,6 +1,7 @@ """Support for the sensors in a GreenEye Monitor.""" from homeassistant.const import ( CONF_NAME, + CONF_SENSOR_TYPE, CONF_TEMPERATURE_UNIT, POWER_WATT, TIME_HOURS, @@ -16,7 +17,6 @@ from . import ( CONF_MONITOR_SERIAL_NUMBER, CONF_NET_METERING, CONF_NUMBER, - CONF_SENSOR_TYPE, CONF_TIME_UNIT, DATA_GREENEYE_MONITOR, SENSOR_TYPE_CURRENT, From dc26fd51495348b3e9f7a8a2e39c3fa8b2091d76 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Feb 2021 12:22:38 -1000 Subject: [PATCH 289/796] Ensure creating an index that already exists is forgiving for postgresql (#46185) Unlikely sqlite and mysql, postgresql throws ProgrammingError instead of InternalError or OperationalError when trying to create an index that already exists. --- .../components/recorder/migration.py | 16 +++++----- tests/components/recorder/test_migrate.py | 30 ++++++++++++++++++- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 4501b25385e..aeb62cc111d 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -3,7 +3,12 @@ import logging from sqlalchemy import ForeignKeyConstraint, MetaData, Table, text from sqlalchemy.engine import reflection -from sqlalchemy.exc import InternalError, OperationalError, SQLAlchemyError +from sqlalchemy.exc import ( + InternalError, + OperationalError, + ProgrammingError, + SQLAlchemyError, +) from sqlalchemy.schema import AddConstraint, DropConstraint from .const import DOMAIN @@ -69,7 +74,7 @@ def _create_index(engine, table_name, index_name): ) try: index.create(engine) - except OperationalError as err: + except (InternalError, ProgrammingError, OperationalError) as err: lower_err_str = str(err).lower() if "already exists" not in lower_err_str and "duplicate" not in lower_err_str: @@ -78,13 +83,6 @@ def _create_index(engine, table_name, index_name): _LOGGER.warning( "Index %s already exists on %s, continuing", index_name, table_name ) - except InternalError as err: - if "duplicate" not in str(err).lower(): - raise - - _LOGGER.warning( - "Index %s already exists on %s, continuing", index_name, table_name - ) _LOGGER.debug("Finished creating %s", index_name) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index d10dad43d75..c29dad2d495 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -1,9 +1,10 @@ """The tests for the Recorder component.""" # pylint: disable=protected-access -from unittest.mock import call, patch +from unittest.mock import Mock, PropertyMock, call, patch import pytest from sqlalchemy import create_engine +from sqlalchemy.exc import InternalError, OperationalError, ProgrammingError from sqlalchemy.pool import StaticPool from homeassistant.bootstrap import async_setup_component @@ -79,3 +80,30 @@ def test_forgiving_add_index(): engine = create_engine("sqlite://", poolclass=StaticPool) models.Base.metadata.create_all(engine) migration._create_index(engine, "states", "ix_states_context_id") + + +@pytest.mark.parametrize( + "exception_type", [OperationalError, ProgrammingError, InternalError] +) +def test_forgiving_add_index_with_other_db_types(caplog, exception_type): + """Test that add index will continue if index exists on mysql and postgres.""" + mocked_index = Mock() + type(mocked_index).name = "ix_states_context_id" + mocked_index.create = Mock( + side_effect=exception_type( + "CREATE INDEX ix_states_old_state_id ON states (old_state_id);", + [], + 'relation "ix_states_old_state_id" already exists', + ) + ) + + mocked_table = Mock() + type(mocked_table).indexes = PropertyMock(return_value=[mocked_index]) + + with patch( + "homeassistant.components.recorder.migration.Table", return_value=mocked_table + ): + migration._create_index(Mock(), "states", "ix_states_context_id") + + assert "already exists on states" in caplog.text + assert "continuing" in caplog.text From 6467eff09cf66e035c1003577e1422d8e55808ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Feb 2021 12:23:02 -1000 Subject: [PATCH 290/796] Fix incorrect current temperature for homekit water heaters (#46076) --- .../components/homekit/type_thermostats.py | 13 +++++++++---- tests/components/homekit/test_type_thermostats.py | 8 ++++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 54e2e9f92a8..a1c13432614 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -595,10 +595,15 @@ class WaterHeater(HomeAccessory): def async_update_state(self, new_state): """Update water_heater state after state change.""" # Update current and target temperature - 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) + target_temperature = _get_target_temperature(new_state, self._unit) + if target_temperature is not None: + if target_temperature != self.char_target_temp.value: + self.char_target_temp.set_value(target_temperature) + + current_temperature = _get_current_temperature(new_state, self._unit) + if current_temperature is not None: + if current_temperature != self.char_current_temp.value: + self.char_current_temp.set_value(current_temperature) # Update display units if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT: diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index ce17cf7ea07..79b5ca21097 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -1599,11 +1599,15 @@ async def test_water_heater(hass, hk_driver, events): hass.states.async_set( entity_id, HVAC_MODE_HEAT, - {ATTR_HVAC_MODE: HVAC_MODE_HEAT, ATTR_TEMPERATURE: 56.0}, + { + ATTR_HVAC_MODE: HVAC_MODE_HEAT, + ATTR_TEMPERATURE: 56.0, + ATTR_CURRENT_TEMPERATURE: 35.0, + }, ) await hass.async_block_till_done() assert acc.char_target_temp.value == 56.0 - assert acc.char_current_temp.value == 50.0 + assert acc.char_current_temp.value == 35.0 assert acc.char_target_heat_cool.value == 1 assert acc.char_current_heat_cool.value == 1 assert acc.char_display_units.value == 0 From c602c619a27d8b387c93049acef19415fc0fd6ac Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 8 Feb 2021 18:13:58 -0500 Subject: [PATCH 291/796] Use core constants for hikvision (#46247) --- homeassistant/components/hikvision/binary_sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/hikvision/binary_sensor.py b/homeassistant/components/hikvision/binary_sensor.py index 359f966d119..90c4b6ce8b9 100644 --- a/homeassistant/components/hikvision/binary_sensor.py +++ b/homeassistant/components/hikvision/binary_sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import ( ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE, + CONF_DELAY, CONF_HOST, CONF_NAME, CONF_PASSWORD, @@ -30,7 +31,6 @@ from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) CONF_IGNORED = "ignored" -CONF_DELAY = "delay" DEFAULT_PORT = 80 DEFAULT_IGNORED = False @@ -139,7 +139,6 @@ class HikvisionData: def __init__(self, hass, url, port, name, username, password): """Initialize the data object.""" - self._url = url self._port = port self._name = name From 58b4a91a5b4fcf03bb9236ad85a8383c6f4cb47a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 9 Feb 2021 00:34:18 +0100 Subject: [PATCH 292/796] Test that variables are passed to wait_for_trigger script action (#46221) --- tests/common.py | 72 +++++++++++++++++++++++++++++++++++- tests/helpers/test_script.py | 43 +++++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/tests/common.py b/tests/common.py index 2621f2f4b15..ab5da25e38d 100644 --- a/tests/common.py +++ b/tests/common.py @@ -12,6 +12,9 @@ import os import pathlib import threading import time +from time import monotonic +import types +from typing import Any, Awaitable, Collection, Optional from unittest.mock import AsyncMock, Mock, patch import uuid @@ -43,7 +46,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import State +from homeassistant.core import BLOCK_LOG_TIMEOUT, State from homeassistant.helpers import ( area_registry, device_registry, @@ -190,9 +193,76 @@ async def async_test_home_assistant(loop): return orig_async_create_task(coroutine) + async def async_wait_for_task_count(self, max_remaining_tasks: int = 0) -> None: + """Block until at most max_remaining_tasks remain. + + Based on HomeAssistant.async_block_till_done + """ + # To flush out any call_soon_threadsafe + await asyncio.sleep(0) + start_time: Optional[float] = None + + while len(self._pending_tasks) > max_remaining_tasks: + pending = [ + task for task in self._pending_tasks if not task.done() + ] # type: Collection[Awaitable[Any]] + self._pending_tasks.clear() + if len(pending) > max_remaining_tasks: + remaining_pending = await self._await_count_and_log_pending( + pending, max_remaining_tasks=max_remaining_tasks + ) + self._pending_tasks.extend(remaining_pending) + + if start_time is None: + # Avoid calling monotonic() until we know + # we may need to start logging blocked tasks. + start_time = 0 + elif start_time == 0: + # If we have waited twice then we set the start + # time + start_time = monotonic() + elif monotonic() - start_time > BLOCK_LOG_TIMEOUT: + # We have waited at least three loops and new tasks + # continue to block. At this point we start + # logging all waiting tasks. + for task in pending: + _LOGGER.debug("Waiting for task: %s", task) + else: + self._pending_tasks.extend(pending) + await asyncio.sleep(0) + + async def _await_count_and_log_pending( + self, pending: Collection[Awaitable[Any]], max_remaining_tasks: int = 0 + ) -> Collection[Awaitable[Any]]: + """Block at most max_remaining_tasks remain and log tasks that take a long time. + + Based on HomeAssistant._await_and_log_pending + """ + wait_time = 0 + + return_when = asyncio.ALL_COMPLETED + if max_remaining_tasks: + return_when = asyncio.FIRST_COMPLETED + + while len(pending) > max_remaining_tasks: + _, pending = await asyncio.wait( + pending, timeout=BLOCK_LOG_TIMEOUT, return_when=return_when + ) + if not pending or max_remaining_tasks: + return pending + wait_time += BLOCK_LOG_TIMEOUT + for task in pending: + _LOGGER.debug("Waited %s seconds for task: %s", wait_time, task) + + return [] + hass.async_add_job = async_add_job hass.async_add_executor_job = async_add_executor_job hass.async_create_task = async_create_task + hass.async_wait_for_task_count = types.MethodType(async_wait_for_task_count, hass) + hass._await_count_and_log_pending = types.MethodType( + _await_count_and_log_pending, hass + ) hass.data[loader.DATA_CUSTOM_COMPONENTS] = {} diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 5cd9a9d2449..a22cf27acdc 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -545,6 +545,49 @@ async def test_wait_basic(hass, action_type): assert script_obj.last_action is None +async def test_wait_for_trigger_variables(hass): + """Test variables are passed to wait_for_trigger action.""" + context = Context() + wait_alias = "wait step" + actions = [ + { + "alias": "variables", + "variables": {"seconds": 5}, + }, + { + "alias": wait_alias, + "wait_for_trigger": { + "platform": "state", + "entity_id": "switch.test", + "to": "off", + "for": {"seconds": "{{ seconds }}"}, + }, + }, + ] + sequence = cv.SCRIPT_SCHEMA(actions) + sequence = await script.async_validate_actions_config(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + wait_started_flag = async_watch_for_action(script_obj, wait_alias) + + try: + hass.states.async_set("switch.test", "on") + hass.async_create_task(script_obj.async_run(context=context)) + await asyncio.wait_for(wait_started_flag.wait(), 1) + assert script_obj.is_running + assert script_obj.last_action == wait_alias + hass.states.async_set("switch.test", "off") + # the script task + 2 tasks created by wait_for_trigger script step + await hass.async_wait_for_task_count(3) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + except (AssertionError, asyncio.TimeoutError): + await script_obj.async_stop() + raise + else: + assert not script_obj.is_running + assert script_obj.last_action is None + + @pytest.mark.parametrize("action_type", ["template", "trigger"]) async def test_wait_basic_times_out(hass, action_type): """Test wait actions times out when the action does not happen.""" From 93fafedf7213d72115cf74196ea4df95bfb3700f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Feb 2021 13:37:32 -1000 Subject: [PATCH 293/796] Cleanup bond identifiers and device info (#46192) --- homeassistant/components/bond/__init__.py | 26 ++++++++-- homeassistant/components/bond/const.py | 2 + homeassistant/components/bond/entity.py | 18 ++++++- homeassistant/components/bond/utils.py | 29 ++++++++++- tests/components/bond/test_init.py | 60 ++++++++++++++++++++++- 5 files changed, 126 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 9af92f6e7e7..88ea084d25f 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -7,12 +7,12 @@ from bond_api import Bond from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import SLOW_UPDATE_WARNING -from .const import DOMAIN +from .const import BRIDGE_MAKE, DOMAIN from .utils import BondHub PLATFORMS = ["cover", "fan", "light", "switch"] @@ -29,6 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Bond from a config entry.""" host = entry.data[CONF_HOST] token = entry.data[CONF_ACCESS_TOKEN] + config_entry_id = entry.entry_id bond = Bond(host=host, token=token, timeout=ClientTimeout(total=_API_TIMEOUT)) hub = BondHub(bond) @@ -37,21 +38,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except (ClientError, AsyncIOTimeoutError, OSError) as error: raise ConfigEntryNotReady from error - hass.data[DOMAIN][entry.entry_id] = hub + hass.data[DOMAIN][config_entry_id] = hub if not entry.unique_id: hass.config_entries.async_update_entry(entry, unique_id=hub.bond_id) device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( - config_entry_id=entry.entry_id, + config_entry_id=config_entry_id, identifiers={(DOMAIN, hub.bond_id)}, - manufacturer="Olibra", + manufacturer=BRIDGE_MAKE, name=hub.bond_id, model=hub.target, sw_version=hub.fw_ver, ) + _async_remove_old_device_identifiers(config_entry_id, device_registry, hub) + for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) @@ -75,3 +78,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +@callback +def _async_remove_old_device_identifiers( + config_entry_id: str, device_registry: dr.DeviceRegistry, hub: BondHub +): + """Remove the non-unique device registry entries.""" + for device in hub.devices: + dev = device_registry.async_get_device(identifiers={(DOMAIN, device.device_id)}) + if dev is None: + continue + if config_entry_id in dev.config_entries: + device_registry.async_remove_device(dev.id) diff --git a/homeassistant/components/bond/const.py b/homeassistant/components/bond/const.py index 843c3f9f1dc..3031c159b0f 100644 --- a/homeassistant/components/bond/const.py +++ b/homeassistant/components/bond/const.py @@ -1,5 +1,7 @@ """Constants for the Bond integration.""" +BRIDGE_MAKE = "Olibra" + DOMAIN = "bond" CONF_BOND_ID: str = "bond_id" diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index 501d6574960..2819182c9b5 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -43,11 +43,25 @@ class BondEntity(Entity): @property def device_info(self) -> Optional[Dict[str, Any]]: """Get a an HA device representing this Bond controlled device.""" - return { + device_info = { ATTR_NAME: self.name, - "identifiers": {(DOMAIN, self._device.device_id)}, + "manufacturer": self._hub.make, + "identifiers": {(DOMAIN, self._hub.bond_id, self._device.device_id)}, "via_device": (DOMAIN, self._hub.bond_id), } + if not self._hub.is_bridge: + device_info["model"] = self._hub.model + device_info["sw_version"] = self._hub.fw_ver + else: + model_data = [] + if self._device.branding_profile: + model_data.append(self._device.branding_profile) + if self._device.template: + model_data.append(self._device.template) + if model_data: + device_info["model"] = " ".join(model_data) + + return device_info @property def assumed_state(self) -> bool: diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py index 5a9fff692fa..df3373ed7a1 100644 --- a/homeassistant/components/bond/utils.py +++ b/homeassistant/components/bond/utils.py @@ -4,6 +4,8 @@ from typing import List, Optional from bond_api import Action, Bond +from .const import BRIDGE_MAKE + _LOGGER = logging.getLogger(__name__) @@ -34,6 +36,21 @@ class BondDevice: """Get the type of this device.""" return self._attrs["type"] + @property + def location(self) -> str: + """Get the location of this device.""" + return self._attrs["location"] + + @property + def template(self) -> str: + """Return this model template.""" + return self._attrs.get("template") + + @property + def branding_profile(self) -> str: + """Return this branding profile.""" + return self.props.get("branding_profile") + @property def trust_state(self) -> bool: """Check if Trust State is turned on.""" @@ -100,9 +117,19 @@ class BondHub: @property def target(self) -> str: - """Return this hub model.""" + """Return this hub target.""" return self._version.get("target") + @property + def model(self) -> str: + """Return this hub model.""" + return self._version.get("model") + + @property + def make(self) -> str: + """Return this hub make.""" + return self._version.get("make", BRIDGE_MAKE) + @property def fw_ver(self) -> str: """Return this hub firmware version.""" diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index 98d86058c49..e2bb6314126 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -1,5 +1,6 @@ """Tests for the Bond module.""" from aiohttp import ClientConnectionError +from bond_api import DeviceType from homeassistant.components.bond.const import DOMAIN from homeassistant.config_entries import ( @@ -12,7 +13,15 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component -from .common import patch_bond_version, patch_setup_entry, setup_bond_entity +from .common import ( + patch_bond_device, + patch_bond_device_ids, + patch_bond_device_properties, + patch_bond_device_state, + patch_bond_version, + patch_setup_entry, + setup_bond_entity, +) from tests.common import MockConfigEntry @@ -105,3 +114,52 @@ async def test_unload_config_entry(hass: HomeAssistant): assert config_entry.entry_id not in hass.data[DOMAIN] assert config_entry.state == ENTRY_STATE_NOT_LOADED + + +async def test_old_identifiers_are_removed(hass: HomeAssistant): + """Test we remove the old non-unique identifiers.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, + ) + + old_identifers = (DOMAIN, "device_id") + new_identifiers = (DOMAIN, "test-bond-id", "device_id") + device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={old_identifers}, + manufacturer="any", + name="old", + ) + + config_entry.add_to_hass(hass) + + with patch_bond_version( + return_value={ + "bondid": "test-bond-id", + "target": "test-model", + "fw_ver": "test-version", + } + ), patch_bond_device_ids( + return_value=["bond-device-id", "device_id"] + ), patch_bond_device( + return_value={ + "name": "test1", + "type": DeviceType.GENERIC_DEVICE, + } + ), patch_bond_device_properties( + return_value={} + ), patch_bond_device_state( + return_value={} + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) is True + await hass.async_block_till_done() + + assert config_entry.entry_id in hass.data[DOMAIN] + assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.unique_id == "test-bond-id" + + # verify the device info is cleaned up + assert device_registry.async_get_device(identifiers={old_identifers}) is None + assert device_registry.async_get_device(identifiers={new_identifiers}) is not None From 6563c37ab196f638585f3b202159d73c0dcc6806 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Feb 2021 13:39:21 -1000 Subject: [PATCH 294/796] Add support for generic lights to bond (#46193) --- homeassistant/components/bond/light.py | 8 +++++++- tests/components/bond/test_light.py | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py index 77771167e14..c0809a0aee7 100644 --- a/homeassistant/components/bond/light.py +++ b/homeassistant/components/bond/light.py @@ -47,7 +47,13 @@ async def async_setup_entry( if DeviceType.is_fireplace(device.type) and device.supports_light() ] - async_add_entities(fan_lights + fireplaces + fp_lights, True) + lights: List[Entity] = [ + BondLight(hub, device) + for device in hub.devices + if DeviceType.is_light(device.type) + ] + + async_add_entities(fan_lights + fireplaces + fp_lights + lights, True) class BondLight(BondEntity, LightEntity): diff --git a/tests/components/bond/test_light.py b/tests/components/bond/test_light.py index 6d871187e26..3f7a3ef62f9 100644 --- a/tests/components/bond/test_light.py +++ b/tests/components/bond/test_light.py @@ -29,6 +29,15 @@ from .common import ( from tests.common import async_fire_time_changed +def light(name: str): + """Create a light with a given name.""" + return { + "name": name, + "type": DeviceType.LIGHT, + "actions": [Action.TURN_LIGHT_ON, Action.TURN_LIGHT_OFF, Action.SET_BRIGHTNESS], + } + + def ceiling_fan(name: str): """Create a ceiling fan (that has built-in light) with given name.""" return { @@ -117,6 +126,21 @@ async def test_fireplace_with_light_entity_registry(hass: core.HomeAssistant): assert entity_light.unique_id == "test-hub-id_test-device-id_light" +async def test_light_entity_registry(hass: core.HomeAssistant): + """Tests lights are registered in the entity registry.""" + await setup_platform( + hass, + LIGHT_DOMAIN, + light("light-name"), + bond_version={"bondid": "test-hub-id"}, + bond_device_id="test-device-id", + ) + + registry: EntityRegistry = await hass.helpers.entity_registry.async_get_registry() + entity = registry.entities["light.light_name"] + assert entity.unique_id == "test-hub-id_test-device-id" + + async def test_sbb_trust_state(hass: core.HomeAssistant): """Assumed state should be False if device is a Smart by Bond.""" version = { From 936ee7d7336888b2fbb3f62b7500b5f701a4c989 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 9 Feb 2021 00:07:22 +0000 Subject: [PATCH 295/796] [ci skip] Translation update --- homeassistant/components/media_player/translations/ca.json | 7 +++++++ homeassistant/components/media_player/translations/en.json | 7 +++++++ homeassistant/components/media_player/translations/et.json | 7 +++++++ .../components/media_player/translations/zh-Hant.json | 7 +++++++ .../components/mysensors/translations/zh-Hant.json | 2 ++ 5 files changed, 30 insertions(+) diff --git a/homeassistant/components/media_player/translations/ca.json b/homeassistant/components/media_player/translations/ca.json index 67f7aad655b..e1fce334053 100644 --- a/homeassistant/components/media_player/translations/ca.json +++ b/homeassistant/components/media_player/translations/ca.json @@ -6,6 +6,13 @@ "is_on": "{entity_name} est\u00e0 enc\u00e8s", "is_paused": "{entity_name} est\u00e0 en pausa", "is_playing": "{entity_name} est\u00e0 reproduint" + }, + "trigger_type": { + "idle": "{entity_name} es torna inactiu", + "paused": "{entity_name} est\u00e0 en pausa", + "playing": "{entity_name} comen\u00e7a a reproduir", + "turned_off": "{entity_name} s'ha apagat", + "turned_on": "{entity_name} s'ha engegat" } }, "state": { diff --git a/homeassistant/components/media_player/translations/en.json b/homeassistant/components/media_player/translations/en.json index 3a96a2b3a90..aa995be9904 100644 --- a/homeassistant/components/media_player/translations/en.json +++ b/homeassistant/components/media_player/translations/en.json @@ -6,6 +6,13 @@ "is_on": "{entity_name} is on", "is_paused": "{entity_name} is paused", "is_playing": "{entity_name} is playing" + }, + "trigger_type": { + "idle": "{entity_name} becomes idle", + "paused": "{entity_name} is paused", + "playing": "{entity_name} starts playing", + "turned_off": "{entity_name} turned off", + "turned_on": "{entity_name} turned on" } }, "state": { diff --git a/homeassistant/components/media_player/translations/et.json b/homeassistant/components/media_player/translations/et.json index 4d71a30a8ac..687a0e5953d 100644 --- a/homeassistant/components/media_player/translations/et.json +++ b/homeassistant/components/media_player/translations/et.json @@ -6,6 +6,13 @@ "is_on": "{entity_name} on sisse l\u00fclitatud", "is_paused": "{entity_name} on peatatud", "is_playing": "{entity_name} m\u00e4ngib" + }, + "trigger_type": { + "idle": "{entity_name} muutub j\u00f5udeolekusse", + "paused": "{entity_name} on pausil", + "playing": "{entity_name} alustab taasesitamist", + "turned_off": "{entity_name} l\u00fclitus v\u00e4lja", + "turned_on": "{entity_name} l\u00fclitus sisse" } }, "state": { diff --git a/homeassistant/components/media_player/translations/zh-Hant.json b/homeassistant/components/media_player/translations/zh-Hant.json index 3ae786cbed9..a3a4b82380e 100644 --- a/homeassistant/components/media_player/translations/zh-Hant.json +++ b/homeassistant/components/media_player/translations/zh-Hant.json @@ -6,6 +6,13 @@ "is_on": "{entity_name}\u958b\u555f", "is_paused": "{entity_name}\u5df2\u66ab\u505c", "is_playing": "{entity_name}\u6b63\u5728\u64ad\u653e" + }, + "trigger_type": { + "idle": "{entity_name}\u8b8a\u6210\u9592\u7f6e", + "paused": "{entity_name}\u5df2\u66ab\u505c", + "playing": "{entity_name}\u958b\u59cb\u64ad\u653e", + "turned_off": "{entity_name}\u5df2\u95dc\u9589", + "turned_on": "{entity_name}\u5df2\u958b\u555f" } }, "state": { diff --git a/homeassistant/components/mysensors/translations/zh-Hant.json b/homeassistant/components/mysensors/translations/zh-Hant.json index 0d4db4502e5..d0067c2d0ce 100644 --- a/homeassistant/components/mysensors/translations/zh-Hant.json +++ b/homeassistant/components/mysensors/translations/zh-Hant.json @@ -3,10 +3,12 @@ "abort": { "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", + "duplicate_persistence_file": "Persistence \u6a94\u6848\u5df2\u4f7f\u7528\u4e2d", "duplicate_topic": "\u4e3b\u984c\u5df2\u4f7f\u7528\u4e2d", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "invalid_device": "\u88dd\u7f6e\u7121\u6548", "invalid_ip": "IP \u4f4d\u5740\u7121\u6548", + "invalid_persistence_file": "Persistence \u6a94\u6848\u7121\u6548", "invalid_port": "\u901a\u8a0a\u57e0\u865f\u78bc\u7121\u6548", "invalid_publish_topic": "\u767c\u5e03\u4e3b\u984c\u7121\u6548", "invalid_serial": "\u5e8f\u5217\u57e0\u7121\u6548", From 889baef4567e573d9da3f6fbb4af8bf08e74fc39 Mon Sep 17 00:00:00 2001 From: Pascal Reeb Date: Tue, 9 Feb 2021 04:11:27 +0100 Subject: [PATCH 296/796] Add DHCP discovery support to Nuki integration (#46032) --- homeassistant/components/nuki/config_flow.py | 27 ++++++- homeassistant/components/nuki/manifest.json | 3 +- homeassistant/generated/dhcp.py | 4 + tests/components/nuki/mock.py | 25 ++++++ tests/components/nuki/test_config_flow.py | 81 +++++++++++++++----- 5 files changed, 119 insertions(+), 21 deletions(-) create mode 100644 tests/components/nuki/mock.py diff --git a/homeassistant/components/nuki/config_flow.py b/homeassistant/components/nuki/config_flow.py index 9af74cb4423..f065f1c27ef 100644 --- a/homeassistant/components/nuki/config_flow.py +++ b/homeassistant/components/nuki/config_flow.py @@ -7,6 +7,7 @@ from requests.exceptions import RequestException import voluptuous as vol from homeassistant import config_entries, exceptions +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN from .const import ( # pylint: disable=unused-import @@ -54,6 +55,10 @@ async def validate_input(hass, data): class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Nuki config flow.""" + def __init__(self): + """Initialize the Nuki config flow.""" + self.discovery_schema = {} + async def async_step_import(self, user_input=None): """Handle a flow initiated by import.""" return await self.async_step_validate(user_input) @@ -62,7 +67,23 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initiated by the user.""" return await self.async_step_validate(user_input) - async def async_step_validate(self, user_input): + async def async_step_dhcp(self, discovery_info: dict): + """Prepare configuration for a DHCP discovered Nuki bridge.""" + await self.async_set_unique_id(int(discovery_info.get(HOSTNAME)[12:], 16)) + + self._abort_if_unique_id_configured() + + self.discovery_schema = vol.Schema( + { + vol.Required(CONF_HOST, default=discovery_info[IP_ADDRESS]): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + vol.Required(CONF_TOKEN): str, + } + ) + + return await self.async_step_validate() + + async def async_step_validate(self, user_input=None): """Handle init step of a flow.""" errors = {} @@ -84,8 +105,10 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=info["ids"]["hardwareId"], data=user_input ) + data_schema = self.discovery_schema or USER_SCHEMA + return self.async_show_form( - step_id="user", data_schema=USER_SCHEMA, errors=errors + step_id="user", data_schema=data_schema, errors=errors ) diff --git a/homeassistant/components/nuki/manifest.json b/homeassistant/components/nuki/manifest.json index 9385821845a..7fb9a134c4c 100644 --- a/homeassistant/components/nuki/manifest.json +++ b/homeassistant/components/nuki/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/nuki", "requirements": ["pynuki==1.3.8"], "codeowners": ["@pschmitt", "@pvizeli", "@pree"], - "config_flow": true + "config_flow": true, + "dhcp": [{ "hostname": "nuki_bridge_*" }] } \ No newline at end of file diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 61223bf00f7..31ee42bc48c 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -70,6 +70,10 @@ DHCP = [ "hostname": "nuheat", "macaddress": "002338*" }, + { + "domain": "nuki", + "hostname": "nuki_bridge_*" + }, { "domain": "powerwall", "hostname": "1118431-*", diff --git a/tests/components/nuki/mock.py b/tests/components/nuki/mock.py new file mode 100644 index 00000000000..a7870ce0906 --- /dev/null +++ b/tests/components/nuki/mock.py @@ -0,0 +1,25 @@ +"""Mockup Nuki device.""" +from homeassistant import setup + +from tests.common import MockConfigEntry + +NAME = "Nuki_Bridge_75BCD15" +HOST = "1.1.1.1" +MAC = "01:23:45:67:89:ab" + +HW_ID = 123456789 + +MOCK_INFO = {"ids": {"hardwareId": HW_ID}} + + +async def setup_nuki_integration(hass): + """Create the Nuki device.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain="nuki", + unique_id=HW_ID, + data={"host": HOST, "port": 8080, "token": "test-token"}, + ) + entry.add_to_hass(hass) + + return entry diff --git a/tests/components/nuki/test_config_flow.py b/tests/components/nuki/test_config_flow.py index bcdedad371a..4933ea52b77 100644 --- a/tests/components/nuki/test_config_flow.py +++ b/tests/components/nuki/test_config_flow.py @@ -5,9 +5,10 @@ from pynuki.bridge import InvalidCredentialsException from requests.exceptions import RequestException from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.components.nuki.const import DOMAIN -from tests.common import MockConfigEntry +from .mock import HOST, MAC, MOCK_INFO, NAME, setup_nuki_integration async def test_form(hass): @@ -19,11 +20,9 @@ async def test_form(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} - mock_info = {"ids": {"hardwareId": "0001"}} - with patch( "homeassistant.components.nuki.config_flow.NukiBridge.info", - return_value=mock_info, + return_value=MOCK_INFO, ), patch( "homeassistant.components.nuki.async_setup", return_value=True ) as mock_setup, patch( @@ -41,7 +40,7 @@ async def test_form(hass): await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "0001" + assert result2["title"] == 123456789 assert result2["data"] == { "host": "1.1.1.1", "port": 8080, @@ -55,11 +54,9 @@ async def test_import(hass): """Test that the import works.""" await setup.async_setup_component(hass, "persistent_notification", {}) - mock_info = {"ids": {"hardwareId": "0001"}} - with patch( "homeassistant.components.nuki.config_flow.NukiBridge.info", - return_value=mock_info, + return_value=MOCK_INFO, ), patch( "homeassistant.components.nuki.async_setup", return_value=True ) as mock_setup, patch( @@ -72,7 +69,7 @@ async def test_import(hass): data={"host": "1.1.1.1", "port": 8080, "token": "test-token"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "0001" + assert result["title"] == 123456789 assert result["data"] == { "host": "1.1.1.1", "port": 8080, @@ -155,21 +152,14 @@ async def test_form_unknown_exception(hass): async def test_form_already_configured(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - entry = MockConfigEntry( - domain="nuki", - unique_id="0001", - data={"host": "1.1.1.1", "port": 8080, "token": "test-token"}, - ) - entry.add_to_hass(hass) - + await setup_nuki_integration(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( "homeassistant.components.nuki.config_flow.NukiBridge.info", - return_value={"ids": {"hardwareId": "0001"}}, + return_value=MOCK_INFO, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -182,3 +172,58 @@ async def test_form_already_configured(hass): assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result2["reason"] == "already_configured" + + +async def test_dhcp_flow(hass): + """Test that DHCP discovery for new bridge works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={HOSTNAME: NAME, IP_ADDRESS: HOST, MAC_ADDRESS: MAC}, + context={"source": config_entries.SOURCE_DHCP}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == config_entries.SOURCE_USER + + with patch( + "homeassistant.components.nuki.config_flow.NukiBridge.info", + return_value=MOCK_INFO, + ), patch( + "homeassistant.components.nuki.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.nuki.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "port": 8080, + "token": "test-token", + }, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == 123456789 + assert result2["data"] == { + "host": "1.1.1.1", + "port": 8080, + "token": "test-token", + } + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_dhcp_flow_already_configured(hass): + """Test that DHCP doesn't setup already configured devices.""" + await setup_nuki_integration(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={HOSTNAME: NAME, IP_ADDRESS: HOST, MAC_ADDRESS: MAC}, + context={"source": config_entries.SOURCE_DHCP}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" From 2bcf87b980b7f394b842c5af6b592049d2fff09b Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 8 Feb 2021 19:53:28 -0800 Subject: [PATCH 297/796] Change the API boundary between camera and stream with initial improvement for nest expiring stream urls (#45431) * Change the API boundary between stream and camera Shift more of the stream lifecycle management to the camera. The motivation is to support stream urls that expire giving the camera the ability to change the stream once it is created. * Document stream lifecycle and simplify stream/camera interaction * Reorder create_stream function to reduce diffs * Increase test coverage for camera_sdm.py * Fix ffmpeg typo. * Add a stream identifier for each stream, managed by camera * Remove stream record service * Update homeassistant/components/stream/__init__.py Co-authored-by: Paulus Schoutsen * Unroll changes to Stream interface back into camera component * Fix preload stream to actually start the background worker * Reduce unncessary diffs for readability * Remove redundant camera stream start code Co-authored-by: Paulus Schoutsen --- homeassistant/components/camera/__init__.py | 132 ++++++---------- homeassistant/components/camera/const.py | 5 + homeassistant/components/nest/camera_sdm.py | 7 + homeassistant/components/stream/__init__.py | 143 +++++++----------- homeassistant/components/stream/const.py | 7 - homeassistant/components/stream/core.py | 6 +- homeassistant/components/stream/services.yaml | 15 -- tests/components/camera/test_init.py | 56 +++---- tests/components/generic/test_camera.py | 49 ++++-- tests/components/nest/camera_sdm_test.py | 6 + tests/components/stream/common.py | 10 -- tests/components/stream/test_hls.py | 30 ++-- tests/components/stream/test_init.py | 86 ----------- tests/components/stream/test_recorder.py | 58 +++++-- 14 files changed, 254 insertions(+), 356 deletions(-) delete mode 100644 homeassistant/components/stream/services.yaml delete mode 100644 tests/components/stream/test_init.py diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 25505800709..cbc98edf19b 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -23,16 +23,8 @@ from homeassistant.components.media_player.const import ( DOMAIN as DOMAIN_MP, SERVICE_PLAY_MEDIA, ) -from homeassistant.components.stream import request_stream -from homeassistant.components.stream.const import ( - CONF_DURATION, - CONF_LOOKBACK, - CONF_STREAM_SOURCE, - DOMAIN as DOMAIN_STREAM, - FORMAT_CONTENT_TYPE, - OUTPUT_FORMATS, - SERVICE_RECORD, -) +from homeassistant.components.stream import Stream, create_stream +from homeassistant.components.stream.const import FORMAT_CONTENT_TYPE, OUTPUT_FORMATS from homeassistant.const import ( ATTR_ENTITY_ID, CONF_FILENAME, @@ -53,7 +45,13 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.network import get_url from homeassistant.loader import bind_hass -from .const import DATA_CAMERA_PREFS, DOMAIN +from .const import ( + CONF_DURATION, + CONF_LOOKBACK, + DATA_CAMERA_PREFS, + DOMAIN, + SERVICE_RECORD, +) from .prefs import CameraPreferences # mypy: allow-untyped-calls, allow-untyped-defs @@ -130,23 +128,7 @@ class Image: async def async_request_stream(hass, entity_id, fmt): """Request a stream for a camera entity.""" camera = _get_camera_from_entity_id(hass, entity_id) - camera_prefs = hass.data[DATA_CAMERA_PREFS].get(entity_id) - - async with async_timeout.timeout(10): - source = await camera.stream_source() - - if not source: - raise HomeAssistantError( - f"{camera.entity_id} does not support play stream service" - ) - - return request_stream( - hass, - source, - fmt=fmt, - keepalive=camera_prefs.preload_stream, - options=camera.stream_options, - ) + return await _async_stream_endpoint_url(hass, camera, fmt) @bind_hass @@ -267,14 +249,11 @@ async def async_setup(hass, config): camera_prefs = prefs.get(camera.entity_id) if not camera_prefs.preload_stream: continue - - async with async_timeout.timeout(10): - source = await camera.stream_source() - - if not source: + stream = await camera.create_stream() + if not stream: continue - - request_stream(hass, source, keepalive=True, options=camera.stream_options) + stream.add_provider("hls") + stream.start() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, preload_stream) @@ -330,6 +309,7 @@ class Camera(Entity): def __init__(self): """Initialize a camera.""" self.is_streaming = False + self.stream = None self.stream_options = {} self.content_type = DEFAULT_CONTENT_TYPE self.access_tokens: collections.deque = collections.deque([], 2) @@ -375,6 +355,17 @@ class Camera(Entity): """Return the interval between frames of the mjpeg stream.""" return 0.5 + async def create_stream(self) -> Stream: + """Create a Stream for stream_source.""" + # There is at most one stream (a decode worker) per camera + if not self.stream: + async with async_timeout.timeout(10): + source = await self.stream_source() + if not source: + return None + self.stream = create_stream(self.hass, source, options=self.stream_options) + return self.stream + async def stream_source(self): """Return the source of the stream.""" return None @@ -586,24 +577,7 @@ async def ws_camera_stream(hass, connection, msg): try: entity_id = msg["entity_id"] camera = _get_camera_from_entity_id(hass, entity_id) - camera_prefs = hass.data[DATA_CAMERA_PREFS].get(entity_id) - - async with async_timeout.timeout(10): - source = await camera.stream_source() - - if not source: - raise HomeAssistantError( - f"{camera.entity_id} does not support play stream service" - ) - - fmt = msg["format"] - url = request_stream( - hass, - source, - fmt=fmt, - keepalive=camera_prefs.preload_stream, - options=camera.stream_options, - ) + url = await _async_stream_endpoint_url(hass, camera, fmt=msg["format"]) connection.send_result(msg["id"], {"url": url}) except HomeAssistantError as ex: _LOGGER.error("Error requesting stream: %s", ex) @@ -676,32 +650,17 @@ async def async_handle_snapshot_service(camera, service): async def async_handle_play_stream_service(camera, service_call): """Handle play stream services calls.""" - async with async_timeout.timeout(10): - source = await camera.stream_source() - - if not source: - raise HomeAssistantError( - f"{camera.entity_id} does not support play stream service" - ) + fmt = service_call.data[ATTR_FORMAT] + url = await _async_stream_endpoint_url(camera.hass, camera, fmt) hass = camera.hass - camera_prefs = hass.data[DATA_CAMERA_PREFS].get(camera.entity_id) - fmt = service_call.data[ATTR_FORMAT] - entity_ids = service_call.data[ATTR_MEDIA_PLAYER] - - url = request_stream( - hass, - source, - fmt=fmt, - keepalive=camera_prefs.preload_stream, - options=camera.stream_options, - ) data = { ATTR_MEDIA_CONTENT_ID: f"{get_url(hass)}{url}", ATTR_MEDIA_CONTENT_TYPE: FORMAT_CONTENT_TYPE[fmt], } # It is required to send a different payload for cast media players + entity_ids = service_call.data[ATTR_MEDIA_PLAYER] cast_entity_ids = [ entity for entity, source in entity_sources(hass).items() @@ -740,12 +699,28 @@ async def async_handle_play_stream_service(camera, service_call): ) +async def _async_stream_endpoint_url(hass, camera, fmt): + stream = await camera.create_stream() + if not stream: + raise HomeAssistantError( + f"{camera.entity_id} does not support play stream service" + ) + + # Update keepalive setting which manages idle shutdown + camera_prefs = hass.data[DATA_CAMERA_PREFS].get(camera.entity_id) + stream.keepalive = camera_prefs.preload_stream + + stream.add_provider(fmt) + stream.start() + return stream.endpoint_url(fmt) + + async def async_handle_record_service(camera, call): """Handle stream recording service calls.""" async with async_timeout.timeout(10): - source = await camera.stream_source() + stream = await camera.create_stream() - if not source: + if not stream: raise HomeAssistantError(f"{camera.entity_id} does not support record service") hass = camera.hass @@ -753,13 +728,6 @@ async def async_handle_record_service(camera, call): filename.hass = hass video_path = filename.async_render(variables={ATTR_ENTITY_ID: camera}) - data = { - CONF_STREAM_SOURCE: source, - CONF_FILENAME: video_path, - CONF_DURATION: call.data[CONF_DURATION], - CONF_LOOKBACK: call.data[CONF_LOOKBACK], - } - - await hass.services.async_call( - DOMAIN_STREAM, SERVICE_RECORD, data, blocking=True, context=call.context + await stream.async_record( + video_path, duration=call.data[CONF_DURATION], lookback=call.data[CONF_LOOKBACK] ) diff --git a/homeassistant/components/camera/const.py b/homeassistant/components/camera/const.py index 563f0554f0f..615e54b0eca 100644 --- a/homeassistant/components/camera/const.py +++ b/homeassistant/components/camera/const.py @@ -4,3 +4,8 @@ DOMAIN = "camera" DATA_CAMERA_PREFS = "camera_prefs" PREF_PRELOAD_STREAM = "preload_stream" + +SERVICE_RECORD = "record" + +CONF_LOOKBACK = "lookback" +CONF_DURATION = "duration" diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index aa8e100059a..8f5ba88fdd4 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -146,6 +146,13 @@ class NestCamera(Camera): # Next attempt to catch a url will get a new one self._stream = None return + # Stop any existing stream worker since the url is invalid. The next + # request for this stream will restart it with the right url. + # Issue #42793 tracks improvements (e.g. preserve keepalive, smoother + # transitions across streams) + if self.stream: + self.stream.stop() + self.stream = None self._schedule_stream_refresh() async def async_will_remove_from_hass(self): diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 6980f7ead8f..a8b344a98e9 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -1,28 +1,35 @@ -"""Provide functionality to stream video source.""" +"""Provide functionality to stream video source. + +Components use create_stream with a stream source (e.g. an rtsp url) to create +a new Stream object. Stream manages: + - Background work to fetch and decode a stream + - Desired output formats + - Home Assistant URLs for viewing a stream + - Access tokens for URLs for viewing a stream + +A Stream consists of a background worker, and one or more output formats each +with their own idle timeout managed by the stream component. When an output +format is no longer in use, the stream component will expire it. When there +are no active output formats, the background worker is shut down and access +tokens are expired. Alternatively, a Stream can be configured with keepalive +to always keep workers active. +""" import logging import secrets import threading import time from types import MappingProxyType -import voluptuous as vol - -from homeassistant.const import CONF_FILENAME, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.config_validation as cv -from homeassistant.loader import bind_hass from .const import ( ATTR_ENDPOINTS, ATTR_STREAMS, - CONF_DURATION, - CONF_LOOKBACK, - CONF_STREAM_SOURCE, DOMAIN, MAX_SEGMENTS, OUTPUT_IDLE_TIMEOUT, - SERVICE_RECORD, STREAM_RESTART_INCREMENT, STREAM_RESTART_RESET_TIME, ) @@ -31,20 +38,13 @@ from .hls import async_setup_hls _LOGGER = logging.getLogger(__name__) -STREAM_SERVICE_SCHEMA = vol.Schema({vol.Required(CONF_STREAM_SOURCE): cv.string}) -SERVICE_RECORD_SCHEMA = STREAM_SERVICE_SCHEMA.extend( - { - vol.Required(CONF_FILENAME): cv.string, - vol.Optional(CONF_DURATION, default=30): int, - vol.Optional(CONF_LOOKBACK, default=0): int, - } -) +def create_stream(hass, stream_source, options=None): + """Create a stream with the specified identfier based on the source url. - -@bind_hass -def request_stream(hass, stream_source, *, fmt="hls", keepalive=False, options=None): - """Set up stream with token.""" + The stream_source is typically an rtsp url and options are passed into + pyav / ffmpeg as options. + """ if DOMAIN not in hass.config.components: raise HomeAssistantError("Stream integration is not set up.") @@ -59,25 +59,9 @@ def request_stream(hass, stream_source, *, fmt="hls", keepalive=False, options=N **options, } - try: - streams = hass.data[DOMAIN][ATTR_STREAMS] - stream = streams.get(stream_source) - if not stream: - stream = Stream(hass, stream_source, options=options, keepalive=keepalive) - streams[stream_source] = stream - else: - # Update keepalive option on existing stream - stream.keepalive = keepalive - - # Add provider - stream.add_provider(fmt) - - if not stream.access_token: - stream.access_token = secrets.token_hex() - stream.start() - return hass.data[DOMAIN][ATTR_ENDPOINTS][fmt].format(stream.access_token) - except Exception as err: - raise HomeAssistantError("Unable to get stream") from err + stream = Stream(hass, stream_source, options=options) + hass.data[DOMAIN][ATTR_STREAMS].append(stream) + return stream async def async_setup(hass, config): @@ -92,7 +76,7 @@ async def async_setup(hass, config): hass.data[DOMAIN] = {} hass.data[DOMAIN][ATTR_ENDPOINTS] = {} - hass.data[DOMAIN][ATTR_STREAMS] = {} + hass.data[DOMAIN][ATTR_STREAMS] = [] # Setup HLS hls_endpoint = async_setup_hls(hass) @@ -104,33 +88,25 @@ async def async_setup(hass, config): @callback def shutdown(event): """Stop all stream workers.""" - for stream in hass.data[DOMAIN][ATTR_STREAMS].values(): + for stream in hass.data[DOMAIN][ATTR_STREAMS]: stream.keepalive = False stream.stop() _LOGGER.info("Stopped stream workers") hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) - async def async_record(call): - """Call record stream service handler.""" - await async_handle_record_service(hass, call) - - hass.services.async_register( - DOMAIN, SERVICE_RECORD, async_record, schema=SERVICE_RECORD_SCHEMA - ) - return True class Stream: """Represents a single stream.""" - def __init__(self, hass, source, options=None, keepalive=False): + def __init__(self, hass, source, options=None): """Initialize a stream.""" self.hass = hass self.source = source self.options = options - self.keepalive = keepalive + self.keepalive = False self.access_token = None self._thread = None self._thread_quit = None @@ -139,6 +115,14 @@ class Stream: if self.options is None: self.options = {} + def endpoint_url(self, fmt): + """Start the stream and returns a url for the output format.""" + if fmt not in self._outputs: + raise ValueError(f"Stream is not configured for format '{fmt}'") + if not self.access_token: + self.access_token = secrets.token_hex() + return self.hass.data[DOMAIN][ATTR_ENDPOINTS][fmt].format(self.access_token) + @property def outputs(self): """Return a copy of the stream outputs.""" @@ -244,39 +228,28 @@ class Stream: self._thread = None _LOGGER.info("Stopped stream: %s", self.source) + async def async_record(self, video_path, duration=30, lookback=5): + """Make a .mp4 recording from a provided stream.""" -async def async_handle_record_service(hass, call): - """Handle save video service calls.""" - stream_source = call.data[CONF_STREAM_SOURCE] - video_path = call.data[CONF_FILENAME] - duration = call.data[CONF_DURATION] - lookback = call.data[CONF_LOOKBACK] + # Check for file access + if not self.hass.config.is_allowed_path(video_path): + raise HomeAssistantError(f"Can't write {video_path}, no access to path!") - # Check for file access - if not hass.config.is_allowed_path(video_path): - raise HomeAssistantError(f"Can't write {video_path}, no access to path!") + # Add recorder + recorder = self.outputs.get("recorder") + if recorder: + raise HomeAssistantError( + f"Stream already recording to {recorder.video_path}!" + ) + recorder = self.add_provider("recorder", timeout=duration) + recorder.video_path = video_path - # Check for active stream - streams = hass.data[DOMAIN][ATTR_STREAMS] - stream = streams.get(stream_source) - if not stream: - stream = Stream(hass, stream_source) - streams[stream_source] = stream + self.start() - # Add recorder - recorder = stream.outputs.get("recorder") - if recorder: - raise HomeAssistantError(f"Stream already recording to {recorder.video_path}!") - - recorder = stream.add_provider("recorder", timeout=duration) - recorder.video_path = video_path - - stream.start() - - # Take advantage of lookback - hls = stream.outputs.get("hls") - if lookback > 0 and hls: - num_segments = min(int(lookback // hls.target_duration), MAX_SEGMENTS) - # Wait for latest segment, then add the lookback - await hls.recv() - recorder.prepend(list(hls.get_segment())[-num_segments:]) + # Take advantage of lookback + hls = self.outputs.get("hls") + if lookback > 0 and hls: + num_segments = min(int(lookback // hls.target_duration), MAX_SEGMENTS) + # Wait for latest segment, then add the lookback + await hls.recv() + recorder.prepend(list(hls.get_segment())[-num_segments:]) diff --git a/homeassistant/components/stream/const.py b/homeassistant/components/stream/const.py index 45fa3d9e76a..4ee9f2a9814 100644 --- a/homeassistant/components/stream/const.py +++ b/homeassistant/components/stream/const.py @@ -1,15 +1,8 @@ """Constants for Stream component.""" DOMAIN = "stream" -CONF_STREAM_SOURCE = "stream_source" -CONF_LOOKBACK = "lookback" -CONF_DURATION = "duration" - ATTR_ENDPOINTS = "endpoints" ATTR_STREAMS = "streams" -ATTR_KEEPALIVE = "keepalive" - -SERVICE_RECORD = "record" OUTPUT_FORMATS = ["hls"] diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 5427172a55c..31c7940b8e1 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -194,11 +194,7 @@ class StreamView(HomeAssistantView): hass = request.app["hass"] stream = next( - ( - s - for s in hass.data[DOMAIN][ATTR_STREAMS].values() - if s.access_token == token - ), + (s for s in hass.data[DOMAIN][ATTR_STREAMS] if s.access_token == token), None, ) diff --git a/homeassistant/components/stream/services.yaml b/homeassistant/components/stream/services.yaml deleted file mode 100644 index a8652335bf1..00000000000 --- a/homeassistant/components/stream/services.yaml +++ /dev/null @@ -1,15 +0,0 @@ -record: - description: Make a .mp4 recording from a provided stream. - fields: - stream_source: - description: The input source for the stream. - example: "rtsp://my.stream.feed:554" - filename: - description: The file name string. - example: "/tmp/my_stream.mp4" - duration: - description: "Target recording length (in seconds). Default: 30" - example: 30 - lookback: - description: "Target lookback period (in seconds) to include in addition to duration. Only available if there is currently an active HLS stream for stream_source. Default: 0" - example: 5 diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 2c2d744deb9..340a4b5d756 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -155,25 +155,20 @@ async def test_websocket_camera_thumbnail(hass, hass_ws_client, mock_camera): async def test_websocket_stream_no_source( hass, hass_ws_client, mock_camera, mock_stream ): - """Test camera/stream websocket command.""" + """Test camera/stream websocket command with camera with no source.""" await async_setup_component(hass, "camera", {}) - with patch( - "homeassistant.components.camera.request_stream", - return_value="http://home.assistant/playlist.m3u8", - ) as mock_request_stream: - # Request playlist through WebSocket - client = await hass_ws_client(hass) - await client.send_json( - {"id": 6, "type": "camera/stream", "entity_id": "camera.demo_camera"} - ) - msg = await client.receive_json() + # Request playlist through WebSocket + client = await hass_ws_client(hass) + await client.send_json( + {"id": 6, "type": "camera/stream", "entity_id": "camera.demo_camera"} + ) + msg = await client.receive_json() - # Assert WebSocket response - assert not mock_request_stream.called - assert msg["id"] == 6 - assert msg["type"] == TYPE_RESULT - assert not msg["success"] + # Assert WebSocket response + assert msg["id"] == 6 + assert msg["type"] == TYPE_RESULT + assert not msg["success"] async def test_websocket_camera_stream(hass, hass_ws_client, mock_camera, mock_stream): @@ -181,9 +176,9 @@ async def test_websocket_camera_stream(hass, hass_ws_client, mock_camera, mock_s await async_setup_component(hass, "camera", {}) with patch( - "homeassistant.components.camera.request_stream", + "homeassistant.components.camera.Stream.endpoint_url", return_value="http://home.assistant/playlist.m3u8", - ) as mock_request_stream, patch( + ) as mock_stream_view_url, patch( "homeassistant.components.demo.camera.DemoCamera.stream_source", return_value="http://example.com", ): @@ -195,7 +190,7 @@ async def test_websocket_camera_stream(hass, hass_ws_client, mock_camera, mock_s msg = await client.receive_json() # Assert WebSocket response - assert mock_request_stream.called + assert mock_stream_view_url.called assert msg["id"] == 6 assert msg["type"] == TYPE_RESULT assert msg["success"] @@ -248,9 +243,7 @@ async def test_play_stream_service_no_source(hass, mock_camera, mock_stream): ATTR_ENTITY_ID: "camera.demo_camera", camera.ATTR_MEDIA_PLAYER: "media_player.test", } - with patch("homeassistant.components.camera.request_stream"), pytest.raises( - HomeAssistantError - ): + with pytest.raises(HomeAssistantError): # Call service await hass.services.async_call( camera.DOMAIN, camera.SERVICE_PLAY_STREAM, data, blocking=True @@ -265,7 +258,7 @@ async def test_handle_play_stream_service(hass, mock_camera, mock_stream): ) await async_setup_component(hass, "media_player", {}) with patch( - "homeassistant.components.camera.request_stream" + "homeassistant.components.camera.Stream.endpoint_url", ) as mock_request_stream, patch( "homeassistant.components.demo.camera.DemoCamera.stream_source", return_value="http://example.com", @@ -289,7 +282,7 @@ async def test_no_preload_stream(hass, mock_stream): """Test camera preload preference.""" demo_prefs = CameraEntityPreferences({PREF_PRELOAD_STREAM: False}) with patch( - "homeassistant.components.camera.request_stream" + "homeassistant.components.camera.Stream.endpoint_url", ) as mock_request_stream, patch( "homeassistant.components.camera.prefs.CameraPreferences.get", return_value=demo_prefs, @@ -308,8 +301,8 @@ async def test_preload_stream(hass, mock_stream): """Test camera preload preference.""" demo_prefs = CameraEntityPreferences({PREF_PRELOAD_STREAM: True}) with patch( - "homeassistant.components.camera.request_stream" - ) as mock_request_stream, patch( + "homeassistant.components.camera.create_stream" + ) as mock_create_stream, patch( "homeassistant.components.camera.prefs.CameraPreferences.get", return_value=demo_prefs, ), patch( @@ -322,7 +315,7 @@ async def test_preload_stream(hass, mock_stream): await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - assert mock_request_stream.called + assert mock_create_stream.called async def test_record_service_invalid_path(hass, mock_camera): @@ -348,10 +341,9 @@ async def test_record_service(hass, mock_camera, mock_stream): "homeassistant.components.demo.camera.DemoCamera.stream_source", return_value="http://example.com", ), patch( - "homeassistant.components.stream.async_handle_record_service", - ) as mock_record_service, patch.object( - hass.config, "is_allowed_path", return_value=True - ): + "homeassistant.components.stream.Stream.async_record", + autospec=True, + ) as mock_record: # Call service await hass.services.async_call( camera.DOMAIN, @@ -361,4 +353,4 @@ async def test_record_service(hass, mock_camera, mock_stream): ) # So long as we call stream.record, the rest should be covered # by those tests. - assert mock_record_service.called + assert mock_record.called diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index 9a147995541..65f5306c4d8 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -176,17 +176,18 @@ async def test_stream_source(aioclient_mock, hass, hass_client, hass_ws_client): "still_image_url": "https://example.com", "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}', "limit_refetch_to_url_change": True, - } + }, }, ) + assert await async_setup_component(hass, "stream", {}) await hass.async_block_till_done() hass.states.async_set("sensor.temp", "5") with patch( - "homeassistant.components.camera.request_stream", + "homeassistant.components.camera.Stream.endpoint_url", return_value="http://home.assistant/playlist.m3u8", - ) as mock_request_stream: + ) as mock_stream_url: # Request playlist through WebSocket client = await hass_ws_client(hass) @@ -196,25 +197,47 @@ async def test_stream_source(aioclient_mock, hass, hass_client, hass_ws_client): msg = await client.receive_json() # Assert WebSocket response - assert mock_request_stream.call_count == 1 - assert mock_request_stream.call_args[0][1] == "http://example.com/5a" + assert mock_stream_url.call_count == 1 assert msg["id"] == 1 assert msg["type"] == TYPE_RESULT assert msg["success"] assert msg["result"]["url"][-13:] == "playlist.m3u8" - # Cause a template render error - hass.states.async_remove("sensor.temp") + +async def test_stream_source_error(aioclient_mock, hass, hass_client, hass_ws_client): + """Test that the stream source has an error.""" + assert await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "still_image_url": "https://example.com", + # Does not exist + "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}', + "limit_refetch_to_url_change": True, + }, + }, + ) + assert await async_setup_component(hass, "stream", {}) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.camera.Stream.endpoint_url", + return_value="http://home.assistant/playlist.m3u8", + ) as mock_stream_url: + # Request playlist through WebSocket + client = await hass_ws_client(hass) await client.send_json( - {"id": 2, "type": "camera/stream", "entity_id": "camera.config_test"} + {"id": 1, "type": "camera/stream", "entity_id": "camera.config_test"} ) msg = await client.receive_json() - # Assert that no new call to the stream request should have been made - assert mock_request_stream.call_count == 1 - # Assert the websocket error message - assert msg["id"] == 2 + # Assert WebSocket response + assert mock_stream_url.call_count == 0 + assert msg["id"] == 1 assert msg["type"] == TYPE_RESULT assert msg["success"] is False assert msg["error"] == { @@ -240,7 +263,7 @@ async def test_no_stream_source(aioclient_mock, hass, hass_client, hass_ws_clien await hass.async_block_till_done() with patch( - "homeassistant.components.camera.request_stream", + "homeassistant.components.camera.Stream.endpoint_url", return_value="http://home.assistant/playlist.m3u8", ) as mock_request_stream: # Request playlist through WebSocket diff --git a/tests/components/nest/camera_sdm_test.py b/tests/components/nest/camera_sdm_test.py index 117c8e97884..956d6036aed 100644 --- a/tests/components/nest/camera_sdm_test.py +++ b/tests/components/nest/camera_sdm_test.py @@ -16,6 +16,7 @@ import pytest from homeassistant.components import camera from homeassistant.components.camera import STATE_IDLE from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from .common import async_setup_sdm_platform @@ -245,12 +246,17 @@ async def test_refresh_expired_stream_token(hass, auth): DEVICE_TRAITS, auth=auth, ) + assert await async_setup_component(hass, "stream", {}) assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") assert cam is not None assert cam.state == STATE_IDLE + # Request a stream for the camera entity to exercise nest cam + camera interaction + # and shutdown on url expiration + await camera.async_request_stream(hass, cam.entity_id, "hls") + stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") assert stream_source == "rtsp://some/url?auth=g.1.streamingToken" diff --git a/tests/components/stream/common.py b/tests/components/stream/common.py index c99cdef7984..5ec4f4217ce 100644 --- a/tests/components/stream/common.py +++ b/tests/components/stream/common.py @@ -5,9 +5,6 @@ import io import av import numpy as np -from homeassistant.components.stream import Stream -from homeassistant.components.stream.const import ATTR_STREAMS, DOMAIN - AUDIO_SAMPLE_RATE = 8000 @@ -93,10 +90,3 @@ def generate_h264_video(container_format="mp4", audio_codec=None): output.seek(0) return output - - -def preload_stream(hass, stream_source): - """Preload a stream for use in tests.""" - stream = Stream(hass, stream_source) - hass.data[DOMAIN][ATTR_STREAMS][stream_source] = stream - return stream diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index ab49a56ca02..b575b3877fa 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -5,13 +5,13 @@ from urllib.parse import urlparse import av -from homeassistant.components.stream import request_stream +from homeassistant.components.stream import create_stream from homeassistant.const import HTTP_NOT_FOUND from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed -from tests.components.stream.common import generate_h264_video, preload_stream +from tests.components.stream.common import generate_h264_video async def test_hls_stream(hass, hass_client, stream_worker_sync): @@ -27,11 +27,12 @@ async def test_hls_stream(hass, hass_client, stream_worker_sync): # Setup demo HLS track source = generate_h264_video() - stream = preload_stream(hass, source) - stream.add_provider("hls") + stream = create_stream(hass, source) # Request stream - url = request_stream(hass, source) + stream.add_provider("hls") + stream.start() + url = stream.endpoint_url("hls") http_client = await hass_client() @@ -72,11 +73,12 @@ async def test_stream_timeout(hass, hass_client, stream_worker_sync): # Setup demo HLS track source = generate_h264_video() - stream = preload_stream(hass, source) - stream.add_provider("hls") + stream = create_stream(hass, source) # Request stream - url = request_stream(hass, source) + stream.add_provider("hls") + stream.start() + url = stream.endpoint_url("hls") http_client = await hass_client() @@ -113,11 +115,13 @@ async def test_stream_ended(hass, stream_worker_sync): # Setup demo HLS track source = generate_h264_video() - stream = preload_stream(hass, source) + stream = create_stream(hass, source) track = stream.add_provider("hls") # Request stream - request_stream(hass, source) + stream.add_provider("hls") + stream.start() + stream.endpoint_url("hls") # Run it dead while True: @@ -142,9 +146,10 @@ async def test_stream_keepalive(hass): # Setup demo HLS track source = "test_stream_keepalive_source" - stream = preload_stream(hass, source) + stream = create_stream(hass, source) track = stream.add_provider("hls") track.num_segments = 2 + stream.start() cur_time = 0 @@ -163,7 +168,8 @@ async def test_stream_keepalive(hass): av_open.side_effect = av.error.InvalidDataError(-2, "error") mock_time.time.side_effect = time_side_effect # Request stream - request_stream(hass, source, keepalive=True) + stream.keepalive = True + stream.start() stream._thread.join() stream._thread = None assert av_open.call_count == 2 diff --git a/tests/components/stream/test_init.py b/tests/components/stream/test_init.py deleted file mode 100644 index 2e13493b641..00000000000 --- a/tests/components/stream/test_init.py +++ /dev/null @@ -1,86 +0,0 @@ -"""The tests for stream.""" -from unittest.mock import AsyncMock, MagicMock, patch - -import pytest - -from homeassistant.components.stream.const import ( - ATTR_STREAMS, - CONF_LOOKBACK, - CONF_STREAM_SOURCE, - DOMAIN, - SERVICE_RECORD, -) -from homeassistant.const import CONF_FILENAME -from homeassistant.exceptions import HomeAssistantError -from homeassistant.setup import async_setup_component - - -async def test_record_service_invalid_file(hass): - """Test record service call with invalid file.""" - await async_setup_component(hass, "stream", {"stream": {}}) - data = {CONF_STREAM_SOURCE: "rtsp://my.video", CONF_FILENAME: "/my/invalid/path"} - with pytest.raises(HomeAssistantError): - await hass.services.async_call(DOMAIN, SERVICE_RECORD, data, blocking=True) - - -async def test_record_service_init_stream(hass): - """Test record service call with invalid file.""" - await async_setup_component(hass, "stream", {"stream": {}}) - data = {CONF_STREAM_SOURCE: "rtsp://my.video", CONF_FILENAME: "/my/invalid/path"} - with patch("homeassistant.components.stream.Stream") as stream_mock, patch.object( - hass.config, "is_allowed_path", return_value=True - ): - # Setup stubs - stream_mock.return_value.outputs = {} - - # Call Service - await hass.services.async_call(DOMAIN, SERVICE_RECORD, data, blocking=True) - - # Assert - assert stream_mock.called - - -async def test_record_service_existing_record_session(hass): - """Test record service call with invalid file.""" - await async_setup_component(hass, "stream", {"stream": {}}) - source = "rtsp://my.video" - data = {CONF_STREAM_SOURCE: source, CONF_FILENAME: "/my/invalid/path"} - - # Setup stubs - stream_mock = MagicMock() - stream_mock.return_value.outputs = {"recorder": MagicMock()} - hass.data[DOMAIN][ATTR_STREAMS][source] = stream_mock - - with patch.object(hass.config, "is_allowed_path", return_value=True), pytest.raises( - HomeAssistantError - ): - # Call Service - await hass.services.async_call(DOMAIN, SERVICE_RECORD, data, blocking=True) - - -async def test_record_service_lookback(hass): - """Test record service call with invalid file.""" - await async_setup_component(hass, "stream", {"stream": {}}) - data = { - CONF_STREAM_SOURCE: "rtsp://my.video", - CONF_FILENAME: "/my/invalid/path", - CONF_LOOKBACK: 4, - } - - with patch("homeassistant.components.stream.Stream") as stream_mock, patch.object( - hass.config, "is_allowed_path", return_value=True - ): - # Setup stubs - hls_mock = MagicMock() - hls_mock.target_duration = 2 - hls_mock.recv = AsyncMock(return_value=None) - stream_mock.return_value.outputs = {"hls": hls_mock} - - # Call Service - await hass.services.async_call(DOMAIN, SERVICE_RECORD, data, blocking=True) - - assert stream_mock.called - stream_mock.return_value.add_provider.assert_called_once_with( - "recorder", timeout=30 - ) - assert hls_mock.recv.called diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index bda53a9cc17..9d418c360b1 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -8,13 +8,15 @@ from unittest.mock import patch import av import pytest +from homeassistant.components.stream import create_stream from homeassistant.components.stream.core import Segment from homeassistant.components.stream.recorder import recorder_save_worker +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed -from tests.components.stream.common import generate_h264_video, preload_stream +from tests.components.stream.common import generate_h264_video TEST_TIMEOUT = 10 @@ -75,10 +77,11 @@ async def test_record_stream(hass, hass_client, stream_worker_sync, record_worke # Setup demo track source = generate_h264_video() - stream = preload_stream(hass, source) - recorder = stream.add_provider("recorder") - stream.start() + stream = create_stream(hass, source) + with patch.object(hass.config, "is_allowed_path", return_value=True): + await stream.async_record("/example/path") + recorder = stream.add_provider("recorder") while True: segment = await recorder.recv() if not segment: @@ -95,6 +98,27 @@ async def test_record_stream(hass, hass_client, stream_worker_sync, record_worke record_worker_sync.join() +async def test_record_lookback( + hass, hass_client, stream_worker_sync, record_worker_sync +): + """Exercise record with loopback.""" + await async_setup_component(hass, "stream", {"stream": {}}) + + source = generate_h264_video() + stream = create_stream(hass, source) + + # Start an HLS feed to enable lookback + stream.add_provider("hls") + stream.start() + + with patch.object(hass.config, "is_allowed_path", return_value=True): + await stream.async_record("/example/path", lookback=4) + + # This test does not need recorder cleanup since it is not fully exercised + + stream.stop() + + async def test_recorder_timeout(hass, hass_client, stream_worker_sync): """ Test recorder timeout. @@ -109,9 +133,11 @@ async def test_recorder_timeout(hass, hass_client, stream_worker_sync): with patch("homeassistant.components.stream.IdleTimer.fire") as mock_timeout: # Setup demo track source = generate_h264_video() - stream = preload_stream(hass, source) - recorder = stream.add_provider("recorder", timeout=30) - stream.start() + + stream = create_stream(hass, source) + with patch.object(hass.config, "is_allowed_path", return_value=True): + await stream.async_record("/example/path") + recorder = stream.add_provider("recorder") await recorder.recv() @@ -128,6 +154,19 @@ async def test_recorder_timeout(hass, hass_client, stream_worker_sync): await hass.async_block_till_done() +async def test_record_path_not_allowed(hass, hass_client): + """Test where the output path is not allowed by home assistant configuration.""" + await async_setup_component(hass, "stream", {"stream": {}}) + + # Setup demo track + source = generate_h264_video() + stream = create_stream(hass, source) + with patch.object( + hass.config, "is_allowed_path", return_value=False + ), pytest.raises(HomeAssistantError): + await stream.async_record("/example/path") + + async def test_recorder_save(tmpdir): """Test recorder save.""" # Setup @@ -165,9 +204,10 @@ async def test_record_stream_audio( source = generate_h264_video( container_format="mov", audio_codec=a_codec ) # mov can store PCM - stream = preload_stream(hass, source) + stream = create_stream(hass, source) + with patch.object(hass.config, "is_allowed_path", return_value=True): + await stream.async_record("/example/path") recorder = stream.add_provider("recorder") - stream.start() while True: segment = await recorder.recv() From b33753f334b96faa1cfd6f66ab44507d11d8a610 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 8 Feb 2021 21:21:14 -0800 Subject: [PATCH 298/796] Move camera timeouts to constants (#46262) Addresses feedback from pr #45431. Also removes an redundant `create_stream` timeout. --- homeassistant/components/camera/__init__.py | 9 +++++---- homeassistant/components/camera/const.py | 3 +++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index cbc98edf19b..ba61f155473 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -46,6 +46,8 @@ from homeassistant.helpers.network import get_url from homeassistant.loader import bind_hass from .const import ( + CAMERA_IMAGE_TIMEOUT, + CAMERA_STREAM_SOURCE_TIMEOUT, CONF_DURATION, CONF_LOOKBACK, DATA_CAMERA_PREFS, @@ -359,7 +361,7 @@ class Camera(Entity): """Create a Stream for stream_source.""" # There is at most one stream (a decode worker) per camera if not self.stream: - async with async_timeout.timeout(10): + async with async_timeout.timeout(CAMERA_STREAM_SOURCE_TIMEOUT): source = await self.stream_source() if not source: return None @@ -506,7 +508,7 @@ class CameraImageView(CameraView): async def handle(self, request: web.Request, camera: Camera) -> web.Response: """Serve camera image.""" with suppress(asyncio.CancelledError, asyncio.TimeoutError): - async with async_timeout.timeout(10): + async with async_timeout.timeout(CAMERA_IMAGE_TIMEOUT): image = await camera.async_camera_image() if image: @@ -717,8 +719,7 @@ async def _async_stream_endpoint_url(hass, camera, fmt): async def async_handle_record_service(camera, call): """Handle stream recording service calls.""" - async with async_timeout.timeout(10): - stream = await camera.create_stream() + stream = await camera.create_stream() if not stream: raise HomeAssistantError(f"{camera.entity_id} does not support record service") diff --git a/homeassistant/components/camera/const.py b/homeassistant/components/camera/const.py index 615e54b0eca..7218b19f8fe 100644 --- a/homeassistant/components/camera/const.py +++ b/homeassistant/components/camera/const.py @@ -9,3 +9,6 @@ SERVICE_RECORD = "record" CONF_LOOKBACK = "lookback" CONF_DURATION = "duration" + +CAMERA_STREAM_SOURCE_TIMEOUT = 10 +CAMERA_IMAGE_TIMEOUT = 10 From 20f45f8ab9e744de7488fe66c5e7f8aca9b60e2d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 9 Feb 2021 08:31:29 +0100 Subject: [PATCH 299/796] Improve deCONZ tests by using aioclient_mock rather than patching web requests (#45927) * Don't patch web requests, use aioclient_mock instead * Remove stale prints * Remove tests for old way of loading platforms * Remove unused imports --- tests/components/deconz/test_binary_sensor.py | 60 ++-- tests/components/deconz/test_climate.py | 290 ++++++++---------- tests/components/deconz/test_config_flow.py | 34 +- tests/components/deconz/test_cover.py | 259 +++++++--------- tests/components/deconz/test_deconz_event.py | 6 +- .../components/deconz/test_device_trigger.py | 16 +- tests/components/deconz/test_fan.py | 145 ++++----- tests/components/deconz/test_gateway.py | 61 +++- tests/components/deconz/test_init.py | 45 +-- tests/components/deconz/test_light.py | 245 +++++++-------- tests/components/deconz/test_lock.py | 64 ++-- tests/components/deconz/test_logbook.py | 6 +- tests/components/deconz/test_scene.py | 52 ++-- tests/components/deconz/test_sensor.py | 53 ++-- tests/components/deconz/test_services.py | 105 ++++--- tests/components/deconz/test_switch.py | 116 +++---- 16 files changed, 700 insertions(+), 857 deletions(-) diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index f64a4c4c259..70d3db4149b 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -1,12 +1,10 @@ """deCONZ binary sensor platform tests.""" from copy import deepcopy -from unittest.mock import patch from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOTION, DEVICE_CLASS_VIBRATION, - DOMAIN as BINARY_SENSOR_DOMAIN, ) from homeassistant.components.deconz.const import ( CONF_ALLOW_CLIP_SENSOR, @@ -18,9 +16,12 @@ from homeassistant.components.deconz.gateway import get_gateway_from_config_entr from homeassistant.components.deconz.services import SERVICE_DEVICE_REFRESH from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE 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 .test_gateway import ( + DECONZ_WEB_REQUEST, + mock_deconz_request, + setup_deconz_integration, +) SENSORS = { "1": { @@ -63,28 +64,19 @@ SENSORS = { } -async def test_platform_manually_configured(hass): - """Test that we do not discover anything or try to set up a gateway.""" - assert ( - await async_setup_component( - hass, BINARY_SENSOR_DOMAIN, {"binary_sensor": {"platform": DECONZ_DOMAIN}} - ) - is True - ) - assert DECONZ_DOMAIN not in hass.data - - -async def test_no_binary_sensors(hass): +async def test_no_binary_sensors(hass, aioclient_mock): """Test that no sensors in deconz results in no sensor entities.""" - await setup_deconz_integration(hass) + await setup_deconz_integration(hass, aioclient_mock) assert len(hass.states.async_all()) == 0 -async def test_binary_sensors(hass): +async def test_binary_sensors(hass, aioclient_mock): """Test successful creation of binary sensor entities.""" data = deepcopy(DECONZ_WEB_REQUEST) data["sensors"] = deepcopy(SENSORS) - config_entry = await setup_deconz_integration(hass, get_state_response=data) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 3 @@ -118,12 +110,13 @@ async def test_binary_sensors(hass): assert len(hass.states.async_all()) == 0 -async def test_allow_clip_sensor(hass): +async def test_allow_clip_sensor(hass, aioclient_mock): """Test that CLIP sensors can be allowed.""" data = deepcopy(DECONZ_WEB_REQUEST) data["sensors"] = deepcopy(SENSORS) config_entry = await setup_deconz_integration( hass, + aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: True}, get_state_response=data, ) @@ -155,9 +148,9 @@ async def test_allow_clip_sensor(hass): assert hass.states.get("binary_sensor.clip_presence_sensor").state == STATE_OFF -async def test_add_new_binary_sensor(hass): +async def test_add_new_binary_sensor(hass, aioclient_mock): """Test that adding a new binary sensor works.""" - config_entry = await setup_deconz_integration(hass) + config_entry = await setup_deconz_integration(hass, aioclient_mock) gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 0 @@ -175,10 +168,11 @@ async def test_add_new_binary_sensor(hass): assert hass.states.get("binary_sensor.presence_sensor").state == STATE_OFF -async def test_add_new_binary_sensor_ignored(hass): +async def test_add_new_binary_sensor_ignored(hass, aioclient_mock): """Test that adding a new binary sensor is not allowed.""" config_entry = await setup_deconz_integration( hass, + aioclient_mock, options={CONF_MASTER_GATEWAY: True, CONF_ALLOW_NEW_DEVICES: False}, ) gateway = get_gateway_from_config_entry(hass, config_entry) @@ -202,16 +196,16 @@ async def test_add_new_binary_sensor_ignored(hass): 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() + aioclient_mock.clear_requests() + data = { + "groups": {}, + "lights": {}, + "sensors": {"1": deepcopy(SENSORS["1"])}, + } + mock_deconz_request(aioclient_mock, config_entry.data, data) + + 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") diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index fcb6e16f07f..0a37debade3 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -1,7 +1,6 @@ """deCONZ climate platform tests.""" from copy import deepcopy -from unittest.mock import patch import pytest @@ -34,10 +33,7 @@ from homeassistant.components.deconz.climate import ( DECONZ_FAN_SMART, DECONZ_PRESET_MANUAL, ) -from homeassistant.components.deconz.const import ( - CONF_ALLOW_CLIP_SENSOR, - DOMAIN as DECONZ_DOMAIN, -) +from homeassistant.components.deconz.const import CONF_ALLOW_CLIP_SENSOR from homeassistant.components.deconz.gateway import get_gateway_from_config_entry from homeassistant.const import ( ATTR_ENTITY_ID, @@ -45,9 +41,12 @@ from homeassistant.const import ( STATE_OFF, STATE_UNAVAILABLE, ) -from homeassistant.setup import async_setup_component -from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import ( + DECONZ_WEB_REQUEST, + mock_deconz_put_request, + setup_deconz_integration, +) SENSORS = { "1": { @@ -75,24 +74,13 @@ SENSORS = { } -async def test_platform_manually_configured(hass): - """Test that we do not discover anything or try to set up a gateway.""" - assert ( - await async_setup_component( - hass, CLIMATE_DOMAIN, {"climate": {"platform": DECONZ_DOMAIN}} - ) - is True - ) - assert DECONZ_DOMAIN not in hass.data - - -async def test_no_sensors(hass): +async def test_no_sensors(hass, aioclient_mock): """Test that no sensors in deconz results in no climate entities.""" - await setup_deconz_integration(hass) + await setup_deconz_integration(hass, aioclient_mock) assert len(hass.states.async_all()) == 0 -async def test_simple_climate_device(hass): +async def test_simple_climate_device(hass, aioclient_mock): """Test successful creation of climate entities. This is a simple water heater that only supports setting temperature and on and off. @@ -130,7 +118,9 @@ async def test_simple_climate_device(hass): "uniqueid": "14:b4:57:ff:fe:d5:4e:77-01-0201", } } - config_entry = await setup_deconz_integration(hass, get_state_response=data) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 2 @@ -174,37 +164,31 @@ async def test_simple_climate_device(hass): # Verify service calls - thermostat_device = gateway.api.sensors["0"] + mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/0/config") # Service turn on thermostat - with patch.object(thermostat_device, "_request", return_value=True) as set_callback: - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_HEAT}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/sensors/0/config", json={"on": True}) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_HEAT}, + blocking=True, + ) + assert aioclient_mock.mock_calls[1][2] == {"on": True} # Service turn on thermostat - with patch.object(thermostat_device, "_request", return_value=True) as set_callback: - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_OFF}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/sensors/0/config", json={"on": False}) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_OFF}, + blocking=True, + ) + assert aioclient_mock.mock_calls[2][2] == {"on": False} # Service set HVAC mode to unsupported value - with patch.object( - thermostat_device, "_request", return_value=True - ) as set_callback, pytest.raises(ValueError): + with pytest.raises(ValueError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, @@ -213,11 +197,13 @@ async def test_simple_climate_device(hass): ) -async def test_climate_device_without_cooling_support(hass): +async def test_climate_device_without_cooling_support(hass, aioclient_mock): """Test successful creation of sensor entities.""" data = deepcopy(DECONZ_WEB_REQUEST) data["sensors"] = deepcopy(SENSORS) - config_entry = await setup_deconz_integration(hass, get_state_response=data) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 2 @@ -280,54 +266,41 @@ async def test_climate_device_without_cooling_support(hass): # Verify service calls - thermostat_device = gateway.api.sensors["1"] + mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/1/config") # Service set HVAC mode to auto - with patch.object(thermostat_device, "_request", return_value=True) as set_callback: - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_AUTO}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with( - "put", "/sensors/1/config", json={"mode": "auto"} - ) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_AUTO}, + blocking=True, + ) + assert aioclient_mock.mock_calls[1][2] == {"mode": "auto"} # Service set HVAC mode to heat - with patch.object(thermostat_device, "_request", return_value=True) as set_callback: - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_HEAT}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with( - "put", "/sensors/1/config", json={"mode": "heat"} - ) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_HEAT}, + blocking=True, + ) + assert aioclient_mock.mock_calls[2][2] == {"mode": "heat"} # Service set HVAC mode to off - with patch.object(thermostat_device, "_request", return_value=True) as set_callback: - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_OFF}, - blocking=True, - ) - set_callback.assert_called_with( - "put", "/sensors/1/config", json={"mode": "off"} - ) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_OFF}, + blocking=True, + ) + assert aioclient_mock.mock_calls[3][2] == {"mode": "off"} # Service set HVAC mode to unsupported value - with patch.object( - thermostat_device, "_request", return_value=True - ) as set_callback, pytest.raises(ValueError): + with pytest.raises(ValueError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, @@ -337,22 +310,17 @@ async def test_climate_device_without_cooling_support(hass): # Service set temperature to 20 - with patch.object(thermostat_device, "_request", return_value=True) as set_callback: - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: "climate.thermostat", ATTR_TEMPERATURE: 20}, - blocking=True, - ) - set_callback.assert_called_with( - "put", "/sensors/1/config", json={"heatsetpoint": 2000.0} - ) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: "climate.thermostat", ATTR_TEMPERATURE: 20}, + blocking=True, + ) + assert aioclient_mock.mock_calls[4][2] == {"heatsetpoint": 2000.0} # Service set temperature without providing temperature attribute - with patch.object( - thermostat_device, "_request", return_value=True - ) as set_callback, pytest.raises(ValueError): + with pytest.raises(ValueError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -376,7 +344,7 @@ async def test_climate_device_without_cooling_support(hass): assert len(hass.states.async_all()) == 0 -async def test_climate_device_with_cooling_support(hass): +async def test_climate_device_with_cooling_support(hass, aioclient_mock): """Test successful creation of sensor entities.""" data = deepcopy(DECONZ_WEB_REQUEST) data["sensors"] = { @@ -406,7 +374,9 @@ async def test_climate_device_with_cooling_support(hass): "uniqueid": "00:24:46:00:00:11:6f:56-01-0201", } } - config_entry = await setup_deconz_integration(hass, get_state_response=data) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 2 @@ -438,23 +408,20 @@ async def test_climate_device_with_cooling_support(hass): # Verify service calls - thermostat_device = gateway.api.sensors["0"] + mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/0/config") # Service set temperature to 20 - with patch.object(thermostat_device, "_request", return_value=True) as set_callback: - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: "climate.zen_01", ATTR_TEMPERATURE: 20}, - blocking=True, - ) - set_callback.assert_called_with( - "put", "/sensors/0/config", json={"coolsetpoint": 2000.0} - ) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: "climate.zen_01", ATTR_TEMPERATURE: 20}, + blocking=True, + ) + assert aioclient_mock.mock_calls[1][2] == {"coolsetpoint": 2000.0} -async def test_climate_device_with_fan_support(hass): +async def test_climate_device_with_fan_support(hass, aioclient_mock): """Test successful creation of sensor entities.""" data = deepcopy(DECONZ_WEB_REQUEST) data["sensors"] = { @@ -484,7 +451,9 @@ async def test_climate_device_with_fan_support(hass): "uniqueid": "00:24:46:00:00:11:6f:56-01-0201", } } - config_entry = await setup_deconz_integration(hass, get_state_response=data) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 2 @@ -546,39 +515,31 @@ async def test_climate_device_with_fan_support(hass): # Verify service calls - thermostat_device = gateway.api.sensors["0"] + mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/0/config") # Service set fan mode to off - with patch.object(thermostat_device, "_request", return_value=True) as set_callback: - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: "climate.zen_01", ATTR_FAN_MODE: FAN_OFF}, - blocking=True, - ) - set_callback.assert_called_with( - "put", "/sensors/0/config", json={"fanmode": "off"} - ) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: "climate.zen_01", ATTR_FAN_MODE: FAN_OFF}, + blocking=True, + ) + assert aioclient_mock.mock_calls[1][2] == {"fanmode": "off"} # Service set fan mode to custom deCONZ mode smart - with patch.object(thermostat_device, "_request", return_value=True) as set_callback: - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: "climate.zen_01", ATTR_FAN_MODE: DECONZ_FAN_SMART}, - blocking=True, - ) - set_callback.assert_called_with( - "put", "/sensors/0/config", json={"fanmode": "smart"} - ) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: "climate.zen_01", ATTR_FAN_MODE: DECONZ_FAN_SMART}, + blocking=True, + ) + assert aioclient_mock.mock_calls[2][2] == {"fanmode": "smart"} # Service set fan mode to unsupported value - with patch.object( - thermostat_device, "_request", return_value=True - ) as set_callback, pytest.raises(ValueError): + with pytest.raises(ValueError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, @@ -587,7 +548,7 @@ async def test_climate_device_with_fan_support(hass): ) -async def test_climate_device_with_preset(hass): +async def test_climate_device_with_preset(hass, aioclient_mock): """Test successful creation of sensor entities.""" data = deepcopy(DECONZ_WEB_REQUEST) data["sensors"] = { @@ -618,7 +579,9 @@ async def test_climate_device_with_preset(hass): "uniqueid": "00:24:46:00:00:11:6f:56-01-0201", } } - config_entry = await setup_deconz_integration(hass, get_state_response=data) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 2 @@ -671,41 +634,31 @@ async def test_climate_device_with_preset(hass): # Verify service calls - thermostat_device = gateway.api.sensors["0"] + mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/0/config") # Service set preset to HASS preset - with patch.object(thermostat_device, "_request", return_value=True) as set_callback: - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: "climate.zen_01", ATTR_PRESET_MODE: PRESET_COMFORT}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with( - "put", "/sensors/0/config", json={"preset": "comfort"} - ) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: "climate.zen_01", ATTR_PRESET_MODE: PRESET_COMFORT}, + blocking=True, + ) + assert aioclient_mock.mock_calls[1][2] == {"preset": "comfort"} # Service set preset to custom deCONZ preset - with patch.object(thermostat_device, "_request", return_value=True) as set_callback: - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: "climate.zen_01", ATTR_PRESET_MODE: DECONZ_PRESET_MANUAL}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with( - "put", "/sensors/0/config", json={"preset": "manual"} - ) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: "climate.zen_01", ATTR_PRESET_MODE: DECONZ_PRESET_MANUAL}, + blocking=True, + ) + assert aioclient_mock.mock_calls[2][2] == {"preset": "manual"} # Service set preset to unsupported value - with patch.object( - thermostat_device, "_request", return_value=True - ) as set_callback, pytest.raises(ValueError): + with pytest.raises(ValueError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, @@ -714,12 +667,13 @@ async def test_climate_device_with_preset(hass): ) -async def test_clip_climate_device(hass): +async def test_clip_climate_device(hass, aioclient_mock): """Test successful creation of sensor entities.""" data = deepcopy(DECONZ_WEB_REQUEST) data["sensors"] = deepcopy(SENSORS) config_entry = await setup_deconz_integration( hass, + aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: True}, get_state_response=data, ) @@ -751,11 +705,13 @@ async def test_clip_climate_device(hass): assert hass.states.get("climate.clip_thermostat").state == HVAC_MODE_HEAT -async def test_verify_state_update(hass): +async def test_verify_state_update(hass, aioclient_mock): """Test that state update properly.""" data = deepcopy(DECONZ_WEB_REQUEST) data["sensors"] = deepcopy(SENSORS) - config_entry = await setup_deconz_integration(hass, get_state_response=data) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) gateway = get_gateway_from_config_entry(hass, config_entry) assert hass.states.get("climate.thermostat").state == HVAC_MODE_AUTO @@ -774,9 +730,9 @@ async def test_verify_state_update(hass): assert gateway.api.sensors["1"].changed_keys == {"state", "r", "t", "on", "e", "id"} -async def test_add_new_climate_device(hass): +async def test_add_new_climate_device(hass, aioclient_mock): """Test that adding a new climate device works.""" - config_entry = await setup_deconz_integration(hass) + config_entry = await setup_deconz_integration(hass, aioclient_mock) gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index e18418ff9ae..19f544fabc9 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -212,7 +212,7 @@ async def test_manual_configuration_after_discovery_ResponseError(hass, aioclien async def test_manual_configuration_update_configuration(hass, aioclient_mock): """Test that manual configuration can update existing config entry.""" - config_entry = await setup_deconz_integration(hass) + config_entry = await setup_deconz_integration(hass, aioclient_mock) aioclient_mock.get( pydeconz.utils.URL_DISCOVER, @@ -258,7 +258,7 @@ async def test_manual_configuration_update_configuration(hass, aioclient_mock): async def test_manual_configuration_dont_update_configuration(hass, aioclient_mock): """Test that _create_entry work and that bridgeid can be requested.""" - await setup_deconz_integration(hass) + await setup_deconz_integration(hass, aioclient_mock) aioclient_mock.get( pydeconz.utils.URL_DISCOVER, @@ -374,7 +374,7 @@ async def test_link_get_api_key_ResponseError(hass, aioclient_mock): async def test_reauth_flow_update_configuration(hass, aioclient_mock): """Verify reauth flow can update gateway API key.""" - config_entry = await setup_deconz_integration(hass) + config_entry = await setup_deconz_integration(hass, aioclient_mock) result = await hass.config_entries.flow.async_init( DECONZ_DOMAIN, @@ -442,9 +442,9 @@ async def test_flow_ssdp_discovery(hass, aioclient_mock): } -async def test_ssdp_discovery_update_configuration(hass): +async def test_ssdp_discovery_update_configuration(hass, aioclient_mock): """Test if a discovered bridge is configured but updates with new attributes.""" - config_entry = await setup_deconz_integration(hass) + config_entry = await setup_deconz_integration(hass, aioclient_mock) with patch( "homeassistant.components.deconz.async_setup_entry", @@ -467,9 +467,9 @@ async def test_ssdp_discovery_update_configuration(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_ssdp_discovery_dont_update_configuration(hass): +async def test_ssdp_discovery_dont_update_configuration(hass, aioclient_mock): """Test if a discovered bridge has already been configured.""" - config_entry = await setup_deconz_integration(hass) + config_entry = await setup_deconz_integration(hass, aioclient_mock) result = await hass.config_entries.flow.async_init( DECONZ_DOMAIN, @@ -486,9 +486,13 @@ async def test_ssdp_discovery_dont_update_configuration(hass): assert config_entry.data[CONF_HOST] == "1.2.3.4" -async def test_ssdp_discovery_dont_update_existing_hassio_configuration(hass): +async def test_ssdp_discovery_dont_update_existing_hassio_configuration( + hass, aioclient_mock +): """Test to ensure the SSDP discovery does not update an Hass.io entry.""" - config_entry = await setup_deconz_integration(hass, source=SOURCE_HASSIO) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, source=SOURCE_HASSIO + ) result = await hass.config_entries.flow.async_init( DECONZ_DOMAIN, @@ -543,9 +547,9 @@ async def test_flow_hassio_discovery(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_hassio_discovery_update_configuration(hass): +async def test_hassio_discovery_update_configuration(hass, aioclient_mock): """Test we can update an existing config entry.""" - config_entry = await setup_deconz_integration(hass) + config_entry = await setup_deconz_integration(hass, aioclient_mock) with patch( "homeassistant.components.deconz.async_setup_entry", @@ -571,9 +575,9 @@ async def test_hassio_discovery_update_configuration(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_hassio_discovery_dont_update_configuration(hass): +async def test_hassio_discovery_dont_update_configuration(hass, aioclient_mock): """Test we can update an existing config entry.""" - await setup_deconz_integration(hass) + await setup_deconz_integration(hass, aioclient_mock) result = await hass.config_entries.flow.async_init( DECONZ_DOMAIN, @@ -590,9 +594,9 @@ async def test_hassio_discovery_dont_update_configuration(hass): assert result["reason"] == "already_configured" -async def test_option_flow(hass): +async def test_option_flow(hass, aioclient_mock): """Test config flow options.""" - config_entry = await setup_deconz_integration(hass) + config_entry = await setup_deconz_integration(hass, aioclient_mock) result = await hass.config_entries.options.async_init(config_entry.entry_id) diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 43364208f4f..e48d44fb61e 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -1,7 +1,6 @@ """deCONZ cover platform tests.""" from copy import deepcopy -from unittest.mock import patch from homeassistant.components.cover import ( ATTR_CURRENT_TILT_POSITION, @@ -17,7 +16,6 @@ from homeassistant.components.cover import ( SERVICE_STOP_COVER, SERVICE_STOP_COVER_TILT, ) -from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.gateway import get_gateway_from_config_entry from homeassistant.const import ( ATTR_ENTITY_ID, @@ -25,9 +23,12 @@ from homeassistant.const import ( STATE_OPEN, STATE_UNAVAILABLE, ) -from homeassistant.setup import async_setup_component -from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import ( + DECONZ_WEB_REQUEST, + mock_deconz_put_request, + setup_deconz_integration, +) COVERS = { "1": { @@ -72,28 +73,19 @@ COVERS = { } -async def test_platform_manually_configured(hass): - """Test that we do not discover anything or try to set up a gateway.""" - assert ( - await async_setup_component( - hass, COVER_DOMAIN, {"cover": {"platform": DECONZ_DOMAIN}} - ) - is True - ) - assert DECONZ_DOMAIN not in hass.data - - -async def test_no_covers(hass): +async def test_no_covers(hass, aioclient_mock): """Test that no cover entities are created.""" - await setup_deconz_integration(hass) + await setup_deconz_integration(hass, aioclient_mock) assert len(hass.states.async_all()) == 0 -async def test_cover(hass): +async def test_cover(hass, aioclient_mock): """Test that all supported cover entities are created.""" data = deepcopy(DECONZ_WEB_REQUEST) data["lights"] = deepcopy(COVERS) - config_entry = await setup_deconz_integration(hass, get_state_response=data) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 5 @@ -119,123 +111,91 @@ async def test_cover(hass): # Verify service calls for cover - windows_covering_device = gateway.api.lights["2"] + mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/2/state") # Service open cover - with patch.object( - windows_covering_device, "_request", return_value=True - ) as set_callback: - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.window_covering_device"}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/2/state", json={"open": True}) + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.window_covering_device"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[1][2] == {"open": True} # Service close cover - with patch.object( - windows_covering_device, "_request", return_value=True - ) as set_callback: - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.window_covering_device"}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/2/state", json={"open": False}) + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.window_covering_device"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[2][2] == {"open": False} # Service set cover position - with patch.object( - windows_covering_device, "_request", return_value=True - ) as set_callback: - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: "cover.window_covering_device", ATTR_POSITION: 40}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/2/state", json={"lift": 60}) + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.window_covering_device", ATTR_POSITION: 40}, + blocking=True, + ) + assert aioclient_mock.mock_calls[3][2] == {"lift": 60} # Service stop cover movement - with patch.object( - windows_covering_device, "_request", return_value=True - ) as set_callback: - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_STOP_COVER, - {ATTR_ENTITY_ID: "cover.window_covering_device"}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/2/state", json={"stop": True}) + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: "cover.window_covering_device"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[4][2] == {"stop": True} # Verify service calls for legacy cover - level_controllable_cover_device = gateway.api.lights["1"] + mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state") # Service open cover - with patch.object( - level_controllable_cover_device, "_request", return_value=True - ) as set_callback: - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.level_controllable_cover"}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/1/state", json={"on": False}) + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.level_controllable_cover"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[5][2] == {"on": False} # Service close cover - with patch.object( - level_controllable_cover_device, "_request", return_value=True - ) as set_callback: - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.level_controllable_cover"}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/1/state", json={"on": True}) + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.level_controllable_cover"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[6][2] == {"on": True} # Service set cover position - with patch.object( - level_controllable_cover_device, "_request", return_value=True - ) as set_callback: - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: "cover.level_controllable_cover", ATTR_POSITION: 40}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/1/state", json={"bri": 152}) + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.level_controllable_cover", ATTR_POSITION: 40}, + blocking=True, + ) + assert aioclient_mock.mock_calls[7][2] == {"bri": 152} # Service stop cover movement - with patch.object( - level_controllable_cover_device, "_request", return_value=True - ) as set_callback: - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_STOP_COVER, - {ATTR_ENTITY_ID: "cover.level_controllable_cover"}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/1/state", json={"bri_inc": 0}) + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: "cover.level_controllable_cover"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[8][2] == {"bri_inc": 0} # Test that a reported cover position of 255 (deconz-rest-api < 2.05.73) is interpreted correctly. assert hass.states.get("cover.deconz_old_brightness_cover").state == STATE_OPEN @@ -266,7 +226,7 @@ async def test_cover(hass): assert len(hass.states.async_all()) == 0 -async def test_tilt_cover(hass): +async def test_tilt_cover(hass, aioclient_mock): """Test that tilting a cover works.""" data = deepcopy(DECONZ_WEB_REQUEST) data["lights"] = { @@ -290,54 +250,55 @@ async def test_tilt_cover(hass): "uniqueid": "00:24:46:00:00:12:34:56-01", } } - config_entry = await setup_deconz_integration(hass, get_state_response=data) - gateway = get_gateway_from_config_entry(hass, config_entry) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) assert len(hass.states.async_all()) == 1 entity = hass.states.get("cover.covering_device") assert entity.state == STATE_OPEN assert entity.attributes[ATTR_CURRENT_TILT_POSITION] == 100 - covering_device = gateway.api.lights["0"] + # Verify service calls for tilting cover - with patch.object(covering_device, "_request", return_value=True) as set_callback: - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_SET_COVER_TILT_POSITION, - {ATTR_ENTITY_ID: "cover.covering_device", ATTR_TILT_POSITION: 40}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/0/state", json={"tilt": 60}) + mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/0/state") - with patch.object(covering_device, "_request", return_value=True) as set_callback: - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_OPEN_COVER_TILT, - {ATTR_ENTITY_ID: "cover.covering_device"}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/0/state", json={"tilt": 0}) + # Service set tilt cover - with patch.object(covering_device, "_request", return_value=True) as set_callback: - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_CLOSE_COVER_TILT, - {ATTR_ENTITY_ID: "cover.covering_device"}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/0/state", json={"tilt": 100}) + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: "cover.covering_device", ATTR_TILT_POSITION: 40}, + blocking=True, + ) + assert aioclient_mock.mock_calls[1][2] == {"tilt": 60} + + # Service open tilt cover + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: "cover.covering_device"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[2][2] == {"tilt": 0} + + # Service close tilt cover + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: "cover.covering_device"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[3][2] == {"tilt": 100} # Service stop cover movement - with patch.object(covering_device, "_request", return_value=True) as set_callback: - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_STOP_COVER_TILT, - {ATTR_ENTITY_ID: "cover.covering_device"}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/0/state", json={"stop": True}) + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER_TILT, + {ATTR_ENTITY_ID: "cover.covering_device"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[4][2] == {"stop": True} diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index 232de5eacd2..1212d72a6ee 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -54,11 +54,13 @@ SENSORS = { } -async def test_deconz_events(hass): +async def test_deconz_events(hass, aioclient_mock): """Test successful creation of deconz events.""" data = deepcopy(DECONZ_WEB_REQUEST) data["sensors"] = deepcopy(SENSORS) - config_entry = await setup_deconz_integration(hass, get_state_response=data) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 3 diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index a5399fe4796..b9d538588cd 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -44,11 +44,13 @@ SENSORS = { } -async def test_get_triggers(hass): +async def test_get_triggers(hass, aioclient_mock): """Test triggers work.""" data = deepcopy(DECONZ_WEB_REQUEST) data["sensors"] = deepcopy(SENSORS) - config_entry = await setup_deconz_integration(hass, get_state_response=data) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) gateway = get_gateway_from_config_entry(hass, config_entry) device_id = gateway.events[0].device_id triggers = await async_get_device_automations(hass, "trigger", device_id) @@ -108,20 +110,22 @@ async def test_get_triggers(hass): assert_lists_same(triggers, expected_triggers) -async def test_helper_successful(hass): +async def test_helper_successful(hass, aioclient_mock): """Verify trigger helper.""" data = deepcopy(DECONZ_WEB_REQUEST) data["sensors"] = deepcopy(SENSORS) - config_entry = await setup_deconz_integration(hass, get_state_response=data) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) gateway = get_gateway_from_config_entry(hass, config_entry) device_id = gateway.events[0].device_id deconz_event = device_trigger._get_deconz_event_from_device_id(hass, device_id) assert deconz_event == gateway.events[0] -async def test_helper_no_match(hass): +async def test_helper_no_match(hass, aioclient_mock): """Verify trigger helper returns None when no event could be matched.""" - await setup_deconz_integration(hass) + await setup_deconz_integration(hass, aioclient_mock) deconz_event = device_trigger._get_deconz_event_from_device_id(hass, "mock-id") assert deconz_event is None diff --git a/tests/components/deconz/test_fan.py b/tests/components/deconz/test_fan.py index 7f225196744..c6acbb7f6aa 100644 --- a/tests/components/deconz/test_fan.py +++ b/tests/components/deconz/test_fan.py @@ -1,11 +1,9 @@ """deCONZ fan platform tests.""" from copy import deepcopy -from unittest.mock import patch import pytest -from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.gateway import get_gateway_from_config_entry from homeassistant.components.fan import ( ATTR_SPEED, @@ -19,9 +17,12 @@ from homeassistant.components.fan import ( SPEED_OFF, ) from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE -from homeassistant.setup import async_setup_component -from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import ( + DECONZ_WEB_REQUEST, + mock_deconz_put_request, + setup_deconz_integration, +) FANS = { "1": { @@ -44,28 +45,19 @@ FANS = { } -async def test_platform_manually_configured(hass): - """Test that we do not discover anything or try to set up a gateway.""" - assert ( - await async_setup_component( - hass, FAN_DOMAIN, {"fan": {"platform": DECONZ_DOMAIN}} - ) - is True - ) - assert DECONZ_DOMAIN not in hass.data - - -async def test_no_fans(hass): +async def test_no_fans(hass, aioclient_mock): """Test that no fan entities are created.""" - await setup_deconz_integration(hass) + await setup_deconz_integration(hass, aioclient_mock) assert len(hass.states.async_all()) == 0 -async def test_fans(hass): +async def test_fans(hass, aioclient_mock): """Test that all supported fan entities are created.""" data = deepcopy(DECONZ_WEB_REQUEST) data["lights"] = deepcopy(FANS) - config_entry = await setup_deconz_integration(hass, get_state_response=data) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 2 # Light and fan @@ -91,104 +83,77 @@ async def test_fans(hass): # Test service calls - ceiling_fan_device = gateway.api.lights["1"] + mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state") # Service turn on fan - with patch.object( - ceiling_fan_device, "_request", return_value=True - ) as set_callback: - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.ceiling_fan"}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/1/state", json={"speed": 4}) + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "fan.ceiling_fan"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[1][2] == {"speed": 4} # Service turn off fan - with patch.object( - ceiling_fan_device, "_request", return_value=True - ) as set_callback: - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "fan.ceiling_fan"}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/1/state", json={"speed": 0}) + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "fan.ceiling_fan"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[2][2] == {"speed": 0} # Service set fan speed to low - with patch.object( - ceiling_fan_device, "_request", return_value=True - ) as set_callback: - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_LOW}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/1/state", json={"speed": 1}) + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_SPEED, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_LOW}, + blocking=True, + ) + assert aioclient_mock.mock_calls[3][2] == {"speed": 1} # Service set fan speed to medium - with patch.object( - ceiling_fan_device, "_request", return_value=True - ) as set_callback: - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_MEDIUM}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/1/state", json={"speed": 2}) + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_SPEED, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_MEDIUM}, + blocking=True, + ) + assert aioclient_mock.mock_calls[4][2] == {"speed": 2} # Service set fan speed to high - with patch.object( - ceiling_fan_device, "_request", return_value=True - ) as set_callback: - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_HIGH}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/1/state", json={"speed": 4}) + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_SPEED, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_HIGH}, + blocking=True, + ) + assert aioclient_mock.mock_calls[5][2] == {"speed": 4} # Service set fan speed to off - with patch.object( - ceiling_fan_device, "_request", return_value=True - ) as set_callback: - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_OFF}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/1/state", json={"speed": 0}) + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_SPEED, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_OFF}, + blocking=True, + ) + assert aioclient_mock.mock_calls[6][2] == {"speed": 0} # Service set fan speed to unsupported value - with patch.object( - ceiling_fan_device, "_request", return_value=True - ) as set_callback, pytest.raises(ValueError): + with pytest.raises(ValueError): await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_SPEED, {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: "bad value"}, blocking=True, ) - await hass.async_block_till_done() # Events with an unsupported speed gets converted to default speed "medium" diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index f670f2a1d10..5c1642ba8f7 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -29,21 +29,25 @@ from homeassistant.components.ssdp import ( ) from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import CONN_CLASS_LOCAL_PUSH, SOURCE_SSDP -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON from homeassistant.helpers.dispatcher import async_dispatcher_connect from tests.common import MockConfigEntry API_KEY = "1234567890ABCDEF" BRIDGEID = "01234E56789A" +HOST = "1.2.3.4" +PORT = 80 -ENTRY_CONFIG = {CONF_API_KEY: API_KEY, CONF_HOST: "1.2.3.4", CONF_PORT: 80} +DEFAULT_URL = f"http://{HOST}:{PORT}/api/{API_KEY}" + +ENTRY_CONFIG = {CONF_API_KEY: API_KEY, CONF_HOST: HOST, CONF_PORT: PORT} ENTRY_OPTIONS = {} DECONZ_CONFIG = { "bridgeid": BRIDGEID, - "ipaddress": "1.2.3.4", + "ipaddress": HOST, "mac": "00:11:22:33:44:55", "modelid": "deCONZ", "name": "deCONZ mock gateway", @@ -60,8 +64,36 @@ DECONZ_WEB_REQUEST = { } +def mock_deconz_request(aioclient_mock, config, data): + """Mock a deCONZ get request.""" + host = config[CONF_HOST] + port = config[CONF_PORT] + api_key = config[CONF_API_KEY] + + aioclient_mock.get( + f"http://{host}:{port}/api/{api_key}", + json=deepcopy(data), + headers={"content-type": CONTENT_TYPE_JSON}, + ) + + +def mock_deconz_put_request(aioclient_mock, config, path): + """Mock a deCONZ put request.""" + host = config[CONF_HOST] + port = config[CONF_PORT] + api_key = config[CONF_API_KEY] + + aioclient_mock.put( + f"http://{host}:{port}/api/{api_key}{path}", + json={}, + headers={"content-type": CONTENT_TYPE_JSON}, + ) + + async def setup_deconz_integration( hass, + aioclient_mock=None, + *, config=ENTRY_CONFIG, options=ENTRY_OPTIONS, get_state_response=DECONZ_WEB_REQUEST, @@ -81,22 +113,23 @@ async def setup_deconz_integration( ) config_entry.add_to_hass(hass) - with patch( - "pydeconz.DeconzSession.request", return_value=deepcopy(get_state_response) - ), patch("pydeconz.DeconzSession.start", return_value=True): + if aioclient_mock: + mock_deconz_request(aioclient_mock, config, get_state_response) + + with patch("pydeconz.DeconzSession.start", return_value=True): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() return config_entry -async def test_gateway_setup(hass): +async def test_gateway_setup(hass, aioclient_mock): """Successful setup.""" with patch( "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", return_value=True, ) as forward_entry_setup: - config_entry = await setup_deconz_integration(hass) + config_entry = await setup_deconz_integration(hass, aioclient_mock) gateway = get_gateway_from_config_entry(hass, config_entry) assert gateway.bridgeid == BRIDGEID assert gateway.master is True @@ -140,9 +173,9 @@ async def test_gateway_setup_fails(hass): assert not hass.data[DECONZ_DOMAIN] -async def test_connection_status_signalling(hass): +async def test_connection_status_signalling(hass, aioclient_mock): """Make sure that connection status triggers a dispatcher send.""" - config_entry = await setup_deconz_integration(hass) + config_entry = await setup_deconz_integration(hass, aioclient_mock) gateway = get_gateway_from_config_entry(hass, config_entry) event_call = Mock() @@ -157,9 +190,9 @@ async def test_connection_status_signalling(hass): unsub() -async def test_update_address(hass): +async def test_update_address(hass, aioclient_mock): """Make sure that connection status triggers a dispatcher send.""" - config_entry = await setup_deconz_integration(hass) + config_entry = await setup_deconz_integration(hass, aioclient_mock) gateway = get_gateway_from_config_entry(hass, config_entry) assert gateway.api.host == "1.2.3.4" @@ -195,9 +228,9 @@ async def test_gateway_trigger_reauth_flow(hass): assert hass.data[DECONZ_DOMAIN] == {} -async def test_reset_after_successful_setup(hass): +async def test_reset_after_successful_setup(hass, aioclient_mock): """Make sure that connection status triggers a dispatcher send.""" - config_entry = await setup_deconz_integration(hass) + config_entry = await setup_deconz_integration(hass, aioclient_mock) gateway = get_gateway_from_config_entry(hass, config_entry) result = await gateway.async_reset() diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index 43c0c48440c..ed7655bf620 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -14,7 +14,6 @@ from homeassistant.components.deconz.const import ( CONF_GROUP_ID_BASE, DOMAIN as DECONZ_DOMAIN, ) -from homeassistant.components.deconz.gateway import get_gateway_from_config_entry from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.helpers import entity_registry @@ -58,59 +57,65 @@ async def test_setup_entry_no_available_bridge(hass): assert not hass.data[DECONZ_DOMAIN] -async def test_setup_entry_successful(hass): +async def test_setup_entry_successful(hass, aioclient_mock): """Test setup entry is successful.""" - config_entry = await setup_deconz_integration(hass) - gateway = get_gateway_from_config_entry(hass, config_entry) + config_entry = await setup_deconz_integration(hass, aioclient_mock) assert hass.data[DECONZ_DOMAIN] - assert gateway.bridgeid in hass.data[DECONZ_DOMAIN] - assert hass.data[DECONZ_DOMAIN][gateway.bridgeid].master + assert config_entry.unique_id in hass.data[DECONZ_DOMAIN] + assert hass.data[DECONZ_DOMAIN][config_entry.unique_id].master -async def test_setup_entry_multiple_gateways(hass): +async def test_setup_entry_multiple_gateways(hass, aioclient_mock): """Test setup entry is successful with multiple gateways.""" - config_entry = await setup_deconz_integration(hass) - gateway = get_gateway_from_config_entry(hass, config_entry) + config_entry = await setup_deconz_integration(hass, aioclient_mock) + aioclient_mock.clear_requests() data = deepcopy(DECONZ_WEB_REQUEST) data["config"]["bridgeid"] = "01234E56789B" config_entry2 = await setup_deconz_integration( - hass, get_state_response=data, entry_id="2", unique_id="01234E56789B" + hass, + aioclient_mock, + get_state_response=data, + entry_id="2", + unique_id="01234E56789B", ) - gateway2 = get_gateway_from_config_entry(hass, config_entry2) assert len(hass.data[DECONZ_DOMAIN]) == 2 - assert hass.data[DECONZ_DOMAIN][gateway.bridgeid].master - assert not hass.data[DECONZ_DOMAIN][gateway2.bridgeid].master + assert hass.data[DECONZ_DOMAIN][config_entry.unique_id].master + assert not hass.data[DECONZ_DOMAIN][config_entry2.unique_id].master -async def test_unload_entry(hass): +async def test_unload_entry(hass, aioclient_mock): """Test being able to unload an entry.""" - config_entry = await setup_deconz_integration(hass) + config_entry = await setup_deconz_integration(hass, aioclient_mock) assert hass.data[DECONZ_DOMAIN] assert await async_unload_entry(hass, config_entry) assert not hass.data[DECONZ_DOMAIN] -async def test_unload_entry_multiple_gateways(hass): +async def test_unload_entry_multiple_gateways(hass, aioclient_mock): """Test being able to unload an entry and master gateway gets moved.""" - config_entry = await setup_deconz_integration(hass) + config_entry = await setup_deconz_integration(hass, aioclient_mock) + aioclient_mock.clear_requests() data = deepcopy(DECONZ_WEB_REQUEST) data["config"]["bridgeid"] = "01234E56789B" config_entry2 = await setup_deconz_integration( - hass, get_state_response=data, entry_id="2", unique_id="01234E56789B" + hass, + aioclient_mock, + get_state_response=data, + entry_id="2", + unique_id="01234E56789B", ) - gateway2 = get_gateway_from_config_entry(hass, config_entry2) assert len(hass.data[DECONZ_DOMAIN]) == 2 assert await async_unload_entry(hass, config_entry) assert len(hass.data[DECONZ_DOMAIN]) == 1 - assert hass.data[DECONZ_DOMAIN][gateway2.bridgeid].master + assert hass.data[DECONZ_DOMAIN][config_entry2.unique_id].master async def test_update_group_unique_id(hass): diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index bdb7fbb8aef..c7f7fab1868 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -1,14 +1,10 @@ """deCONZ light platform tests.""" from copy import deepcopy -from unittest.mock import patch import pytest -from homeassistant.components.deconz.const import ( - CONF_ALLOW_DECONZ_GROUPS, - DOMAIN as DECONZ_DOMAIN, -) +from homeassistant.components.deconz.const import CONF_ALLOW_DECONZ_GROUPS from homeassistant.components.deconz.gateway import get_gateway_from_config_entry from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -33,9 +29,12 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.setup import async_setup_component -from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import ( + DECONZ_WEB_REQUEST, + mock_deconz_put_request, + setup_deconz_integration, +) GROUPS = { "1": { @@ -107,29 +106,20 @@ LIGHTS = { } -async def test_platform_manually_configured(hass): - """Test that we do not discover anything or try to set up a gateway.""" - assert ( - await async_setup_component( - hass, LIGHT_DOMAIN, {"light": {"platform": DECONZ_DOMAIN}} - ) - is True - ) - assert DECONZ_DOMAIN not in hass.data - - -async def test_no_lights_or_groups(hass): +async def test_no_lights_or_groups(hass, aioclient_mock): """Test that no lights or groups entities are created.""" - await setup_deconz_integration(hass) + await setup_deconz_integration(hass, aioclient_mock) assert len(hass.states.async_all()) == 0 -async def test_lights_and_groups(hass): +async def test_lights_and_groups(hass, aioclient_mock): """Test that lights or groups entities are created.""" data = deepcopy(DECONZ_WEB_REQUEST) data["groups"] = deepcopy(GROUPS) data["lights"] = deepcopy(LIGHTS) - config_entry = await setup_deconz_integration(hass, get_state_response=data) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 6 @@ -183,73 +173,63 @@ async def test_lights_and_groups(hass): # Verify service calls - rgb_light_device = gateway.api.lights["1"] + mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state") # Service turn on light with short color loop - with patch.object(rgb_light_device, "_request", return_value=True) as set_callback: - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - { - ATTR_ENTITY_ID: "light.rgb_light", - ATTR_COLOR_TEMP: 2500, - ATTR_BRIGHTNESS: 200, - ATTR_TRANSITION: 5, - ATTR_FLASH: FLASH_SHORT, - ATTR_EFFECT: EFFECT_COLORLOOP, - }, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with( - "put", - "/lights/1/state", - json={ - "ct": 2500, - "bri": 200, - "transitiontime": 50, - "alert": "select", - "effect": "colorloop", - }, - ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.rgb_light", + ATTR_COLOR_TEMP: 2500, + ATTR_BRIGHTNESS: 200, + ATTR_TRANSITION: 5, + ATTR_FLASH: FLASH_SHORT, + ATTR_EFFECT: EFFECT_COLORLOOP, + }, + blocking=True, + ) + assert aioclient_mock.mock_calls[1][2] == { + "ct": 2500, + "bri": 200, + "transitiontime": 50, + "alert": "select", + "effect": "colorloop", + } # Service turn on light disabling color loop with long flashing - with patch.object(rgb_light_device, "_request", return_value=True) as set_callback: - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - { - ATTR_ENTITY_ID: "light.rgb_light", - ATTR_HS_COLOR: (20, 30), - ATTR_FLASH: FLASH_LONG, - ATTR_EFFECT: "None", - }, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with( - "put", - "/lights/1/state", - json={"xy": (0.411, 0.351), "alert": "lselect", "effect": "none"}, - ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.rgb_light", + ATTR_HS_COLOR: (20, 30), + ATTR_FLASH: FLASH_LONG, + ATTR_EFFECT: "None", + }, + blocking=True, + ) + assert aioclient_mock.mock_calls[2][2] == { + "xy": (0.411, 0.351), + "alert": "lselect", + "effect": "none", + } - # Service turn on light with short flashing + # Service turn on light with short flashing not supported - with patch.object(rgb_light_device, "_request", return_value=True) as set_callback: - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - { - ATTR_ENTITY_ID: "light.rgb_light", - ATTR_TRANSITION: 5, - ATTR_FLASH: FLASH_SHORT, - }, - blocking=True, - ) - await hass.async_block_till_done() - assert not set_callback.called + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: "light.rgb_light", + ATTR_TRANSITION: 5, + ATTR_FLASH: FLASH_SHORT, + }, + blocking=True, + ) + assert len(aioclient_mock.mock_calls) == 3 # Not called state_changed_event = { "t": "event", @@ -263,37 +243,31 @@ async def test_lights_and_groups(hass): # Service turn off light with short flashing - with patch.object(rgb_light_device, "_request", return_value=True) as set_callback: - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - { - ATTR_ENTITY_ID: "light.rgb_light", - ATTR_TRANSITION: 5, - ATTR_FLASH: FLASH_SHORT, - }, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with( - "put", - "/lights/1/state", - json={"bri": 0, "transitiontime": 50, "alert": "select"}, - ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: "light.rgb_light", + ATTR_TRANSITION: 5, + ATTR_FLASH: FLASH_SHORT, + }, + blocking=True, + ) + assert aioclient_mock.mock_calls[3][2] == { + "bri": 0, + "transitiontime": 50, + "alert": "select", + } # Service turn off light with long flashing - with patch.object(rgb_light_device, "_request", return_value=True) as set_callback: - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.rgb_light", ATTR_FLASH: FLASH_LONG}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with( - "put", "/lights/1/state", json={"alert": "lselect"} - ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.rgb_light", ATTR_FLASH: FLASH_LONG}, + blocking=True, + ) + assert aioclient_mock.mock_calls[4][2] == {"alert": "lselect"} await hass.config_entries.async_unload(config_entry.entry_id) @@ -307,13 +281,14 @@ async def test_lights_and_groups(hass): assert len(hass.states.async_all()) == 0 -async def test_disable_light_groups(hass): +async def test_disable_light_groups(hass, aioclient_mock): """Test disallowing light groups work.""" data = deepcopy(DECONZ_WEB_REQUEST) data["groups"] = deepcopy(GROUPS) data["lights"] = deepcopy(LIGHTS) config_entry = await setup_deconz_integration( hass, + aioclient_mock, options={CONF_ALLOW_DECONZ_GROUPS: False}, get_state_response=data, ) @@ -341,7 +316,7 @@ async def test_disable_light_groups(hass): assert hass.states.get("light.light_group") is None -async def test_configuration_tool(hass): +async def test_configuration_tool(hass, aioclient_mock): """Test that lights or groups entities are created.""" data = deepcopy(DECONZ_WEB_REQUEST) data["lights"] = { @@ -359,12 +334,12 @@ async def test_configuration_tool(hass): "uniqueid": "00:21:2e:ff:ff:05:a7:a3-01", } } - await setup_deconz_integration(hass, get_state_response=data) + await setup_deconz_integration(hass, aioclient_mock, get_state_response=data) assert len(hass.states.async_all()) == 0 -async def test_lidl_christmas_light(hass): +async def test_lidl_christmas_light(hass, aioclient_mock): """Test that lights or groups entities are created.""" data = deepcopy(DECONZ_WEB_REQUEST) data["lights"] = { @@ -390,33 +365,27 @@ async def test_lidl_christmas_light(hass): "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"] + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) - assert len(hass.states.async_all()) == 1 + mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/0/state") - 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}, - ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.xmas_light", + ATTR_HS_COLOR: (20, 30), + }, + blocking=True, + ) + assert aioclient_mock.mock_calls[1][2] == {"on": True, "hue": 3640, "sat": 76} assert hass.states.get("light.xmas_light") -async def test_non_color_light_reports_color(hass): +async def test_non_color_light_reports_color(hass, aioclient_mock): """Verify hs_color does not crash when a group gets updated with a bad color value. After calling a scene color temp light of certain manufacturers @@ -500,7 +469,9 @@ async def test_non_color_light_reports_color(hass): "uniqueid": "ec:1b:bd:ff:fe:ee:ed:dd-01", }, } - config_entry = await setup_deconz_integration(hass, get_state_response=data) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 3 @@ -531,7 +502,7 @@ async def test_non_color_light_reports_color(hass): assert hass.states.get("light.all").attributes[ATTR_HS_COLOR] -async def test_verify_group_supported_features(hass): +async def test_verify_group_supported_features(hass, aioclient_mock): """Test that group supported features reflect what included lights support.""" data = deepcopy(DECONZ_WEB_REQUEST) data["groups"] = deepcopy( @@ -581,7 +552,7 @@ async def test_verify_group_supported_features(hass): }, } ) - await setup_deconz_integration(hass, get_state_response=data) + await setup_deconz_integration(hass, aioclient_mock, get_state_response=data) assert len(hass.states.async_all()) == 4 diff --git a/tests/components/deconz/test_lock.py b/tests/components/deconz/test_lock.py index d53da74dfdd..a6b4caaec19 100644 --- a/tests/components/deconz/test_lock.py +++ b/tests/components/deconz/test_lock.py @@ -1,9 +1,7 @@ """deCONZ lock platform tests.""" from copy import deepcopy -from unittest.mock import patch -from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.gateway import get_gateway_from_config_entry from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, @@ -16,9 +14,12 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNLOCKED, ) -from homeassistant.setup import async_setup_component -from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import ( + DECONZ_WEB_REQUEST, + mock_deconz_put_request, + setup_deconz_integration, +) LOCKS = { "1": { @@ -37,28 +38,19 @@ LOCKS = { } -async def test_platform_manually_configured(hass): - """Test that we do not discover anything or try to set up a gateway.""" - assert ( - await async_setup_component( - hass, LOCK_DOMAIN, {"lock": {"platform": DECONZ_DOMAIN}} - ) - is True - ) - assert DECONZ_DOMAIN not in hass.data - - -async def test_no_locks(hass): +async def test_no_locks(hass, aioclient_mock): """Test that no lock entities are created.""" - await setup_deconz_integration(hass) + await setup_deconz_integration(hass, aioclient_mock) assert len(hass.states.async_all()) == 0 -async def test_locks(hass): +async def test_locks(hass, aioclient_mock): """Test that all supported lock entities are created.""" data = deepcopy(DECONZ_WEB_REQUEST) data["lights"] = deepcopy(LOCKS) - config_entry = await setup_deconz_integration(hass, get_state_response=data) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 1 @@ -81,31 +73,27 @@ async def test_locks(hass): # Verify service calls - door_lock_device = gateway.api.lights["1"] + mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state") # Service lock door - with patch.object(door_lock_device, "_request", return_value=True) as set_callback: - await hass.services.async_call( - LOCK_DOMAIN, - SERVICE_LOCK, - {ATTR_ENTITY_ID: "lock.door_lock"}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/1/state", json={"on": True}) + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_LOCK, + {ATTR_ENTITY_ID: "lock.door_lock"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[1][2] == {"on": True} # Service unlock door - with patch.object(door_lock_device, "_request", return_value=True) as set_callback: - await hass.services.async_call( - LOCK_DOMAIN, - SERVICE_UNLOCK, - {ATTR_ENTITY_ID: "lock.door_lock"}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/1/state", json={"on": False}) + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_UNLOCK, + {ATTR_ENTITY_ID: "lock.door_lock"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[2][2] == {"on": False} await hass.config_entries.async_unload(config_entry.entry_id) diff --git a/tests/components/deconz/test_logbook.py b/tests/components/deconz/test_logbook.py index 500ca03b7ed..5886a29a8bf 100644 --- a/tests/components/deconz/test_logbook.py +++ b/tests/components/deconz/test_logbook.py @@ -14,7 +14,7 @@ from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration from tests.components.logbook.test_init import MockLazyEventPartialState -async def test_humanifying_deconz_event(hass): +async def test_humanifying_deconz_event(hass, aioclient_mock): """Test humanifying deCONZ event.""" data = deepcopy(DECONZ_WEB_REQUEST) data["sensors"] = { @@ -53,7 +53,9 @@ async def test_humanifying_deconz_event(hass): "uniqueid": "00:00:00:00:00:00:00:04-00", }, } - config_entry = await setup_deconz_integration(hass, get_state_response=data) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) gateway = get_gateway_from_config_entry(hass, config_entry) hass.config.components.add("recorder") diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py index ca8df2c0425..229111bf9ae 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -1,15 +1,15 @@ """deCONZ scene platform tests.""" from copy import deepcopy -from unittest.mock import patch -from homeassistant.components.deconz import DOMAIN as DECONZ_DOMAIN -from homeassistant.components.deconz.gateway import get_gateway_from_config_entry from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, SERVICE_TURN_ON from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.setup import async_setup_component -from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import ( + DECONZ_WEB_REQUEST, + mock_deconz_put_request, + setup_deconz_integration, +) GROUPS = { "1": { @@ -24,48 +24,38 @@ GROUPS = { } -async def test_platform_manually_configured(hass): - """Test that we do not discover anything or try to set up a gateway.""" - assert ( - await async_setup_component( - hass, SCENE_DOMAIN, {"scene": {"platform": DECONZ_DOMAIN}} - ) - is True - ) - assert DECONZ_DOMAIN not in hass.data - - -async def test_no_scenes(hass): +async def test_no_scenes(hass, aioclient_mock): """Test that scenes can be loaded without scenes being available.""" - await setup_deconz_integration(hass) + await setup_deconz_integration(hass, aioclient_mock) assert len(hass.states.async_all()) == 0 -async def test_scenes(hass): +async def test_scenes(hass, aioclient_mock): """Test that scenes works.""" data = deepcopy(DECONZ_WEB_REQUEST) data["groups"] = deepcopy(GROUPS) - config_entry = await setup_deconz_integration(hass, get_state_response=data) - gateway = get_gateway_from_config_entry(hass, config_entry) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) assert len(hass.states.async_all()) == 1 assert hass.states.get("scene.light_group_scene") # Verify service calls - group_scene = gateway.api.groups["1"].scenes["1"] + mock_deconz_put_request( + aioclient_mock, config_entry.data, "/groups/1/scenes/1/recall" + ) # Service turn on scene - with patch.object(group_scene, "_request", return_value=True) as set_callback: - await hass.services.async_call( - SCENE_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "scene.light_group_scene"}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/groups/1/scenes/1/recall", json={}) + await hass.services.async_call( + SCENE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "scene.light_group_scene"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[1][2] == {} await hass.config_entries.async_unload(config_entry.entry_id) diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 426a88b8bb6..8a00385ccb9 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -2,19 +2,14 @@ from copy import deepcopy -from homeassistant.components.deconz.const import ( - CONF_ALLOW_CLIP_SENSOR, - DOMAIN as DECONZ_DOMAIN, -) +from homeassistant.components.deconz.const import CONF_ALLOW_CLIP_SENSOR from homeassistant.components.deconz.gateway import get_gateway_from_config_entry -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, STATE_UNAVAILABLE, ) -from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration @@ -86,28 +81,19 @@ SENSORS = { } -async def test_platform_manually_configured(hass): - """Test that we do not discover anything or try to set up a gateway.""" - assert ( - await async_setup_component( - hass, SENSOR_DOMAIN, {"sensor": {"platform": DECONZ_DOMAIN}} - ) - is True - ) - assert DECONZ_DOMAIN not in hass.data - - -async def test_no_sensors(hass): +async def test_no_sensors(hass, aioclient_mock): """Test that no sensors in deconz results in no sensor entities.""" - await setup_deconz_integration(hass) + await setup_deconz_integration(hass, aioclient_mock) assert len(hass.states.async_all()) == 0 -async def test_sensors(hass): +async def test_sensors(hass, aioclient_mock): """Test successful creation of sensor entities.""" data = deepcopy(DECONZ_WEB_REQUEST) data["sensors"] = deepcopy(SENSORS) - config_entry = await setup_deconz_integration(hass, get_state_response=data) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 5 @@ -176,12 +162,13 @@ async def test_sensors(hass): assert len(hass.states.async_all()) == 0 -async def test_allow_clip_sensors(hass): +async def test_allow_clip_sensors(hass, aioclient_mock): """Test that CLIP sensors can be allowed.""" data = deepcopy(DECONZ_WEB_REQUEST) data["sensors"] = deepcopy(SENSORS) config_entry = await setup_deconz_integration( hass, + aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: True}, get_state_response=data, ) @@ -210,9 +197,9 @@ async def test_allow_clip_sensors(hass): assert hass.states.get("sensor.clip_light_level_sensor") -async def test_add_new_sensor(hass): +async def test_add_new_sensor(hass, aioclient_mock): """Test that adding a new sensor works.""" - config_entry = await setup_deconz_integration(hass) + config_entry = await setup_deconz_integration(hass, aioclient_mock) gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 0 @@ -230,11 +217,13 @@ async def test_add_new_sensor(hass): assert hass.states.get("sensor.light_level_sensor").state == "999.8" -async def test_add_battery_later(hass): +async def test_add_battery_later(hass, aioclient_mock): """Test that a sensor without an initial battery state creates a battery sensor once state exist.""" data = deepcopy(DECONZ_WEB_REQUEST) data["sensors"] = {"1": deepcopy(SENSORS["3"])} - config_entry = await setup_deconz_integration(hass, get_state_response=data) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) gateway = get_gateway_from_config_entry(hass, config_entry) remote = gateway.api.sensors["1"] @@ -252,7 +241,7 @@ async def test_add_battery_later(hass): assert hass.states.get("sensor.switch_1_battery_level") -async def test_air_quality_sensor(hass): +async def test_air_quality_sensor(hass, aioclient_mock): """Test successful creation of air quality sensor entities.""" data = deepcopy(DECONZ_WEB_REQUEST) data["sensors"] = { @@ -274,7 +263,7 @@ async def test_air_quality_sensor(hass): "uniqueid": "00:12:4b:00:14:4d:00:07-02-fdef", } } - await setup_deconz_integration(hass, get_state_response=data) + await setup_deconz_integration(hass, aioclient_mock, get_state_response=data) assert len(hass.states.async_all()) == 1 @@ -282,7 +271,7 @@ async def test_air_quality_sensor(hass): assert air_quality.state == "poor" -async def test_time_sensor(hass): +async def test_time_sensor(hass, aioclient_mock): """Test successful creation of time sensor entities.""" data = deepcopy(DECONZ_WEB_REQUEST) data["sensors"] = { @@ -305,7 +294,7 @@ async def test_time_sensor(hass): "uniqueid": "cc:cc:cc:ff:fe:38:4d:b3-01-000a", } } - await setup_deconz_integration(hass, get_state_response=data) + await setup_deconz_integration(hass, aioclient_mock, get_state_response=data) assert len(hass.states.async_all()) == 2 @@ -316,13 +305,13 @@ async def test_time_sensor(hass): assert time_battery.state == "40" -async def test_unsupported_sensor(hass): +async def test_unsupported_sensor(hass, aioclient_mock): """Test that unsupported sensors doesn't break anything.""" data = deepcopy(DECONZ_WEB_REQUEST) data["sensors"] = { "0": {"type": "not supported", "name": "name", "state": {}, "config": {}} } - await setup_deconz_integration(hass, get_state_response=data) + await setup_deconz_integration(hass, aioclient_mock, get_state_response=data) assert len(hass.states.async_all()) == 1 diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index faa1d3485bb..41eefa95785 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -25,7 +25,13 @@ from homeassistant.components.deconz.services import ( from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.helpers.entity_registry import async_entries_for_config_entry -from .test_gateway import BRIDGEID, DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import ( + BRIDGEID, + DECONZ_WEB_REQUEST, + mock_deconz_put_request, + mock_deconz_request, + setup_deconz_integration, +) GROUP = { "1": { @@ -114,72 +120,66 @@ async def test_service_unload_not_registered(hass): async_remove.assert_not_called() -async def test_configure_service_with_field(hass): +async def test_configure_service_with_field(hass, aioclient_mock): """Test that service invokes pydeconz with the correct path and data.""" - await setup_deconz_integration(hass) + config_entry = await setup_deconz_integration(hass, aioclient_mock) data = { - SERVICE_FIELD: "/light/2", + SERVICE_FIELD: "/lights/2", CONF_BRIDGE_ID: BRIDGEID, SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, } - with patch("pydeconz.DeconzSession.request", return_value=Mock(True)) as put_state: - await hass.services.async_call( - DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data - ) - await hass.async_block_till_done() - put_state.assert_called_with( - "put", "/light/2", json={"on": True, "attr1": 10, "attr2": 20} - ) + mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/2") + + await hass.services.async_call( + DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data, blocking=True + ) + assert aioclient_mock.mock_calls[1][2] == {"on": True, "attr1": 10, "attr2": 20} -async def test_configure_service_with_entity(hass): +async def test_configure_service_with_entity(hass, aioclient_mock): """Test that service invokes pydeconz with the correct path and data.""" - config_entry = await setup_deconz_integration(hass) + config_entry = await setup_deconz_integration(hass, aioclient_mock) gateway = get_gateway_from_config_entry(hass, config_entry) - gateway.deconz_ids["light.test"] = "/light/1" + gateway.deconz_ids["light.test"] = "/lights/1" data = { SERVICE_ENTITY: "light.test", SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, } - with patch("pydeconz.DeconzSession.request", return_value=Mock(True)) as put_state: - await hass.services.async_call( - DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data - ) - await hass.async_block_till_done() - put_state.assert_called_with( - "put", "/light/1", json={"on": True, "attr1": 10, "attr2": 20} - ) + mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1") + + await hass.services.async_call( + DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data, blocking=True + ) + assert aioclient_mock.mock_calls[1][2] == {"on": True, "attr1": 10, "attr2": 20} -async def test_configure_service_with_entity_and_field(hass): +async def test_configure_service_with_entity_and_field(hass, aioclient_mock): """Test that service invokes pydeconz with the correct path and data.""" - config_entry = await setup_deconz_integration(hass) + config_entry = await setup_deconz_integration(hass, aioclient_mock) gateway = get_gateway_from_config_entry(hass, config_entry) - gateway.deconz_ids["light.test"] = "/light/1" + gateway.deconz_ids["light.test"] = "/lights/1" data = { SERVICE_ENTITY: "light.test", SERVICE_FIELD: "/state", SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, } - with patch("pydeconz.DeconzSession.request", return_value=Mock(True)) as put_state: - await hass.services.async_call( - DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data - ) - await hass.async_block_till_done() - put_state.assert_called_with( - "put", "/light/1/state", json={"on": True, "attr1": 10, "attr2": 20} - ) + mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state") + + await hass.services.async_call( + DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data, blocking=True + ) + assert aioclient_mock.mock_calls[1][2] == {"on": True, "attr1": 10, "attr2": 20} -async def test_configure_service_with_faulty_field(hass): +async def test_configure_service_with_faulty_field(hass, aioclient_mock): """Test that service invokes pydeconz with the correct path and data.""" - await setup_deconz_integration(hass) + await setup_deconz_integration(hass, aioclient_mock) data = {SERVICE_FIELD: "light/2", SERVICE_DATA: {}} @@ -190,9 +190,9 @@ async def test_configure_service_with_faulty_field(hass): await hass.async_block_till_done() -async def test_configure_service_with_faulty_entity(hass): +async def test_configure_service_with_faulty_entity(hass, aioclient_mock): """Test that service invokes pydeconz with the correct path and data.""" - await setup_deconz_integration(hass) + await setup_deconz_integration(hass, aioclient_mock) data = { SERVICE_ENTITY: "light.nonexisting", @@ -207,21 +207,24 @@ async def test_configure_service_with_faulty_entity(hass): put_state.assert_not_called() -async def test_service_refresh_devices(hass): +async def test_service_refresh_devices(hass, aioclient_mock): """Test that service can refresh devices.""" - config_entry = await setup_deconz_integration(hass) + config_entry = await setup_deconz_integration(hass, aioclient_mock) gateway = get_gateway_from_config_entry(hass, config_entry) + aioclient_mock.clear_requests() data = {CONF_BRIDGE_ID: BRIDGEID} - with patch( - "pydeconz.DeconzSession.request", - return_value={"groups": GROUP, "lights": LIGHT, "sensors": SENSOR}, - ): - await hass.services.async_call( - DECONZ_DOMAIN, SERVICE_DEVICE_REFRESH, service_data=data - ) - await hass.async_block_till_done() + mock_deconz_request( + aioclient_mock, + config_entry.data, + {"groups": GROUP, "lights": LIGHT, "sensors": SENSOR}, + ) + + await hass.services.async_call( + DECONZ_DOMAIN, SERVICE_DEVICE_REFRESH, service_data=data + ) + await hass.async_block_till_done() assert gateway.deconz_ids == { "light.group_1_name": "/groups/1", @@ -231,12 +234,14 @@ async def test_service_refresh_devices(hass): } -async def test_remove_orphaned_entries_service(hass): +async def test_remove_orphaned_entries_service(hass, aioclient_mock): """Test service works and also don't remove more than expected.""" data = deepcopy(DECONZ_WEB_REQUEST) data["lights"] = deepcopy(LIGHT) data["sensors"] = deepcopy(SWITCH) - config_entry = await setup_deconz_integration(hass, get_state_response=data) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) data = {CONF_BRIDGE_ID: BRIDGEID} diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index 22ce182cb62..6aafac1bd42 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -1,9 +1,7 @@ """deCONZ switch platform tests.""" from copy import deepcopy -from unittest.mock import patch -from homeassistant.components.deconz import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.gateway import get_gateway_from_config_entry from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, @@ -11,9 +9,12 @@ from homeassistant.components.switch import ( SERVICE_TURN_ON, ) from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE -from homeassistant.setup import async_setup_component -from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import ( + DECONZ_WEB_REQUEST, + mock_deconz_put_request, + setup_deconz_integration, +) POWER_PLUGS = { "1": { @@ -64,28 +65,19 @@ SIRENS = { } -async def test_platform_manually_configured(hass): - """Test that we do not discover anything or try to set up a gateway.""" - assert ( - await async_setup_component( - hass, SWITCH_DOMAIN, {"switch": {"platform": DECONZ_DOMAIN}} - ) - is True - ) - assert DECONZ_DOMAIN not in hass.data - - -async def test_no_switches(hass): +async def test_no_switches(hass, aioclient_mock): """Test that no switch entities are created.""" - await setup_deconz_integration(hass) + await setup_deconz_integration(hass, aioclient_mock) assert len(hass.states.async_all()) == 0 -async def test_power_plugs(hass): +async def test_power_plugs(hass, aioclient_mock): """Test that all supported switch entities are created.""" data = deepcopy(DECONZ_WEB_REQUEST) data["lights"] = deepcopy(POWER_PLUGS) - config_entry = await setup_deconz_integration(hass, get_state_response=data) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 4 @@ -107,35 +99,27 @@ async def test_power_plugs(hass): # Verify service calls - on_off_switch_device = gateway.api.lights["1"] + mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state") # Service turn on power plug - with patch.object( - on_off_switch_device, "_request", return_value=True - ) as set_callback: - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.on_off_switch"}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/1/state", json={"on": True}) + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.on_off_switch"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[1][2] == {"on": True} # Service turn off power plug - with patch.object( - on_off_switch_device, "_request", return_value=True - ) as set_callback: - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.on_off_switch"}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with("put", "/lights/1/state", json={"on": False}) + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.on_off_switch"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[2][2] == {"on": False} await hass.config_entries.async_unload(config_entry.entry_id) @@ -149,11 +133,13 @@ async def test_power_plugs(hass): assert len(hass.states.async_all()) == 0 -async def test_sirens(hass): +async def test_sirens(hass, aioclient_mock): """Test that siren entities are created.""" data = deepcopy(DECONZ_WEB_REQUEST) data["lights"] = deepcopy(SIRENS) - config_entry = await setup_deconz_integration(hass, get_state_response=data) + config_entry = await setup_deconz_integration( + hass, aioclient_mock, get_state_response=data + ) gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 2 @@ -173,39 +159,27 @@ async def test_sirens(hass): # Verify service calls - warning_device_device = gateway.api.lights["1"] + mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state") # Service turn on siren - with patch.object( - warning_device_device, "_request", return_value=True - ) as set_callback: - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.warning_device"}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with( - "put", "/lights/1/state", json={"alert": "lselect"} - ) + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.warning_device"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[1][2] == {"alert": "lselect"} # Service turn off siren - with patch.object( - warning_device_device, "_request", return_value=True - ) as set_callback: - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.warning_device"}, - blocking=True, - ) - await hass.async_block_till_done() - set_callback.assert_called_with( - "put", "/lights/1/state", json={"alert": "none"} - ) + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.warning_device"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[2][2] == {"alert": "none"} await hass.config_entries.async_unload(config_entry.entry_id) From 2fc1c19a45ce2bd7479c027a02b225454c0fe1f6 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 9 Feb 2021 09:28:40 +0100 Subject: [PATCH 300/796] Allow to setup of a previously discovered sleeping Shelly device (#46124) Co-authored-by: Franck Nijhof --- .../components/shelly/config_flow.py | 18 ++++ homeassistant/components/shelly/strings.json | 4 +- tests/components/shelly/test_config_flow.py | 98 ++++++++++++++++++- 3 files changed, 117 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 09fc477e512..026021a992f 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -65,6 +65,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH host = None info = None + device_info = None async def async_step_user(self, user_input=None): """Handle the initial step.""" @@ -160,6 +161,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(info["mac"]) self._abort_if_unique_id_configured({CONF_HOST: zeroconf_info["host"]}) self.host = zeroconf_info["host"] + + if not info["auth"] and info.get("sleep_mode", False): + try: + self.device_info = await validate_input(self.hass, self.host, {}) + except HTTP_CONNECT_ERRORS: + return self.async_abort(reason="cannot_connect") + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { "name": zeroconf_info.get("name", "").split(".")[0] @@ -173,6 +181,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self.info["auth"]: return await self.async_step_credentials() + if self.device_info: + return self.async_create_entry( + title=self.device_info["title"] or self.device_info["hostname"], + data={ + "host": self.host, + "sleep_period": self.device_info["sleep_period"], + "model": self.device_info["model"], + }, + ) + try: device_info = await validate_input(self.hass, self.host, {}) except HTTP_CONNECT_ERRORS: diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 341328801cc..85a1fa87d0c 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -3,7 +3,7 @@ "flow_title": "{name}", "step": { "user": { - "description": "Before set up, battery-powered devices must be woken up by pressing the button on the device.", + "description": "Before set up, battery-powered devices must be woken up, you can now wake the device up using a button on it.", "data": { "host": "[%key:common::config_flow::data::host%]" } @@ -15,7 +15,7 @@ } }, "confirm_discovery": { - "description": "Do you want to set up the {model} at {host}?\n\nBefore set up, battery-powered devices must be woken up by pressing the button on the device." + "description": "Do you want to set up the {model} at {host}?\n\nBattery-powered devices that are password protected must be woken up before continuing with setting up.\nBattery-powered devices that are not password protected will be added when the device wakes up, you can now manually wake the device up using a button on it or wait for the next data update from the device." } }, "error": { diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 1d5099cec1c..450bf8efb24 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -14,7 +14,6 @@ from tests.common import MockConfigEntry MOCK_SETTINGS = { "name": "Test name", "device": {"mac": "test-mac", "hostname": "test-host", "type": "SHSW-1"}, - "sleep_period": 0, } DISCOVERY_INFO = { "host": "1.1.1.1", @@ -383,6 +382,103 @@ async def test_zeroconf(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_zeroconf_sleeping_device(hass): + """Test sleeping device configuration via zeroconf.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "aioshelly.get_info", + return_value={ + "mac": "test-mac", + "type": "SHSW-1", + "auth": False, + "sleep_mode": True, + }, + ), patch( + "aioshelly.Device.create", + new=AsyncMock( + return_value=Mock( + settings={ + "name": "Test name", + "device": { + "mac": "test-mac", + "hostname": "test-host", + "type": "SHSW-1", + }, + "sleep_mode": {"period": 10, "unit": "m"}, + }, + ) + ), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == data_entry_flow.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"] == "shelly1pm-12345" + with patch( + "homeassistant.components.shelly.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.shelly.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"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "Test name" + assert result2["data"] == { + "host": "1.1.1.1", + "model": "SHSW-1", + "sleep_period": 600, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "error", + [ + (aiohttp.ClientResponseError(Mock(), (), status=400), "cannot_connect"), + (asyncio.TimeoutError, "cannot_connect"), + ], +) +async def test_zeroconf_sleeping_device_error(hass, error): + """Test sleeping device configuration via zeroconf with error.""" + exc = error + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "aioshelly.get_info", + return_value={ + "mac": "test-mac", + "type": "SHSW-1", + "auth": False, + "sleep_mode": True, + }, + ), patch( + "aioshelly.Device.create", + new=AsyncMock(side_effect=exc), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + @pytest.mark.parametrize( "error", [(asyncio.TimeoutError, "cannot_connect"), (ValueError, "unknown")] ) From 6a62ebb6a490c7f22ca2fac98f7a813fbeb27d4c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Feb 2021 22:43:38 -1000 Subject: [PATCH 301/796] Add BPUP (push updates) support to bond (#45550) --- homeassistant/components/bond/__init__.py | 17 +++- homeassistant/components/bond/config_flow.py | 2 +- homeassistant/components/bond/const.py | 5 ++ homeassistant/components/bond/cover.py | 14 +-- homeassistant/components/bond/entity.py | 90 +++++++++++++++++--- homeassistant/components/bond/fan.py | 16 ++-- homeassistant/components/bond/light.py | 28 +++--- homeassistant/components/bond/manifest.json | 2 +- homeassistant/components/bond/switch.py | 14 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bond/common.py | 20 +++-- tests/components/bond/test_init.py | 3 +- 13 files changed, 163 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 88ea084d25f..4e6705cbe09 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -3,7 +3,7 @@ import asyncio from asyncio import TimeoutError as AsyncIOTimeoutError from aiohttp import ClientError, ClientTimeout -from bond_api import Bond +from bond_api import Bond, BPUPSubscriptions, start_bpup from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST @@ -12,7 +12,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import SLOW_UPDATE_WARNING -from .const import BRIDGE_MAKE, DOMAIN +from .const import BPUP_STOP, BPUP_SUBS, BRIDGE_MAKE, DOMAIN, HUB from .utils import BondHub PLATFORMS = ["cover", "fan", "light", "switch"] @@ -38,7 +38,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except (ClientError, AsyncIOTimeoutError, OSError) as error: raise ConfigEntryNotReady from error - hass.data[DOMAIN][config_entry_id] = hub + bpup_subs = BPUPSubscriptions() + stop_bpup = await start_bpup(host, bpup_subs) + + hass.data[DOMAIN][entry.entry_id] = { + HUB: hub, + BPUP_SUBS: bpup_subs, + BPUP_STOP: stop_bpup, + } if not entry.unique_id: hass.config_entries.async_update_entry(entry, unique_id=hub.bond_id) @@ -74,6 +81,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ) + data = hass.data[DOMAIN][entry.entry_id] + if BPUP_STOP in data: + data[BPUP_STOP]() + if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 6666cd57ca3..2004da0c81e 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -55,7 +55,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Bond.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH _discovered: dict = None diff --git a/homeassistant/components/bond/const.py b/homeassistant/components/bond/const.py index 3031c159b0f..818288a5764 100644 --- a/homeassistant/components/bond/const.py +++ b/homeassistant/components/bond/const.py @@ -5,3 +5,8 @@ BRIDGE_MAKE = "Olibra" DOMAIN = "bond" CONF_BOND_ID: str = "bond_id" + + +HUB = "hub" +BPUP_SUBS = "bpup_subs" +BPUP_STOP = "bpup_stop" diff --git a/homeassistant/components/bond/cover.py b/homeassistant/components/bond/cover.py index dc0fc6d500c..6b3c8d6bc02 100644 --- a/homeassistant/components/bond/cover.py +++ b/homeassistant/components/bond/cover.py @@ -1,14 +1,14 @@ """Support for Bond covers.""" from typing import Any, Callable, List, Optional -from bond_api import Action, DeviceType +from bond_api import Action, BPUPSubscriptions, DeviceType from homeassistant.components.cover import DEVICE_CLASS_SHADE, CoverEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from .const import DOMAIN +from .const import BPUP_SUBS, DOMAIN, HUB from .entity import BondEntity from .utils import BondDevice, BondHub @@ -19,10 +19,12 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up Bond cover devices.""" - hub: BondHub = hass.data[DOMAIN][entry.entry_id] + data = hass.data[DOMAIN][entry.entry_id] + hub: BondHub = data[HUB] + bpup_subs: BPUPSubscriptions = data[BPUP_SUBS] covers = [ - BondCover(hub, device) + BondCover(hub, device, bpup_subs) for device in hub.devices if device.type == DeviceType.MOTORIZED_SHADES ] @@ -33,9 +35,9 @@ async def async_setup_entry( class BondCover(BondEntity, CoverEntity): """Representation of a Bond cover.""" - def __init__(self, hub: BondHub, device: BondDevice): + def __init__(self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions): """Create HA entity representing Bond cover.""" - super().__init__(hub, device) + super().__init__(hub, device, bpup_subs) self._closed: Optional[bool] = None diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index 2819182c9b5..769794a31e8 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -1,37 +1,51 @@ """An abstract class common to all Bond entities.""" from abc import abstractmethod -from asyncio import TimeoutError as AsyncIOTimeoutError +from asyncio import Lock, TimeoutError as AsyncIOTimeoutError +from datetime import timedelta import logging from typing import Any, Dict, Optional from aiohttp import ClientError +from bond_api import BPUPSubscriptions from homeassistant.const import ATTR_NAME +from homeassistant.core import callback from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval from .const import DOMAIN from .utils import BondDevice, BondHub _LOGGER = logging.getLogger(__name__) +_FALLBACK_SCAN_INTERVAL = timedelta(seconds=10) + class BondEntity(Entity): """Generic Bond entity encapsulating common features of any Bond controlled device.""" def __init__( - self, hub: BondHub, device: BondDevice, sub_device: Optional[str] = None + self, + hub: BondHub, + device: BondDevice, + bpup_subs: BPUPSubscriptions, + sub_device: Optional[str] = None, ): """Initialize entity with API and device info.""" self._hub = hub self._device = device + self._device_id = device.device_id self._sub_device = sub_device self._available = True + self._bpup_subs = bpup_subs + self._update_lock = None + self._initialized = False @property def unique_id(self) -> Optional[str]: """Get unique ID for the entity.""" hub_id = self._hub.bond_id - device_id = self._device.device_id + device_id = self._device_id sub_device_id: str = f"_{self._sub_device}" if self._sub_device else "" return f"{hub_id}_{device_id}{sub_device_id}" @@ -40,13 +54,18 @@ class BondEntity(Entity): """Get entity name.""" return self._device.name + @property + def should_poll(self): + """No polling needed.""" + return False + @property def device_info(self) -> Optional[Dict[str, Any]]: """Get a an HA device representing this Bond controlled device.""" device_info = { ATTR_NAME: self.name, "manufacturer": self._hub.make, - "identifiers": {(DOMAIN, self._hub.bond_id, self._device.device_id)}, + "identifiers": {(DOMAIN, self._hub.bond_id, self._device_id)}, "via_device": (DOMAIN, self._hub.bond_id), } if not self._hub.is_bridge: @@ -75,8 +94,29 @@ class BondEntity(Entity): async def async_update(self): """Fetch assumed state of the cover from the hub using API.""" + await self._async_update_from_api() + + async def _async_update_if_bpup_not_alive(self, *_): + """Fetch via the API if BPUP is not alive.""" + if self._bpup_subs.alive and self._initialized: + return + + if self._update_lock.locked(): + _LOGGER.warning( + "Updating %s took longer than the scheduled update interval %s", + self.entity_id, + _FALLBACK_SCAN_INTERVAL, + ) + return + + async with self._update_lock: + await self._async_update_from_api() + self.async_write_ha_state() + + async def _async_update_from_api(self): + """Fetch via the API.""" try: - state: dict = await self._hub.bond.device_state(self._device.device_id) + state: dict = await self._hub.bond.device_state(self._device_id) except (ClientError, AsyncIOTimeoutError, OSError) as error: if self._available: _LOGGER.warning( @@ -84,12 +124,42 @@ class BondEntity(Entity): ) self._available = False else: - _LOGGER.debug("Device state for %s is:\n%s", self.entity_id, state) - if not self._available: - _LOGGER.info("Entity %s has come back", self.entity_id) - self._available = True - self._apply_state(state) + self._async_state_callback(state) @abstractmethod def _apply_state(self, state: dict): raise NotImplementedError + + @callback + def _async_state_callback(self, state): + """Process a state change.""" + self._initialized = True + if not self._available: + _LOGGER.info("Entity %s has come back", self.entity_id) + self._available = True + _LOGGER.debug( + "Device state for %s (%s) is:\n%s", self.name, self.entity_id, state + ) + self._apply_state(state) + + @callback + def _async_bpup_callback(self, state): + """Process a state change from BPUP.""" + self._async_state_callback(state) + self.async_write_ha_state() + + async def async_added_to_hass(self): + """Subscribe to BPUP and start polling.""" + await super().async_added_to_hass() + self._update_lock = Lock() + self._bpup_subs.subscribe(self._device_id, self._async_bpup_callback) + self.async_on_remove( + async_track_time_interval( + self.hass, self._async_update_if_bpup_not_alive, _FALLBACK_SCAN_INTERVAL + ) + ) + + async def async_will_remove_from_hass(self) -> None: + """Unsubscribe from BPUP data on remove.""" + await super().async_will_remove_from_hass() + self._bpup_subs.unsubscribe(self._device_id, self._async_bpup_callback) diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index 18eeb912ed8..9b70195db5d 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -3,7 +3,7 @@ import logging import math from typing import Any, Callable, List, Optional, Tuple -from bond_api import Action, DeviceType, Direction +from bond_api import Action, BPUPSubscriptions, DeviceType, Direction from homeassistant.components.fan import ( DIRECTION_FORWARD, @@ -20,7 +20,7 @@ from homeassistant.util.percentage import ( ranged_value_to_percentage, ) -from .const import DOMAIN +from .const import BPUP_SUBS, DOMAIN, HUB from .entity import BondEntity from .utils import BondDevice, BondHub @@ -33,10 +33,14 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up Bond fan devices.""" - hub: BondHub = hass.data[DOMAIN][entry.entry_id] + data = hass.data[DOMAIN][entry.entry_id] + hub: BondHub = data[HUB] + bpup_subs: BPUPSubscriptions = data[BPUP_SUBS] fans = [ - BondFan(hub, device) for device in hub.devices if DeviceType.is_fan(device.type) + BondFan(hub, device, bpup_subs) + for device in hub.devices + if DeviceType.is_fan(device.type) ] async_add_entities(fans, True) @@ -45,9 +49,9 @@ async def async_setup_entry( class BondFan(BondEntity, FanEntity): """Representation of a Bond fan.""" - def __init__(self, hub: BondHub, device: BondDevice): + def __init__(self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions): """Create HA entity representing Bond fan.""" - super().__init__(hub, device) + super().__init__(hub, device, bpup_subs) self._power: Optional[bool] = None self._speed: Optional[int] = None diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py index c0809a0aee7..8d0dfe85246 100644 --- a/homeassistant/components/bond/light.py +++ b/homeassistant/components/bond/light.py @@ -2,7 +2,7 @@ import logging from typing import Any, Callable, List, Optional -from bond_api import Action, DeviceType +from bond_api import Action, BPUPSubscriptions, DeviceType from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from . import BondHub -from .const import DOMAIN +from .const import BPUP_SUBS, DOMAIN, HUB from .entity import BondEntity from .utils import BondDevice @@ -27,28 +27,30 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up Bond light devices.""" - hub: BondHub = hass.data[DOMAIN][entry.entry_id] + data = hass.data[DOMAIN][entry.entry_id] + hub: BondHub = data[HUB] + bpup_subs: BPUPSubscriptions = data[BPUP_SUBS] fan_lights: List[Entity] = [ - BondLight(hub, device) + BondLight(hub, device, bpup_subs) for device in hub.devices if DeviceType.is_fan(device.type) and device.supports_light() ] fireplaces: List[Entity] = [ - BondFireplace(hub, device) + BondFireplace(hub, device, bpup_subs) for device in hub.devices if DeviceType.is_fireplace(device.type) ] fp_lights: List[Entity] = [ - BondLight(hub, device, "light") + BondLight(hub, device, bpup_subs, "light") for device in hub.devices if DeviceType.is_fireplace(device.type) and device.supports_light() ] lights: List[Entity] = [ - BondLight(hub, device) + BondLight(hub, device, bpup_subs) for device in hub.devices if DeviceType.is_light(device.type) ] @@ -60,10 +62,14 @@ class BondLight(BondEntity, LightEntity): """Representation of a Bond light.""" def __init__( - self, hub: BondHub, device: BondDevice, sub_device: Optional[str] = None + self, + hub: BondHub, + device: BondDevice, + bpup_subs: BPUPSubscriptions, + sub_device: Optional[str] = None, ): """Create HA entity representing Bond fan.""" - super().__init__(hub, device, sub_device) + super().__init__(hub, device, bpup_subs, sub_device) self._brightness: Optional[int] = None self._light: Optional[int] = None @@ -110,9 +116,9 @@ class BondLight(BondEntity, LightEntity): class BondFireplace(BondEntity, LightEntity): """Representation of a Bond-controlled fireplace.""" - def __init__(self, hub: BondHub, device: BondDevice): + def __init__(self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions): """Create HA entity representing Bond fireplace.""" - super().__init__(hub, device) + super().__init__(hub, device, bpup_subs) self._power: Optional[bool] = None # Bond flame level, 0-100 diff --git a/homeassistant/components/bond/manifest.json b/homeassistant/components/bond/manifest.json index 3f62403dba7..e1ec5e5dd46 100644 --- a/homeassistant/components/bond/manifest.json +++ b/homeassistant/components/bond/manifest.json @@ -3,7 +3,7 @@ "name": "Bond", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bond", - "requirements": ["bond-api==0.1.8"], + "requirements": ["bond-api==0.1.9"], "zeroconf": ["_bond._tcp.local."], "codeowners": ["@prystupa"], "quality_scale": "platinum" diff --git a/homeassistant/components/bond/switch.py b/homeassistant/components/bond/switch.py index d2f1797225d..8319d31c714 100644 --- a/homeassistant/components/bond/switch.py +++ b/homeassistant/components/bond/switch.py @@ -1,14 +1,14 @@ """Support for Bond generic devices.""" from typing import Any, Callable, List, Optional -from bond_api import Action, DeviceType +from bond_api import Action, BPUPSubscriptions, DeviceType from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from .const import DOMAIN +from .const import BPUP_SUBS, DOMAIN, HUB from .entity import BondEntity from .utils import BondDevice, BondHub @@ -19,10 +19,12 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up Bond generic devices.""" - hub: BondHub = hass.data[DOMAIN][entry.entry_id] + data = hass.data[DOMAIN][entry.entry_id] + hub: BondHub = data[HUB] + bpup_subs: BPUPSubscriptions = data[BPUP_SUBS] switches = [ - BondSwitch(hub, device) + BondSwitch(hub, device, bpup_subs) for device in hub.devices if DeviceType.is_generic(device.type) ] @@ -33,9 +35,9 @@ async def async_setup_entry( class BondSwitch(BondEntity, SwitchEntity): """Representation of a Bond generic device.""" - def __init__(self, hub: BondHub, device: BondDevice): + def __init__(self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions): """Create HA entity representing Bond generic device (switch).""" - super().__init__(hub, device) + super().__init__(hub, device, bpup_subs) self._power: Optional[bool] = None diff --git a/requirements_all.txt b/requirements_all.txt index a28eee2096e..e785542768f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -367,7 +367,7 @@ blockchain==1.4.4 # bme680==1.0.5 # homeassistant.components.bond -bond-api==0.1.8 +bond-api==0.1.9 # homeassistant.components.amazon_polly # homeassistant.components.route53 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f2f022e9ffe..fd6d5800461 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -201,7 +201,7 @@ blebox_uniapi==1.3.2 blinkpy==0.16.4 # homeassistant.components.bond -bond-api==0.1.8 +bond-api==0.1.9 # homeassistant.components.braviatv bravia-tv==1.0.8 diff --git a/tests/components/bond/common.py b/tests/components/bond/common.py index 9aaaf9a249d..ba4d10c8892 100644 --- a/tests/components/bond/common.py +++ b/tests/components/bond/common.py @@ -3,7 +3,7 @@ from asyncio import TimeoutError as AsyncIOTimeoutError from contextlib import nullcontext from datetime import timedelta from typing import Any, Dict, Optional -from unittest.mock import patch +from unittest.mock import MagicMock, patch from homeassistant import core from homeassistant.components.bond.const import DOMAIN as BOND_DOMAIN @@ -33,9 +33,11 @@ async def setup_bond_entity( """Set up Bond entity.""" config_entry.add_to_hass(hass) - with patch_bond_version(enabled=patch_version), patch_bond_device_ids( - enabled=patch_device_ids - ), patch_setup_entry("cover", enabled=patch_platforms), patch_setup_entry( + with patch_start_bpup(), patch_bond_version( + enabled=patch_version + ), patch_bond_device_ids(enabled=patch_device_ids), patch_setup_entry( + "cover", enabled=patch_platforms + ), patch_setup_entry( "fan", enabled=patch_platforms ), patch_setup_entry( "light", enabled=patch_platforms @@ -65,7 +67,7 @@ async def setup_platform( with patch("homeassistant.components.bond.PLATFORMS", [platform]): with patch_bond_version(return_value=bond_version), patch_bond_device_ids( return_value=[bond_device_id] - ), patch_bond_device( + ), patch_start_bpup(), patch_bond_device( return_value=discovered_device ), patch_bond_device_properties( return_value=props @@ -118,6 +120,14 @@ def patch_bond_device(return_value=None): ) +def patch_start_bpup(): + """Patch start_bpup.""" + return patch( + "homeassistant.components.bond.start_bpup", + return_value=MagicMock(), + ) + + def patch_bond_action(): """Patch Bond API action endpoint.""" return patch("homeassistant.components.bond.Bond.action") diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index e2bb6314126..4dc7ae5c8d4 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -20,6 +20,7 @@ from .common import ( patch_bond_device_state, patch_bond_version, patch_setup_entry, + patch_start_bpup, setup_bond_entity, ) @@ -141,7 +142,7 @@ async def test_old_identifiers_are_removed(hass: HomeAssistant): "target": "test-model", "fw_ver": "test-version", } - ), patch_bond_device_ids( + ), patch_start_bpup(), patch_bond_device_ids( return_value=["bond-device-id", "device_id"] ), patch_bond_device( return_value={ From f27066e77369d09563d24f835ee8e03c2267ffd6 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Tue, 9 Feb 2021 09:46:36 +0100 Subject: [PATCH 302/796] Raise ConditionError for state errors (#46244) --- .../components/bayesian/binary_sensor.py | 7 +++- homeassistant/helpers/condition.py | 15 ++++++-- tests/helpers/test_condition.py | 36 +++++++++++++++++-- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 15176d45349..69553e921eb 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -356,7 +356,12 @@ class BayesianBinarySensor(BinarySensorEntity): """Return True if state conditions are met.""" entity = entity_observation["entity_id"] - return condition.state(self.hass, entity, entity_observation.get("to_state")) + try: + return condition.state( + self.hass, entity, entity_observation.get("to_state") + ) + except ConditionError: + return False @property def name(self): diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index e47374a9d17..126513608c7 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -314,11 +314,22 @@ def state( Async friendly. """ + if entity is None: + raise ConditionError("No entity specified") + if isinstance(entity, str): + entity_id = entity entity = hass.states.get(entity) - if entity is None or (attribute is not None and attribute not in entity.attributes): - return False + if entity is None: + raise ConditionError(f"Unknown entity {entity_id}") + else: + entity_id = entity.entity_id + + if attribute is not None and attribute not in entity.attributes: + raise ConditionError( + f"Attribute '{attribute}' (of entity {entity_id}) does not exist" + ) assert isinstance(entity, State) diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index cd4039f5262..485c51a8bb7 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -359,6 +359,37 @@ async def test_if_numeric_state_raises_on_unavailable(hass, caplog): assert len(caplog.record_tuples) == 0 +async def test_state_raises(hass): + """Test that state raises ConditionError on errors.""" + # Unknown entity_id + with pytest.raises(ConditionError, match="Unknown entity"): + test = await condition.async_from_config( + hass, + { + "condition": "state", + "entity_id": "sensor.door_unknown", + "state": "open", + }, + ) + + test(hass) + + # Unknown attribute + with pytest.raises(ConditionError, match=r"Attribute .* does not exist"): + test = await condition.async_from_config( + hass, + { + "condition": "state", + "entity_id": "sensor.door", + "attribute": "model", + "state": "acme", + }, + ) + + hass.states.async_set("sensor.door", "open") + test(hass) + + async def test_state_multiple_entities(hass): """Test with multiple entities in condition.""" test = await condition.async_from_config( @@ -466,7 +497,8 @@ async def test_state_attribute_boolean(hass): assert not test(hass) hass.states.async_set("sensor.temperature", 100, {"no_happening": 201}) - assert not test(hass) + with pytest.raises(ConditionError): + test(hass) hass.states.async_set("sensor.temperature", 100, {"happening": False}) assert test(hass) @@ -567,7 +599,7 @@ async def test_numeric_state_raises(hass): }, ) - assert test(hass) + test(hass) # Unknown attribute with pytest.raises(ConditionError, match=r"Attribute .* does not exist"): From da67cde369a52b6461189c2a6e4f6d7dddc191ab Mon Sep 17 00:00:00 2001 From: tkdrob Date: Tue, 9 Feb 2021 06:02:53 -0500 Subject: [PATCH 303/796] Use core constants for homematic (#46248) --- homeassistant/components/homematic/__init__.py | 7 +++---- homeassistant/components/homematic/const.py | 3 --- homeassistant/components/homematic/entity.py | 1 - 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 1f727bab4e1..6df738037bf 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -10,10 +10,13 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, ATTR_NAME, + ATTR_TIME, CONF_HOST, CONF_HOSTS, CONF_PASSWORD, + CONF_PATH, CONF_PLATFORM, + CONF_PORT, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, @@ -37,7 +40,6 @@ from .const import ( ATTR_PARAMSET, ATTR_PARAMSET_KEY, ATTR_RX_MODE, - ATTR_TIME, ATTR_UNIQUE_ID, ATTR_VALUE, ATTR_VALUE_TYPE, @@ -47,8 +49,6 @@ from .const import ( CONF_JSONPORT, CONF_LOCAL_IP, CONF_LOCAL_PORT, - CONF_PATH, - CONF_PORT, CONF_RESOLVENAMES, CONF_RESOLVENAMES_OPTIONS, DATA_CONF, @@ -209,7 +209,6 @@ SCHEMA_SERVICE_PUT_PARAMSET = vol.Schema( def setup(hass, config): """Set up the Homematic component.""" - conf = config[DOMAIN] hass.data[DATA_CONF] = remotes = {} hass.data[DATA_STORE] = set() diff --git a/homeassistant/components/homematic/const.py b/homeassistant/components/homematic/const.py index a6ff19a6eea..e8fa272b0e5 100644 --- a/homeassistant/components/homematic/const.py +++ b/homeassistant/components/homematic/const.py @@ -21,7 +21,6 @@ ATTR_VALUE_TYPE = "value_type" ATTR_INTERFACE = "interface" ATTR_ERRORCODE = "error" ATTR_MESSAGE = "message" -ATTR_TIME = "time" ATTR_UNIQUE_ID = "unique_id" ATTR_PARAMSET_KEY = "paramset_key" ATTR_PARAMSET = "paramset" @@ -232,8 +231,6 @@ DATA_CONF = "homematic_conf" CONF_INTERFACES = "interfaces" CONF_LOCAL_IP = "local_ip" CONF_LOCAL_PORT = "local_port" -CONF_PORT = "port" -CONF_PATH = "path" CONF_CALLBACK_IP = "callback_ip" CONF_CALLBACK_PORT = "callback_port" CONF_RESOLVENAMES = "resolvenames" diff --git a/homeassistant/components/homematic/entity.py b/homeassistant/components/homematic/entity.py index a391fa80461..bb87d691fc0 100644 --- a/homeassistant/components/homematic/entity.py +++ b/homeassistant/components/homematic/entity.py @@ -73,7 +73,6 @@ class HMDevice(Entity): @property def device_state_attributes(self): """Return device specific state attributes.""" - # Static attributes attr = { "id": self._hmdevice.ADDRESS, From c69c493cf963c913a507c7bd0c02804cdb6e8e1e Mon Sep 17 00:00:00 2001 From: tkdrob Date: Tue, 9 Feb 2021 08:03:14 -0500 Subject: [PATCH 304/796] Use core constants for image_processing (#46269) --- homeassistant/components/image_processing/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 82672c22015..261278da401 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -5,7 +5,13 @@ import logging import voluptuous as vol -from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, CONF_ENTITY_ID, CONF_NAME +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_NAME, + CONF_ENTITY_ID, + CONF_NAME, + CONF_SOURCE, +) from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -39,7 +45,6 @@ ATTR_GLASSES = "glasses" ATTR_MOTION = "motion" ATTR_TOTAL_FACES = "total_faces" -CONF_SOURCE = "source" CONF_CONFIDENCE = "confidence" DEFAULT_TIMEOUT = 10 From f46dc3c48e3af445ad9c266ef7d5b59964ed722e Mon Sep 17 00:00:00 2001 From: tkdrob Date: Tue, 9 Feb 2021 14:20:20 -0500 Subject: [PATCH 305/796] Use core constants for elkm1 (#46091) --- homeassistant/components/elkm1/__init__.py | 3 +-- homeassistant/components/elkm1/config_flow.py | 4 ++-- homeassistant/components/elkm1/const.py | 4 +--- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index e33e1722edf..d50c5d65d90 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_HOST, CONF_INCLUDE, CONF_PASSWORD, + CONF_PREFIX, CONF_TEMPERATURE_UNIT, CONF_USERNAME, TEMP_CELSIUS, @@ -38,7 +39,6 @@ from .const import ( CONF_KEYPAD, CONF_OUTPUT, CONF_PLC, - CONF_PREFIX, CONF_SETTING, CONF_TASK, CONF_THERMOSTAT, @@ -197,7 +197,6 @@ def _async_find_matching_config_entry(hass, prefix): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Elk-M1 Control from a config entry.""" - conf = entry.data _LOGGER.debug("Setting up elkm1 %s", conf["host"]) diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index 0248025795b..b72cfa19335 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -11,6 +11,7 @@ from homeassistant.const import ( CONF_ADDRESS, CONF_HOST, CONF_PASSWORD, + CONF_PREFIX, CONF_PROTOCOL, CONF_TEMPERATURE_UNIT, CONF_USERNAME, @@ -20,7 +21,7 @@ from homeassistant.const import ( from homeassistant.util import slugify from . import async_wait_for_elk_to_sync -from .const import CONF_AUTO_CONFIGURE, CONF_PREFIX +from .const import CONF_AUTO_CONFIGURE from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) @@ -50,7 +51,6 @@ async def validate_input(data): Data has the keys from DATA_SCHEMA with values provided by the user. """ - userid = data.get(CONF_USERNAME) password = data.get(CONF_PASSWORD) diff --git a/homeassistant/components/elkm1/const.py b/homeassistant/components/elkm1/const.py index 71646582c99..4d2dac4b1de 100644 --- a/homeassistant/components/elkm1/const.py +++ b/homeassistant/components/elkm1/const.py @@ -3,7 +3,7 @@ from elkm1_lib.const import Max import voluptuous as vol -from homeassistant.const import ATTR_CODE +from homeassistant.const import ATTR_CODE, CONF_ZONE DOMAIN = "elkm1" @@ -17,8 +17,6 @@ CONF_PLC = "plc" CONF_SETTING = "setting" CONF_TASK = "task" CONF_THERMOSTAT = "thermostat" -CONF_ZONE = "zone" -CONF_PREFIX = "prefix" BARE_TEMP_FAHRENHEIT = "F" From 57ce18295959de1b6a2f661c265499056a3c7c41 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Tue, 9 Feb 2021 14:21:04 -0500 Subject: [PATCH 306/796] Remove unnecessary constant from ihc (#46268) --- homeassistant/components/ihc/__init__.py | 2 -- homeassistant/components/ihc/const.py | 1 - 2 files changed, 3 deletions(-) diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py index 8769f73e365..c539156b759 100644 --- a/homeassistant/components/ihc/__init__.py +++ b/homeassistant/components/ihc/__init__.py @@ -230,7 +230,6 @@ def setup(hass, config): def ihc_setup(hass, config, conf, controller_id): """Set up the IHC component.""" - url = conf[CONF_URL] username = conf[CONF_USERNAME] password = conf[CONF_PASSWORD] @@ -289,7 +288,6 @@ def autosetup_ihc_products( hass: HomeAssistantType, config, ihc_controller, controller_id ): """Auto setup of IHC products from the IHC project file.""" - project_xml = ihc_controller.get_project() if not project_xml: _LOGGER.error("Unable to read project from IHC controller") diff --git a/homeassistant/components/ihc/const.py b/homeassistant/components/ihc/const.py index 30103e2bdba..c751d7990e4 100644 --- a/homeassistant/components/ihc/const.py +++ b/homeassistant/components/ihc/const.py @@ -6,7 +6,6 @@ CONF_DIMMABLE = "dimmable" CONF_INFO = "info" CONF_INVERTING = "inverting" CONF_LIGHT = "light" -CONF_NAME = "name" CONF_NODE = "node" CONF_NOTE = "note" CONF_OFF_ID = "off_id" From 1c1b2f497a9a212efed2facd2e8ce4ca349ec10a Mon Sep 17 00:00:00 2001 From: bsmappee <58250533+bsmappee@users.noreply.github.com> Date: Tue, 9 Feb 2021 20:21:51 +0100 Subject: [PATCH 307/796] bump pysmappee (#46270) --- homeassistant/components/smappee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smappee/manifest.json b/homeassistant/components/smappee/manifest.json index ddbff4e7738..a6dda75ac72 100644 --- a/homeassistant/components/smappee/manifest.json +++ b/homeassistant/components/smappee/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/smappee", "dependencies": ["http"], "requirements": [ - "pysmappee==0.2.16" + "pysmappee==0.2.17" ], "codeowners": [ "@bsmappee" diff --git a/requirements_all.txt b/requirements_all.txt index e785542768f..608887a8bde 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1693,7 +1693,7 @@ pyskyqhub==0.1.3 pysma==0.3.5 # homeassistant.components.smappee -pysmappee==0.2.16 +pysmappee==0.2.17 # homeassistant.components.smartthings pysmartapp==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd6d5800461..259f33e01f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -884,7 +884,7 @@ pysignalclirestapi==0.3.4 pysma==0.3.5 # homeassistant.components.smappee -pysmappee==0.2.16 +pysmappee==0.2.17 # homeassistant.components.smartthings pysmartapp==0.3.3 From a26cf7aeec58422fae1873d3f8610fd4b8074176 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Tue, 9 Feb 2021 14:23:02 -0500 Subject: [PATCH 308/796] Remove unnecessary variable definition in firmata (#46172) --- homeassistant/components/firmata/const.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/firmata/const.py b/homeassistant/components/firmata/const.py index 6259582b5f7..0d859363e2b 100644 --- a/homeassistant/components/firmata/const.py +++ b/homeassistant/components/firmata/const.py @@ -10,7 +10,6 @@ CONF_ARDUINO_INSTANCE_ID = "arduino_instance_id" CONF_ARDUINO_WAIT = "arduino_wait" CONF_DIFFERENTIAL = "differential" CONF_INITIAL_STATE = "initial" -CONF_NAME = "name" CONF_NEGATE_STATE = "negate" CONF_PINS = "pins" CONF_PIN_MODE = "pin_mode" From 6f4cb18fa88d357eb873101b25159c989c0f9125 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Tue, 9 Feb 2021 14:23:46 -0500 Subject: [PATCH 309/796] Use core constants for here_travel_time (#46246) --- homeassistant/components/here_travel_time/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index afc6534d0c6..e51e7a067fc 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_MODE, + CONF_API_KEY, CONF_MODE, CONF_NAME, CONF_UNIT_SYSTEM, @@ -35,7 +36,6 @@ CONF_DESTINATION_ENTITY_ID = "destination_entity_id" CONF_ORIGIN_LATITUDE = "origin_latitude" CONF_ORIGIN_LONGITUDE = "origin_longitude" CONF_ORIGIN_ENTITY_ID = "origin_entity_id" -CONF_API_KEY = "api_key" CONF_TRAFFIC_MODE = "traffic_mode" CONF_ROUTE_MODE = "route_mode" CONF_ARRIVAL = "arrival" @@ -148,7 +148,6 @@ async def async_setup_platform( discovery_info: Optional[DiscoveryInfoType] = None, ) -> None: """Set up the HERE travel time platform.""" - api_key = config[CONF_API_KEY] here_client = herepy.RoutingApi(api_key) From 3381e2f65a0cd5e8e4456bdc21d4703560445b7b Mon Sep 17 00:00:00 2001 From: Khole Date: Tue, 9 Feb 2021 21:03:49 +0000 Subject: [PATCH 310/796] Convert Hive to Async (#46117) * Convert Hive to Async * Update Refresh System * Update load platform to Async * Changes from review feedback * Review Round 2 * Updated service * Updated dict keys * Convert Hive to Async * Update Refresh System * Update load platform to Async * Changes from review feedback * Review Round 2 * Updated service * Updated dict keys * Convert Hive to Async * Update Refresh System * Update load platform to Async * Changes from review feedback * Review Round 2 * Updated service * Updated dict keys * Updated Refresh System --- homeassistant/components/hive/__init__.py | 101 ++++++++---------- .../components/hive/binary_sensor.py | 53 ++++++--- homeassistant/components/hive/climate.py | 87 +++++++-------- homeassistant/components/hive/light.py | 100 +++++++++-------- homeassistant/components/hive/manifest.json | 11 +- homeassistant/components/hive/sensor.py | 78 ++++++++------ homeassistant/components/hive/switch.py | 55 ++++++---- homeassistant/components/hive/water_heater.py | 65 ++++++++--- requirements_all.txt | 2 +- 9 files changed, 308 insertions(+), 244 deletions(-) diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 98d625cbb1d..6245db5ea7e 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -2,7 +2,7 @@ from functools import wraps import logging -from pyhiveapi import Pyhiveapi +from pyhiveapi import Hive import voluptuous as vol from homeassistant.const import ( @@ -13,12 +13,17 @@ from homeassistant.const import ( CONF_USERNAME, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) +ATTR_AVAILABLE = "available" +ATTR_MODE = "mode" DOMAIN = "hive" DATA_HIVE = "data_hive" SERVICES = ["Heating", "HotWater", "TRV"] @@ -69,28 +74,15 @@ BOOST_HOT_WATER_SCHEMA = vol.Schema( ) -class HiveSession: - """Initiate Hive Session Class.""" - - entity_lookup = {} - core = None - heating = None - hotwater = None - light = None - sensor = None - switch = None - weather = None - attributes = None - trv = None - - -def setup(hass, config): +async def async_setup(hass, config): """Set up the Hive Component.""" - def heating_boost(service): + async def heating_boost(service): """Handle the service call.""" - node_id = HiveSession.entity_lookup.get(service.data[ATTR_ENTITY_ID]) - if not node_id: + + entity_lookup = hass.data[DOMAIN]["entity_lookup"] + hive_id = entity_lookup.get(service.data[ATTR_ENTITY_ID]) + if not hive_id: # log or raise error _LOGGER.error("Cannot boost entity id entered") return @@ -98,12 +90,13 @@ def setup(hass, config): minutes = service.data[ATTR_TIME_PERIOD] temperature = service.data[ATTR_TEMPERATURE] - session.heating.turn_boost_on(node_id, minutes, temperature) + hive.heating.turn_boost_on(hive_id, minutes, temperature) - def hot_water_boost(service): + async def hot_water_boost(service): """Handle the service call.""" - node_id = HiveSession.entity_lookup.get(service.data[ATTR_ENTITY_ID]) - if not node_id: + entity_lookup = hass.data[DOMAIN]["entity_lookup"] + hive_id = entity_lookup.get(service.data[ATTR_ENTITY_ID]) + if not hive_id: # log or raise error _LOGGER.error("Cannot boost entity id entered") return @@ -111,45 +104,41 @@ def setup(hass, config): mode = service.data[ATTR_MODE] if mode == "on": - session.hotwater.turn_boost_on(node_id, minutes) + hive.hotwater.turn_boost_on(hive_id, minutes) elif mode == "off": - session.hotwater.turn_boost_off(node_id) + hive.hotwater.turn_boost_off(hive_id) - session = HiveSession() - session.core = Pyhiveapi() + hive = Hive() - username = config[DOMAIN][CONF_USERNAME] - password = config[DOMAIN][CONF_PASSWORD] - update_interval = config[DOMAIN][CONF_SCAN_INTERVAL] + config = {} + config["username"] = config[DOMAIN][CONF_USERNAME] + config["password"] = config[DOMAIN][CONF_PASSWORD] + config["update_interval"] = config[DOMAIN][CONF_SCAN_INTERVAL] - devices = session.core.initialise_api(username, password, update_interval) + devices = await hive.session.startSession(config) if devices is None: _LOGGER.error("Hive API initialization failed") return False - session.sensor = Pyhiveapi.Sensor() - session.heating = Pyhiveapi.Heating() - session.hotwater = Pyhiveapi.Hotwater() - session.light = Pyhiveapi.Light() - session.switch = Pyhiveapi.Switch() - session.weather = Pyhiveapi.Weather() - session.attributes = Pyhiveapi.Attributes() - hass.data[DATA_HIVE] = session + hass.data[DOMAIN][DATA_HIVE] = hive + hass.data[DOMAIN]["entity_lookup"] = {} for ha_type in DEVICETYPES: devicelist = devices.get(DEVICETYPES[ha_type]) if devicelist: - load_platform(hass, ha_type, DOMAIN, devicelist, config) + hass.async_create_task( + async_load_platform(hass, ha_type, DOMAIN, devicelist, config) + ) if ha_type == "climate": - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_BOOST_HEATING, heating_boost, schema=BOOST_HEATING_SCHEMA, ) if ha_type == "water_heater": - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_BOOST_HOT_WATER, hot_water_boost, @@ -163,9 +152,9 @@ def refresh_system(func): """Force update all entities after state change.""" @wraps(func) - def wrapper(self, *args, **kwargs): - func(self, *args, **kwargs) - dispatcher_send(self.hass, DOMAIN) + async def wrapper(self, *args, **kwargs): + await func(self, *args, **kwargs) + async_dispatcher_send(self.hass, DOMAIN) return wrapper @@ -173,20 +162,18 @@ def refresh_system(func): class HiveEntity(Entity): """Initiate Hive Base Class.""" - def __init__(self, session, hive_device): + def __init__(self, hive, hive_device): """Initialize the instance.""" - self.node_id = hive_device["Hive_NodeID"] - self.node_name = hive_device["Hive_NodeName"] - self.device_type = hive_device["HA_DeviceType"] - self.node_device_type = hive_device["Hive_DeviceType"] - self.session = session + self.hive = hive + self.device = hive_device self.attributes = {} - self._unique_id = f"{self.node_id}-{self.device_type}" + self._unique_id = f'{self.device["hiveID"]}-{self.device["hiveType"]}' async def async_added_to_hass(self): """When entity is added to Home Assistant.""" self.async_on_remove( async_dispatcher_connect(self.hass, DOMAIN, self.async_write_ha_state) ) - if self.device_type in SERVICES: - self.session.entity_lookup[self.entity_id] = self.node_id + if self.device["hiveType"] in SERVICES: + entity_lookup = self.hass.data[DOMAIN]["entity_lookup"] + entity_lookup[self.entity_id] = self.device["hiveID"] diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index 120148a8f81..30e5ae049f0 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -1,28 +1,36 @@ """Support for the Hive binary sensors.""" +from datetime import timedelta + from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_MOTION, DEVICE_CLASS_OPENING, BinarySensorEntity, ) -from . import DATA_HIVE, DOMAIN, HiveEntity +from . import ATTR_AVAILABLE, ATTR_MODE, DATA_HIVE, DOMAIN, HiveEntity -DEVICETYPE_DEVICE_CLASS = { - "motionsensor": DEVICE_CLASS_MOTION, +DEVICETYPE = { "contactsensor": DEVICE_CLASS_OPENING, + "motionsensor": DEVICE_CLASS_MOTION, + "Connectivity": DEVICE_CLASS_CONNECTIVITY, } +PARALLEL_UPDATES = 0 +SCAN_INTERVAL = timedelta(seconds=15) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Hive sensor devices.""" +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Hive Binary Sensor.""" if discovery_info is None: return - session = hass.data.get(DATA_HIVE) - devs = [] - for dev in discovery_info: - devs.append(HiveBinarySensorEntity(session, dev)) - add_entities(devs) + hive = hass.data[DOMAIN].get(DATA_HIVE) + devices = hive.devices.get("binary_sensor") + entities = [] + if devices: + for dev in devices: + entities.append(HiveBinarySensorEntity(hive, dev)) + async_add_entities(entities, True) class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity): @@ -41,24 +49,35 @@ class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity): @property def device_class(self): """Return the class of this sensor.""" - return DEVICETYPE_DEVICE_CLASS.get(self.node_device_type) + return DEVICETYPE.get(self.device["hiveType"]) @property def name(self): """Return the name of the binary sensor.""" - return self.node_name + return self.device["haName"] + + @property + def available(self): + """Return if the device is available.""" + if self.device["hiveType"] != "Connectivity": + return self.device["deviceData"]["online"] + return True @property def device_state_attributes(self): """Show Device Attributes.""" - return self.attributes + return { + ATTR_AVAILABLE: self.attributes.get(ATTR_AVAILABLE), + ATTR_MODE: self.attributes.get(ATTR_MODE), + } @property def is_on(self): """Return true if the binary sensor is on.""" - return self.session.sensor.get_state(self.node_id, self.node_device_type) + return self.device["status"]["state"] - def update(self): + async def async_update(self): """Update all Node data from Hive.""" - self.session.core.update_data(self.node_id) - self.attributes = self.session.attributes.state_attributes(self.node_id) + await self.hive.session.updateData(self.device) + self.device = await self.hive.sensor.get_sensor(self.device) + self.attributes = self.device.get("attributes", {}) diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index 33c8fed4eca..f1901147f17 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -1,4 +1,6 @@ """Support for the Hive climate devices.""" +from datetime import timedelta + from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, @@ -12,9 +14,9 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from . import DATA_HIVE, DOMAIN, HiveEntity, refresh_system +from . import ATTR_AVAILABLE, DATA_HIVE, DOMAIN, HiveEntity, refresh_system HIVE_TO_HASS_STATE = { "SCHEDULE": HVAC_MODE_AUTO, @@ -34,21 +36,27 @@ HIVE_TO_HASS_HVAC_ACTION = { True: CURRENT_HVAC_HEAT, } +TEMP_UNIT = {"C": TEMP_CELSIUS, "F": TEMP_FAHRENHEIT} + SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE SUPPORT_HVAC = [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF] SUPPORT_PRESET = [PRESET_NONE, PRESET_BOOST] +PARALLEL_UPDATES = 0 +SCAN_INTERVAL = timedelta(seconds=15) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Hive climate devices.""" +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Hive thermostat.""" if discovery_info is None: return - session = hass.data.get(DATA_HIVE) - devs = [] - for dev in discovery_info: - devs.append(HiveClimateEntity(session, dev)) - add_entities(devs) + hive = hass.data[DOMAIN].get(DATA_HIVE) + devices = hive.devices.get("climate") + entities = [] + if devices: + for dev in devices: + entities.append(HiveClimateEntity(hive, dev)) + async_add_entities(entities, True) class HiveClimateEntity(HiveEntity, ClimateEntity): @@ -57,7 +65,8 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): def __init__(self, hive_session, hive_device): """Initialize the Climate device.""" super().__init__(hive_session, hive_device) - self.thermostat_node_id = hive_device["Thermostat_NodeID"] + self.thermostat_node_id = hive_device["device_id"] + self.temperature_type = TEMP_UNIT.get(hive_device["temperatureunit"]) @property def unique_id(self): @@ -77,19 +86,17 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): @property def name(self): """Return the name of the Climate device.""" - friendly_name = "Heating" - if self.node_name is not None: - if self.device_type == "TRV": - friendly_name = self.node_name - else: - friendly_name = f"{self.node_name} {friendly_name}" + return self.device["haName"] - return friendly_name + @property + def available(self): + """Return if the device is available.""" + return self.device["deviceData"]["online"] @property def device_state_attributes(self): """Show Device Attributes.""" - return self.attributes + return {ATTR_AVAILABLE: self.attributes.get(ATTR_AVAILABLE)} @property def hvac_modes(self): @@ -105,47 +112,42 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): Need to be one of HVAC_MODE_*. """ - return HIVE_TO_HASS_STATE[self.session.heating.get_mode(self.node_id)] + return HIVE_TO_HASS_STATE[self.device["status"]["mode"]] @property def hvac_action(self): """Return current HVAC action.""" - return HIVE_TO_HASS_HVAC_ACTION[ - self.session.heating.operational_status(self.node_id, self.device_type) - ] + return HIVE_TO_HASS_HVAC_ACTION[self.device["status"]["action"]] @property def temperature_unit(self): """Return the unit of measurement.""" - return TEMP_CELSIUS + return self.temperature_type @property def current_temperature(self): """Return the current temperature.""" - return self.session.heating.current_temperature(self.node_id) + return self.device["status"]["current_temperature"] @property def target_temperature(self): """Return the target temperature.""" - return self.session.heating.get_target_temperature(self.node_id) + return self.device["status"]["target_temperature"] @property def min_temp(self): """Return minimum temperature.""" - return self.session.heating.min_temperature(self.node_id) + return self.device["min_temp"] @property def max_temp(self): """Return the maximum temperature.""" - return self.session.heating.max_temperature(self.node_id) + return self.device["max_temp"] @property def preset_mode(self): """Return the current preset mode, e.g., home, away, temp.""" - if ( - self.device_type == "Heating" - and self.session.heating.get_boost(self.node_id) == "ON" - ): + if self.device["status"]["boost"] == "ON": return PRESET_BOOST return None @@ -155,31 +157,30 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): return SUPPORT_PRESET @refresh_system - def set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" new_mode = HASS_TO_HIVE_STATE[hvac_mode] - self.session.heating.set_mode(self.node_id, new_mode) + await self.hive.heating.set_mode(self.device, new_mode) @refresh_system - def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set new target temperature.""" new_temperature = kwargs.get(ATTR_TEMPERATURE) if new_temperature is not None: - self.session.heating.set_target_temperature(self.node_id, new_temperature) + await self.hive.heating.set_target_temperature(self.device, new_temperature) @refresh_system - def set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode): """Set new preset mode.""" if preset_mode == PRESET_NONE and self.preset_mode == PRESET_BOOST: - self.session.heating.turn_boost_off(self.node_id) + await self.hive.heating.turn_boost_off(self.device) elif preset_mode == PRESET_BOOST: curtemp = round(self.current_temperature * 2) / 2 temperature = curtemp + 0.5 - self.session.heating.turn_boost_on(self.node_id, 30, temperature) + await self.hive.heating.turn_boost_on(self.device, 30, temperature) - def update(self): + async def async_update(self): """Update all Node data from Hive.""" - self.session.core.update_data(self.node_id) - self.attributes = self.session.attributes.state_attributes( - self.thermostat_node_id - ) + await self.hive.session.updateData(self.device) + self.device = await self.hive.heating.get_heating(self.device) + self.attributes.update(self.device.get("attributes", {})) diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index 7659d43aeba..f458c27d019 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -1,4 +1,6 @@ -"""Support for the Hive lights.""" +"""Support for Hive light devices.""" +from datetime import timedelta + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -10,29 +12,29 @@ from homeassistant.components.light import ( ) import homeassistant.util.color as color_util -from . import DATA_HIVE, DOMAIN, HiveEntity, refresh_system +from . import ATTR_AVAILABLE, ATTR_MODE, DATA_HIVE, DOMAIN, HiveEntity, refresh_system + +PARALLEL_UPDATES = 0 +SCAN_INTERVAL = timedelta(seconds=15) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Hive light devices.""" +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Hive Light.""" if discovery_info is None: return - session = hass.data.get(DATA_HIVE) - devs = [] - for dev in discovery_info: - devs.append(HiveDeviceLight(session, dev)) - add_entities(devs) + hive = hass.data[DOMAIN].get(DATA_HIVE) + devices = hive.devices.get("light") + entities = [] + if devices: + for dev in devices: + entities.append(HiveDeviceLight(hive, dev)) + async_add_entities(entities, True) class HiveDeviceLight(HiveEntity, LightEntity): """Hive Active Light Device.""" - def __init__(self, hive_session, hive_device): - """Initialize the Light device.""" - super().__init__(hive_session, hive_device) - self.light_device_type = hive_device["Hive_Light_DeviceType"] - @property def unique_id(self): """Return unique ID of entity.""" @@ -46,59 +48,56 @@ class HiveDeviceLight(HiveEntity, LightEntity): @property def name(self): """Return the display name of this light.""" - return self.node_name + return self.device["haName"] + + @property + def available(self): + """Return if the device is available.""" + return self.device["deviceData"]["online"] @property def device_state_attributes(self): """Show Device Attributes.""" - return self.attributes + return { + ATTR_AVAILABLE: self.attributes.get(ATTR_AVAILABLE), + ATTR_MODE: self.attributes.get(ATTR_MODE), + } @property def brightness(self): """Brightness of the light (an integer in the range 1-255).""" - return self.session.light.get_brightness(self.node_id) + return self.device["status"]["brightness"] @property def min_mireds(self): """Return the coldest color_temp that this light supports.""" - if ( - self.light_device_type == "tuneablelight" - or self.light_device_type == "colourtuneablelight" - ): - return self.session.light.get_min_color_temp(self.node_id) + return self.device.get("min_mireds") @property def max_mireds(self): """Return the warmest color_temp that this light supports.""" - if ( - self.light_device_type == "tuneablelight" - or self.light_device_type == "colourtuneablelight" - ): - return self.session.light.get_max_color_temp(self.node_id) + return self.device.get("max_mireds") @property def color_temp(self): """Return the CT color value in mireds.""" - if ( - self.light_device_type == "tuneablelight" - or self.light_device_type == "colourtuneablelight" - ): - return self.session.light.get_color_temp(self.node_id) + return self.device["status"].get("color_temp") @property - def hs_color(self) -> tuple: + def hs_color(self): """Return the hs color value.""" - if self.light_device_type == "colourtuneablelight": - rgb = self.session.light.get_color(self.node_id) + if self.device["status"]["mode"] == "COLOUR": + rgb = self.device["status"].get("hs_color") return color_util.color_RGB_to_hs(*rgb) + return None @property def is_on(self): """Return true if light is on.""" - return self.session.light.get_state(self.node_id) + return self.device["status"]["state"] @refresh_system - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Instruct the light to turn on.""" new_brightness = None new_color_temp = None @@ -116,35 +115,32 @@ class HiveDeviceLight(HiveEntity, LightEntity): get_new_color = kwargs.get(ATTR_HS_COLOR) hue = int(get_new_color[0]) saturation = int(get_new_color[1]) - new_color = (hue, saturation, self.brightness) + new_color = (hue, saturation, 100) - self.session.light.turn_on( - self.node_id, - self.light_device_type, - new_brightness, - new_color_temp, - new_color, + await self.hive.light.turn_on( + self.device, new_brightness, new_color_temp, new_color ) @refresh_system - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Instruct the light to turn off.""" - self.session.light.turn_off(self.node_id) + await self.hive.light.turn_off(self.device) @property def supported_features(self): """Flag supported features.""" supported_features = None - if self.light_device_type == "warmwhitelight": + if self.device["hiveType"] == "warmwhitelight": supported_features = SUPPORT_BRIGHTNESS - elif self.light_device_type == "tuneablelight": + elif self.device["hiveType"] == "tuneablelight": supported_features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP - elif self.light_device_type == "colourtuneablelight": + elif self.device["hiveType"] == "colourtuneablelight": supported_features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR return supported_features - def update(self): + async def async_update(self): """Update all Node data from Hive.""" - self.session.core.update_data(self.node_id) - self.attributes = self.session.attributes.state_attributes(self.node_id) + await self.hive.session.updateData(self.device) + self.device = await self.hive.light.get_light(self.device) + self.attributes.update(self.device.get("attributes", {})) diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index f8fb9bc8c2a..27f235949bf 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -2,6 +2,11 @@ "domain": "hive", "name": "Hive", "documentation": "https://www.home-assistant.io/integrations/hive", - "requirements": ["pyhiveapi==0.2.20.2"], - "codeowners": ["@Rendili", "@KJonline"] -} + "requirements": [ + "pyhiveapi==0.3.4.4" + ], + "codeowners": [ + "@Rendili", + "@KJonline" + ] +} \ No newline at end of file diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index 360fb61bfbe..e828dff9b4e 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -1,31 +1,32 @@ -"""Support for the Hive sensors.""" -from homeassistant.const import TEMP_CELSIUS +"""Support for the Hive sesnors.""" + +from datetime import timedelta + +from homeassistant.components.sensor import DEVICE_CLASS_BATTERY from homeassistant.helpers.entity import Entity -from . import DATA_HIVE, DOMAIN, HiveEntity +from . import ATTR_AVAILABLE, DATA_HIVE, DOMAIN, HiveEntity -FRIENDLY_NAMES = { - "Hub_OnlineStatus": "Hive Hub Status", - "Hive_OutsideTemperature": "Outside Temperature", -} - -DEVICETYPE_ICONS = { - "Hub_OnlineStatus": "mdi:switch", - "Hive_OutsideTemperature": "mdi:thermometer", +PARALLEL_UPDATES = 0 +SCAN_INTERVAL = timedelta(seconds=15) +DEVICETYPE = { + "Battery": {"unit": " % ", "type": DEVICE_CLASS_BATTERY}, } -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Hive sensor devices.""" +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Hive Sensor.""" if discovery_info is None: return - session = hass.data.get(DATA_HIVE) - devs = [] - for dev in discovery_info: - if dev["HA_DeviceType"] in FRIENDLY_NAMES: - devs.append(HiveSensorEntity(session, dev)) - add_entities(devs) + hive = hass.data[DOMAIN].get(DATA_HIVE) + devices = hive.devices.get("sensor") + entities = [] + if devices: + for dev in devices: + if dev["hiveType"] in DEVICETYPE: + entities.append(HiveSensorEntity(hive, dev)) + async_add_entities(entities, True) class HiveSensorEntity(HiveEntity, Entity): @@ -42,29 +43,36 @@ class HiveSensorEntity(HiveEntity, Entity): return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} @property - def name(self): - """Return the name of the sensor.""" - return FRIENDLY_NAMES.get(self.device_type) + def available(self): + """Return if sensor is available.""" + return self.device.get("deviceData", {}).get("online") @property - def state(self): - """Return the state of the sensor.""" - if self.device_type == "Hub_OnlineStatus": - return self.session.sensor.hub_online_status(self.node_id) - if self.device_type == "Hive_OutsideTemperature": - return self.session.weather.temperature() + def device_class(self): + """Device class of the entity.""" + return DEVICETYPE[self.device["hiveType"]].get("type") @property def unit_of_measurement(self): """Return the unit of measurement.""" - if self.device_type == "Hive_OutsideTemperature": - return TEMP_CELSIUS + return DEVICETYPE[self.device["hiveType"]].get("unit") @property - def icon(self): - """Return the icon to use.""" - return DEVICETYPE_ICONS.get(self.device_type) + def name(self): + """Return the name of the sensor.""" + return self.device["haName"] - def update(self): + @property + def state(self): + """Return the state of the sensor.""" + return self.device["status"]["state"] + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return {ATTR_AVAILABLE: self.attributes.get(ATTR_AVAILABLE)} + + async def async_update(self): """Update all Node data from Hive.""" - self.session.core.update_data(self.node_id) + await self.hive.session.updateData(self.device) + self.device = await self.hive.sensor.get_sensor(self.device) diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index 734581b0db3..8ab820589cf 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -1,19 +1,26 @@ """Support for the Hive switches.""" +from datetime import timedelta + from homeassistant.components.switch import SwitchEntity -from . import DATA_HIVE, DOMAIN, HiveEntity, refresh_system +from . import ATTR_AVAILABLE, ATTR_MODE, DATA_HIVE, DOMAIN, HiveEntity, refresh_system + +PARALLEL_UPDATES = 0 +SCAN_INTERVAL = timedelta(seconds=15) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Hive switches.""" +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Hive Switch.""" if discovery_info is None: return - session = hass.data.get(DATA_HIVE) - devs = [] - for dev in discovery_info: - devs.append(HiveDevicePlug(session, dev)) - add_entities(devs) + hive = hass.data[DOMAIN].get(DATA_HIVE) + devices = hive.devices.get("switch") + entities = [] + if devices: + for dev in devices: + entities.append(HiveDevicePlug(hive, dev)) + async_add_entities(entities, True) class HiveDevicePlug(HiveEntity, SwitchEntity): @@ -32,34 +39,44 @@ class HiveDevicePlug(HiveEntity, SwitchEntity): @property def name(self): """Return the name of this Switch device if any.""" - return self.node_name + return self.device["haName"] + + @property + def available(self): + """Return if the device is available.""" + return self.device["deviceData"].get("online") @property def device_state_attributes(self): """Show Device Attributes.""" - return self.attributes + return { + ATTR_AVAILABLE: self.attributes.get(ATTR_AVAILABLE), + ATTR_MODE: self.attributes.get(ATTR_MODE), + } @property def current_power_w(self): """Return the current power usage in W.""" - return self.session.switch.get_power_usage(self.node_id) + return self.device["status"]["power_usage"] @property def is_on(self): """Return true if switch is on.""" - return self.session.switch.get_state(self.node_id) + return self.device["status"]["state"] @refresh_system - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the switch on.""" - self.session.switch.turn_on(self.node_id) + if self.device["hiveType"] == "activeplug": + await self.hive.switch.turn_on(self.device) @refresh_system - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off.""" - self.session.switch.turn_off(self.node_id) + if self.device["hiveType"] == "activeplug": + await self.hive.switch.turn_off(self.device) - def update(self): + async def async_update(self): """Update all Node data from Hive.""" - self.session.core.update_data(self.node_id) - self.attributes = self.session.attributes.state_attributes(self.node_id) + await self.hive.session.updateData(self.device) + self.device = await self.hive.switch.get_plug(self.device) diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index 693fd6f322b..56e98a690b8 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -1,4 +1,7 @@ """Support for hive water heaters.""" + +from datetime import timedelta + from homeassistant.components.water_heater import ( STATE_ECO, STATE_OFF, @@ -11,22 +14,36 @@ from homeassistant.const import TEMP_CELSIUS from . import DATA_HIVE, DOMAIN, HiveEntity, refresh_system SUPPORT_FLAGS_HEATER = SUPPORT_OPERATION_MODE +HOTWATER_NAME = "Hot Water" +PARALLEL_UPDATES = 0 +SCAN_INTERVAL = timedelta(seconds=15) +HIVE_TO_HASS_STATE = { + "SCHEDULE": STATE_ECO, + "ON": STATE_ON, + "OFF": STATE_OFF, +} + +HASS_TO_HIVE_STATE = { + STATE_ECO: "SCHEDULE", + STATE_ON: "MANUAL", + STATE_OFF: "OFF", +} -HIVE_TO_HASS_STATE = {"SCHEDULE": STATE_ECO, "ON": STATE_ON, "OFF": STATE_OFF} -HASS_TO_HIVE_STATE = {STATE_ECO: "SCHEDULE", STATE_ON: "ON", STATE_OFF: "OFF"} SUPPORT_WATER_HEATER = [STATE_ECO, STATE_ON, STATE_OFF] -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Hive water heater devices.""" +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Hive Hotwater.""" if discovery_info is None: return - session = hass.data.get(DATA_HIVE) - devs = [] - for dev in discovery_info: - devs.append(HiveWaterHeater(session, dev)) - add_entities(devs) + hive = hass.data[DOMAIN].get(DATA_HIVE) + devices = hive.devices.get("water_heater") + entities = [] + if devices: + for dev in devices: + entities.append(HiveWaterHeater(hive, dev)) + async_add_entities(entities, True) class HiveWaterHeater(HiveEntity, WaterHeaterEntity): @@ -50,9 +67,12 @@ class HiveWaterHeater(HiveEntity, WaterHeaterEntity): @property def name(self): """Return the name of the water heater.""" - if self.node_name is None: - self.node_name = "Hot Water" - return self.node_name + return HOTWATER_NAME + + @property + def available(self): + """Return if the device is available.""" + return self.device["deviceData"]["online"] @property def temperature_unit(self): @@ -62,7 +82,7 @@ class HiveWaterHeater(HiveEntity, WaterHeaterEntity): @property def current_operation(self): """Return current operation.""" - return HIVE_TO_HASS_STATE[self.session.hotwater.get_mode(self.node_id)] + return HIVE_TO_HASS_STATE[self.device["status"]["current_operation"]] @property def operation_list(self): @@ -70,11 +90,22 @@ class HiveWaterHeater(HiveEntity, WaterHeaterEntity): return SUPPORT_WATER_HEATER @refresh_system - def set_operation_mode(self, operation_mode): + async def async_turn_on(self, **kwargs): + """Turn on hotwater.""" + await self.hive.hotwater.set_mode(self.device, "MANUAL") + + @refresh_system + async def async_turn_off(self, **kwargs): + """Turn on hotwater.""" + await self.hive.hotwater.set_mode(self.device, "OFF") + + @refresh_system + async def async_set_operation_mode(self, operation_mode): """Set operation mode.""" new_mode = HASS_TO_HIVE_STATE[operation_mode] - self.session.hotwater.set_mode(self.node_id, new_mode) + await self.hive.hotwater.set_mode(self.device, new_mode) - def update(self): + async def async_update(self): """Update all Node data from Hive.""" - self.session.core.update_data(self.node_id) + await self.hive.session.updateData(self.device) + self.device = await self.hive.hotwater.get_hotwater(self.device) diff --git a/requirements_all.txt b/requirements_all.txt index 608887a8bde..63c8b98f73d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1434,7 +1434,7 @@ pyheos==0.7.2 pyhik==0.2.8 # homeassistant.components.hive -pyhiveapi==0.2.20.2 +pyhiveapi==0.3.4.4 # homeassistant.components.homematic pyhomematic==0.1.71 From 5fcb948e28e6f58af1e6385c8b4b8db618904381 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 10 Feb 2021 00:05:10 +0000 Subject: [PATCH 311/796] [ci skip] Translation update --- homeassistant/components/media_player/translations/ru.json | 7 +++++++ homeassistant/components/shelly/translations/ca.json | 4 ++-- homeassistant/components/shelly/translations/en.json | 4 ++-- homeassistant/components/shelly/translations/et.json | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/media_player/translations/ru.json b/homeassistant/components/media_player/translations/ru.json index 8ed46953675..df0b00d2482 100644 --- a/homeassistant/components/media_player/translations/ru.json +++ b/homeassistant/components/media_player/translations/ru.json @@ -6,6 +6,13 @@ "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_paused": "{entity_name} \u043d\u0430 \u043f\u0430\u0443\u0437\u0435", "is_playing": "{entity_name} \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442 \u043c\u0435\u0434\u0438\u0430" + }, + "trigger_type": { + "idle": "{entity_name} \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0438\u0442 \u0432 \u0440\u0435\u0436\u0438\u043c \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f", + "paused": "{entity_name} \u043d\u0430 \u043f\u0430\u0443\u0437\u0435", + "playing": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u0435", + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" } }, "state": { diff --git a/homeassistant/components/shelly/translations/ca.json b/homeassistant/components/shelly/translations/ca.json index c2df82c0b16..13cc79ac3d8 100644 --- a/homeassistant/components/shelly/translations/ca.json +++ b/homeassistant/components/shelly/translations/ca.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "confirm_discovery": { - "description": "Vols configurar el {model} a {host}? \n\nAbans de configurar-lo, els dispositius amb bateria s'han de desperar prement el bot\u00f3 del dispositiu." + "description": "Vols configurar el {model} a {host}? \n\nAbans de configurar-lo, els dispositius amb bateria protegits amb contrasenya s'han de desperar prement el bot\u00f3 del dispositiu.\nEls dispositius que no tinguin contrasenya s'afegiran tan bon punt es despertin. Ja pots despertar el dispositiu manualment mitjan\u00e7ant el bot\u00f3 o esperar a la seg\u00fcent transmissi\u00f3 de dades del dispositiu." }, "credentials": { "data": { @@ -24,7 +24,7 @@ "data": { "host": "Amfitri\u00f3" }, - "description": "Abans de configurar-lo, els dispositius amb bateria s'han de desperar prement el bot\u00f3 del dispositiu." + "description": "Abans de configurar-lo, els dispositius amb bateria s'han de desperar, ja pots clicar el bot\u00f3 del dispositiu per a despertar-lo." } } }, diff --git a/homeassistant/components/shelly/translations/en.json b/homeassistant/components/shelly/translations/en.json index a9ad6092a08..b60d9dfbe3e 100644 --- a/homeassistant/components/shelly/translations/en.json +++ b/homeassistant/components/shelly/translations/en.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "confirm_discovery": { - "description": "Do you want to set up the {model} at {host}?\n\nBefore set up, battery-powered devices must be woken up by pressing the button on the device." + "description": "Do you want to set up the {model} at {host}?\n\nBattery-powered devices that are password protected must be woken up before continuing with setting up.\nBattery-powered devices that are not password protected will be added when the device wakes up, you can now manually wake the device up using a button on it or wait for the next data update from the device." }, "credentials": { "data": { @@ -24,7 +24,7 @@ "data": { "host": "Host" }, - "description": "Before set up, battery-powered devices must be woken up by pressing the button on the device." + "description": "Before set up, battery-powered devices must be woken up, you can now wake the device up using a button on it." } } }, diff --git a/homeassistant/components/shelly/translations/et.json b/homeassistant/components/shelly/translations/et.json index d2514876a81..7059ce6b3d3 100644 --- a/homeassistant/components/shelly/translations/et.json +++ b/homeassistant/components/shelly/translations/et.json @@ -12,7 +12,7 @@ "flow_title": "", "step": { "confirm_discovery": { - "description": "Kas soovid seadistada {model} saidil {host} ?\n\n Enne seadistamist tuleb akutoitega seade \u00e4ratada vajutades seadmel nuppu." + "description": "Kas soovid seadistada seadet {model} saidil {host} ? \n\n Enne seadistamise j\u00e4tkamist tuleb parooliga kaitstud akutoitega seadmed \u00e4ratada.\n Patareitoitega seadmed, mis pole parooliga kaitstud, lisatakse seadme \u00e4rkamisel. N\u00fc\u00fcd saad seadme k\u00e4sitsi \u00fcles \u00e4ratada, kasutades sellel olevat nuppu v\u00f5i oodata seadme j\u00e4rgmist andmete v\u00e4rskendamist." }, "credentials": { "data": { From 00aebec90d349dff6ce9b27adb633b595f6d7731 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 9 Feb 2021 21:59:49 -0800 Subject: [PATCH 312/796] Fix bug in test found by manual log inspection (#46309) --- tests/components/stream/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py index 75ac9377b7c..1b017667ee6 100644 --- a/tests/components/stream/conftest.py +++ b/tests/components/stream/conftest.py @@ -40,7 +40,8 @@ class WorkerSync: # Block the worker thread until the test has a chance to verify # the segments under test. logging.debug("blocking worker") - self._event.wait() + if self._event: + self._event.wait() # Forward to actual implementation self._original(stream) From 26f455223bb5a8adf70b1f43c892ce3ef972f922 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 9 Feb 2021 23:53:34 -0800 Subject: [PATCH 313/796] Update nest stream URLs expiration (#46311) --- homeassistant/components/nest/camera_sdm.py | 8 +- homeassistant/components/stream/__init__.py | 18 ++++- tests/components/stream/test_worker.py | 82 ++++++++++++++++++++- 3 files changed, 96 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 8f5ba88fdd4..cc2730fad8a 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -146,13 +146,9 @@ class NestCamera(Camera): # Next attempt to catch a url will get a new one self._stream = None return - # Stop any existing stream worker since the url is invalid. The next - # request for this stream will restart it with the right url. - # Issue #42793 tracks improvements (e.g. preserve keepalive, smoother - # transitions across streams) + # Update the stream worker with the latest valid url if self.stream: - self.stream.stop() - self.stream = None + self.stream.update_source(self._stream.rtsp_stream_url) self._schedule_stream_refresh() async def async_will_remove_from_hass(self): diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index a8b344a98e9..e871963d2ba 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -109,8 +109,9 @@ class Stream: self.keepalive = False self.access_token = None self._thread = None - self._thread_quit = None + self._thread_quit = threading.Event() self._outputs = {} + self._fast_restart_once = False if self.options is None: self.options = {} @@ -167,7 +168,7 @@ class Stream: # The thread must have crashed/exited. Join to clean up the # previous thread. self._thread.join(timeout=0) - self._thread_quit = threading.Event() + self._thread_quit.clear() self._thread = threading.Thread( name="stream_worker", target=self._run_worker, @@ -175,6 +176,13 @@ class Stream: self._thread.start() _LOGGER.info("Started stream: %s", self.source) + def update_source(self, new_source): + """Restart the stream with a new stream source.""" + _LOGGER.debug("Updating stream source %s", self.source) + self.source = new_source + self._fast_restart_once = True + self._thread_quit.set() + def _run_worker(self): """Handle consuming streams and restart keepalive streams.""" # Keep import here so that we can import stream integration without installing reqs @@ -186,8 +194,12 @@ class Stream: start_time = time.time() stream_worker(self.hass, self, self._thread_quit) if not self.keepalive or self._thread_quit.is_set(): + if self._fast_restart_once: + # The stream source is updated, restart without any delay. + self._fast_restart_once = False + self._thread_quit.clear() + continue break - # To avoid excessive restarts, wait before restarting # As the required recovery time may be different for different setups, start # with trying a short wait_timeout and increase it on each reconnection attempt. diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 91d02664d74..0d5be68d93c 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -14,6 +14,7 @@ failure modes or corner cases like how out of order packets are handled. """ import fractions +import io import math import threading from unittest.mock import patch @@ -44,6 +45,7 @@ LONGER_TEST_SEQUENCE_LENGTH = 20 * VIDEO_FRAME_RATE OUT_OF_ORDER_PACKET_INDEX = 3 * VIDEO_FRAME_RATE PACKETS_PER_SEGMENT = SEGMENT_DURATION / PACKET_DURATION SEGMENTS_PER_PACKET = PACKET_DURATION / SEGMENT_DURATION +TIMEOUT = 15 class FakePyAvStream: @@ -178,9 +180,9 @@ class MockPyAv: def open(self, stream_source, *args, **kwargs): """Return a stream or buffer depending on args.""" - if stream_source == STREAM_SOURCE: - return self.container - return self.capture_buffer + if isinstance(stream_source, io.BytesIO): + return self.capture_buffer + return self.container async def async_decode_stream(hass, packets, py_av=None): @@ -469,3 +471,77 @@ async def test_pts_out_of_order(hass): assert all([s.duration == SEGMENT_DURATION for s in segments]) assert len(decoded_stream.video_packets) == len(packets) assert len(decoded_stream.audio_packets) == 0 + + +async def test_stream_stopped_while_decoding(hass): + """Tests that worker quits when stop() is called while decodign.""" + # Add some synchronization so that the test can pause the background + # worker. When the worker is stopped, the test invokes stop() which + # will cause the worker thread to exit once it enters the decode + # loop + worker_open = threading.Event() + worker_wake = threading.Event() + + stream = Stream(hass, STREAM_SOURCE) + stream.add_provider(STREAM_OUTPUT_FORMAT) + + py_av = MockPyAv() + py_av.container.packets = PacketSequence(TEST_SEQUENCE_LENGTH) + + def blocking_open(stream_source, *args, **kwargs): + # Let test know the thread is running + worker_open.set() + # Block worker thread until test wakes up + worker_wake.wait() + return py_av.open(stream_source, args, kwargs) + + with patch("av.open", new=blocking_open): + stream.start() + assert worker_open.wait(TIMEOUT) + # Note: There is a race here where the worker could start as soon + # as the wake event is sent, completing all decode work. + worker_wake.set() + stream.stop() + + +async def test_update_stream_source(hass): + """Tests that the worker is re-invoked when the stream source is updated.""" + worker_open = threading.Event() + worker_wake = threading.Event() + + stream = Stream(hass, STREAM_SOURCE) + stream.add_provider(STREAM_OUTPUT_FORMAT) + # Note that keepalive is not set here. The stream is "restarted" even though + # it is not stopping due to failure. + + py_av = MockPyAv() + py_av.container.packets = PacketSequence(TEST_SEQUENCE_LENGTH) + + last_stream_source = None + + def blocking_open(stream_source, *args, **kwargs): + nonlocal last_stream_source + if not isinstance(stream_source, io.BytesIO): + last_stream_source = stream_source + # Let test know the thread is running + worker_open.set() + # Block worker thread until test wakes up + worker_wake.wait() + return py_av.open(stream_source, args, kwargs) + + with patch("av.open", new=blocking_open): + stream.start() + assert worker_open.wait(TIMEOUT) + assert last_stream_source == STREAM_SOURCE + + # Update the stream source, then the test wakes up the worker and assert + # that it re-opens the new stream (the test again waits on thread_started) + worker_open.clear() + stream.update_source(STREAM_SOURCE + "-updated-source") + worker_wake.set() + assert worker_open.wait(TIMEOUT) + assert last_stream_source == STREAM_SOURCE + "-updated-source" + worker_wake.set() + + # Ccleanup + stream.stop() From 175f2f0275c89d2e778a8be8235fd1a89ff29129 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 10 Feb 2021 09:09:34 +0100 Subject: [PATCH 314/796] Add fan platform to knx (#46161) --- homeassistant/components/knx/__init__.py | 4 + homeassistant/components/knx/const.py | 7 +- homeassistant/components/knx/factory.py | 22 ++++++ homeassistant/components/knx/fan.py | 93 ++++++++++++++++++++++++ homeassistant/components/knx/schema.py | 22 ++++++ 5 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/knx/fan.py diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 1492e5df7b7..1879d9a6415 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -45,6 +45,7 @@ from .schema import ( ConnectionSchema, CoverSchema, ExposeSchema, + FanSchema, LightSchema, NotifySchema, SceneSchema, @@ -136,6 +137,9 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(SupportedPlatforms.weather.value): vol.All( cv.ensure_list, [WeatherSchema.SCHEMA] ), + vol.Optional(SupportedPlatforms.fan.value): vol.All( + cv.ensure_list, [FanSchema.SCHEMA] + ), } ), ) diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index e434aed395d..6a76de6a97f 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -33,14 +33,15 @@ class ColorTempModes(Enum): class SupportedPlatforms(Enum): """Supported platforms.""" - cover = "cover" - light = "light" binary_sensor = "binary_sensor" climate = "climate" - switch = "switch" + cover = "cover" + fan = "fan" + light = "light" notify = "notify" scene = "scene" sensor = "sensor" + switch = "switch" weather = "weather" diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index c1e73733b22..20a887b628d 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -8,6 +8,7 @@ from xknx.devices import ( ClimateMode as XknxClimateMode, Cover as XknxCover, Device as XknxDevice, + Fan as XknxFan, Light as XknxLight, Notification as XknxNotification, Scene as XknxScene, @@ -24,6 +25,7 @@ from .schema import ( BinarySensorSchema, ClimateSchema, CoverSchema, + FanSchema, LightSchema, SceneSchema, SensorSchema, @@ -65,6 +67,9 @@ def create_knx_device( if platform is SupportedPlatforms.weather: return _create_weather(knx_module, config) + if platform is SupportedPlatforms.fan: + return _create_fan(knx_module, config) + def _create_cover(knx_module: XKNX, config: ConfigType) -> XknxCover: """Return a KNX Cover device to be used within XKNX.""" @@ -353,3 +358,20 @@ def _create_weather(knx_module: XKNX, config: ConfigType) -> XknxWeather: ), group_address_humidity=config.get(WeatherSchema.CONF_KNX_HUMIDITY_ADDRESS), ) + + +def _create_fan(knx_module: XKNX, config: ConfigType) -> XknxFan: + """Return a KNX Fan device to be used within XKNX.""" + + fan = XknxFan( + knx_module, + name=config[CONF_NAME], + group_address_speed=config.get(CONF_ADDRESS), + group_address_speed_state=config.get(FanSchema.CONF_STATE_ADDRESS), + group_address_oscillation=config.get(FanSchema.CONF_OSCILLATION_ADDRESS), + group_address_oscillation_state=config.get( + FanSchema.CONF_OSCILLATION_STATE_ADDRESS + ), + max_step=config.get(FanSchema.CONF_MAX_STEP), + ) + return fan diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py new file mode 100644 index 00000000000..d5dfb25ccd4 --- /dev/null +++ b/homeassistant/components/knx/fan.py @@ -0,0 +1,93 @@ +"""Support for KNX/IP fans.""" +import math +from typing import Any, Optional + +from xknx.devices import Fan as XknxFan +from xknx.devices.fan import FanSpeedMode + +from homeassistant.components.fan import SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) + +from .const import DOMAIN +from .knx_entity import KnxEntity + +DEFAULT_PERCENTAGE = 50 + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up fans for KNX platform.""" + entities = [] + for device in hass.data[DOMAIN].xknx.devices: + if isinstance(device, XknxFan): + entities.append(KNXFan(device)) + async_add_entities(entities) + + +class KNXFan(KnxEntity, FanEntity): + """Representation of a KNX fan.""" + + def __init__(self, device: XknxFan): + """Initialize of KNX fan.""" + super().__init__(device) + + if self._device.mode == FanSpeedMode.Step: + self._step_range = (1, device.max_step) + + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed of the fan, as a percentage.""" + if self._device.mode == FanSpeedMode.Step: + step = math.ceil(percentage_to_ranged_value(self._step_range, percentage)) + await self._device.set_speed(step) + else: + await self._device.set_speed(percentage) + + @property + def supported_features(self) -> int: + """Flag supported features.""" + flags = SUPPORT_SET_SPEED + + if self._device.supports_oscillation: + flags |= SUPPORT_OSCILLATE + + return flags + + @property + def percentage(self) -> Optional[int]: + """Return the current speed as a percentage.""" + if self._device.current_speed is None: + return None + + if self._device.mode == FanSpeedMode.Step: + return ranged_value_to_percentage( + self._step_range, self._device.current_speed + ) + return self._device.current_speed + + async def async_turn_on( + self, + speed: Optional[str] = None, + percentage: Optional[int] = None, + preset_mode: Optional[str] = None, + **kwargs, + ) -> None: + """Turn on the fan.""" + if percentage is None: + await self.async_set_percentage(DEFAULT_PERCENTAGE) + else: + await self.async_set_percentage(percentage) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the fan off.""" + await self.async_set_percentage(0) + + async def async_oscillate(self, oscillating: bool) -> None: + """Oscillate the fan.""" + await self._device.set_oscillation(oscillating) + + @property + def oscillating(self): + """Return whether or not the fan is currently oscillating.""" + return self._device.current_oscillation diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 22599014d0f..a9b65b85352 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -430,3 +430,25 @@ class WeatherSchema: vol.Optional(CONF_KNX_HUMIDITY_ADDRESS): cv.string, } ) + + +class FanSchema: + """Voluptuous schema for KNX fans.""" + + CONF_STATE_ADDRESS = CONF_STATE_ADDRESS + CONF_OSCILLATION_ADDRESS = "oscillation_address" + CONF_OSCILLATION_STATE_ADDRESS = "oscillation_state_address" + CONF_MAX_STEP = "max_step" + + DEFAULT_NAME = "KNX Fan" + + SCHEMA = vol.Schema( + { + vol.Required(CONF_ADDRESS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_STATE_ADDRESS): cv.string, + vol.Optional(CONF_OSCILLATION_ADDRESS): cv.string, + vol.Optional(CONF_OSCILLATION_STATE_ADDRESS): cv.string, + vol.Optional(CONF_MAX_STEP): cv.byte, + } + ) From b0b81246f0de0777e688aa30e4f50314db6915d2 Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Wed, 10 Feb 2021 09:27:25 +0000 Subject: [PATCH 315/796] Bump roonapi to 0.0.32 (#46286) --- homeassistant/components/roon/manifest.json | 2 +- homeassistant/components/roon/server.py | 11 ----------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/roon/manifest.json b/homeassistant/components/roon/manifest.json index 0d5d0c131ae..e4c4a25dcb5 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.31" + "roonapi==0.0.32" ], "codeowners": [ "@pavoni" diff --git a/homeassistant/components/roon/server.py b/homeassistant/components/roon/server.py index d5b8d81c2aa..83b620e176e 100644 --- a/homeassistant/components/roon/server.py +++ b/homeassistant/components/roon/server.py @@ -141,17 +141,6 @@ class RoonServer: async_dispatcher_send(self.hass, "roon_media_player", player_data) self.offline_devices.add(dev_id) - async def async_update_playlists(self): - """Store lists in memory with all playlists - could be used by a custom lovelace card.""" - all_playlists = [] - roon_playlists = self.roonapi.playlists() - if roon_playlists and "items" in roon_playlists: - all_playlists += [item["title"] for item in roon_playlists["items"]] - roon_playlists = self.roonapi.internet_radio() - if roon_playlists and "items" in roon_playlists: - all_playlists += [item["title"] for item in roon_playlists["items"]] - self.all_playlists = all_playlists - async def async_create_player_data(self, zone, output): """Create player object dict by combining zone with output.""" new_dict = zone.copy() diff --git a/requirements_all.txt b/requirements_all.txt index 63c8b98f73d..f7955c9eb1f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1967,7 +1967,7 @@ rokuecp==0.6.0 roombapy==1.6.2 # homeassistant.components.roon -roonapi==0.0.31 +roonapi==0.0.32 # homeassistant.components.rova rova==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 259f33e01f7..df82a9d7429 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -998,7 +998,7 @@ rokuecp==0.6.0 roombapy==1.6.2 # homeassistant.components.roon -roonapi==0.0.31 +roonapi==0.0.32 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 From 78b7fbf7b16f64c426c747f3473e6f68703ff481 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 10 Feb 2021 10:50:44 +0100 Subject: [PATCH 316/796] Fix race in EntityRegistry.async_device_modified (#46319) --- homeassistant/helpers/entity_registry.py | 4 ++- tests/helpers/test_entity_registry.py | 33 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 15218afc227..052e7398ba1 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -334,7 +334,9 @@ class EntityRegistry: device_registry = await self.hass.helpers.device_registry.async_get_registry() device = device_registry.async_get(event.data["device_id"]) - if not device.disabled: + + # The device may be deleted already if the event handling is late + if not device or not device.disabled: entities = async_entries_for_device( self, event.data["device_id"], include_disabled_entities=True ) diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 21f4392122e..0fa2d486a9f 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -710,6 +710,39 @@ async def test_remove_device_removes_entities(hass, registry): assert not registry.async_is_registered(entry.entity_id) +async def test_update_device_race(hass, registry): + """Test race when a device is created, updated and removed.""" + device_registry = mock_device_registry(hass) + config_entry = MockConfigEntry(domain="light") + + # Create device + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={("mac", "12:34:56:AB:CD:EF")}, + ) + # Updatete it + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={("bridgeid", "0123")}, + connections={("mac", "12:34:56:AB:CD:EF")}, + ) + # Add entity to the device + entry = registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry, + device_id=device_entry.id, + ) + + assert registry.async_is_registered(entry.entity_id) + + device_registry.async_remove_device(device_entry.id) + await hass.async_block_till_done() + + assert not registry.async_is_registered(entry.entity_id) + + async def test_disable_device_disables_entities(hass, registry): """Test that we disable entities tied to a device.""" device_registry = mock_device_registry(hass) From 1fea24502c1769e7c4d1db0c9460c240e717ff67 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 10 Feb 2021 18:14:03 +0800 Subject: [PATCH 317/796] Bump pyav version to 8.03 (#46315) --- homeassistant/components/stream/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index 3d194bdf0d4..19b9e7b2e8a 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -2,7 +2,7 @@ "domain": "stream", "name": "Stream", "documentation": "https://www.home-assistant.io/integrations/stream", - "requirements": ["av==8.0.2"], + "requirements": ["av==8.0.3"], "dependencies": ["http"], "codeowners": ["@hunterjm", "@uvjustin"], "quality_scale": "internal" diff --git a/requirements_all.txt b/requirements_all.txt index f7955c9eb1f..3c6e4f8a659 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -297,7 +297,7 @@ auroranoaa==0.0.2 aurorapy==0.2.6 # homeassistant.components.stream -av==8.0.2 +av==8.0.3 # homeassistant.components.avea # avea==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df82a9d7429..6c3a1f9dc88 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -177,7 +177,7 @@ async-upnp-client==0.14.13 auroranoaa==0.0.2 # homeassistant.components.stream -av==8.0.2 +av==8.0.3 # homeassistant.components.axis axis==43 From bfd5a62bad68011db8b7da5de3b69d498f843490 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 10 Feb 2021 11:31:51 +0100 Subject: [PATCH 318/796] Fix typo (#46321) --- tests/helpers/test_entity_registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 0fa2d486a9f..b176f7022d5 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -720,7 +720,7 @@ async def test_update_device_race(hass, registry): config_entry_id=config_entry.entry_id, connections={("mac", "12:34:56:AB:CD:EF")}, ) - # Updatete it + # Update it device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers={("bridgeid", "0123")}, From 7d2d98fc3c50b20c3d4fa802b1d49f0e387c70cb Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 10 Feb 2021 13:38:16 +0200 Subject: [PATCH 319/796] Revert multiple interfaces (#46300) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index fffa98b6870..c38869b3e0d 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==0.5.4"], + "requirements": ["aioshelly==0.6.0"], "zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }], "codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3c6e4f8a659..ca536e1cc8b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -218,7 +218,7 @@ aiopylgtv==0.3.3 aiorecollect==1.0.1 # homeassistant.components.shelly -aioshelly==0.5.4 +aioshelly==0.6.0 # homeassistant.components.switcher_kis aioswitcher==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c3a1f9dc88..7f1f5df2d81 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -137,7 +137,7 @@ aiopylgtv==0.3.3 aiorecollect==1.0.1 # homeassistant.components.shelly -aioshelly==0.5.4 +aioshelly==0.6.0 # homeassistant.components.switcher_kis aioswitcher==1.2.1 From 4b493c5ab951bf978b691fc0ce5e9bae2a739208 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 10 Feb 2021 12:42:28 +0100 Subject: [PATCH 320/796] Add target to service call API (#45898) * Add target to service call API * Fix _async_call_service_step * CONF_SERVICE_ENTITY_ID overrules target * Move merging up before processing schema * Restore services.yaml * Add test --- homeassistant/components/api/__init__.py | 2 +- .../components/websocket_api/commands.py | 2 + homeassistant/core.py | 9 +++- homeassistant/helpers/script.py | 12 +++--- homeassistant/helpers/service.py | 32 +++++++++++---- .../components/websocket_api/test_commands.py | 41 +++++++++++++++++++ 6 files changed, 82 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index e7bac8532ee..a82309094e3 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -378,7 +378,7 @@ class APIDomainServicesView(HomeAssistantView): with AsyncTrackStates(hass) as changed_states: try: await hass.services.async_call( - domain, service, data, True, self.context(request) + domain, service, data, blocking=True, context=self.context(request) ) except (vol.Invalid, ServiceNotFound) as ex: raise HTTPBadRequest() from ex diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 77521c1ed98..ddd7548cd68 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -121,6 +121,7 @@ def handle_unsubscribe_events(hass, connection, msg): vol.Required("type"): "call_service", vol.Required("domain"): str, vol.Required("service"): str, + vol.Optional("target"): cv.ENTITY_SERVICE_FIELDS, vol.Optional("service_data"): dict, } ) @@ -139,6 +140,7 @@ async def handle_call_service(hass, connection, msg): msg.get("service_data"), blocking, context, + target=msg.get("target"), ) connection.send_message( messages.result_message(msg["id"], {"context": context}) diff --git a/homeassistant/core.py b/homeassistant/core.py index 4294eb530a7..13f8b153047 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1358,6 +1358,7 @@ class ServiceRegistry: blocking: bool = False, context: Optional[Context] = None, limit: Optional[float] = SERVICE_CALL_LIMIT, + target: Optional[Dict] = None, ) -> Optional[bool]: """ Call a service. @@ -1365,7 +1366,9 @@ class ServiceRegistry: See description of async_call for details. """ return asyncio.run_coroutine_threadsafe( - self.async_call(domain, service, service_data, blocking, context, limit), + self.async_call( + domain, service, service_data, blocking, context, limit, target + ), self._hass.loop, ).result() @@ -1377,6 +1380,7 @@ class ServiceRegistry: blocking: bool = False, context: Optional[Context] = None, limit: Optional[float] = SERVICE_CALL_LIMIT, + target: Optional[Dict] = None, ) -> Optional[bool]: """ Call a service. @@ -1404,6 +1408,9 @@ class ServiceRegistry: except KeyError: raise ServiceNotFound(domain, service) from None + if target: + service_data.update(target) + if handler.schema: try: processed_data = handler.schema(service_data) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 56accf9cf49..a0e8311048e 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -433,14 +433,14 @@ class _ScriptRun: self._script.last_action = self._action.get(CONF_ALIAS, "call service") self._log("Executing step %s", self._script.last_action) - domain, service_name, service_data = service.async_prepare_call_from_config( + params = service.async_prepare_call_from_config( self._hass, self._action, self._variables ) running_script = ( - domain == "automation" - and service_name == "trigger" - or domain in ("python_script", "script") + params["domain"] == "automation" + and params["service_name"] == "trigger" + or params["domain"] in ("python_script", "script") ) # If this might start a script then disable the call timeout. # Otherwise use the normal service call limit. @@ -451,9 +451,7 @@ class _ScriptRun: service_task = self._hass.async_create_task( self._hass.services.async_call( - domain, - service_name, - service_data, + **params, blocking=True, context=self._context, limit=limit, diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 13dcd779b25..a13b866a418 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -14,6 +14,7 @@ from typing import ( Optional, Set, Tuple, + TypedDict, Union, cast, ) @@ -70,6 +71,15 @@ _LOGGER = logging.getLogger(__name__) SERVICE_DESCRIPTION_CACHE = "service_description_cache" +class ServiceParams(TypedDict): + """Type for service call parameters.""" + + domain: str + service: str + service_data: Dict[str, Any] + target: Optional[Dict] + + @dataclasses.dataclass class SelectedEntities: """Class to hold the selected entities.""" @@ -136,7 +146,7 @@ async def async_call_from_config( raise _LOGGER.error(ex) else: - await hass.services.async_call(*params, blocking, context) + await hass.services.async_call(**params, blocking=blocking, context=context) @ha.callback @@ -146,7 +156,7 @@ def async_prepare_call_from_config( config: ConfigType, variables: TemplateVarsType = None, validate_config: bool = False, -) -> Tuple[str, str, Dict[str, Any]]: +) -> ServiceParams: """Prepare to call a service based on a config hash.""" if validate_config: try: @@ -177,10 +187,9 @@ def async_prepare_call_from_config( domain, service = domain_service.split(".", 1) - service_data = {} + target = config.get(CONF_TARGET) - if CONF_TARGET in config: - service_data.update(config[CONF_TARGET]) + service_data = {} for conf in [CONF_SERVICE_DATA, CONF_SERVICE_DATA_TEMPLATE]: if conf not in config: @@ -192,9 +201,17 @@ def async_prepare_call_from_config( raise HomeAssistantError(f"Error rendering data template: {ex}") from ex if CONF_SERVICE_ENTITY_ID in config: - service_data[ATTR_ENTITY_ID] = config[CONF_SERVICE_ENTITY_ID] + if target: + target[ATTR_ENTITY_ID] = config[CONF_SERVICE_ENTITY_ID] + else: + target = {ATTR_ENTITY_ID: config[CONF_SERVICE_ENTITY_ID]} - return domain, service, service_data + return { + "domain": domain, + "service": service, + "service_data": service_data, + "target": target, + } @bind_hass @@ -431,6 +448,7 @@ async def async_get_all_descriptions( description = descriptions_cache[cache_key] = { "description": yaml_description.get("description", ""), + "target": yaml_description.get("target"), "fields": yaml_description.get("fields", {}), } diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index a7aa17db6d3..1f7abc42c4e 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -52,6 +52,47 @@ async def test_call_service(hass, websocket_client): assert call.data == {"hello": "world"} +async def test_call_service_target(hass, websocket_client): + """Test call service command with target.""" + calls = [] + + @callback + def service_call(call): + calls.append(call) + + hass.services.async_register("domain_test", "test_service", service_call) + + await websocket_client.send_json( + { + "id": 5, + "type": "call_service", + "domain": "domain_test", + "service": "test_service", + "service_data": {"hello": "world"}, + "target": { + "entity_id": ["entity.one", "entity.two"], + "device_id": "deviceid", + }, + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + assert len(calls) == 1 + call = calls[0] + + assert call.domain == "domain_test" + assert call.service == "test_service" + assert call.data == { + "hello": "world", + "entity_id": ["entity.one", "entity.two"], + "device_id": ["deviceid"], + } + + async def test_call_service_not_found(hass, websocket_client): """Test call service command.""" await websocket_client.send_json( From a6358430b46a68b6aee74dd223da29511ecb9bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 10 Feb 2021 15:16:58 +0200 Subject: [PATCH 321/796] Fix deprecated asyncio.wait use with coroutines (#44981) https://docs.python.org/3/library/asyncio-task.html#asyncio-example-wait-coroutine --- homeassistant/components/config/__init__.py | 4 ++-- homeassistant/components/device_tracker/legacy.py | 2 +- homeassistant/components/forked_daapd/media_player.py | 2 +- homeassistant/components/image_processing/__init__.py | 2 +- homeassistant/components/mailbox/__init__.py | 2 +- homeassistant/components/microsoft_face/__init__.py | 4 +++- homeassistant/components/notify/__init__.py | 2 +- homeassistant/components/stt/__init__.py | 2 +- homeassistant/components/tts/__init__.py | 2 +- homeassistant/helpers/script.py | 2 +- homeassistant/helpers/service.py | 6 ++++-- 11 files changed, 17 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 1098594a04c..7d07710a4d0 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -65,11 +65,11 @@ async def async_setup(hass, config): hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded) - tasks = [setup_panel(panel_name) for panel_name in SECTIONS] + tasks = [asyncio.create_task(setup_panel(panel_name)) for panel_name in SECTIONS] for panel_name in ON_DEMAND: if panel_name in hass.config.components: - tasks.append(setup_panel(panel_name)) + tasks.append(asyncio.create_task(setup_panel(panel_name))) if tasks: await asyncio.wait(tasks) diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 5f60d84f406..b7583d80f82 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -153,7 +153,7 @@ async def async_setup_integration(hass: HomeAssistantType, config: ConfigType) - legacy_platforms = await async_extract_config(hass, config) setup_tasks = [ - legacy_platform.async_setup_legacy(hass, tracker) + asyncio.create_task(legacy_platform.async_setup_legacy(hass, tracker)) for legacy_platform in legacy_platforms ] diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index 195ebf7e2cf..724db80fabd 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -855,7 +855,7 @@ class ForkedDaapdUpdater: ) if update_events: await asyncio.wait( - [event.wait() for event in update_events.values()] + [asyncio.create_task(event.wait()) for event in update_events.values()] ) # make sure callbacks done before update async_dispatcher_send( self.hass, SIGNAL_UPDATE_MASTER.format(self._entry_id), True diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 261278da401..e885a9ca7a9 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -81,7 +81,7 @@ async def async_setup(hass, config): update_tasks = [] for entity in image_entities: entity.async_set_context(service.context) - update_tasks.append(entity.async_update_ha_state(True)) + update_tasks.append(asyncio.create_task(entity.async_update_ha_state(True))) if update_tasks: await asyncio.wait(update_tasks) diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index e5a0f16863d..5d05596fb23 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -84,7 +84,7 @@ async def async_setup(hass, config): await component.async_add_entities([mailbox_entity]) setup_tasks = [ - async_setup_platform(p_type, p_config) + asyncio.create_task(async_setup_platform(p_type, p_config)) for p_type, p_config in config_per_platform(config, DOMAIN) ] diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index 69a738724c3..b9046429603 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -275,7 +275,9 @@ class MicrosoftFace: for person in persons: self._store[g_id][person["name"]] = person["personId"] - tasks.append(self._entities[g_id].async_update_ha_state()) + tasks.append( + asyncio.create_task(self._entities[g_id].async_update_ha_state()) + ) if tasks: await asyncio.wait(tasks) diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index d3439baf4fb..1e9c7d8595a 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -312,7 +312,7 @@ async def async_setup(hass, config): ) setup_tasks = [ - async_setup_platform(integration_name, p_config) + asyncio.create_task(async_setup_platform(integration_name, p_config)) for integration_name, p_config in config_per_platform(config, DOMAIN) ] diff --git a/homeassistant/components/stt/__init__.py b/homeassistant/components/stt/__init__.py index 43ef01a497e..0ad621f0707 100644 --- a/homeassistant/components/stt/__init__.py +++ b/homeassistant/components/stt/__init__.py @@ -62,7 +62,7 @@ async def async_setup(hass: HomeAssistantType, config): return setup_tasks = [ - async_setup_platform(p_type, p_config) + asyncio.create_task(async_setup_platform(p_type, p_config)) for p_type, p_config in config_per_platform(config, DOMAIN) ] diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index d278283baaf..ff1bf946e83 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -194,7 +194,7 @@ async def async_setup(hass, config): ) setup_tasks = [ - async_setup_platform(p_type, p_config) + asyncio.create_task(async_setup_platform(p_type, p_config)) for p_type, p_config in config_per_platform(config, DOMAIN) ] diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index a0e8311048e..3cc8348961f 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1055,7 +1055,7 @@ class Script: raise async def _async_stop(self, update_state): - aws = [run.async_stop() for run in self._runs] + aws = [asyncio.create_task(run.async_stop()) for run in self._runs] if not aws: return await asyncio.wait(aws) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index a13b866a418..b2fa97d51cc 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -602,8 +602,10 @@ async def entity_service_call( done, pending = await asyncio.wait( [ - entity.async_request_call( - _handle_entity_call(hass, entity, func, data, call.context) + asyncio.create_task( + entity.async_request_call( + _handle_entity_call(hass, entity, func, data, call.context) + ) ) for entity in entities ] From 22389043eb7e72d8f8e6215b41743b2686e62903 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 10 Feb 2021 14:31:11 +0100 Subject: [PATCH 322/796] Remove base_url fallback (#46316) --- homeassistant/components/http/__init__.py | 78 +---- homeassistant/core.py | 35 +-- homeassistant/helpers/network.py | 79 +---- .../ambiclimate/test_config_flow.py | 13 +- tests/components/http/test_init.py | 191 ------------ tests/components/ifttt/test_init.py | 15 +- tests/components/twilio/test_init.py | 14 +- tests/helpers/test_network.py | 284 +----------------- tests/test_core.py | 35 --- 9 files changed, 38 insertions(+), 706 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 63c88427a5b..d09cfe754a9 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -4,7 +4,6 @@ from ipaddress import ip_network import logging import os import ssl -from traceback import extract_stack from typing import Dict, Optional, cast from aiohttp import web @@ -125,69 +124,6 @@ class ApiConfig: self.port = port self.use_ssl = use_ssl - host = host.rstrip("/") - if host.startswith(("http://", "https://")): - self.deprecated_base_url = host - elif use_ssl: - self.deprecated_base_url = f"https://{host}" - else: - self.deprecated_base_url = f"http://{host}" - - if port is not None: - self.deprecated_base_url += f":{port}" - - @property - def base_url(self) -> str: - """Proxy property to find caller of this deprecated property.""" - found_frame = None - for frame in reversed(extract_stack()[:-1]): - for path in ("custom_components/", "homeassistant/components/"): - try: - index = frame.filename.index(path) - - # Skip webhook from the stack - if frame.filename[index:].startswith( - "homeassistant/components/webhook/" - ): - continue - - found_frame = frame - break - except ValueError: - continue - - if found_frame is not None: - break - - # Did not source from an integration? Hard error. - if found_frame is None: - raise RuntimeError( - "Detected use of deprecated `base_url` property in the Home Assistant core. Please report this issue." - ) - - # If a frame was found, it originated from an integration - if found_frame: - start = index + len(path) - end = found_frame.filename.index("/", start) - - integration = found_frame.filename[start:end] - - if path == "custom_components/": - extra = " to the custom component author" - else: - extra = "" - - _LOGGER.warning( - "Detected use of deprecated `base_url` property, use `homeassistant.helpers.network.get_url` method instead. Please report issue%s for %s using this method at %s, line %s: %s", - extra, - integration, - found_frame.filename[index:], - found_frame.lineno, - found_frame.line.strip(), - ) - - return self.deprecated_base_url - async def async_setup(hass, config): """Set up the HTTP API and debug interface.""" @@ -256,20 +192,16 @@ async def async_setup(hass, config): hass.http = server - host = conf.get(CONF_BASE_URL) local_ip = await hass.async_add_executor_job(hass_util.get_local_ip) - if host: - port = None - elif server_host is not None: + host = local_ip + if server_host is not None: # Assume the first server host name provided as API host host = server_host[0] - port = server_port - else: - host = local_ip - port = server_port - hass.config.api = ApiConfig(local_ip, host, port, ssl_certificate is not None) + hass.config.api = ApiConfig( + local_ip, host, server_port, ssl_certificate is not None + ) return True diff --git a/homeassistant/core.py b/homeassistant/core.py index 13f8b153047..fff16cdd31f 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -8,7 +8,6 @@ import asyncio import datetime import enum import functools -from ipaddress import ip_address import logging import os import pathlib @@ -70,7 +69,7 @@ from homeassistant.exceptions import ( ServiceNotFound, Unauthorized, ) -from homeassistant.util import location, network +from homeassistant.util import location from homeassistant.util.async_ import ( fire_coroutine_threadsafe, run_callback_threadsafe, @@ -1687,39 +1686,7 @@ class Config: ) data = await store.async_load() - async def migrate_base_url(_: Event) -> None: - """Migrate base_url to internal_url/external_url.""" - if self.hass.config.api is None: - return - - base_url = yarl.URL(self.hass.config.api.deprecated_base_url) - - # Check if this is an internal URL - if str(base_url.host).endswith(".local") or ( - network.is_ip_address(str(base_url.host)) - and network.is_private(ip_address(base_url.host)) - ): - await self.async_update( - internal_url=network.normalize_url(str(base_url)) - ) - return - - # External, ensure this is not a loopback address - if not ( - network.is_ip_address(str(base_url.host)) - and network.is_loopback(ip_address(base_url.host)) - ): - await self.async_update( - external_url=network.normalize_url(str(base_url)) - ) - if data: - # Try to migrate base_url to internal_url/external_url - if "external_url" not in data: - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, migrate_base_url - ) - self._update( source=SOURCE_STORAGE, latitude=data.get("latitude"), diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index 4e066eaa13c..21f69dc539a 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -8,13 +8,7 @@ from homeassistant.components import http from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass -from homeassistant.util.network import ( - is_ip_address, - is_local, - is_loopback, - is_private, - normalize_url, -) +from homeassistant.util.network import is_ip_address, is_loopback, normalize_url TYPE_URL_INTERNAL = "internal_url" TYPE_URL_EXTERNAL = "external_url" @@ -151,19 +145,6 @@ def _get_internal_url( ): return normalize_url(str(internal_url)) - # Fallback to old base_url - try: - return _get_deprecated_base_url( - hass, - internal=True, - allow_ip=allow_ip, - require_current_request=require_current_request, - require_ssl=require_ssl, - require_standard_port=require_standard_port, - ) - except NoURLAvailableError: - pass - # Fallback to detected local IP if allow_ip and not ( require_ssl or hass.config.api is None or hass.config.api.use_ssl @@ -217,17 +198,6 @@ def _get_external_url( ): return normalize_url(str(external_url)) - try: - return _get_deprecated_base_url( - hass, - allow_ip=allow_ip, - require_current_request=require_current_request, - require_ssl=require_ssl, - require_standard_port=require_standard_port, - ) - except NoURLAvailableError: - pass - if allow_cloud: try: return _get_cloud_url(hass, require_current_request=require_current_request) @@ -250,50 +220,3 @@ def _get_cloud_url(hass: HomeAssistant, require_current_request: bool = False) - return normalize_url(str(cloud_url)) raise NoURLAvailableError - - -@bind_hass -def _get_deprecated_base_url( - hass: HomeAssistant, - *, - internal: bool = False, - allow_ip: bool = True, - require_current_request: bool = False, - require_ssl: bool = False, - require_standard_port: bool = False, -) -> str: - """Work with the deprecated `base_url`, used as fallback.""" - if hass.config.api is None or not hass.config.api.deprecated_base_url: - raise NoURLAvailableError - - base_url = yarl.URL(hass.config.api.deprecated_base_url) - # Rules that apply to both internal and external - if ( - (allow_ip or not is_ip_address(str(base_url.host))) - and (not require_current_request or base_url.host == _get_request_host()) - and (not require_ssl or base_url.scheme == "https") - and (not require_standard_port or base_url.is_default_port()) - ): - # Check to ensure an internal URL - if internal and ( - str(base_url.host).endswith(".local") - or ( - is_ip_address(str(base_url.host)) - and not is_loopback(ip_address(base_url.host)) - and is_private(ip_address(base_url.host)) - ) - ): - return normalize_url(str(base_url)) - - # Check to ensure an external URL (a little) - if ( - not internal - and not str(base_url.host).endswith(".local") - and not ( - is_ip_address(str(base_url.host)) - and is_local(ip_address(str(base_url.host))) - ) - ): - return normalize_url(str(base_url)) - - raise NoURLAvailableError diff --git a/tests/components/ambiclimate/test_config_flow.py b/tests/components/ambiclimate/test_config_flow.py index b87c2171815..3a325490064 100644 --- a/tests/components/ambiclimate/test_config_flow.py +++ b/tests/components/ambiclimate/test_config_flow.py @@ -5,6 +5,7 @@ import ambiclimate from homeassistant import data_entry_flow from homeassistant.components.ambiclimate import config_flow +from homeassistant.config import async_process_ha_core_config from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.setup import async_setup_component from homeassistant.util import aiohttp @@ -12,9 +13,11 @@ from homeassistant.util import aiohttp async def init_config_flow(hass): """Init a configuration flow.""" - await async_setup_component( - hass, "http", {"http": {"base_url": "https://hass.com"}} + await async_process_ha_core_config( + hass, + {"external_url": "https://example.com"}, ) + await async_setup_component(hass, "http", {}) config_flow.register_flow_implementation(hass, "id", "secret") flow = config_flow.AmbiclimateFlowHandler() @@ -58,20 +61,20 @@ async def test_full_flow_implementation(hass): assert result["step_id"] == "auth" assert ( result["description_placeholders"]["cb_url"] - == "https://hass.com/api/ambiclimate" + == "https://example.com/api/ambiclimate" ) url = result["description_placeholders"]["authorization_url"] assert "https://api.ambiclimate.com/oauth2/authorize" in url assert "client_id=id" in url assert "response_type=code" in url - assert "redirect_uri=https%3A%2F%2Fhass.com%2Fapi%2Fambiclimate" in url + assert "redirect_uri=https%3A%2F%2Fexample.com%2Fapi%2Fambiclimate" in url with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value="test"): result = await flow.async_step_code("123ABC") assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "Ambiclimate" - assert result["data"]["callback_url"] == "https://hass.com/api/ambiclimate" + assert result["data"]["callback_url"] == "https://example.com/api/ambiclimate" assert result["data"][CONF_CLIENT_SECRET] == "secret" assert result["data"][CONF_CLIENT_ID] == "id" diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index e7a3884c481..9621b269081 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -60,73 +60,6 @@ async def test_registering_view_while_running( hass.http.register_view(TestView) -def test_api_base_url_with_domain(mock_stack): - """Test setting API URL with domain.""" - api_config = http.ApiConfig("127.0.0.1", "example.com") - assert api_config.base_url == "http://example.com:8123" - - -def test_api_base_url_with_ip(mock_stack): - """Test setting API URL with IP.""" - api_config = http.ApiConfig("127.0.0.1", "1.1.1.1") - assert api_config.base_url == "http://1.1.1.1:8123" - - -def test_api_base_url_with_ip_and_port(mock_stack): - """Test setting API URL with IP and port.""" - api_config = http.ApiConfig("127.0.0.1", "1.1.1.1", 8124) - assert api_config.base_url == "http://1.1.1.1:8124" - - -def test_api_base_url_with_protocol(mock_stack): - """Test setting API URL with protocol.""" - api_config = http.ApiConfig("127.0.0.1", "https://example.com") - assert api_config.base_url == "https://example.com:8123" - - -def test_api_base_url_with_protocol_and_port(mock_stack): - """Test setting API URL with protocol and port.""" - api_config = http.ApiConfig("127.0.0.1", "https://example.com", 433) - assert api_config.base_url == "https://example.com:433" - - -def test_api_base_url_with_ssl_enable(mock_stack): - """Test setting API URL with use_ssl enabled.""" - api_config = http.ApiConfig("127.0.0.1", "example.com", use_ssl=True) - assert api_config.base_url == "https://example.com:8123" - - -def test_api_base_url_with_ssl_enable_and_port(mock_stack): - """Test setting API URL with use_ssl enabled and port.""" - api_config = http.ApiConfig("127.0.0.1", "1.1.1.1", use_ssl=True, port=8888) - assert api_config.base_url == "https://1.1.1.1:8888" - - -def test_api_base_url_with_protocol_and_ssl_enable(mock_stack): - """Test setting API URL with specific protocol and use_ssl enabled.""" - api_config = http.ApiConfig("127.0.0.1", "http://example.com", use_ssl=True) - assert api_config.base_url == "http://example.com:8123" - - -def test_api_base_url_removes_trailing_slash(mock_stack): - """Test a trialing slash is removed when setting the API URL.""" - api_config = http.ApiConfig("127.0.0.1", "http://example.com/") - assert api_config.base_url == "http://example.com:8123" - - -def test_api_local_ip(mock_stack): - """Test a trialing slash is removed when setting the API URL.""" - api_config = http.ApiConfig("127.0.0.1", "http://example.com/") - assert api_config.local_ip == "127.0.0.1" - - -async def test_api_no_base_url(hass, mock_stack): - """Test setting api url.""" - result = await async_setup_component(hass, "http", {"http": {}}) - assert result - assert hass.config.api.base_url == "http://127.0.0.1:8123" - - async def test_not_log_password(hass, aiohttp_client, caplog, legacy_auth): """Test access with password doesn't get logged.""" assert await async_setup_component(hass, "api", {"http": {}}) @@ -263,127 +196,3 @@ async def test_storing_config(hass, aiohttp_client, aiohttp_unused_port): restored["trusted_proxies"][0] = ip_network(restored["trusted_proxies"][0]) assert restored == http.HTTP_SCHEMA(config) - - -async def test_use_of_base_url(hass): - """Test detection base_url usage when called without integration context.""" - await async_setup_component(hass, "http", {"http": {}}) - with patch( - "homeassistant.components.http.extract_stack", - return_value=[ - Mock( - filename="/home/frenck/homeassistant/core.py", - lineno="21", - line="do_something()", - ), - Mock( - filename="/home/frenck/homeassistant/core.py", - lineno="42", - line="url = hass.config.api.base_url", - ), - Mock( - filename="/home/frenck/example/client.py", - lineno="21", - line="something()", - ), - ], - ), pytest.raises(RuntimeError): - hass.config.api.base_url - - -async def test_use_of_base_url_integration(hass, caplog): - """Test detection base_url usage when called with integration context.""" - await async_setup_component(hass, "http", {"http": {}}) - with patch( - "homeassistant.components.http.extract_stack", - return_value=[ - Mock( - filename="/home/frenck/homeassistant/core.py", - lineno="21", - line="do_something()", - ), - Mock( - filename="/home/frenck/homeassistant/components/example/__init__.py", - lineno="42", - line="url = hass.config.api.base_url", - ), - Mock( - filename="/home/frenck/example/client.py", - lineno="21", - line="something()", - ), - ], - ): - assert hass.config.api.base_url == "http://127.0.0.1:8123" - - assert ( - "Detected use of deprecated `base_url` property, use `homeassistant.helpers.network.get_url` method instead. Please report issue for example using this method at homeassistant/components/example/__init__.py, line 42: url = hass.config.api.base_url" - in caplog.text - ) - - -async def test_use_of_base_url_integration_webhook(hass, caplog): - """Test detection base_url usage when called with integration context.""" - await async_setup_component(hass, "http", {"http": {}}) - with patch( - "homeassistant.components.http.extract_stack", - return_value=[ - Mock( - filename="/home/frenck/homeassistant/core.py", - lineno="21", - line="do_something()", - ), - Mock( - filename="/home/frenck/homeassistant/components/example/__init__.py", - lineno="42", - line="url = hass.config.api.base_url", - ), - Mock( - filename="/home/frenck/homeassistant/components/webhook/__init__.py", - lineno="42", - line="return get_url(hass)", - ), - Mock( - filename="/home/frenck/example/client.py", - lineno="21", - line="something()", - ), - ], - ): - assert hass.config.api.base_url == "http://127.0.0.1:8123" - - assert ( - "Detected use of deprecated `base_url` property, use `homeassistant.helpers.network.get_url` method instead. Please report issue for example using this method at homeassistant/components/example/__init__.py, line 42: url = hass.config.api.base_url" - in caplog.text - ) - - -async def test_use_of_base_url_custom_component(hass, caplog): - """Test detection base_url usage when called with custom component context.""" - await async_setup_component(hass, "http", {"http": {}}) - with patch( - "homeassistant.components.http.extract_stack", - return_value=[ - Mock( - filename="/home/frenck/homeassistant/core.py", - lineno="21", - line="do_something()", - ), - Mock( - filename="/home/frenck/.homeassistant/custom_components/example/__init__.py", - lineno="42", - line="url = hass.config.api.base_url", - ), - Mock( - filename="/home/frenck/example/client.py", - lineno="21", - line="something()", - ), - ], - ): - assert hass.config.api.base_url == "http://127.0.0.1:8123" - - assert ( - "Detected use of deprecated `base_url` property, use `homeassistant.helpers.network.get_url` method instead. Please report issue to the custom component author for example using this method at custom_components/example/__init__.py, line 42: url = hass.config.api.base_url" - in caplog.text - ) diff --git a/tests/components/ifttt/test_init.py b/tests/components/ifttt/test_init.py index d10df2492d4..41885f0cd26 100644 --- a/tests/components/ifttt/test_init.py +++ b/tests/components/ifttt/test_init.py @@ -1,17 +1,20 @@ """Test the init file of IFTTT.""" -from unittest.mock import patch - from homeassistant import data_entry_flow from homeassistant.components import ifttt +from homeassistant.config import async_process_ha_core_config from homeassistant.core import callback async def test_config_flow_registers_webhook(hass, aiohttp_client): """Test setting up IFTTT and sending webhook.""" - with patch("homeassistant.util.get_local_ip", return_value="example.com"): - result = await hass.config_entries.flow.async_init( - "ifttt", context={"source": "user"} - ) + await async_process_ha_core_config( + hass, + {"internal_url": "http://example.local:8123"}, + ) + + result = await hass.config_entries.flow.async_init( + "ifttt", context={"source": "user"} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) diff --git a/tests/components/twilio/test_init.py b/tests/components/twilio/test_init.py index ee7f072a65c..580e5f83ebf 100644 --- a/tests/components/twilio/test_init.py +++ b/tests/components/twilio/test_init.py @@ -1,17 +1,19 @@ """Test the init file of Twilio.""" -from unittest.mock import patch - from homeassistant import data_entry_flow from homeassistant.components import twilio +from homeassistant.config import async_process_ha_core_config from homeassistant.core import callback async def test_config_flow_registers_webhook(hass, aiohttp_client): """Test setting up Twilio and sending webhook.""" - with patch("homeassistant.util.get_local_ip", return_value="example.com"): - result = await hass.config_entries.flow.async_init( - "twilio", context={"source": "user"} - ) + await async_process_ha_core_config( + hass, + {"internal_url": "http://example.local:8123"}, + ) + result = await hass.config_entries.flow.async_init( + "twilio", context={"source": "user"} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index 06158558d5e..aad37e2fd49 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -9,7 +9,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.network import ( NoURLAvailableError, _get_cloud_url, - _get_deprecated_base_url, _get_external_url, _get_internal_url, _get_request_host, @@ -166,9 +165,7 @@ async def test_get_url_internal_fallback(hass: HomeAssistant): """Test getting an instance URL when the user has not set an internal URL.""" assert hass.config.internal_url is None - hass.config.api = Mock( - use_ssl=False, port=8123, deprecated_base_url=None, local_ip="192.168.123.123" - ) + hass.config.api = Mock(use_ssl=False, port=8123, local_ip="192.168.123.123") assert _get_internal_url(hass) == "http://192.168.123.123:8123" with pytest.raises(NoURLAvailableError): @@ -180,9 +177,7 @@ async def test_get_url_internal_fallback(hass: HomeAssistant): with pytest.raises(NoURLAvailableError): _get_internal_url(hass, require_ssl=True) - hass.config.api = Mock( - use_ssl=False, port=80, deprecated_base_url=None, local_ip="192.168.123.123" - ) + hass.config.api = Mock(use_ssl=False, port=80, local_ip="192.168.123.123") assert _get_internal_url(hass) == "http://192.168.123.123" assert ( _get_internal_url(hass, require_standard_port=True) == "http://192.168.123.123" @@ -194,7 +189,7 @@ async def test_get_url_internal_fallback(hass: HomeAssistant): with pytest.raises(NoURLAvailableError): _get_internal_url(hass, require_ssl=True) - hass.config.api = Mock(use_ssl=True, port=443, deprecated_base_url=None) + hass.config.api = Mock(use_ssl=True, port=443) with pytest.raises(NoURLAvailableError): _get_internal_url(hass) @@ -208,9 +203,7 @@ async def test_get_url_internal_fallback(hass: HomeAssistant): _get_internal_url(hass, require_ssl=True) # Do no accept any local loopback address as fallback - hass.config.api = Mock( - use_ssl=False, port=80, deprecated_base_url=None, local_ip="127.0.0.1" - ) + hass.config.api = Mock(use_ssl=False, port=80, local_ip="127.0.0.1") with pytest.raises(NoURLAvailableError): _get_internal_url(hass) @@ -457,9 +450,7 @@ async def test_get_url(hass: HomeAssistant): with pytest.raises(NoURLAvailableError): get_url(hass) - hass.config.api = Mock( - use_ssl=False, port=8123, deprecated_base_url=None, local_ip="192.168.123.123" - ) + hass.config.api = Mock(use_ssl=False, port=8123, local_ip="192.168.123.123") assert get_url(hass) == "http://192.168.123.123:8123" assert get_url(hass, prefer_external=True) == "http://192.168.123.123:8123" @@ -543,274 +534,11 @@ async def test_get_request_host(hass: HomeAssistant): assert _get_request_host() == "example.com" -async def test_get_deprecated_base_url_internal(hass: HomeAssistant): - """Test getting an internal instance URL from the deprecated base_url.""" - # Test with SSL local URL - hass.config.api = Mock(deprecated_base_url="https://example.local") - assert _get_deprecated_base_url(hass, internal=True) == "https://example.local" - assert ( - _get_deprecated_base_url(hass, internal=True, allow_ip=False) - == "https://example.local" - ) - assert ( - _get_deprecated_base_url(hass, internal=True, require_ssl=True) - == "https://example.local" - ) - assert ( - _get_deprecated_base_url(hass, internal=True, require_standard_port=True) - == "https://example.local" - ) - - # Test with no SSL, local IP URL - hass.config.api = Mock(deprecated_base_url="http://10.10.10.10:8123") - assert _get_deprecated_base_url(hass, internal=True) == "http://10.10.10.10:8123" - - with pytest.raises(NoURLAvailableError): - _get_deprecated_base_url(hass, internal=True, allow_ip=False) - - with pytest.raises(NoURLAvailableError): - _get_deprecated_base_url(hass, internal=True, require_ssl=True) - - with pytest.raises(NoURLAvailableError): - _get_deprecated_base_url(hass, internal=True, require_standard_port=True) - - # Test with SSL, local IP URL - hass.config.api = Mock(deprecated_base_url="https://10.10.10.10") - assert _get_deprecated_base_url(hass, internal=True) == "https://10.10.10.10" - assert ( - _get_deprecated_base_url(hass, internal=True, require_ssl=True) - == "https://10.10.10.10" - ) - assert ( - _get_deprecated_base_url(hass, internal=True, require_standard_port=True) - == "https://10.10.10.10" - ) - - # Test external URL - hass.config.api = Mock(deprecated_base_url="https://example.com") - with pytest.raises(NoURLAvailableError): - _get_deprecated_base_url(hass, internal=True) - - with pytest.raises(NoURLAvailableError): - _get_deprecated_base_url(hass, internal=True, require_ssl=True) - - with pytest.raises(NoURLAvailableError): - _get_deprecated_base_url(hass, internal=True, require_standard_port=True) - - with pytest.raises(NoURLAvailableError): - _get_deprecated_base_url(hass, internal=True, allow_ip=False) - - # Test with loopback - hass.config.api = Mock(deprecated_base_url="https://127.0.0.42") - with pytest.raises(NoURLAvailableError): - assert _get_deprecated_base_url(hass, internal=True) - - with pytest.raises(NoURLAvailableError): - _get_deprecated_base_url(hass, internal=True, allow_ip=False) - - with pytest.raises(NoURLAvailableError): - _get_deprecated_base_url(hass, internal=True, require_ssl=True) - - with pytest.raises(NoURLAvailableError): - _get_deprecated_base_url(hass, internal=True, require_standard_port=True) - - -async def test_get_deprecated_base_url_external(hass: HomeAssistant): - """Test getting an external instance URL from the deprecated base_url.""" - # Test with SSL and external domain on standard port - hass.config.api = Mock(deprecated_base_url="https://example.com:443/") - assert _get_deprecated_base_url(hass) == "https://example.com" - assert _get_deprecated_base_url(hass, require_ssl=True) == "https://example.com" - assert ( - _get_deprecated_base_url(hass, require_standard_port=True) - == "https://example.com" - ) - - # Test without SSL and external domain on non-standard port - hass.config.api = Mock(deprecated_base_url="http://example.com:8123/") - assert _get_deprecated_base_url(hass) == "http://example.com:8123" - - with pytest.raises(NoURLAvailableError): - _get_deprecated_base_url(hass, require_ssl=True) - - with pytest.raises(NoURLAvailableError): - _get_deprecated_base_url(hass, require_standard_port=True) - - # Test SSL on external IP - hass.config.api = Mock(deprecated_base_url="https://1.1.1.1") - assert _get_deprecated_base_url(hass) == "https://1.1.1.1" - assert _get_deprecated_base_url(hass, require_ssl=True) == "https://1.1.1.1" - assert ( - _get_deprecated_base_url(hass, require_standard_port=True) == "https://1.1.1.1" - ) - - with pytest.raises(NoURLAvailableError): - _get_deprecated_base_url(hass, allow_ip=False) - - # Test with private IP - hass.config.api = Mock(deprecated_base_url="https://10.10.10.10") - with pytest.raises(NoURLAvailableError): - assert _get_deprecated_base_url(hass) - - with pytest.raises(NoURLAvailableError): - _get_deprecated_base_url(hass, allow_ip=False) - - with pytest.raises(NoURLAvailableError): - _get_deprecated_base_url(hass, require_ssl=True) - - with pytest.raises(NoURLAvailableError): - _get_deprecated_base_url(hass, require_standard_port=True) - - # Test with local domain - hass.config.api = Mock(deprecated_base_url="https://example.local") - with pytest.raises(NoURLAvailableError): - assert _get_deprecated_base_url(hass) - - with pytest.raises(NoURLAvailableError): - _get_deprecated_base_url(hass, allow_ip=False) - - with pytest.raises(NoURLAvailableError): - _get_deprecated_base_url(hass, require_ssl=True) - - with pytest.raises(NoURLAvailableError): - _get_deprecated_base_url(hass, require_standard_port=True) - - # Test with loopback - hass.config.api = Mock(deprecated_base_url="https://127.0.0.42") - with pytest.raises(NoURLAvailableError): - assert _get_deprecated_base_url(hass) - - with pytest.raises(NoURLAvailableError): - _get_deprecated_base_url(hass, allow_ip=False) - - with pytest.raises(NoURLAvailableError): - _get_deprecated_base_url(hass, require_ssl=True) - - with pytest.raises(NoURLAvailableError): - _get_deprecated_base_url(hass, require_standard_port=True) - - -async def test_get_internal_url_with_base_url_fallback(hass: HomeAssistant): - """Test getting an internal instance URL with the deprecated base_url fallback.""" - hass.config.api = Mock( - use_ssl=False, port=8123, deprecated_base_url=None, local_ip="192.168.123.123" - ) - assert hass.config.internal_url is None - assert _get_internal_url(hass) == "http://192.168.123.123:8123" - - with pytest.raises(NoURLAvailableError): - _get_internal_url(hass, allow_ip=False) - - with pytest.raises(NoURLAvailableError): - _get_internal_url(hass, require_standard_port=True) - - with pytest.raises(NoURLAvailableError): - _get_internal_url(hass, require_ssl=True) - - # Add base_url - hass.config.api = Mock( - use_ssl=False, port=8123, deprecated_base_url="https://example.local" - ) - assert _get_internal_url(hass) == "https://example.local" - assert _get_internal_url(hass, allow_ip=False) == "https://example.local" - assert ( - _get_internal_url(hass, require_standard_port=True) == "https://example.local" - ) - assert _get_internal_url(hass, require_ssl=True) == "https://example.local" - - # Add internal URL - await async_process_ha_core_config( - hass, - {"internal_url": "https://internal.local"}, - ) - assert _get_internal_url(hass) == "https://internal.local" - assert _get_internal_url(hass, allow_ip=False) == "https://internal.local" - assert ( - _get_internal_url(hass, require_standard_port=True) == "https://internal.local" - ) - assert _get_internal_url(hass, require_ssl=True) == "https://internal.local" - - # Add internal URL, mixed results - await async_process_ha_core_config( - hass, - {"internal_url": "http://internal.local:8123"}, - ) - assert _get_internal_url(hass) == "http://internal.local:8123" - assert _get_internal_url(hass, allow_ip=False) == "http://internal.local:8123" - assert ( - _get_internal_url(hass, require_standard_port=True) == "https://example.local" - ) - assert _get_internal_url(hass, require_ssl=True) == "https://example.local" - - # Add internal URL set to an IP - await async_process_ha_core_config( - hass, - {"internal_url": "http://10.10.10.10:8123"}, - ) - assert _get_internal_url(hass) == "http://10.10.10.10:8123" - assert _get_internal_url(hass, allow_ip=False) == "https://example.local" - assert ( - _get_internal_url(hass, require_standard_port=True) == "https://example.local" - ) - assert _get_internal_url(hass, require_ssl=True) == "https://example.local" - - -async def test_get_external_url_with_base_url_fallback(hass: HomeAssistant): - """Test getting an external instance URL with the deprecated base_url fallback.""" - hass.config.api = Mock(use_ssl=False, port=8123, deprecated_base_url=None) - assert hass.config.internal_url is None - - with pytest.raises(NoURLAvailableError): - _get_external_url(hass) - - # Test with SSL and external domain on standard port - hass.config.api = Mock(deprecated_base_url="https://example.com:443/") - assert _get_external_url(hass) == "https://example.com" - assert _get_external_url(hass, allow_ip=False) == "https://example.com" - assert _get_external_url(hass, require_ssl=True) == "https://example.com" - assert _get_external_url(hass, require_standard_port=True) == "https://example.com" - - # Add external URL - await async_process_ha_core_config( - hass, - {"external_url": "https://external.example.com"}, - ) - assert _get_external_url(hass) == "https://external.example.com" - assert _get_external_url(hass, allow_ip=False) == "https://external.example.com" - assert ( - _get_external_url(hass, require_standard_port=True) - == "https://external.example.com" - ) - assert _get_external_url(hass, require_ssl=True) == "https://external.example.com" - - # Add external URL, mixed results - await async_process_ha_core_config( - hass, - {"external_url": "http://external.example.com:8123"}, - ) - assert _get_external_url(hass) == "http://external.example.com:8123" - assert _get_external_url(hass, allow_ip=False) == "http://external.example.com:8123" - assert _get_external_url(hass, require_standard_port=True) == "https://example.com" - assert _get_external_url(hass, require_ssl=True) == "https://example.com" - - # Add external URL set to an IP - await async_process_ha_core_config( - hass, - {"external_url": "http://1.1.1.1:8123"}, - ) - assert _get_external_url(hass) == "http://1.1.1.1:8123" - assert _get_external_url(hass, allow_ip=False) == "https://example.com" - assert _get_external_url(hass, require_standard_port=True) == "https://example.com" - assert _get_external_url(hass, require_ssl=True) == "https://example.com" - - async def test_get_current_request_url_with_known_host( hass: HomeAssistant, current_request ): """Test getting current request URL with known hosts addresses.""" - hass.config.api = Mock( - use_ssl=False, port=8123, local_ip="127.0.0.1", deprecated_base_url=None - ) + hass.config.api = Mock(use_ssl=False, port=8123, local_ip="127.0.0.1") assert hass.config.internal_url is None with pytest.raises(NoURLAvailableError): diff --git a/tests/test_core.py b/tests/test_core.py index 0bf00d92c45..dfd5b925e1c 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1294,41 +1294,6 @@ def test_valid_entity_id(): assert ha.valid_entity_id(valid), valid -async def test_migration_base_url(hass, hass_storage): - """Test that we migrate base url to internal/external url.""" - config = ha.Config(hass) - stored = {"version": 1, "data": {}} - hass_storage[ha.CORE_STORAGE_KEY] = stored - with patch.object(hass.bus, "async_listen_once") as mock_listen: - # Empty config - await config.async_load() - assert len(mock_listen.mock_calls) == 0 - - # With just a name - stored["data"] = {"location_name": "Test Name"} - await config.async_load() - assert len(mock_listen.mock_calls) == 1 - - # With external url - stored["data"]["external_url"] = "https://example.com" - await config.async_load() - assert len(mock_listen.mock_calls) == 1 - - # Test that the event listener works - assert mock_listen.mock_calls[0][1][0] == EVENT_HOMEASSISTANT_START - - # External - hass.config.api = Mock(deprecated_base_url="https://loaded-example.com") - await mock_listen.mock_calls[0][1][1](None) - assert config.external_url == "https://loaded-example.com" - - # Internal - for internal in ("http://hass.local", "http://192.168.1.100:8123"): - hass.config.api = Mock(deprecated_base_url=internal) - await mock_listen.mock_calls[0][1][1](None) - assert config.internal_url == internal - - async def test_additional_data_in_core_config(hass, hass_storage): """Test that we can handle additional data in core configuration.""" config = ha.Config(hass) From 6e1f3b78617553e3848536b1a0b4d3c375e01572 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Wed, 10 Feb 2021 08:35:11 -0500 Subject: [PATCH 323/796] Use core constants for joaoapps_join (#46291) --- homeassistant/components/joaoapps_join/__init__.py | 4 +--- homeassistant/components/joaoapps_join/notify.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/joaoapps_join/__init__.py b/homeassistant/components/joaoapps_join/__init__.py index 1bc4ae298c4..a65a7ffd7fe 100644 --- a/homeassistant/components/joaoapps_join/__init__.py +++ b/homeassistant/components/joaoapps_join/__init__.py @@ -12,14 +12,13 @@ from pyjoin import ( ) import voluptuous as vol -from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.const import CONF_API_KEY, CONF_DEVICE_ID, CONF_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DOMAIN = "joaoapps_join" -CONF_DEVICE_ID = "device_id" CONF_DEVICE_IDS = "device_ids" CONF_DEVICE_NAMES = "device_names" @@ -115,7 +114,6 @@ def register_device(hass, api_key, name, device_id, device_ids, device_names): def setup(hass, config): """Set up the Join services.""" - for device in config[DOMAIN]: api_key = device.get(CONF_API_KEY) device_id = device.get(CONF_DEVICE_ID) diff --git a/homeassistant/components/joaoapps_join/notify.py b/homeassistant/components/joaoapps_join/notify.py index d01e49c77d8..7ba089e5dab 100644 --- a/homeassistant/components/joaoapps_join/notify.py +++ b/homeassistant/components/joaoapps_join/notify.py @@ -11,12 +11,11 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) -from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_API_KEY, CONF_DEVICE_ID import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_DEVICE_ID = "device_id" CONF_DEVICE_IDS = "device_ids" CONF_DEVICE_NAMES = "device_names" @@ -61,7 +60,6 @@ class JoinNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) data = kwargs.get(ATTR_DATA) or {} send_notification( From ad400d91bc8202b5388fa32618c00eaa746c312e Mon Sep 17 00:00:00 2001 From: tkdrob Date: Wed, 10 Feb 2021 08:36:05 -0500 Subject: [PATCH 324/796] Use core constants for sensor integration (#46290) --- homeassistant/components/integration/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index a776920b8e6..6c59035adb4 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + CONF_METHOD, CONF_NAME, STATE_UNAVAILABLE, STATE_UNKNOWN, @@ -31,7 +32,6 @@ CONF_ROUND_DIGITS = "round" CONF_UNIT_PREFIX = "unit_prefix" CONF_UNIT_TIME = "unit_time" CONF_UNIT_OF_MEASUREMENT = "unit" -CONF_METHOD = "method" TRAPEZOIDAL_METHOD = "trapezoidal" LEFT_METHOD = "left" From c66d9ea25c9b5e8025d6ea9da6b4c032d432bab8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 10 Feb 2021 14:39:10 +0100 Subject: [PATCH 325/796] Hide volume control for cast devices with fixed volume (#46328) --- homeassistant/components/cast/manifest.json | 2 +- homeassistant/components/cast/media_player.py | 7 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/cast/test_media_player.py | 65 +++++++++++++++++++ 5 files changed, 73 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 88dabc8d04d..5963e93cf8c 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==8.0.0"], + "requirements": ["pychromecast==8.1.0"], "after_dependencies": ["cloud", "http", "media_source", "plex", "tts", "zeroconf"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"] diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 6bedae1cac5..981d67f0caa 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -10,6 +10,7 @@ import pychromecast from pychromecast.controllers.homeassistant import HomeAssistantController from pychromecast.controllers.multizone import MultizoneManager from pychromecast.controllers.plex import PlexController +from pychromecast.controllers.receiver import VOLUME_CONTROL_TYPE_FIXED from pychromecast.quick_play import quick_play from pychromecast.socket_client import ( CONNECTION_STATUS_CONNECTED, @@ -82,8 +83,6 @@ SUPPORT_CAST = ( | SUPPORT_STOP | SUPPORT_TURN_OFF | SUPPORT_TURN_ON - | SUPPORT_VOLUME_MUTE - | SUPPORT_VOLUME_SET ) @@ -743,6 +742,10 @@ class CastDevice(MediaPlayerEntity): support = SUPPORT_CAST media_status = self._media_status()[0] + if self.cast_status: + if self.cast_status.volume_control_type != VOLUME_CONTROL_TYPE_FIXED: + support |= SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET + if media_status: if media_status.supports_queue_next: support |= SUPPORT_PREVIOUS_TRACK diff --git a/requirements_all.txt b/requirements_all.txt index ca536e1cc8b..9f2aef26012 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1310,7 +1310,7 @@ pycfdns==1.2.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==8.0.0 +pychromecast==8.1.0 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f1f5df2d81..143ec43a5d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -681,7 +681,7 @@ pybotvac==0.0.20 pycfdns==1.2.1 # homeassistant.components.cast -pychromecast==8.0.0 +pychromecast==8.1.0 # homeassistant.components.comfoconnect pycomfoconnect==0.4 diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 050d6a6932d..be24afcb538 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -11,6 +11,19 @@ import pytest from homeassistant.components import tts from homeassistant.components.cast import media_player as cast from homeassistant.components.cast.media_player import ChromecastInfo +from homeassistant.components.media_player.const import ( + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, +) from homeassistant.config import async_process_ha_core_config from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import PlatformNotReady @@ -662,6 +675,17 @@ async def test_entity_cast_status(hass: HomeAssistantType): assert state.state == "unknown" assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) + assert state.attributes.get("supported_features") == ( + SUPPORT_PAUSE + | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA + | SUPPORT_STOP + | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET + ) + cast_status = MagicMock() cast_status.volume_level = 0.5 cast_status.volume_muted = False @@ -680,6 +704,21 @@ async def test_entity_cast_status(hass: HomeAssistantType): assert state.attributes.get("volume_level") == 0.2 assert state.attributes.get("is_volume_muted") + # Disable support for volume control + cast_status = MagicMock() + cast_status.volume_control_type = "fixed" + cast_status_cb(cast_status) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.attributes.get("supported_features") == ( + SUPPORT_PAUSE + | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA + | SUPPORT_STOP + | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON + ) + async def test_entity_play_media(hass: HomeAssistantType): """Test playing media.""" @@ -894,6 +933,17 @@ async def test_entity_control(hass: HomeAssistantType): assert state.state == "unknown" assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) + assert state.attributes.get("supported_features") == ( + SUPPORT_PAUSE + | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA + | SUPPORT_STOP + | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET + ) + # Turn on await common.async_turn_on(hass, entity_id) chromecast.play_media.assert_called_once_with( @@ -940,6 +990,21 @@ async def test_entity_control(hass: HomeAssistantType): media_status_cb(media_status) await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.attributes.get("supported_features") == ( + SUPPORT_PAUSE + | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA + | SUPPORT_STOP + | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_SEEK + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET + ) + # Media previous await common.async_media_previous_track(hass, entity_id) chromecast.media_controller.queue_prev.assert_called_once_with() From b7e11347d5e65e8380234e856e3b95c70844c686 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 10 Feb 2021 14:56:54 +0100 Subject: [PATCH 326/796] Remove defunct Crime Reports integration (#46312) --- .coveragerc | 1 - .../components/crimereports/__init__.py | 1 - .../components/crimereports/manifest.json | 7 - .../components/crimereports/sensor.py | 127 ------------------ requirements_all.txt | 3 - 5 files changed, 139 deletions(-) delete mode 100644 homeassistant/components/crimereports/__init__.py delete mode 100644 homeassistant/components/crimereports/manifest.json delete mode 100644 homeassistant/components/crimereports/sensor.py diff --git a/.coveragerc b/.coveragerc index 581c6350a05..cd1d6a9f6d3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -157,7 +157,6 @@ omit = homeassistant/components/coolmaster/const.py homeassistant/components/cppm_tracker/device_tracker.py homeassistant/components/cpuspeed/sensor.py - homeassistant/components/crimereports/sensor.py homeassistant/components/cups/sensor.py homeassistant/components/currencylayer/sensor.py homeassistant/components/daikin/* diff --git a/homeassistant/components/crimereports/__init__.py b/homeassistant/components/crimereports/__init__.py deleted file mode 100644 index 57af9df4dbf..00000000000 --- a/homeassistant/components/crimereports/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The crimereports component.""" diff --git a/homeassistant/components/crimereports/manifest.json b/homeassistant/components/crimereports/manifest.json deleted file mode 100644 index 624d812f5f3..00000000000 --- a/homeassistant/components/crimereports/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "crimereports", - "name": "Crime Reports", - "documentation": "https://www.home-assistant.io/integrations/crimereports", - "requirements": ["crimereports==1.0.1"], - "codeowners": [] -} diff --git a/homeassistant/components/crimereports/sensor.py b/homeassistant/components/crimereports/sensor.py deleted file mode 100644 index 8919b2d09b1..00000000000 --- a/homeassistant/components/crimereports/sensor.py +++ /dev/null @@ -1,127 +0,0 @@ -"""Sensor for Crime Reports.""" -from collections import defaultdict -from datetime import timedelta - -import crimereports -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_LATITUDE, - ATTR_LONGITUDE, - CONF_EXCLUDE, - CONF_INCLUDE, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, - CONF_RADIUS, - LENGTH_KILOMETERS, - LENGTH_METERS, -) -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.util import slugify -from homeassistant.util.distance import convert -from homeassistant.util.dt import now - -DOMAIN = "crimereports" - -EVENT_INCIDENT = f"{DOMAIN}_incident" - -SCAN_INTERVAL = timedelta(minutes=30) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_RADIUS): vol.Coerce(float), - vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude, - vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude, - vol.Optional(CONF_INCLUDE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_EXCLUDE): vol.All(cv.ensure_list, [cv.string]), - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Crime Reports platform.""" - latitude = config.get(CONF_LATITUDE, hass.config.latitude) - longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - name = config[CONF_NAME] - radius = config[CONF_RADIUS] - include = config.get(CONF_INCLUDE) - exclude = config.get(CONF_EXCLUDE) - - add_entities( - [CrimeReportsSensor(hass, name, latitude, longitude, radius, include, exclude)], - True, - ) - - -class CrimeReportsSensor(Entity): - """Representation of a Crime Reports Sensor.""" - - def __init__(self, hass, name, latitude, longitude, radius, include, exclude): - """Initialize the Crime Reports sensor.""" - self._hass = hass - self._name = name - self._include = include - self._exclude = exclude - radius_kilometers = convert(radius, LENGTH_METERS, LENGTH_KILOMETERS) - self._crimereports = crimereports.CrimeReports( - (latitude, longitude), radius_kilometers - ) - self._attributes = None - self._state = None - self._previous_incidents = set() - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._attributes - - def _incident_event(self, incident): - """Fire if an event occurs.""" - data = { - "type": incident.get("type"), - "description": incident.get("friendly_description"), - "timestamp": incident.get("timestamp"), - "location": incident.get("location"), - } - if incident.get("coordinates"): - data.update( - { - ATTR_LATITUDE: incident.get("coordinates")[0], - ATTR_LONGITUDE: incident.get("coordinates")[1], - } - ) - self._hass.bus.fire(EVENT_INCIDENT, data) - - def update(self): - """Update device state.""" - incident_counts = defaultdict(int) - incidents = self._crimereports.get_incidents( - now().date(), include=self._include, exclude=self._exclude - ) - fire_events = len(self._previous_incidents) > 0 - if len(incidents) < len(self._previous_incidents): - self._previous_incidents = set() - for incident in incidents: - incident_type = slugify(incident.get("type")) - incident_counts[incident_type] += 1 - if fire_events and incident.get("id") not in self._previous_incidents: - self._incident_event(incident) - self._previous_incidents.add(incident.get("id")) - self._attributes = {ATTR_ATTRIBUTION: crimereports.ATTRIBUTION} - self._attributes.update(incident_counts) - self._state = len(incidents) diff --git a/requirements_all.txt b/requirements_all.txt index 9f2aef26012..202049341e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -450,9 +450,6 @@ coronavirus==1.1.1 # homeassistant.scripts.credstash # credstash==1.15.0 -# homeassistant.components.crimereports -crimereports==1.0.1 - # homeassistant.components.datadog datadog==0.15.0 From ad727152121d4e7ab127432cf80a409f9b2d10b5 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Wed, 10 Feb 2021 08:57:53 -0500 Subject: [PATCH 327/796] Use core constants for konnected (#46322) --- homeassistant/components/konnected/__init__.py | 4 ++-- homeassistant/components/konnected/config_flow.py | 4 ++-- homeassistant/components/konnected/const.py | 2 -- homeassistant/components/konnected/panel.py | 4 ++-- homeassistant/components/konnected/switch.py | 2 +- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index a6bc7eff5ca..348eaeda3ac 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -18,11 +18,13 @@ from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_BINARY_SENSORS, CONF_DEVICES, + CONF_DISCOVERY, CONF_HOST, CONF_ID, CONF_NAME, CONF_PIN, CONF_PORT, + CONF_REPEAT, CONF_SENSORS, CONF_SWITCHES, CONF_TYPE, @@ -48,12 +50,10 @@ from .const import ( CONF_ACTIVATION, CONF_API_HOST, CONF_BLINK, - CONF_DISCOVERY, CONF_INVERSE, CONF_MOMENTARY, CONF_PAUSE, CONF_POLL_INTERVAL, - CONF_REPEAT, DOMAIN, PIN_TO_ZONE, STATE_HIGH, diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py index 4e8d13c999e..dc15e7a86c4 100644 --- a/homeassistant/components/konnected/config_flow.py +++ b/homeassistant/components/konnected/config_flow.py @@ -17,10 +17,12 @@ from homeassistant.components.ssdp import ATTR_UPNP_MANUFACTURER, ATTR_UPNP_MODE from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_BINARY_SENSORS, + CONF_DISCOVERY, CONF_HOST, CONF_ID, CONF_NAME, CONF_PORT, + CONF_REPEAT, CONF_SENSORS, CONF_SWITCHES, CONF_TYPE, @@ -34,13 +36,11 @@ from .const import ( CONF_API_HOST, CONF_BLINK, CONF_DEFAULT_OPTIONS, - CONF_DISCOVERY, CONF_INVERSE, CONF_MODEL, CONF_MOMENTARY, CONF_PAUSE, CONF_POLL_INTERVAL, - CONF_REPEAT, DOMAIN, STATE_HIGH, STATE_LOW, diff --git a/homeassistant/components/konnected/const.py b/homeassistant/components/konnected/const.py index c1e7d6b6f26..270b2604538 100644 --- a/homeassistant/components/konnected/const.py +++ b/homeassistant/components/konnected/const.py @@ -9,10 +9,8 @@ CONF_MOMENTARY = "momentary" CONF_PAUSE = "pause" CONF_POLL_INTERVAL = "poll_interval" CONF_PRECISION = "precision" -CONF_REPEAT = "repeat" CONF_INVERSE = "inverse" CONF_BLINK = "blink" -CONF_DISCOVERY = "discovery" CONF_DHT_SENSORS = "dht_sensors" CONF_DS18B20_SENSORS = "ds18b20_sensors" CONF_MODEL = "model" diff --git a/homeassistant/components/konnected/panel.py b/homeassistant/components/konnected/panel.py index 76e75159290..18f2ed64a1d 100644 --- a/homeassistant/components/konnected/panel.py +++ b/homeassistant/components/konnected/panel.py @@ -10,11 +10,13 @@ from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_BINARY_SENSORS, CONF_DEVICES, + CONF_DISCOVERY, CONF_HOST, CONF_ID, CONF_NAME, CONF_PIN, CONF_PORT, + CONF_REPEAT, CONF_SENSORS, CONF_SWITCHES, CONF_TYPE, @@ -31,13 +33,11 @@ from .const import ( CONF_BLINK, CONF_DEFAULT_OPTIONS, CONF_DHT_SENSORS, - CONF_DISCOVERY, CONF_DS18B20_SENSORS, CONF_INVERSE, CONF_MOMENTARY, CONF_PAUSE, CONF_POLL_INTERVAL, - CONF_REPEAT, DOMAIN, ENDPOINT_ROOT, STATE_LOW, diff --git a/homeassistant/components/konnected/switch.py b/homeassistant/components/konnected/switch.py index 1d26f7875c7..b599fe55242 100644 --- a/homeassistant/components/konnected/switch.py +++ b/homeassistant/components/konnected/switch.py @@ -5,6 +5,7 @@ from homeassistant.const import ( ATTR_STATE, CONF_DEVICES, CONF_NAME, + CONF_REPEAT, CONF_SWITCHES, CONF_ZONE, ) @@ -14,7 +15,6 @@ from .const import ( CONF_ACTIVATION, CONF_MOMENTARY, CONF_PAUSE, - CONF_REPEAT, DOMAIN as KONNECTED_DOMAIN, STATE_HIGH, STATE_LOW, From 7928cda080f2932dd36784cc146ff836555f3c15 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 10 Feb 2021 15:25:24 +0100 Subject: [PATCH 328/796] Add `already_in_progress` string to roku config flow (#46333) --- homeassistant/components/roku/strings.json | 1 + homeassistant/components/roku/translations/en.json | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/roku/strings.json b/homeassistant/components/roku/strings.json index 55b533d4f1c..3523615ff33 100644 --- a/homeassistant/components/roku/strings.json +++ b/homeassistant/components/roku/strings.json @@ -19,6 +19,7 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "unknown": "[%key:common::config_flow::error::unknown%]" } } diff --git a/homeassistant/components/roku/translations/en.json b/homeassistant/components/roku/translations/en.json index 08db89f3677..2b54cafe890 100644 --- a/homeassistant/components/roku/translations/en.json +++ b/homeassistant/components/roku/translations/en.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", "unknown": "Unexpected error" }, "error": { From ea4ad854883b77d39c8b2f5c5cf3915f258e780c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 10 Feb 2021 15:25:44 +0100 Subject: [PATCH 329/796] Replace StrictVersion with AwesomeVersion (#46331) --- homeassistant/components/updater/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 13497da8290..9d65bb4c5d4 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -1,10 +1,10 @@ """Support to check for available updates.""" import asyncio from datetime import timedelta -from distutils.version import StrictVersion import logging import async_timeout +from awesomeversion import AwesomeVersion from distro import linux_distribution # pylint: disable=import-error import voluptuous as vol @@ -83,16 +83,16 @@ async def async_setup(hass, config): # Validate version update_available = False - if StrictVersion(newest) > StrictVersion(current_version): + if AwesomeVersion(newest) > AwesomeVersion(current_version): _LOGGER.debug( "The latest available version of Home Assistant is %s", newest ) update_available = True - elif StrictVersion(newest) == StrictVersion(current_version): + elif AwesomeVersion(newest) == AwesomeVersion(current_version): _LOGGER.debug( "You are on the latest version (%s) of Home Assistant", newest ) - elif StrictVersion(newest) < StrictVersion(current_version): + elif AwesomeVersion(newest) < AwesomeVersion(current_version): _LOGGER.debug( "Local version (%s) is newer than the latest available version (%s)", current_version, From dbb98e6cac6cef0ff394633fabf06cf36e9de10d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 10 Feb 2021 15:26:38 +0100 Subject: [PATCH 330/796] Replace LooseVersion with AwesomeVersion (#46330) --- homeassistant/config.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 5cd4a0700e5..73d0273d1c0 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -1,6 +1,5 @@ """Module to help with parsing and generating configuration files.""" from collections import OrderedDict -from distutils.version import LooseVersion # pylint: disable=import-error import logging import os import re @@ -8,6 +7,7 @@ import shutil from types import ModuleType from typing import Any, Callable, Dict, Optional, Sequence, Set, Tuple, Union +from awesomeversion import AwesomeVersion import voluptuous as vol from voluptuous.humanize import humanize_error @@ -364,15 +364,15 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: "Upgrading configuration directory from %s to %s", conf_version, __version__ ) - version_obj = LooseVersion(conf_version) + version_obj = AwesomeVersion(conf_version) - if version_obj < LooseVersion("0.50"): + if version_obj < AwesomeVersion("0.50"): # 0.50 introduced persistent deps dir. lib_path = hass.config.path("deps") if os.path.isdir(lib_path): shutil.rmtree(lib_path) - if version_obj < LooseVersion("0.92"): + if version_obj < AwesomeVersion("0.92"): # 0.92 moved google/tts.py to google_translate/tts.py config_path = hass.config.path(YAML_CONFIG_FILE) @@ -388,7 +388,7 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: except OSError: _LOGGER.exception("Migrating to google_translate tts failed") - if version_obj < LooseVersion("0.94") and is_docker_env(): + if version_obj < AwesomeVersion("0.94") and is_docker_env(): # In 0.94 we no longer install packages inside the deps folder when # running inside a Docker container. lib_path = hass.config.path("deps") From 917a616ce1b29f51f2a536df67a285656c0ece7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 10 Feb 2021 15:58:26 +0100 Subject: [PATCH 331/796] Replace parse_version with AwesomeVersion (#46329) --- homeassistant/components/blueprint/models.py | 4 ++-- homeassistant/components/hyperion/__init__.py | 4 ++-- tests/components/hyperion/__init__.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py index 32fc30b60b9..84931a04310 100644 --- a/homeassistant/components/blueprint/models.py +++ b/homeassistant/components/blueprint/models.py @@ -5,7 +5,7 @@ import pathlib import shutil from typing import Any, Dict, List, Optional, Union -from pkg_resources import parse_version +from awesomeversion import AwesomeVersion import voluptuous as vol from voluptuous.humanize import humanize_error @@ -114,7 +114,7 @@ class Blueprint: metadata = self.metadata min_version = metadata.get(CONF_HOMEASSISTANT, {}).get(CONF_MIN_VERSION) - if min_version is not None and parse_version(__version__) < parse_version( + if min_version is not None and AwesomeVersion(__version__) < AwesomeVersion( min_version ): errors.append(f"Requires at least Home Assistant {min_version}") diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py index b3606880f8c..9e35ae2e6b8 100644 --- a/homeassistant/components/hyperion/__init__.py +++ b/homeassistant/components/hyperion/__init__.py @@ -4,8 +4,8 @@ import asyncio import logging from typing import Any, Callable, Dict, List, Optional, Set, Tuple, cast +from awesomeversion import AwesomeVersion 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.components.switch import DOMAIN as SWITCH_DOMAIN @@ -159,7 +159,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b version = await hyperion_client.async_sysinfo_version() if version is not None: try: - if parse_version(version) < parse_version(HYPERION_VERSION_WARN_CUTOFF): + if AwesomeVersion(version) < AwesomeVersion(HYPERION_VERSION_WARN_CUTOFF): _LOGGER.warning( "Using a Hyperion server version < %s is not recommended -- " "some features may be unavailable or may not function correctly. " diff --git a/tests/components/hyperion/__init__.py b/tests/components/hyperion/__init__.py index f3b2ad383bd..e50de207d00 100644 --- a/tests/components/hyperion/__init__.py +++ b/tests/components/hyperion/__init__.py @@ -98,7 +98,7 @@ 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_sysinfo_version = AsyncMock(return_value=TEST_SYSINFO_VERSION) 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( From 74647e1fa862fcc537494fa384550a5d2c4d1448 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 10 Feb 2021 16:30:16 +0100 Subject: [PATCH 332/796] Add guards for missing value in binary_sensor platform of zwave_js integration (#46293) --- homeassistant/components/zwave_js/binary_sensor.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index 8a483c34e12..8c56869449a 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -268,8 +268,10 @@ class ZWaveBooleanBinarySensor(ZWaveBaseEntity, BinarySensorEntity): self._name = self.generate_name(include_value_name=True) @property - def is_on(self) -> bool: + def is_on(self) -> Optional[bool]: """Return if the sensor is on or off.""" + if self.info.primary_value.value is None: + return None return bool(self.info.primary_value.value) @property @@ -312,8 +314,10 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity): self._mapping_info = self._get_sensor_mapping() @property - def is_on(self) -> bool: + def is_on(self) -> Optional[bool]: """Return if the sensor is on or off.""" + if self.info.primary_value.value is None: + return None return int(self.info.primary_value.value) == int(self.state_key) @property @@ -361,8 +365,10 @@ class ZWavePropertyBinarySensor(ZWaveBaseEntity, BinarySensorEntity): self._name = self.generate_name(include_value_name=True) @property - def is_on(self) -> bool: + def is_on(self) -> Optional[bool]: """Return if the sensor is on or off.""" + if self.info.primary_value.value is None: + return None return self.info.primary_value.value in self._mapping_info["on_states"] @property From 538df17a28269f3b85e1c6a4f0fd0070e1d47e97 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 10 Feb 2021 16:30:29 +0100 Subject: [PATCH 333/796] Restore Google/Alexa extra significant change checks (#46335) --- .../components/alexa/state_report.py | 38 ++++++-- .../google_assistant/report_state.py | 37 ++++++-- homeassistant/helpers/significant_change.py | 92 +++++++++++++------ tests/components/alexa/test_state_report.py | 25 ++++- .../google_assistant/test_report_state.py | 18 ++++ tests/helpers/test_significant_change.py | 30 +++++- 6 files changed, 191 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index aa4110ea686..d66906810b2 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -8,7 +8,7 @@ import aiohttp import async_timeout from homeassistant.const import HTTP_ACCEPTED, MATCH_ALL, STATE_ON -from homeassistant.core import State +from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.significant_change import create_checker import homeassistant.util.dt as dt_util @@ -28,7 +28,20 @@ async def async_enable_proactive_mode(hass, smart_home_config): # Validate we can get access token. await smart_home_config.async_get_access_token() - checker = await create_checker(hass, DOMAIN) + @callback + def extra_significant_check( + hass: HomeAssistant, + old_state: str, + old_attrs: dict, + old_extra_arg: dict, + new_state: str, + new_attrs: dict, + new_extra_arg: dict, + ): + """Check if the serialized data has changed.""" + return old_extra_arg is not None and old_extra_arg != new_extra_arg + + checker = await create_checker(hass, DOMAIN, extra_significant_check) async def async_entity_state_listener( changed_entity: str, @@ -70,15 +83,22 @@ async def async_enable_proactive_mode(hass, smart_home_config): if not should_report and not should_doorbell: return - if not checker.async_is_significant_change(new_state): - return - if should_doorbell: should_report = False + if should_report: + alexa_properties = list(alexa_changed_entity.serialize_properties()) + else: + alexa_properties = None + + if not checker.async_is_significant_change( + new_state, extra_arg=alexa_properties + ): + return + if should_report: await async_send_changereport_message( - hass, smart_home_config, alexa_changed_entity + hass, smart_home_config, alexa_changed_entity, alexa_properties ) elif should_doorbell: @@ -92,7 +112,7 @@ async def async_enable_proactive_mode(hass, smart_home_config): async def async_send_changereport_message( - hass, config, alexa_entity, *, invalidate_access_token=True + hass, config, alexa_entity, alexa_properties, *, invalidate_access_token=True ): """Send a ChangeReport message for an Alexa entity. @@ -107,7 +127,7 @@ async def async_send_changereport_message( payload = { API_CHANGE: { "cause": {"type": Cause.APP_INTERACTION}, - "properties": list(alexa_entity.serialize_properties()), + "properties": alexa_properties, } } @@ -146,7 +166,7 @@ async def async_send_changereport_message( ): config.async_invalidate_access_token() return await async_send_changereport_message( - hass, config, alexa_entity, invalidate_access_token=False + hass, config, alexa_entity, alexa_properties, invalidate_access_token=False ) _LOGGER.error( diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index 8943d4d211e..cdfb06c5c39 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -38,42 +38,59 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig if not entity.is_supported(): return - if not checker.async_is_significant_change(new_state): - return - try: entity_data = entity.query_serialize() except SmartHomeError as err: _LOGGER.debug("Not reporting state for %s: %s", changed_entity, err.code) return + if not checker.async_is_significant_change(new_state, extra_arg=entity_data): + return + _LOGGER.debug("Reporting state for %s: %s", changed_entity, entity_data) await google_config.async_report_state_all( {"devices": {"states": {changed_entity: entity_data}}} ) + @callback + def extra_significant_check( + hass: HomeAssistant, + old_state: str, + old_attrs: dict, + old_extra_arg: dict, + new_state: str, + new_attrs: dict, + new_extra_arg: dict, + ): + """Check if the serialized data has changed.""" + return old_extra_arg != new_extra_arg + async def inital_report(_now): """Report initially all states.""" nonlocal unsub, checker entities = {} - checker = await create_checker(hass, DOMAIN) + checker = await create_checker(hass, DOMAIN, extra_significant_check) for entity in async_get_entities(hass, google_config): if not entity.should_expose(): continue - # Tell our significant change checker that we're reporting - # So it knows with subsequent changes what was already reported. - if not checker.async_is_significant_change(entity.state): - continue - try: - entities[entity.entity_id] = entity.query_serialize() + entity_data = entity.query_serialize() except SmartHomeError: continue + # Tell our significant change checker that we're reporting + # So it knows with subsequent changes what was already reported. + if not checker.async_is_significant_change( + entity.state, extra_arg=entity_data + ): + continue + + entities[entity.entity_id] = entity_data + if not entities: return diff --git a/homeassistant/helpers/significant_change.py b/homeassistant/helpers/significant_change.py index 0a5b6aae10d..694acfcf2bd 100644 --- a/homeassistant/helpers/significant_change.py +++ b/homeassistant/helpers/significant_change.py @@ -27,7 +27,7 @@ The following cases will never be passed to your function: - state adding/removing """ from types import MappingProxyType -from typing import Any, Callable, Dict, Optional, Union +from typing import Any, Callable, Dict, Optional, Tuple, Union from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant, State, callback @@ -47,13 +47,28 @@ CheckTypeFunc = Callable[ Optional[bool], ] +ExtraCheckTypeFunc = Callable[ + [ + HomeAssistant, + str, + Union[dict, MappingProxyType], + Any, + str, + Union[dict, MappingProxyType], + Any, + ], + Optional[bool], +] + async def create_checker( - hass: HomeAssistant, _domain: str + hass: HomeAssistant, + _domain: str, + extra_significant_check: Optional[ExtraCheckTypeFunc] = None, ) -> "SignificantlyChangedChecker": """Create a significantly changed checker for a domain.""" await _initialize(hass) - return SignificantlyChangedChecker(hass) + return SignificantlyChangedChecker(hass, extra_significant_check) # Marked as singleton so multiple calls all wait for same output. @@ -105,34 +120,46 @@ class SignificantlyChangedChecker: Will always compare the entity to the last entity that was considered significant. """ - def __init__(self, hass: HomeAssistant) -> None: + def __init__( + self, + hass: HomeAssistant, + extra_significant_check: Optional[ExtraCheckTypeFunc] = None, + ) -> None: """Test if an entity has significantly changed.""" self.hass = hass - self.last_approved_entities: Dict[str, State] = {} + self.last_approved_entities: Dict[str, Tuple[State, Any]] = {} + self.extra_significant_check = extra_significant_check @callback - def async_is_significant_change(self, new_state: State) -> bool: - """Return if this was a significant change.""" - old_state: Optional[State] = self.last_approved_entities.get( + def async_is_significant_change( + self, new_state: State, *, extra_arg: Optional[Any] = None + ) -> bool: + """Return if this was a significant change. + + Extra kwargs are passed to the extra significant checker. + """ + old_data: Optional[Tuple[State, Any]] = self.last_approved_entities.get( new_state.entity_id ) # First state change is always ok to report - if old_state is None: - self.last_approved_entities[new_state.entity_id] = new_state + if old_data is None: + self.last_approved_entities[new_state.entity_id] = (new_state, extra_arg) return True + old_state, old_extra_arg = old_data + # Handle state unknown or unavailable if new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE): if new_state.state == old_state.state: return False - self.last_approved_entities[new_state.entity_id] = new_state + self.last_approved_entities[new_state.entity_id] = (new_state, extra_arg) return True # If last state was unknown/unavailable, also significant. if old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE): - self.last_approved_entities[new_state.entity_id] = new_state + self.last_approved_entities[new_state.entity_id] = (new_state, extra_arg) return True functions: Optional[Dict[str, CheckTypeFunc]] = self.hass.data.get( @@ -144,23 +171,36 @@ class SignificantlyChangedChecker: check_significantly_changed = functions.get(new_state.domain) - # No platform available means always true. - if check_significantly_changed is None: - self.last_approved_entities[new_state.entity_id] = new_state - return True + if check_significantly_changed is not None: + result = check_significantly_changed( + self.hass, + old_state.state, + old_state.attributes, + new_state.state, + new_state.attributes, + ) - result = check_significantly_changed( - self.hass, - old_state.state, - old_state.attributes, - new_state.state, - new_state.attributes, - ) + if result is False: + return False - if result is False: - return False + if self.extra_significant_check is not None: + result = self.extra_significant_check( + self.hass, + old_state.state, + old_state.attributes, + old_extra_arg, + new_state.state, + new_state.attributes, + extra_arg, + ) + + if result is False: + return False # Result is either True or None. # None means the function doesn't know. For now assume it's True - self.last_approved_entities[new_state.entity_id] = new_state + self.last_approved_entities[new_state.entity_id] = ( + new_state, + extra_arg, + ) return True diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index 809bca5638b..a057eada531 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -178,6 +178,7 @@ async def test_doorbell_event(hass, aioclient_mock): async def test_proactive_mode_filter_states(hass, aioclient_mock): """Test all the cases that filter states.""" + aioclient_mock.post(TEST_URL, text="", status=202) await state_report.async_enable_proactive_mode(hass, DEFAULT_CONFIG) # First state should report @@ -186,7 +187,8 @@ async def test_proactive_mode_filter_states(hass, aioclient_mock): "on", {"friendly_name": "Test Contact Sensor", "device_class": "door"}, ) - assert len(aioclient_mock.mock_calls) == 0 + await hass.async_block_till_done() + assert len(aioclient_mock.mock_calls) == 1 aioclient_mock.clear_requests() @@ -238,3 +240,24 @@ async def test_proactive_mode_filter_states(hass, aioclient_mock): await hass.async_block_till_done() await hass.async_block_till_done() assert len(aioclient_mock.mock_calls) == 0 + + # If serializes to same properties, it should not report + aioclient_mock.post(TEST_URL, text="", status=202) + with patch( + "homeassistant.components.alexa.entities.AlexaEntity.serialize_properties", + return_value=[{"same": "info"}], + ): + hass.states.async_set( + "binary_sensor.same_serialize", + "off", + {"friendly_name": "Test Contact Sensor", "device_class": "door"}, + ) + await hass.async_block_till_done() + hass.states.async_set( + "binary_sensor.same_serialize", + "off", + {"friendly_name": "Test Contact Sensor", "device_class": "door"}, + ) + + await hass.async_block_till_done() + assert len(aioclient_mock.mock_calls) == 1 diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py index 72130dbfdb9..f464be60bb9 100644 --- a/tests/components/google_assistant/test_report_state.py +++ b/tests/components/google_assistant/test_report_state.py @@ -46,6 +46,24 @@ async def test_report_state(hass, caplog, legacy_patchable_time): "devices": {"states": {"light.kitchen": {"on": True, "online": True}}} } + # Test that if serialize returns same value, we don't send + with patch( + "homeassistant.components.google_assistant.report_state.GoogleEntity.query_serialize", + return_value={"same": "info"}, + ), patch.object(BASIC_CONFIG, "async_report_state_all", AsyncMock()) as mock_report: + # New state, so reported + hass.states.async_set("light.double_report", "on") + await hass.async_block_till_done() + + # Changed, but serialize is same, so filtered out by extra check + hass.states.async_set("light.double_report", "off") + await hass.async_block_till_done() + + assert len(mock_report.mock_calls) == 1 + assert mock_report.mock_calls[0][1][0] == { + "devices": {"states": {"light.double_report": {"same": "info"}}} + } + # Test that only significant state changes are reported with patch.object( BASIC_CONFIG, "async_report_state_all", AsyncMock() diff --git a/tests/helpers/test_significant_change.py b/tests/helpers/test_significant_change.py index e72951d36dd..79f3dd3fe3e 100644 --- a/tests/helpers/test_significant_change.py +++ b/tests/helpers/test_significant_change.py @@ -5,7 +5,6 @@ from homeassistant.components.sensor import DEVICE_CLASS_BATTERY from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import State from homeassistant.helpers import significant_change -from homeassistant.setup import async_setup_component @pytest.fixture(name="checker") @@ -26,8 +25,6 @@ async def checker_fixture(hass): async def test_signicant_change(hass, checker): """Test initialize helper works.""" - assert await async_setup_component(hass, "sensor", {}) - ent_id = "test_domain.test_entity" attrs = {ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY} @@ -48,3 +45,30 @@ async def test_signicant_change(hass, checker): # State turned unavailable assert checker.async_is_significant_change(State(ent_id, "100", attrs)) assert checker.async_is_significant_change(State(ent_id, STATE_UNAVAILABLE, attrs)) + + +async def test_significant_change_extra(hass, checker): + """Test extra significant checker works.""" + ent_id = "test_domain.test_entity" + attrs = {ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY} + + assert checker.async_is_significant_change(State(ent_id, "100", attrs), extra_arg=1) + assert checker.async_is_significant_change(State(ent_id, "200", attrs), extra_arg=1) + + # Reset the last significiant change to 100 to repeat test but with + # extra checker installed. + assert checker.async_is_significant_change(State(ent_id, "100", attrs), extra_arg=1) + + def extra_significant_check( + hass, old_state, old_attrs, old_extra_arg, new_state, new_attrs, new_extra_arg + ): + return old_extra_arg != new_extra_arg + + checker.extra_significant_check = extra_significant_check + + # This is normally a significant change (100 -> 200), but the extra arg check marks it + # as insignificant. + assert not checker.async_is_significant_change( + State(ent_id, "200", attrs), extra_arg=1 + ) + assert checker.async_is_significant_change(State(ent_id, "200", attrs), extra_arg=2) From cdd78316c440750c7dc4423244f4bd81d6027151 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Wed, 10 Feb 2021 10:01:24 -0800 Subject: [PATCH 334/796] Use oauthv3 for Tesla (#45766) --- homeassistant/components/tesla/__init__.py | 4 ++++ homeassistant/components/tesla/config_flow.py | 2 ++ homeassistant/components/tesla/manifest.json | 8 ++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tesla/test_config_flow.py | 2 ++ 6 files changed, 14 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index 51090d34271..8981b269a56 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -103,6 +103,8 @@ async def async_setup(hass, base_config): _update_entry( email, data={ + CONF_USERNAME: email, + CONF_PASSWORD: password, CONF_ACCESS_TOKEN: info[CONF_ACCESS_TOKEN], CONF_TOKEN: info[CONF_TOKEN], }, @@ -136,6 +138,8 @@ async def async_setup_entry(hass, config_entry): try: controller = TeslaAPI( websession, + email=config.get(CONF_USERNAME), + password=config.get(CONF_PASSWORD), refresh_token=config[CONF_TOKEN], access_token=config[CONF_ACCESS_TOKEN], update_interval=config_entry.options.get( diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py index debe896c9cf..683ef314a06 100644 --- a/homeassistant/components/tesla/config_flow.py +++ b/homeassistant/components/tesla/config_flow.py @@ -140,6 +140,8 @@ async def validate_input(hass: core.HomeAssistant, data): (config[CONF_TOKEN], config[CONF_ACCESS_TOKEN]) = await controller.connect( test_login=True ) + config[CONF_USERNAME] = data[CONF_USERNAME] + config[CONF_PASSWORD] = data[CONF_PASSWORD] except TeslaException as ex: if ex.code == HTTP_UNAUTHORIZED: _LOGGER.error("Invalid credentials: %s", ex) diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index 3679c0f74d1..9236aae7fb6 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -3,11 +3,11 @@ "name": "Tesla", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tesla", - "requirements": ["teslajsonpy==0.10.4"], + "requirements": ["teslajsonpy==0.11.5"], "codeowners": ["@zabuldon", "@alandtse"], "dhcp": [ - {"hostname":"tesla_*","macaddress":"4CFCAA*"}, - {"hostname":"tesla_*","macaddress":"044EAF*"}, - {"hostname":"tesla_*","macaddress":"98ED5C*"} + { "hostname": "tesla_*", "macaddress": "4CFCAA*" }, + { "hostname": "tesla_*", "macaddress": "044EAF*" }, + { "hostname": "tesla_*", "macaddress": "98ED5C*" } ] } diff --git a/requirements_all.txt b/requirements_all.txt index 202049341e5..d0959d87e8f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2184,7 +2184,7 @@ temperusb==1.5.3 tesla-powerwall==0.3.3 # homeassistant.components.tesla -teslajsonpy==0.10.4 +teslajsonpy==0.11.5 # homeassistant.components.tensorflow # tf-models-official==2.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 143ec43a5d4..ec4bce771f6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1105,7 +1105,7 @@ tellduslive==0.10.11 tesla-powerwall==0.3.3 # homeassistant.components.tesla -teslajsonpy==0.10.4 +teslajsonpy==0.11.5 # homeassistant.components.toon toonapi==0.2.0 diff --git a/tests/components/tesla/test_config_flow.py b/tests/components/tesla/test_config_flow.py index 7fb308ecc43..136633c9a5c 100644 --- a/tests/components/tesla/test_config_flow.py +++ b/tests/components/tesla/test_config_flow.py @@ -48,6 +48,8 @@ async def test_form(hass): assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["title"] == "test@email.com" assert result2["data"] == { + CONF_USERNAME: "test@email.com", + CONF_PASSWORD: "test", CONF_TOKEN: "test-refresh-token", CONF_ACCESS_TOKEN: "test-access-token", } From 2db102e0239ae937b58c0e68b36260a50da78835 Mon Sep 17 00:00:00 2001 From: Leonardo Figueiro Date: Wed, 10 Feb 2021 16:08:39 -0300 Subject: [PATCH 335/796] Add WiLight Cover (#46065) Co-authored-by: J. Nick Koston --- homeassistant/components/wilight/__init__.py | 5 +- .../components/wilight/config_flow.py | 4 +- homeassistant/components/wilight/const.py | 14 -- homeassistant/components/wilight/cover.py | 105 ++++++++++++++ homeassistant/components/wilight/light.py | 17 +-- .../components/wilight/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/wilight/__init__.py | 13 +- tests/components/wilight/test_config_flow.py | 2 +- tests/components/wilight/test_cover.py | 136 ++++++++++++++++++ tests/components/wilight/test_init.py | 2 +- 12 files changed, 264 insertions(+), 40 deletions(-) delete mode 100644 homeassistant/components/wilight/const.py create mode 100644 homeassistant/components/wilight/cover.py create mode 100644 tests/components/wilight/test_cover.py diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py index 97b48257103..0e08fec2c31 100644 --- a/homeassistant/components/wilight/__init__.py +++ b/homeassistant/components/wilight/__init__.py @@ -1,16 +1,17 @@ """The WiLight integration.""" import asyncio +from pywilight.const import DOMAIN + from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.entity import Entity -from .const import DOMAIN from .parent_device import WiLightParent # List the platforms that you want to support. -PLATFORMS = ["fan", "light"] +PLATFORMS = ["cover", "fan", "light"] async def async_setup(hass: HomeAssistant, config: dict): diff --git a/homeassistant/components/wilight/config_flow.py b/homeassistant/components/wilight/config_flow.py index 3f1b12395ba..c3148a4d045 100644 --- a/homeassistant/components/wilight/config_flow.py +++ b/homeassistant/components/wilight/config_flow.py @@ -7,7 +7,7 @@ from homeassistant.components import ssdp from homeassistant.config_entries import CONN_CLASS_LOCAL_PUSH, ConfigFlow from homeassistant.const import CONF_HOST -from .const import DOMAIN # pylint: disable=unused-import +DOMAIN = "wilight" CONF_SERIAL_NUMBER = "serial_number" CONF_MODEL_NAME = "model_name" @@ -15,7 +15,7 @@ CONF_MODEL_NAME = "model_name" WILIGHT_MANUFACTURER = "All Automacao Ltda" # List the components supported by this integration. -ALLOWED_WILIGHT_COMPONENTS = ["light", "fan"] +ALLOWED_WILIGHT_COMPONENTS = ["cover", "fan", "light"] class WiLightFlowHandler(ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/wilight/const.py b/homeassistant/components/wilight/const.py deleted file mode 100644 index a3d77da44ef..00000000000 --- a/homeassistant/components/wilight/const.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Constants for the WiLight integration.""" - -DOMAIN = "wilight" - -# Item types -ITEM_LIGHT = "light" - -# Light types -LIGHT_ON_OFF = "light_on_off" -LIGHT_DIMMER = "light_dimmer" -LIGHT_COLOR = "light_rgb" - -# Light service support -SUPPORT_NONE = 0 diff --git a/homeassistant/components/wilight/cover.py b/homeassistant/components/wilight/cover.py new file mode 100644 index 00000000000..bbe723b413a --- /dev/null +++ b/homeassistant/components/wilight/cover.py @@ -0,0 +1,105 @@ +"""Support for WiLight Cover.""" + +from pywilight.const import ( + COVER_V1, + DOMAIN, + ITEM_COVER, + WL_CLOSE, + WL_CLOSING, + WL_OPEN, + WL_OPENING, + WL_STOP, + WL_STOPPED, +) + +from homeassistant.components.cover import ATTR_POSITION, CoverEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from . import WiLightDevice + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): + """Set up WiLight covers from a config entry.""" + parent = hass.data[DOMAIN][entry.entry_id] + + # Handle a discovered WiLight device. + entities = [] + for item in parent.api.items: + if item["type"] != ITEM_COVER: + continue + index = item["index"] + item_name = item["name"] + if item["sub_type"] != COVER_V1: + continue + entity = WiLightCover(parent.api, index, item_name) + entities.append(entity) + + async_add_entities(entities) + + +def wilight_to_hass_position(value): + """Convert wilight position 1..255 to hass format 0..100.""" + return min(100, round((value * 100) / 255)) + + +def hass_to_wilight_position(value): + """Convert hass position 0..100 to wilight 1..255 scale.""" + return min(255, round((value * 255) / 100)) + + +class WiLightCover(WiLightDevice, CoverEntity): + """Representation of a WiLights cover.""" + + @property + def current_cover_position(self): + """Return current position of cover. + + None is unknown, 0 is closed, 100 is fully open. + """ + if "position_current" in self._status: + return wilight_to_hass_position(self._status["position_current"]) + return None + + @property + def is_opening(self): + """Return if the cover is opening or not.""" + if "motor_state" not in self._status: + return None + return self._status["motor_state"] == WL_OPENING + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + if "motor_state" not in self._status: + return None + return self._status["motor_state"] == WL_CLOSING + + @property + def is_closed(self): + """Return if the cover is closed or not.""" + if "motor_state" not in self._status or "position_current" not in self._status: + return None + return ( + self._status["motor_state"] == WL_STOPPED + and wilight_to_hass_position(self._status["position_current"]) == 0 + ) + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + await self._client.cover_command(self._index, WL_OPEN) + + async def async_close_cover(self, **kwargs): + """Close cover.""" + await self._client.cover_command(self._index, WL_CLOSE) + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + position = hass_to_wilight_position(kwargs[ATTR_POSITION]) + await self._client.set_cover_position(self._index, position) + + async def async_stop_cover(self, **kwargs): + """Stop the cover.""" + await self._client.cover_command(self._index, WL_STOP) diff --git a/homeassistant/components/wilight/light.py b/homeassistant/components/wilight/light.py index e4bf504165d..2f3c9e3c5f2 100644 --- a/homeassistant/components/wilight/light.py +++ b/homeassistant/components/wilight/light.py @@ -1,5 +1,14 @@ """Support for WiLight lights.""" +from pywilight.const import ( + DOMAIN, + ITEM_LIGHT, + LIGHT_COLOR, + LIGHT_DIMMER, + LIGHT_ON_OFF, + SUPPORT_NONE, +) + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, @@ -11,14 +20,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from . import WiLightDevice -from .const import ( - DOMAIN, - ITEM_LIGHT, - LIGHT_COLOR, - LIGHT_DIMMER, - LIGHT_ON_OFF, - SUPPORT_NONE, -) def entities_from_discovered_wilight(hass, api_device): diff --git a/homeassistant/components/wilight/manifest.json b/homeassistant/components/wilight/manifest.json index c9f4fb049fc..5b8a93c6039 100644 --- a/homeassistant/components/wilight/manifest.json +++ b/homeassistant/components/wilight/manifest.json @@ -3,7 +3,7 @@ "name": "WiLight", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wilight", - "requirements": ["pywilight==0.0.66"], + "requirements": ["pywilight==0.0.68"], "ssdp": [ { "manufacturer": "All Automacao Ltda" diff --git a/requirements_all.txt b/requirements_all.txt index d0959d87e8f..4c890be38df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1901,7 +1901,7 @@ pywebpush==1.9.2 pywemo==0.6.1 # homeassistant.components.wilight -pywilight==0.0.66 +pywilight==0.0.68 # homeassistant.components.xeoma pyxeoma==1.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ec4bce771f6..4f7c0344da7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -971,7 +971,7 @@ pywebpush==1.9.2 pywemo==0.6.1 # homeassistant.components.wilight -pywilight==0.0.66 +pywilight==0.0.68 # homeassistant.components.zerproc pyzerproc==0.4.7 diff --git a/tests/components/wilight/__init__.py b/tests/components/wilight/__init__.py index e1c31345235..7ee7f0119a4 100644 --- a/tests/components/wilight/__init__.py +++ b/tests/components/wilight/__init__.py @@ -1,4 +1,7 @@ """Tests for the WiLight component.""" + +from pywilight.const import DOMAIN + from homeassistant.components.ssdp import ( ATTR_SSDP_LOCATION, ATTR_UPNP_MANUFACTURER, @@ -10,7 +13,6 @@ from homeassistant.components.wilight.config_flow import ( CONF_MODEL_NAME, CONF_SERIAL_NUMBER, ) -from homeassistant.components.wilight.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.helpers.typing import HomeAssistantType @@ -24,6 +26,7 @@ UPNP_MODEL_NAME_P_B = "WiLight 0102001800010009-10010010" UPNP_MODEL_NAME_DIMMER = "WiLight 0100001700020009-10010010" UPNP_MODEL_NAME_COLOR = "WiLight 0107001800020009-11010" UPNP_MODEL_NAME_LIGHT_FAN = "WiLight 0104001800010009-10" +UPNP_MODEL_NAME_COVER = "WiLight 0103001800010009-10" UPNP_MODEL_NUMBER = "123456789012345678901234567890123456" UPNP_SERIAL = "000000000099" UPNP_MAC_ADDRESS = "5C:CF:7F:8B:CA:56" @@ -53,14 +56,6 @@ MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTORER = { ATTR_UPNP_SERIAL: ATTR_UPNP_SERIAL, } -MOCK_SSDP_DISCOVERY_INFO_LIGHT_FAN = { - ATTR_SSDP_LOCATION: SSDP_LOCATION, - ATTR_UPNP_MANUFACTURER: UPNP_MANUFACTURER, - ATTR_UPNP_MODEL_NAME: UPNP_MODEL_NAME_LIGHT_FAN, - ATTR_UPNP_MODEL_NUMBER: UPNP_MODEL_NUMBER, - ATTR_UPNP_SERIAL: ATTR_UPNP_SERIAL, -} - async def setup_integration( hass: HomeAssistantType, diff --git a/tests/components/wilight/test_config_flow.py b/tests/components/wilight/test_config_flow.py index d44780092ec..9888dbe3ef9 100644 --- a/tests/components/wilight/test_config_flow.py +++ b/tests/components/wilight/test_config_flow.py @@ -2,12 +2,12 @@ from unittest.mock import patch import pytest +from pywilight.const import DOMAIN from homeassistant.components.wilight.config_flow import ( CONF_MODEL_NAME, CONF_SERIAL_NUMBER, ) -from homeassistant.components.wilight.const import DOMAIN from homeassistant.config_entries import SOURCE_SSDP from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.data_entry_flow import ( diff --git a/tests/components/wilight/test_cover.py b/tests/components/wilight/test_cover.py new file mode 100644 index 00000000000..85f62c9d120 --- /dev/null +++ b/tests/components/wilight/test_cover.py @@ -0,0 +1,136 @@ +"""Tests for the WiLight integration.""" +from unittest.mock import patch + +import pytest +import pywilight + +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_POSITION, + DOMAIN as COVER_DOMAIN, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, + SERVICE_STOP_COVER, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + HOST, + UPNP_MAC_ADDRESS, + UPNP_MODEL_NAME_COVER, + UPNP_MODEL_NUMBER, + UPNP_SERIAL, + WILIGHT_ID, + setup_integration, +) + + +@pytest.fixture(name="dummy_device_from_host_cover") +def mock_dummy_device_from_host_light_fan(): + """Mock a valid api_devce.""" + + device = pywilight.wilight_from_discovery( + f"http://{HOST}:45995/wilight.xml", + UPNP_MAC_ADDRESS, + UPNP_MODEL_NAME_COVER, + UPNP_SERIAL, + UPNP_MODEL_NUMBER, + ) + + device.set_dummy(True) + + with patch( + "pywilight.device_from_host", + return_value=device, + ): + yield device + + +async def test_loading_cover( + hass: HomeAssistantType, + dummy_device_from_host_cover, +) -> None: + """Test the WiLight configuration entry loading.""" + + entry = await setup_integration(hass) + assert entry + assert entry.unique_id == WILIGHT_ID + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + # First segment of the strip + state = hass.states.get("cover.wl000000000099_1") + assert state + assert state.state == STATE_CLOSED + + entry = entity_registry.async_get("cover.wl000000000099_1") + assert entry + assert entry.unique_id == "WL000000000099_0" + + +async def test_open_close_cover_state( + hass: HomeAssistantType, dummy_device_from_host_cover +) -> None: + """Test the change of state of the cover.""" + await setup_integration(hass) + + # Open + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.wl000000000099_1"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("cover.wl000000000099_1") + assert state + assert state.state == STATE_OPENING + + # Close + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.wl000000000099_1"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("cover.wl000000000099_1") + assert state + assert state.state == STATE_CLOSING + + # Set position + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_POSITION: 50, ATTR_ENTITY_ID: "cover.wl000000000099_1"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("cover.wl000000000099_1") + assert state + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_CURRENT_POSITION) == 50 + + # Stop + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: "cover.wl000000000099_1"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("cover.wl000000000099_1") + assert state + assert state.state == STATE_OPEN diff --git a/tests/components/wilight/test_init.py b/tests/components/wilight/test_init.py index c1557fb44d3..1441564b640 100644 --- a/tests/components/wilight/test_init.py +++ b/tests/components/wilight/test_init.py @@ -3,8 +3,8 @@ from unittest.mock import patch import pytest import pywilight +from pywilight.const import DOMAIN -from homeassistant.components.wilight.const import DOMAIN from homeassistant.config_entries import ( ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED, From fc4fc487635373e14fc79856092b2766780035df Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 10 Feb 2021 20:42:34 +0100 Subject: [PATCH 336/796] Bump hatasmota to 0.2.8 (#46340) --- 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 bd48cae8e59..12604d3ed81 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.2.7"], + "requirements": ["hatasmota==0.2.8"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"] diff --git a/requirements_all.txt b/requirements_all.txt index 4c890be38df..10f81b8813d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -735,7 +735,7 @@ hass-nabucasa==0.41.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.2.7 +hatasmota==0.2.8 # homeassistant.components.jewish_calendar hdate==0.9.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f7c0344da7..a9f2dab232d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -387,7 +387,7 @@ hangups==0.4.11 hass-nabucasa==0.41.0 # homeassistant.components.tasmota -hatasmota==0.2.7 +hatasmota==0.2.8 # homeassistant.components.jewish_calendar hdate==0.9.12 From 2e2eab662b5d3780a17647c4644b4519bd67a633 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Feb 2021 09:48:15 -1000 Subject: [PATCH 337/796] Fix Lutron Integration Protocol reconnect logic (#46264) --- homeassistant/components/lutron_caseta/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index 34ab75dc0cd..88c6eddd0bf 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -3,7 +3,7 @@ "name": "Lutron Caséta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", "requirements": [ - "pylutron-caseta==0.9.0", "aiolip==1.0.1" + "pylutron-caseta==0.9.0", "aiolip==1.1.4" ], "config_flow": true, "zeroconf": ["_leap._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index 10f81b8813d..8a3d5e2b5c8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -191,7 +191,7 @@ aiolifx==0.6.9 aiolifx_effects==0.2.2 # homeassistant.components.lutron_caseta -aiolip==1.0.1 +aiolip==1.1.4 # homeassistant.components.lyric aiolyric==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a9f2dab232d..d55d406cdb0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -113,7 +113,7 @@ aiohue==2.1.0 aiokafka==0.6.0 # homeassistant.components.lutron_caseta -aiolip==1.0.1 +aiolip==1.1.4 # homeassistant.components.lyric aiolyric==1.0.5 From 884df409517bcc126bc8d3937493c69e6c454a5c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Feb 2021 09:50:38 -1000 Subject: [PATCH 338/796] Update powerwall for new authentication requirements (#46254) Co-authored-by: badguy99 <61918526+badguy99@users.noreply.github.com> --- .../components/powerwall/__init__.py | 82 ++++++++++++---- .../components/powerwall/config_flow.py | 58 ++++++++---- .../components/powerwall/manifest.json | 2 +- .../components/powerwall/strings.json | 10 +- .../components/powerwall/translations/en.json | 10 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/powerwall/test_config_flow.py | 94 ++++++++++++++++--- 8 files changed, 201 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 54b7310b7ad..b392b713741 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -4,10 +4,15 @@ from datetime import timedelta import logging import requests -from tesla_powerwall import MissingAttributeError, Powerwall, PowerwallUnreachableError +from tesla_powerwall import ( + AccessDeniedError, + MissingAttributeError, + Powerwall, + PowerwallUnreachableError, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import entity_registry @@ -93,11 +98,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN].setdefault(entry_id, {}) http_session = requests.Session() + + password = entry.data.get(CONF_PASSWORD) power_wall = Powerwall(entry.data[CONF_IP_ADDRESS], http_session=http_session) try: - await hass.async_add_executor_job(power_wall.detect_and_pin_version) - await hass.async_add_executor_job(_fetch_powerwall_data, power_wall) - powerwall_data = await hass.async_add_executor_job(call_base_info, power_wall) + powerwall_data = await hass.async_add_executor_job( + _login_and_fetch_base_info, power_wall, password + ) except PowerwallUnreachableError as err: http_session.close() raise ConfigEntryNotReady from err @@ -105,6 +112,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): http_session.close() await _async_handle_api_changed_error(hass, err) return False + except AccessDeniedError as err: + _LOGGER.debug("Authentication failed", exc_info=err) + http_session.close() + _async_start_reauth(hass, entry) + return False await _migrate_old_unique_ids(hass, entry_id, powerwall_data) @@ -112,22 +124,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Fetch data from API endpoint.""" # Check if we had an error before _LOGGER.debug("Checking if update failed") - if not hass.data[DOMAIN][entry.entry_id][POWERWALL_API_CHANGED]: - _LOGGER.debug("Updating data") - try: - return await hass.async_add_executor_job( - _fetch_powerwall_data, power_wall - ) - except PowerwallUnreachableError as err: - raise UpdateFailed("Unable to fetch data from powerwall") from err - except MissingAttributeError as err: - await _async_handle_api_changed_error(hass, err) - hass.data[DOMAIN][entry.entry_id][POWERWALL_API_CHANGED] = True - # Returns the cached data. This data can also be None - return hass.data[DOMAIN][entry.entry_id][POWERWALL_COORDINATOR].data - else: + if hass.data[DOMAIN][entry.entry_id][POWERWALL_API_CHANGED]: return hass.data[DOMAIN][entry.entry_id][POWERWALL_COORDINATOR].data + _LOGGER.debug("Updating data") + try: + return await _async_update_powerwall_data(hass, entry, power_wall) + except AccessDeniedError: + if password is None: + raise + + # If the session expired, relogin, and try again + await hass.async_add_executor_job(power_wall.login, "", password) + return await _async_update_powerwall_data(hass, entry, power_wall) + coordinator = DataUpdateCoordinator( hass, _LOGGER, @@ -156,6 +166,40 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return True +async def _async_update_powerwall_data( + hass: HomeAssistant, entry: ConfigEntry, power_wall: Powerwall +): + """Fetch updated powerwall data.""" + try: + return await hass.async_add_executor_job(_fetch_powerwall_data, power_wall) + except PowerwallUnreachableError as err: + raise UpdateFailed("Unable to fetch data from powerwall") from err + except MissingAttributeError as err: + await _async_handle_api_changed_error(hass, err) + hass.data[DOMAIN][entry.entry_id][POWERWALL_API_CHANGED] = True + # Returns the cached data. This data can also be None + return hass.data[DOMAIN][entry.entry_id][POWERWALL_COORDINATOR].data + + +def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "reauth"}, + data=entry.data, + ) + ) + _LOGGER.error("Password is no longer valid. Please reauthenticate") + + +def _login_and_fetch_base_info(power_wall: Powerwall, password: str): + """Login to the powerwall and fetch the base info.""" + if password is not None: + power_wall.login("", password) + power_wall.detect_and_pin_version() + return call_base_info(power_wall) + + def call_base_info(power_wall): """Wrap powerwall properties to be a callable.""" serial_numbers = power_wall.get_serial_numbers() diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index 37ee2730bb4..b649b160085 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -1,12 +1,17 @@ """Config flow for Tesla Powerwall integration.""" import logging -from tesla_powerwall import MissingAttributeError, Powerwall, PowerwallUnreachableError +from tesla_powerwall import ( + AccessDeniedError, + MissingAttributeError, + Powerwall, + PowerwallUnreachableError, +) import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.components.dhcp import IP_ADDRESS -from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.core import callback from .const import DOMAIN # pylint:disable=unused-import @@ -14,6 +19,14 @@ from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) +def _login_and_fetch_site_info(power_wall: Powerwall, password: str): + """Login to the powerwall and fetch the base info.""" + if password is not None: + power_wall.login("", password) + power_wall.detect_and_pin_version() + return power_wall.get_site_info() + + async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect. @@ -21,12 +34,12 @@ async def validate_input(hass: core.HomeAssistant, data): """ power_wall = Powerwall(data[CONF_IP_ADDRESS]) + password = data[CONF_PASSWORD] try: - await hass.async_add_executor_job(power_wall.detect_and_pin_version) - site_info = await hass.async_add_executor_job(power_wall.get_site_info) - except PowerwallUnreachableError as err: - raise CannotConnect from err + site_info = await hass.async_add_executor_job( + _login_and_fetch_site_info, power_wall, password + ) except MissingAttributeError as err: # Only log the exception without the traceback _LOGGER.error(str(err)) @@ -62,27 +75,44 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: try: info = await validate_input(self.hass, user_input) - except CannotConnect: - errors["base"] = "cannot_connect" + except PowerwallUnreachableError: + errors[CONF_IP_ADDRESS] = "cannot_connect" except WrongVersion: errors["base"] = "wrong_version" + except AccessDeniedError: + errors[CONF_PASSWORD] = "invalid_auth" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" - if "base" not in errors: - await self.async_set_unique_id(user_input[CONF_IP_ADDRESS]) - self._abort_if_unique_id_configured() + if not errors: + existing_entry = await self.async_set_unique_id( + user_input[CONF_IP_ADDRESS] + ) + if existing_entry: + self.hass.config_entries.async_update_entry( + existing_entry, data=user_input + ) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") return self.async_create_entry(title=info["title"], data=user_input) return self.async_show_form( step_id="user", data_schema=vol.Schema( - {vol.Required(CONF_IP_ADDRESS, default=self.ip_address): str} + { + vol.Required(CONF_IP_ADDRESS, default=self.ip_address): str, + vol.Optional(CONF_PASSWORD): str, + } ), errors=errors, ) + async def async_step_reauth(self, data): + """Handle configuration by re-auth.""" + self.ip_address = data[CONF_IP_ADDRESS] + return await self.async_step_user() + @callback def _async_ip_address_already_configured(self, ip_address): """See if we already have an entry matching the ip_address.""" @@ -92,9 +122,5 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return False -class CannotConnect(exceptions.HomeAssistantError): - """Error to indicate we cannot connect.""" - - class WrongVersion(exceptions.HomeAssistantError): """Error to indicate the powerwall uses a software version we cannot interact with.""" diff --git a/homeassistant/components/powerwall/manifest.json b/homeassistant/components/powerwall/manifest.json index 6b7b147d3c5..40d0a6c50fe 100644 --- a/homeassistant/components/powerwall/manifest.json +++ b/homeassistant/components/powerwall/manifest.json @@ -3,7 +3,7 @@ "name": "Tesla Powerwall", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/powerwall", - "requirements": ["tesla-powerwall==0.3.3"], + "requirements": ["tesla-powerwall==0.3.5"], "codeowners": ["@bdraco", "@jrester"], "dhcp": [ {"hostname":"1118431-*","macaddress":"88DA1A*"}, diff --git a/homeassistant/components/powerwall/strings.json b/homeassistant/components/powerwall/strings.json index ac0d9568154..c576d931756 100644 --- a/homeassistant/components/powerwall/strings.json +++ b/homeassistant/components/powerwall/strings.json @@ -4,18 +4,22 @@ "step": { "user": { "title": "Connect to the powerwall", + "description": "The password is usually the last 5 characters of the serial number for Backup Gateway and can be found in the Telsa app; or the last 5 characters of the password found inside the door for Backup Gateway 2.", "data": { - "ip_address": "[%key:common::config_flow::data::ip%]" + "ip_address": "[%key:common::config_flow::data::ip%]", + "password": "[%key:common::config_flow::data::password%]" } } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "wrong_version": "Your powerwall uses a software version that is not supported. Please consider upgrading or reporting this issue so it can be resolved.", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } } diff --git a/homeassistant/components/powerwall/translations/en.json b/homeassistant/components/powerwall/translations/en.json index 6eb0b77708d..4ebe1e9d5ef 100644 --- a/homeassistant/components/powerwall/translations/en.json +++ b/homeassistant/components/powerwall/translations/en.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", "unknown": "Unexpected error", "wrong_version": "Your powerwall uses a software version that is not supported. Please consider upgrading or reporting this issue so it can be resolved." }, @@ -12,10 +14,12 @@ "step": { "user": { "data": { - "ip_address": "IP Address" + "ip_address": "IP Address", + "password": "Password" }, + "description": "The password is usually the last 5 characters of the serial number for Backup Gateway and can be found in the Telsa app; or the last 5 characters of the password found inside the door for Backup Gateway 2.", "title": "Connect to the powerwall" } } } -} \ No newline at end of file +} diff --git a/requirements_all.txt b/requirements_all.txt index 8a3d5e2b5c8..ee86d62270a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2181,7 +2181,7 @@ temperusb==1.5.3 # tensorflow==2.3.0 # homeassistant.components.powerwall -tesla-powerwall==0.3.3 +tesla-powerwall==0.3.5 # homeassistant.components.tesla teslajsonpy==0.11.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d55d406cdb0..5d63ba30e1c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1102,7 +1102,7 @@ synologydsm-api==1.0.1 tellduslive==0.10.11 # homeassistant.components.powerwall -tesla-powerwall==0.3.3 +tesla-powerwall==0.3.5 # homeassistant.components.tesla teslajsonpy==0.11.5 diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index 0955c16c9ec..be071b45947 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -2,17 +2,23 @@ from unittest.mock import patch -from tesla_powerwall import MissingAttributeError, PowerwallUnreachableError +from tesla_powerwall import ( + AccessDeniedError, + MissingAttributeError, + PowerwallUnreachableError, +) from homeassistant import config_entries, setup from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.components.powerwall.const import DOMAIN -from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from .mocks import _mock_powerwall_side_effect, _mock_powerwall_site_name from tests.common import MockConfigEntry +VALID_CONFIG = {CONF_IP_ADDRESS: "1.2.3.4", CONF_PASSWORD: "00GGX"} + async def test_form_source_user(hass): """Test we get config flow setup form as a user.""" @@ -36,13 +42,13 @@ async def test_form_source_user(hass): ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_IP_ADDRESS: "1.2.3.4"}, + VALID_CONFIG, ) await hass.async_block_till_done() assert result2["type"] == "create_entry" assert result2["title"] == "My site" - assert result2["data"] == {CONF_IP_ADDRESS: "1.2.3.4"} + assert result2["data"] == VALID_CONFIG assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -61,11 +67,32 @@ async def test_form_cannot_connect(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_IP_ADDRESS: "1.2.3.4"}, + VALID_CONFIG, ) assert result2["type"] == "form" - assert result2["errors"] == {"base": "cannot_connect"} + assert result2["errors"] == {CONF_IP_ADDRESS: "cannot_connect"} + + +async def test_invalid_auth(hass): + """Test we handle invalid auth error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_powerwall = _mock_powerwall_side_effect(site_info=AccessDeniedError("any")) + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_CONFIG, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {CONF_PASSWORD: "invalid_auth"} async def test_form_unknown_exeption(hass): @@ -81,8 +108,7 @@ async def test_form_unknown_exeption(hass): return_value=mock_powerwall, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_IP_ADDRESS: "1.2.3.4"}, + result["flow_id"], VALID_CONFIG ) assert result2["type"] == "form" @@ -105,7 +131,7 @@ async def test_form_wrong_version(hass): ): result3 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_IP_ADDRESS: "1.2.3.4"}, + VALID_CONFIG, ) assert result3["type"] == "form" @@ -178,16 +204,54 @@ async def test_dhcp_discovery(hass): ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_IP_ADDRESS: "1.1.1.1", - }, + VALID_CONFIG, ) await hass.async_block_till_done() assert result2["type"] == "create_entry" assert result2["title"] == "Some site" - assert result2["data"] == { - CONF_IP_ADDRESS: "1.1.1.1", - } + assert result2["data"] == VALID_CONFIG + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_reauth(hass): + """Test reauthenticate.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data=VALID_CONFIG, + unique_id="1.2.3.4", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=entry.data + ) + assert result["type"] == "form" + assert result["errors"] == {} + + mock_powerwall = await _mock_powerwall_site_name(hass, "My site") + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_IP_ADDRESS: "1.2.3.4", + CONF_PASSWORD: "new-test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 From 67ab86443e39da1ecb74a56f9e9edf4d0c45fcc1 Mon Sep 17 00:00:00 2001 From: "J.P. Hutchins" <34154542+JPHutchins@users.noreply.github.com> Date: Wed, 10 Feb 2021 11:53:31 -0800 Subject: [PATCH 339/796] Revert transmission to check torrent lists by name rather than object (#46190) --- .../components/transmission/__init__.py | 51 +++++++++++-------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index d020bfe9745..76d9aedd8d5 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -397,42 +397,49 @@ class TransmissionData: def check_completed_torrent(self): """Get completed torrent functionality.""" + old_completed_torrent_names = { + torrent.name for torrent in self._completed_torrents + } + current_completed_torrents = [ torrent for torrent in self._torrents if torrent.status == "seeding" ] - freshly_completed_torrents = set(current_completed_torrents).difference( - self._completed_torrents - ) - self._completed_torrents = current_completed_torrents - for torrent in freshly_completed_torrents: - self.hass.bus.fire( - EVENT_DOWNLOADED_TORRENT, {"name": torrent.name, "id": torrent.id} - ) + for torrent in current_completed_torrents: + if torrent.name not in old_completed_torrent_names: + self.hass.bus.fire( + EVENT_DOWNLOADED_TORRENT, {"name": torrent.name, "id": torrent.id} + ) + + self._completed_torrents = current_completed_torrents def check_started_torrent(self): """Get started torrent functionality.""" + old_started_torrent_names = {torrent.name for torrent in self._started_torrents} + current_started_torrents = [ torrent for torrent in self._torrents if torrent.status == "downloading" ] - freshly_started_torrents = set(current_started_torrents).difference( - self._started_torrents - ) - self._started_torrents = current_started_torrents - for torrent in freshly_started_torrents: - self.hass.bus.fire( - EVENT_STARTED_TORRENT, {"name": torrent.name, "id": torrent.id} - ) + for torrent in current_started_torrents: + if torrent.name not in old_started_torrent_names: + self.hass.bus.fire( + EVENT_STARTED_TORRENT, {"name": torrent.name, "id": torrent.id} + ) + + self._started_torrents = current_started_torrents def check_removed_torrent(self): """Get removed torrent functionality.""" - freshly_removed_torrents = set(self._all_torrents).difference(self._torrents) - self._all_torrents = self._torrents - for torrent in freshly_removed_torrents: - self.hass.bus.fire( - EVENT_REMOVED_TORRENT, {"name": torrent.name, "id": torrent.id} - ) + current_torrent_names = {torrent.name for torrent in self._torrents} + + for torrent in self._all_torrents: + if torrent.name not in current_torrent_names: + self.hass.bus.fire( + EVENT_REMOVED_TORRENT, {"name": torrent.name, "id": torrent.id} + ) + + self._all_torrents = self._torrents.copy() def start_torrents(self): """Start all torrents.""" From c59b1c72c5616cd58fcffd9e5cd944dc3e44bfc1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Feb 2021 09:55:06 -1000 Subject: [PATCH 340/796] Add reauth support for tesla (#46307) --- homeassistant/components/tesla/__init__.py | 43 ++++++-- homeassistant/components/tesla/config_flow.py | 101 ++++++++++-------- homeassistant/components/tesla/strings.json | 4 + .../components/tesla/translations/en.json | 4 + tests/components/tesla/test_config_flow.py | 39 ++++++- 5 files changed, 136 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index 8981b269a56..b31f8ae6dd3 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -5,10 +5,11 @@ from datetime import timedelta import logging import async_timeout -from teslajsonpy import Controller as TeslaAPI, TeslaException +from teslajsonpy import Controller as TeslaAPI +from teslajsonpy.exceptions import IncompleteCredentials, TeslaException import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, @@ -17,8 +18,9 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_USERNAME, + HTTP_UNAUTHORIZED, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.update_coordinator import ( @@ -28,12 +30,7 @@ from homeassistant.helpers.update_coordinator import ( ) from homeassistant.util import slugify -from .config_flow import ( - CannotConnect, - InvalidAuth, - configured_instances, - validate_input, -) +from .config_flow import CannotConnect, InvalidAuth, validate_input from .const import ( CONF_WAKE_ON_START, DATA_LISTENER, @@ -75,6 +72,16 @@ def _async_save_tokens(hass, config_entry, access_token, refresh_token): ) +@callback +def _async_configured_emails(hass): + """Return a set of configured Tesla emails.""" + return { + entry.data[CONF_USERNAME] + for entry in hass.config_entries.async_entries(DOMAIN) + if CONF_USERNAME in entry.data + } + + async def async_setup(hass, base_config): """Set up of Tesla component.""" @@ -95,7 +102,7 @@ async def async_setup(hass, base_config): email = config[CONF_USERNAME] password = config[CONF_PASSWORD] scan_interval = config[CONF_SCAN_INTERVAL] - if email in configured_instances(hass): + if email in _async_configured_emails(hass): try: info = await validate_input(hass, config) except (CannotConnect, InvalidAuth): @@ -151,7 +158,12 @@ async def async_setup_entry(hass, config_entry): CONF_WAKE_ON_START, DEFAULT_WAKE_ON_START ) ) + except IncompleteCredentials: + _async_start_reauth(hass, config_entry) + return False except TeslaException as ex: + if ex.code == HTTP_UNAUTHORIZED: + _async_start_reauth(hass, config_entry) _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message) return False _async_save_tokens(hass, config_entry, access_token, refresh_token) @@ -206,6 +218,17 @@ async def async_unload_entry(hass, config_entry) -> bool: return False +def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "reauth"}, + data=entry.data, + ) + ) + _LOGGER.error("Credentials are no longer valid. Please reauthenticate") + + async def update_listener(hass, config_entry): """Update when config_entry options update.""" controller = hass.data[DOMAIN][config_entry.entry_id]["coordinator"].controller diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py index 683ef314a06..194ea71a3b7 100644 --- a/homeassistant/components/tesla/config_flow.py +++ b/homeassistant/components/tesla/config_flow.py @@ -20,22 +20,12 @@ from .const import ( CONF_WAKE_ON_START, DEFAULT_SCAN_INTERVAL, DEFAULT_WAKE_ON_START, - DOMAIN, MIN_SCAN_INTERVAL, ) +from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) -DATA_SCHEMA = vol.Schema( - {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} -) - - -@callback -def configured_instances(hass): - """Return a set of configured Tesla instances.""" - return {entry.title for entry in hass.config_entries.async_entries(DOMAIN)} - class TeslaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Tesla.""" @@ -43,46 +33,56 @@ class TeslaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + def __init__(self): + """Initialize the tesla flow.""" + self.username = None + async def async_step_import(self, import_config): """Import a config entry from configuration.yaml.""" return await self.async_step_user(import_config) async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" + errors = {} - if not user_input: - return self.async_show_form( - step_id="user", - data_schema=DATA_SCHEMA, - errors={}, - description_placeholders={}, - ) + if user_input is not None: + existing_entry = self._async_entry_for_username(user_input[CONF_USERNAME]) + if ( + existing_entry + and existing_entry.data[CONF_PASSWORD] == user_input[CONF_PASSWORD] + ): + return self.async_abort(reason="already_configured") - if user_input[CONF_USERNAME] in configured_instances(self.hass): - return self.async_show_form( - step_id="user", - data_schema=DATA_SCHEMA, - errors={CONF_USERNAME: "already_configured"}, - description_placeholders={}, - ) + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" - try: - info = await validate_input(self.hass, user_input) - except CannotConnect: - return self.async_show_form( - step_id="user", - data_schema=DATA_SCHEMA, - errors={"base": "cannot_connect"}, - description_placeholders={}, - ) - except InvalidAuth: - return self.async_show_form( - step_id="user", - data_schema=DATA_SCHEMA, - errors={"base": "invalid_auth"}, - description_placeholders={}, - ) - return self.async_create_entry(title=user_input[CONF_USERNAME], data=info) + if not errors: + if existing_entry: + self.hass.config_entries.async_update_entry( + existing_entry, data=info + ) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_create_entry( + title=user_input[CONF_USERNAME], data=info + ) + + return self.async_show_form( + step_id="user", + data_schema=self._async_schema(), + errors=errors, + description_placeholders={}, + ) + + async def async_step_reauth(self, data): + """Handle configuration by re-auth.""" + self.username = data[CONF_USERNAME] + return await self.async_step_user() @staticmethod @callback @@ -90,6 +90,23 @@ class TeslaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) + @callback + def _async_schema(self): + """Fetch schema with defaults.""" + return vol.Schema( + { + vol.Required(CONF_USERNAME, default=self.username): str, + vol.Required(CONF_PASSWORD): str, + } + ) + + @callback + def _async_entry_for_username(self, username): + """Find an existing entry for a username.""" + for entry in self._async_current_entries(): + if entry.data.get(CONF_USERNAME) == username: + return entry + class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for Tesla.""" diff --git a/homeassistant/components/tesla/strings.json b/homeassistant/components/tesla/strings.json index 503124eedd4..c75562528de 100644 --- a/homeassistant/components/tesla/strings.json +++ b/homeassistant/components/tesla/strings.json @@ -5,6 +5,10 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, + "abort": { + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/tesla/translations/en.json b/homeassistant/components/tesla/translations/en.json index f2b888552b9..53b213ac19b 100644 --- a/homeassistant/components/tesla/translations/en.json +++ b/homeassistant/components/tesla/translations/en.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" + }, "error": { "already_configured": "Account is already configured", "cannot_connect": "Failed to connect", diff --git a/tests/components/tesla/test_config_flow.py b/tests/components/tesla/test_config_flow.py index 136633c9a5c..b35ab0039d0 100644 --- a/tests/components/tesla/test_config_flow.py +++ b/tests/components/tesla/test_config_flow.py @@ -97,7 +97,12 @@ async def test_form_cannot_connect(hass): async def test_form_repeat_identifier(hass): """Test we handle repeat identifiers.""" - entry = MockConfigEntry(domain=DOMAIN, title="test-username", data={}, options=None) + entry = MockConfigEntry( + domain=DOMAIN, + title="test-username", + data={"username": "test-username", "password": "test-password"}, + options=None, + ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -112,8 +117,36 @@ async def test_form_repeat_identifier(hass): {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, ) - assert result2["type"] == "form" - assert result2["errors"] == {CONF_USERNAME: "already_configured"} + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" + + +async def test_form_reauth(hass): + """Test we handle reauth.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="test-username", + data={"username": "test-username", "password": "same"}, + options=None, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data={"username": "test-username"}, + ) + with patch( + "homeassistant.components.tesla.config_flow.TeslaAPI.connect", + return_value=("test-refresh-token", "test-access-token"), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "test-username", CONF_PASSWORD: "new-password"}, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" async def test_import(hass): From 9bc3c6c1300a5bf16ae3f6c2d9e94950c77c4714 Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Wed, 10 Feb 2021 13:30:52 -0700 Subject: [PATCH 341/796] Bump pymyq to 3.0.1 (#46079) Co-authored-by: J. Nick Koston --- homeassistant/components/myq/__init__.py | 12 +- homeassistant/components/myq/binary_sensor.py | 7 +- homeassistant/components/myq/const.py | 20 ++-- homeassistant/components/myq/cover.py | 103 ++++++++---------- homeassistant/components/myq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/myq/util.py | 32 ++++-- 8 files changed, 95 insertions(+), 85 deletions(-) diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index 959000da3b3..6b3a52ba7b0 100644 --- a/homeassistant/components/myq/__init__.py +++ b/homeassistant/components/myq/__init__.py @@ -11,7 +11,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, PLATFORMS, UPDATE_INTERVAL @@ -40,11 +40,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except MyQError as err: raise ConfigEntryNotReady from err + # Called by DataUpdateCoordinator, allows to capture any MyQError exceptions and to throw an HASS UpdateFailed + # exception instead, preventing traceback in HASS logs. + async def async_update_data(): + try: + return await myq.update_device_info() + except MyQError as err: + raise UpdateFailed(str(err)) from err + coordinator = DataUpdateCoordinator( hass, _LOGGER, name="myq devices", - update_method=myq.update_device_info, + update_method=async_update_data, update_interval=timedelta(seconds=UPDATE_INTERVAL), ) diff --git a/homeassistant/components/myq/binary_sensor.py b/homeassistant/components/myq/binary_sensor.py index 57bd2451d2a..e3832458b9b 100644 --- a/homeassistant/components/myq/binary_sensor.py +++ b/homeassistant/components/myq/binary_sensor.py @@ -1,7 +1,5 @@ """Support for MyQ gateways.""" from pymyq.const import ( - DEVICE_FAMILY as MYQ_DEVICE_FAMILY, - DEVICE_FAMILY_GATEWAY as MYQ_DEVICE_FAMILY_GATEWAY, DEVICE_STATE as MYQ_DEVICE_STATE, DEVICE_STATE_ONLINE as MYQ_DEVICE_STATE_ONLINE, KNOWN_MODELS, @@ -25,9 +23,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] - for device in myq.devices.values(): - if device.device_json[MYQ_DEVICE_FAMILY] == MYQ_DEVICE_FAMILY_GATEWAY: - entities.append(MyQBinarySensorEntity(coordinator, device)) + for device in myq.gateways.values(): + entities.append(MyQBinarySensorEntity(coordinator, device)) async_add_entities(entities, True) diff --git a/homeassistant/components/myq/const.py b/homeassistant/components/myq/const.py index 9251bce7447..6189b1601ea 100644 --- a/homeassistant/components/myq/const.py +++ b/homeassistant/components/myq/const.py @@ -1,9 +1,9 @@ """The MyQ integration.""" -from pymyq.device import ( - STATE_CLOSED as MYQ_STATE_CLOSED, - STATE_CLOSING as MYQ_STATE_CLOSING, - STATE_OPEN as MYQ_STATE_OPEN, - STATE_OPENING as MYQ_STATE_OPENING, +from pymyq.garagedoor import ( + STATE_CLOSED as MYQ_COVER_STATE_CLOSED, + STATE_CLOSING as MYQ_COVER_STATE_CLOSING, + STATE_OPEN as MYQ_COVER_STATE_OPEN, + STATE_OPENING as MYQ_COVER_STATE_OPENING, ) from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING @@ -13,10 +13,10 @@ DOMAIN = "myq" PLATFORMS = ["cover", "binary_sensor"] MYQ_TO_HASS = { - MYQ_STATE_CLOSED: STATE_CLOSED, - MYQ_STATE_CLOSING: STATE_CLOSING, - MYQ_STATE_OPEN: STATE_OPEN, - MYQ_STATE_OPENING: STATE_OPENING, + MYQ_COVER_STATE_CLOSED: STATE_CLOSED, + MYQ_COVER_STATE_CLOSING: STATE_CLOSING, + MYQ_COVER_STATE_OPEN: STATE_OPEN, + MYQ_COVER_STATE_OPENING: STATE_OPENING, } MYQ_GATEWAY = "myq_gateway" @@ -24,7 +24,7 @@ MYQ_COORDINATOR = "coordinator" # myq has some ratelimits in place # and 61 seemed to be work every time -UPDATE_INTERVAL = 61 +UPDATE_INTERVAL = 15 # Estimated time it takes myq to start transition from one # state to the next. diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index 6fef6b25bab..e26a969e724 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -1,14 +1,14 @@ """Support for MyQ-Enabled Garage Doors.""" -import time +import logging from pymyq.const import ( DEVICE_STATE as MYQ_DEVICE_STATE, DEVICE_STATE_ONLINE as MYQ_DEVICE_STATE_ONLINE, - DEVICE_TYPE as MYQ_DEVICE_TYPE, DEVICE_TYPE_GATE as MYQ_DEVICE_TYPE_GATE, KNOWN_MODELS, MANUFACTURER, ) +from pymyq.errors import MyQError from homeassistant.components.cover import ( DEVICE_CLASS_GARAGE, @@ -17,19 +17,12 @@ from homeassistant.components.cover import ( SUPPORT_OPEN, CoverEntity, ) -from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPENING -from homeassistant.core import callback -from homeassistant.helpers.event import async_call_later +from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ( - DOMAIN, - MYQ_COORDINATOR, - MYQ_GATEWAY, - MYQ_TO_HASS, - TRANSITION_COMPLETE_DURATION, - TRANSITION_START_DURATION, -) +from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, MYQ_TO_HASS + +_LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -50,13 +43,11 @@ class MyQDevice(CoordinatorEntity, CoverEntity): """Initialize with API object, device id.""" super().__init__(coordinator) self._device = device - self._last_action_timestamp = 0 - self._scheduled_transition_update = None @property def device_class(self): """Define this cover as a garage door.""" - device_type = self._device.device_json.get(MYQ_DEVICE_TYPE) + device_type = self._device.device_type if device_type is not None and device_type == MYQ_DEVICE_TYPE_GATE: return DEVICE_CLASS_GATE return DEVICE_CLASS_GARAGE @@ -87,6 +78,11 @@ class MyQDevice(CoordinatorEntity, CoverEntity): """Return if the cover is closing or not.""" return MYQ_TO_HASS.get(self._device.state) == STATE_CLOSING + @property + def is_open(self): + """Return if the cover is opening or not.""" + return MYQ_TO_HASS.get(self._device.state) == STATE_OPEN + @property def is_opening(self): """Return if the cover is opening or not.""" @@ -104,37 +100,48 @@ class MyQDevice(CoordinatorEntity, CoverEntity): async def async_close_cover(self, **kwargs): """Issue close command to cover.""" - self._last_action_timestamp = time.time() - await self._device.close() - self._async_schedule_update_for_transition() + if self.is_closing or self.is_closed: + return + + try: + wait_task = await self._device.close(wait_for_state=False) + except MyQError as err: + _LOGGER.error( + "Closing of cover %s failed with error: %s", self._device.name, str(err) + ) + + return + + # Write closing state to HASS + self.async_write_ha_state() + + if not await wait_task: + _LOGGER.error("Closing of cover %s failed", self._device.name) + + # Write final state to HASS + self.async_write_ha_state() async def async_open_cover(self, **kwargs): """Issue open command to cover.""" - self._last_action_timestamp = time.time() - await self._device.open() - self._async_schedule_update_for_transition() + if self.is_opening or self.is_open: + return - @callback - def _async_schedule_update_for_transition(self): + try: + wait_task = await self._device.open(wait_for_state=False) + except MyQError as err: + _LOGGER.error( + "Opening of cover %s failed with error: %s", self._device.name, str(err) + ) + return + + # Write opening state to HASS self.async_write_ha_state() - # Cancel any previous updates - if self._scheduled_transition_update: - self._scheduled_transition_update() + if not await wait_task: + _LOGGER.error("Opening of cover %s failed", self._device.name) - # Schedule an update for when we expect the transition - # to be completed so the garage door or gate does not - # seem like its closing or opening for a long time - self._scheduled_transition_update = async_call_later( - self.hass, - TRANSITION_COMPLETE_DURATION, - self._async_complete_schedule_update, - ) - - async def _async_complete_schedule_update(self, _): - """Update status of the cover via coordinator.""" - self._scheduled_transition_update = None - await self.coordinator.async_request_refresh() + # Write final state to HASS + self.async_write_ha_state() @property def device_info(self): @@ -152,22 +159,8 @@ class MyQDevice(CoordinatorEntity, CoverEntity): device_info["via_device"] = (DOMAIN, self._device.parent_device_id) return device_info - @callback - def _async_consume_update(self): - if time.time() - self._last_action_timestamp <= TRANSITION_START_DURATION: - # If we just started a transition we need - # to prevent a bouncy state - return - - self.async_write_ha_state() - async def async_added_to_hass(self): """Subscribe to updates.""" self.async_on_remove( - self.coordinator.async_add_listener(self._async_consume_update) + self.coordinator.async_add_listener(self.async_write_ha_state) ) - - async def async_will_remove_from_hass(self): - """Undo subscription.""" - if self._scheduled_transition_update: - self._scheduled_transition_update() diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index aba2f24b5bd..9dc8719ed4e 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.14"], + "requirements": ["pymyq==3.0.1"], "codeowners": ["@bdraco"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index ee86d62270a..12ade1a446b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1545,7 +1545,7 @@ pymsteams==0.1.12 pymusiccast==0.1.6 # homeassistant.components.myq -pymyq==2.0.14 +pymyq==3.0.1 # homeassistant.components.mysensors pymysensors==0.20.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d63ba30e1c..f5a629a771f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -808,7 +808,7 @@ pymodbus==2.3.0 pymonoprice==0.3 # homeassistant.components.myq -pymyq==2.0.14 +pymyq==3.0.1 # homeassistant.components.mysensors pymysensors==0.20.1 diff --git a/tests/components/myq/util.py b/tests/components/myq/util.py index 84e85723918..8cb0d17f592 100644 --- a/tests/components/myq/util.py +++ b/tests/components/myq/util.py @@ -1,14 +1,18 @@ """Tests for the myq integration.""" - import json +import logging from unittest.mock import patch +from pymyq.const import ACCOUNTS_ENDPOINT, DEVICES_ENDPOINT + from homeassistant.components.myq.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture +_LOGGER = logging.getLogger(__name__) + async def async_init_integration( hass: HomeAssistant, @@ -20,16 +24,24 @@ async def async_init_integration( devices_json = load_fixture(devices_fixture) devices_dict = json.loads(devices_json) - def _handle_mock_api_request(method, endpoint, **kwargs): - if endpoint == "Login": - return {"SecurityToken": 1234} - if endpoint == "My": - return {"Account": {"Id": 1}} - if endpoint == "Accounts/1/Devices": - return devices_dict - return {} + def _handle_mock_api_oauth_authenticate(): + return 1234, 1800 - with patch("pymyq.api.API.request", side_effect=_handle_mock_api_request): + def _handle_mock_api_request(method, returns, url, **kwargs): + _LOGGER.debug("URL: %s", url) + if url == ACCOUNTS_ENDPOINT: + _LOGGER.debug("Accounts") + return None, {"accounts": [{"id": 1, "name": "mock"}]} + if url == DEVICES_ENDPOINT.format(account_id=1): + _LOGGER.debug("Devices") + return None, devices_dict + _LOGGER.debug("Something else") + return None, {} + + with patch( + "pymyq.api.API._oauth_authenticate", + side_effect=_handle_mock_api_oauth_authenticate, + ), patch("pymyq.api.API.request", side_effect=_handle_mock_api_request): entry = MockConfigEntry( domain=DOMAIN, data={CONF_USERNAME: "mock", CONF_PASSWORD: "mock"} ) From acde33dbbc82a8cedfa4bd3d5c077dc595e16b2f Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Thu, 11 Feb 2021 04:51:16 +0800 Subject: [PATCH 342/796] Keep 1 extra segment around after playlist removal (#46310) * Keep 1 extra segment around after playlist removal * Remove segments length check --- homeassistant/components/stream/const.py | 3 ++- homeassistant/components/stream/hls.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/stream/const.py b/homeassistant/components/stream/const.py index 4ee9f2a9814..41df806d020 100644 --- a/homeassistant/components/stream/const.py +++ b/homeassistant/components/stream/const.py @@ -10,7 +10,8 @@ FORMAT_CONTENT_TYPE = {"hls": "application/vnd.apple.mpegurl"} OUTPUT_IDLE_TIMEOUT = 300 # Idle timeout due to inactivity -MAX_SEGMENTS = 3 # Max number of segments to keep around +NUM_PLAYLIST_SEGMENTS = 3 # Number of segments to use in HLS playlist +MAX_SEGMENTS = 4 # Max number of segments to keep around MIN_SEGMENT_DURATION = 1.5 # Each segment is at least this many seconds PACKETS_TO_WAIT_FOR_AUDIO = 20 # Some streams have an audio stream with no audio diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index 2b305442b80..bd5fbd5e9ae 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -6,7 +6,7 @@ from aiohttp import web from homeassistant.core import callback -from .const import FORMAT_CONTENT_TYPE +from .const import FORMAT_CONTENT_TYPE, NUM_PLAYLIST_SEGMENTS from .core import PROVIDERS, StreamOutput, StreamView from .fmp4utils import get_codec_string, get_init, get_m4s @@ -77,7 +77,7 @@ class HlsPlaylistView(StreamView): @staticmethod def render_playlist(track): """Render playlist.""" - segments = track.segments + segments = track.segments[-NUM_PLAYLIST_SEGMENTS:] if not segments: return [] From d8a2e0e0514aa7ba938398e582fe4b865765ddef Mon Sep 17 00:00:00 2001 From: tkdrob Date: Wed, 10 Feb 2021 17:17:37 -0500 Subject: [PATCH 343/796] Remove unnecessary variables from logbook (#46350) --- homeassistant/components/logbook/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index e2d8a22c251..3b77e6e6409 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -54,8 +54,6 @@ ICON_JSON_EXTRACT = re.compile('"icon": "([^"]+)"') ATTR_MESSAGE = "message" -CONF_DOMAINS = "domains" -CONF_ENTITIES = "entities" CONTINUOUS_DOMAINS = ["proximity", "sensor"] DOMAIN = "logbook" @@ -417,7 +415,6 @@ def _get_events( entity_matches_only=False, ): """Get events for a period of time.""" - entity_attr_cache = EntityAttributeCache(hass) context_lookup = {None: None} From 281fbe1dfac1f7a93f51622578b55075977a9fa5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Feb 2021 12:49:52 -1000 Subject: [PATCH 344/796] Update wilight for new fan entity model (#45869) --- homeassistant/components/wilight/fan.py | 45 ++++++++++--------------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/wilight/fan.py b/homeassistant/components/wilight/fan.py index d59b9398d9e..948d99ac81d 100644 --- a/homeassistant/components/wilight/fan.py +++ b/homeassistant/components/wilight/fan.py @@ -14,19 +14,20 @@ from pywilight.const import ( from homeassistant.components.fan import ( DIRECTION_FORWARD, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, SUPPORT_DIRECTION, SUPPORT_SET_SPEED, FanEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.util.percentage import ( + ordered_list_item_to_percentage, + percentage_to_ordered_list_item, +) from . import WiLightDevice -SUPPORTED_SPEEDS = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] +ORDERED_NAMED_FAN_SPEEDS = [WL_SPEED_LOW, WL_SPEED_MEDIUM, WL_SPEED_HIGH] SUPPORTED_FEATURES = SUPPORT_SET_SPEED | SUPPORT_DIRECTION @@ -77,14 +78,12 @@ class WiLightFan(WiLightDevice, FanEntity): return self._status.get("direction", WL_DIRECTION_OFF) != WL_DIRECTION_OFF @property - def speed(self) -> str: - """Return the current speed.""" - return self._status.get("speed", SPEED_HIGH) - - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return SUPPORTED_SPEEDS + def percentage(self) -> str: + """Return the current speed percentage.""" + wl_speed = self._status.get("speed") + if wl_speed is None: + return None + return ordered_list_item_to_percentage(ORDERED_NAMED_FAN_SPEEDS, wl_speed) @property def current_direction(self) -> str: @@ -94,13 +93,6 @@ class WiLightFan(WiLightDevice, FanEntity): self._direction = self._status["direction"] return self._direction - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # async def async_turn_on( self, speed: str = None, @@ -109,18 +101,17 @@ class WiLightFan(WiLightDevice, FanEntity): **kwargs, ) -> None: """Turn on the fan.""" - if speed is None: + if percentage is None: await self._client.set_fan_direction(self._index, self._direction) else: - await self.async_set_speed(speed) + await self.async_set_percentage(percentage) - async def async_set_speed(self, speed: str): + async def async_set_percentage(self, percentage: int): """Set the speed of the fan.""" - wl_speed = WL_SPEED_HIGH - if speed == SPEED_LOW: - wl_speed = WL_SPEED_LOW - if speed == SPEED_MEDIUM: - wl_speed = WL_SPEED_MEDIUM + if percentage == 0: + await self._client.set_fan_direction(self._index, WL_DIRECTION_OFF) + return + wl_speed = percentage_to_ordered_list_item(ORDERED_NAMED_FAN_SPEEDS, percentage) await self._client.set_fan_speed(self._index, wl_speed) async def async_set_direction(self, direction: str): From 6182fedf3f427af3280344f1e2874f0b6d9dd8b7 Mon Sep 17 00:00:00 2001 From: Leonardo Figueiro Date: Wed, 10 Feb 2021 20:09:03 -0300 Subject: [PATCH 345/796] Update wilight tests for new fan entity model (#46358) --- tests/components/wilight/test_fan.py | 29 +++++++++++++--------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/tests/components/wilight/test_fan.py b/tests/components/wilight/test_fan.py index 9b656236b93..1247b622ae7 100644 --- a/tests/components/wilight/test_fan.py +++ b/tests/components/wilight/test_fan.py @@ -6,15 +6,12 @@ import pywilight from homeassistant.components.fan import ( ATTR_DIRECTION, - ATTR_SPEED, + ATTR_PERCENTAGE, DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN as FAN_DOMAIN, SERVICE_SET_DIRECTION, - SERVICE_SET_SPEED, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, + SERVICE_SET_PERCENTAGE, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -102,7 +99,7 @@ async def test_on_off_fan_state( await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_SPEED: SPEED_LOW, ATTR_ENTITY_ID: "fan.wl000000000099_2"}, + {ATTR_PERCENTAGE: 30, ATTR_ENTITY_ID: "fan.wl000000000099_2"}, blocking=True, ) @@ -110,7 +107,7 @@ async def test_on_off_fan_state( state = hass.states.get("fan.wl000000000099_2") assert state assert state.state == STATE_ON - assert state.attributes.get(ATTR_SPEED) == SPEED_LOW + assert state.attributes.get(ATTR_PERCENTAGE) == 33 # Turn off await hass.services.async_call( @@ -135,41 +132,41 @@ async def test_speed_fan_state( # Set speed Low await hass.services.async_call( FAN_DOMAIN, - SERVICE_SET_SPEED, - {ATTR_SPEED: SPEED_LOW, ATTR_ENTITY_ID: "fan.wl000000000099_2"}, + SERVICE_SET_PERCENTAGE, + {ATTR_PERCENTAGE: 30, ATTR_ENTITY_ID: "fan.wl000000000099_2"}, blocking=True, ) await hass.async_block_till_done() state = hass.states.get("fan.wl000000000099_2") assert state - assert state.attributes.get(ATTR_SPEED) == SPEED_LOW + assert state.attributes.get(ATTR_PERCENTAGE) == 33 # Set speed Medium await hass.services.async_call( FAN_DOMAIN, - SERVICE_SET_SPEED, - {ATTR_SPEED: SPEED_MEDIUM, ATTR_ENTITY_ID: "fan.wl000000000099_2"}, + SERVICE_SET_PERCENTAGE, + {ATTR_PERCENTAGE: 50, ATTR_ENTITY_ID: "fan.wl000000000099_2"}, blocking=True, ) await hass.async_block_till_done() state = hass.states.get("fan.wl000000000099_2") assert state - assert state.attributes.get(ATTR_SPEED) == SPEED_MEDIUM + assert state.attributes.get(ATTR_PERCENTAGE) == 66 # Set speed High await hass.services.async_call( FAN_DOMAIN, - SERVICE_SET_SPEED, - {ATTR_SPEED: SPEED_HIGH, ATTR_ENTITY_ID: "fan.wl000000000099_2"}, + SERVICE_SET_PERCENTAGE, + {ATTR_PERCENTAGE: 90, ATTR_ENTITY_ID: "fan.wl000000000099_2"}, blocking=True, ) await hass.async_block_till_done() state = hass.states.get("fan.wl000000000099_2") assert state - assert state.attributes.get(ATTR_SPEED) == SPEED_HIGH + assert state.attributes.get(ATTR_PERCENTAGE) == 100 async def test_direction_fan_state( From 8007391244c59408c604e195d9b02e4f22a12ecd Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 11 Feb 2021 00:02:56 +0000 Subject: [PATCH 346/796] [ci skip] Translation update --- .../ambiclimate/translations/no.json | 2 +- .../components/foscam/translations/no.json | 2 + .../components/goalzero/translations/no.json | 2 +- .../components/icloud/translations/no.json | 2 +- .../components/konnected/translations/no.json | 2 +- .../lutron_caseta/translations/no.json | 2 +- .../media_player/translations/it.json | 7 +++ .../media_player/translations/no.json | 7 +++ .../components/mysensors/translations/no.json | 51 +++++++++++++++++-- .../components/nest/translations/no.json | 4 +- .../components/plaato/translations/no.json | 9 +++- .../components/point/translations/no.json | 4 +- .../components/powerwall/translations/en.json | 2 +- .../components/roku/translations/ca.json | 1 + .../components/roku/translations/it.json | 1 + .../components/roku/translations/ru.json | 1 + .../components/roku/translations/zh-Hant.json | 1 + .../components/roomba/translations/no.json | 2 +- .../components/shelly/translations/it.json | 4 +- .../components/shelly/translations/no.json | 4 +- .../components/soma/translations/no.json | 2 +- .../components/spotify/translations/no.json | 2 +- .../tellduslive/translations/no.json | 4 +- .../components/toon/translations/no.json | 4 +- .../components/traccar/translations/no.json | 2 +- .../components/vera/translations/no.json | 4 +- .../xiaomi_aqara/translations/no.json | 4 +- .../components/zwave/translations/no.json | 2 +- 28 files changed, 102 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/ambiclimate/translations/no.json b/homeassistant/components/ambiclimate/translations/no.json index c39aa7637f8..6feaabadacc 100644 --- a/homeassistant/components/ambiclimate/translations/no.json +++ b/homeassistant/components/ambiclimate/translations/no.json @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "Vennligst f\u00f8lg denne [linken]({authorization_url}) og **Tillat** tilgang til din Ambiclimate konto, kom deretter tilbake og trykk **Send** nedenfor.\n(Kontroller at den angitte URL-adressen for tilbakeringing er {cb_url})", + "description": "F\u00f8lg denne [linken]({authorization_url}) og **Tillat** tilgang til Ambiclimate-kontoen din, og kom deretter tilbake og trykk **Send** nedenfor.\n(Kontroller at den angitte url-adressen for tilbakeringing er {cb_url})", "title": "Godkjenn Ambiclimate" } } diff --git a/homeassistant/components/foscam/translations/no.json b/homeassistant/components/foscam/translations/no.json index 5e1b494c88a..0184213de27 100644 --- a/homeassistant/components/foscam/translations/no.json +++ b/homeassistant/components/foscam/translations/no.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", + "invalid_response": "Ugyldig respons fra enheten", "unknown": "Uventet feil" }, "step": { @@ -14,6 +15,7 @@ "host": "Vert", "password": "Passord", "port": "Port", + "rtsp_port": "RTSP-port", "stream": "Str\u00f8m", "username": "Brukernavn" } diff --git a/homeassistant/components/goalzero/translations/no.json b/homeassistant/components/goalzero/translations/no.json index 1aaf27d1b09..4ae6f564a99 100644 --- a/homeassistant/components/goalzero/translations/no.json +++ b/homeassistant/components/goalzero/translations/no.json @@ -14,7 +14,7 @@ "host": "Vert", "name": "Navn" }, - "description": "F\u00f8rst m\u00e5 du laste ned appen Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\n F\u00f8lg instruksjonene for \u00e5 koble Yeti til Wifi-nettverket. S\u00e5 f\u00e5 verts-ip fra ruteren din. DHCP m\u00e5 v\u00e6re satt opp i ruteren innstillinger for enheten for \u00e5 sikre at verts-IP ikke endres. Se brukerh\u00e5ndboken til ruteren.", + "description": "F\u00f8rst m\u00e5 du laste ned appen Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\n F\u00f8lg instruksjonene for \u00e5 koble Yeti til Wifi-nettverket. S\u00e5 f\u00e5 verts-IP-en fra ruteren din. DHCP m\u00e5 v\u00e6re satt opp i ruteren innstillinger for enheten for \u00e5 sikre at verts-IP ikke endres. Se ruteren din.", "title": "" } } diff --git a/homeassistant/components/icloud/translations/no.json b/homeassistant/components/icloud/translations/no.json index 62e123eb84c..3e20aef032e 100644 --- a/homeassistant/components/icloud/translations/no.json +++ b/homeassistant/components/icloud/translations/no.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "Ugyldig godkjenning", "send_verification_code": "Kunne ikke sende bekreftelseskode", - "validate_verification_code": "Kunne ikke bekrefte bekreftelseskoden din, velg en tillitsenhet og start bekreftelsen p\u00e5 nytt" + "validate_verification_code": "Kunne ikke bekrefte bekreftelseskoden, pr\u00f8v p\u00e5 nytt" }, "step": { "reauth": { diff --git a/homeassistant/components/konnected/translations/no.json b/homeassistant/components/konnected/translations/no.json index 1a11c7c76d3..47be4c20bf0 100644 --- a/homeassistant/components/konnected/translations/no.json +++ b/homeassistant/components/konnected/translations/no.json @@ -32,7 +32,7 @@ "not_konn_panel": "Ikke en anerkjent Konnected.io-enhet" }, "error": { - "bad_host": "Ugyldig overstyr API-vertsadresse" + "bad_host": "Url-adresse for ugyldig overstyring av API-vert" }, "step": { "options_binary": { diff --git a/homeassistant/components/lutron_caseta/translations/no.json b/homeassistant/components/lutron_caseta/translations/no.json index 477370100af..b985c87caf0 100644 --- a/homeassistant/components/lutron_caseta/translations/no.json +++ b/homeassistant/components/lutron_caseta/translations/no.json @@ -22,7 +22,7 @@ "data": { "host": "Vert" }, - "description": "Skriv inn ip-adressen til enheten.", + "description": "Skriv inn IP-adressen til enheten.", "title": "Koble automatisk til broen" } } diff --git a/homeassistant/components/media_player/translations/it.json b/homeassistant/components/media_player/translations/it.json index 23d1afa0625..a3ebfdfe411 100644 --- a/homeassistant/components/media_player/translations/it.json +++ b/homeassistant/components/media_player/translations/it.json @@ -6,6 +6,13 @@ "is_on": "{entity_name} \u00e8 acceso", "is_paused": "{entity_name} \u00e8 in pausa", "is_playing": "{entity_name} \u00e8 in esecuzione" + }, + "trigger_type": { + "idle": "{entity_name} diventa inattivo", + "paused": "{entity_name} \u00e8 in pausa", + "playing": "{entity_name} inizia l'esecuzione", + "turned_off": "{entity_name} disattivato", + "turned_on": "{entity_name} attivato" } }, "state": { diff --git a/homeassistant/components/media_player/translations/no.json b/homeassistant/components/media_player/translations/no.json index 691ec894a7b..fa5618efc35 100644 --- a/homeassistant/components/media_player/translations/no.json +++ b/homeassistant/components/media_player/translations/no.json @@ -6,6 +6,13 @@ "is_on": "{entity_name} er sl\u00e5tt p\u00e5", "is_paused": "{entity_name} er satt p\u00e5 pause", "is_playing": "{entity_name} spiller n\u00e5" + }, + "trigger_type": { + "idle": "{entity_name} blir inaktiv", + "paused": "{entity_name} er satt p\u00e5 pause", + "playing": "{entity_name} begynner \u00e5 spille", + "turned_off": "{entity_name} sl\u00e5tt av", + "turned_on": "{entity_name} sl\u00e5tt p\u00e5" } }, "state": { diff --git a/homeassistant/components/mysensors/translations/no.json b/homeassistant/components/mysensors/translations/no.json index a13140df640..9d028260a76 100644 --- a/homeassistant/components/mysensors/translations/no.json +++ b/homeassistant/components/mysensors/translations/no.json @@ -3,30 +3,75 @@ "abort": { "already_configured": "Enheten er allerede konfigurert", "cannot_connect": "Tilkobling mislyktes", + "duplicate_persistence_file": "Persistensfil allerede i bruk", + "duplicate_topic": "Emnet er allerede i bruk", "invalid_auth": "Ugyldig godkjenning", + "invalid_device": "Ugyldig enhet", + "invalid_ip": "Ugyldig IP-adresse", + "invalid_persistence_file": "Ugyldig utholdenhetsfil", + "invalid_port": "Ugyldig portnummer", + "invalid_publish_topic": "Ugyldig publiseringsemne", + "invalid_serial": "Ugyldig serieport", + "invalid_subscribe_topic": "Ugyldig abonnementsemne", + "invalid_version": "Ugyldig MySensors-versjon", + "not_a_number": "Vennligst skriv inn et nummer", + "port_out_of_range": "Portnummer m\u00e5 v\u00e6re minst 1 og maksimalt 65535", + "same_topic": "Abonner og publiser emner er de samme", "unknown": "Uventet feil" }, "error": { "already_configured": "Enheten er allerede konfigurert", "cannot_connect": "Tilkobling mislyktes", + "duplicate_persistence_file": "Persistensfil allerede i bruk", + "duplicate_topic": "Emnet er allerede i bruk", "invalid_auth": "Ugyldig godkjenning", + "invalid_device": "Ugyldig enhet", + "invalid_ip": "Ugyldig IP-adresse", + "invalid_persistence_file": "Ugyldig utholdenhetsfil", + "invalid_port": "Ugyldig portnummer", + "invalid_publish_topic": "Ugyldig publiseringsemne", + "invalid_serial": "Ugyldig serieport", + "invalid_subscribe_topic": "Ugyldig abonnementsemne", + "invalid_version": "Ugyldig MySensors-versjon", + "not_a_number": "Vennligst skriv inn et nummer", + "port_out_of_range": "Portnummer m\u00e5 v\u00e6re minst 1 og maksimalt 65535", + "same_topic": "Abonner og publiser emner er de samme", "unknown": "Uventet feil" }, "step": { "gw_mqtt": { "data": { + "persistence_file": "Persistensfil (la den v\u00e6re tom for automatisk generering)", + "retain": "mqtt beholde", + "topic_in_prefix": "prefiks for input-emner (topic_in_prefix)", + "topic_out_prefix": "prefiks for utgangstemaer (topic_out_prefix)", "version": "MySensors versjon" - } + }, + "description": "MQTT gateway-oppsett" }, "gw_serial": { "data": { + "baud_rate": "Overf\u00f8ringshastighet", + "device": "Seriell port", + "persistence_file": "persistensfil (la den v\u00e6re tom for automatisk generering)", "version": "MySensors versjon" - } + }, + "description": "Seriell gatewayoppsett" + }, + "gw_tcp": { + "data": { + "device": "IP-adressen til gatewayen", + "persistence_file": "persistensfil (la den v\u00e6re tom for automatisk generering)", + "tcp_port": "Port", + "version": "MySensors versjon" + }, + "description": "Ethernet gateway-oppsett" }, "user": { "data": { "gateway_type": "" - } + }, + "description": "Velg tilkoblingsmetode til gatewayen" } } }, diff --git a/homeassistant/components/nest/translations/no.json b/homeassistant/components/nest/translations/no.json index dfaf33b3969..14166f7d9a6 100644 --- a/homeassistant/components/nest/translations/no.json +++ b/homeassistant/components/nest/translations/no.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "authorize_url_fail": "Ukjent feil ved generering av godkjenningsadresse", + "authorize_url_fail": "Ukjent feil under generering av en autoriserings-URL.", "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 godkjenningsadresse" + "unknown_authorize_url_generation": "Ukjent feil under generering av en autoriserings-URL." }, "create_entry": { "default": "Vellykket godkjenning" diff --git a/homeassistant/components/plaato/translations/no.json b/homeassistant/components/plaato/translations/no.json index 8873399aaa4..8efbf07945f 100644 --- a/homeassistant/components/plaato/translations/no.json +++ b/homeassistant/components/plaato/translations/no.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Home Assistant forekomsten din m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta webhook meldinger" }, "create_entry": { - "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i Plaato Airlock. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer." + "default": "Plaato {device_type} med navnet **{device_name}** ble konfigurert!" }, "error": { "invalid_webhook_device": "Du har valgt en enhet som ikke st\u00f8tter sending av data til en webhook. Den er kun tilgjengelig for Airlock", @@ -15,6 +15,11 @@ }, "step": { "api_method": { + "data": { + "token": "Lim inn Auth Token her", + "use_webhook": "Bruk webhook" + }, + "description": "For \u00e5 kunne s\u00f8ke p\u00e5 API-en kreves det en `auth_token` som kan oppn\u00e5s ved \u00e5 f\u00f8lge [disse] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instruksjonene \n\n Valgt enhet: **{device_type}** \n\n Hvis du heller bruker den innebygde webhook-metoden (kun luftsperre), vennligst merk av i ruten nedenfor og la Auth Token v\u00e6re tom.", "title": "Velg API-metode" }, "user": { @@ -23,7 +28,7 @@ "device_type": "Type Platon-enhet" }, "description": "Vil du starte oppsettet?", - "title": "Sett opp Plaato Webhook" + "title": "Sett opp Plaato-enhetene" }, "webhook": { "description": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i Plaato Airlock. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer.", diff --git a/homeassistant/components/point/translations/no.json b/homeassistant/components/point/translations/no.json index d0d0b9114fb..a72a8083f6f 100644 --- a/homeassistant/components/point/translations/no.json +++ b/homeassistant/components/point/translations/no.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_setup": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", - "authorize_url_fail": "Ukjent feil ved generering av godkjenningsadresse", + "authorize_url_fail": "Ukjent feil under generering av en autoriserings-URL.", "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 godkjenningsadresse" + "unknown_authorize_url_generation": "Ukjent feil under generering av en autoriserings-URL." }, "create_entry": { "default": "Vellykket godkjenning" diff --git a/homeassistant/components/powerwall/translations/en.json b/homeassistant/components/powerwall/translations/en.json index 4ebe1e9d5ef..ae8122589be 100644 --- a/homeassistant/components/powerwall/translations/en.json +++ b/homeassistant/components/powerwall/translations/en.json @@ -22,4 +22,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/ca.json b/homeassistant/components/roku/translations/ca.json index eb0564b5bde..b60b8f83eb9 100644 --- a/homeassistant/components/roku/translations/ca.json +++ b/homeassistant/components/roku/translations/ca.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "unknown": "Error inesperat" }, "error": { diff --git a/homeassistant/components/roku/translations/it.json b/homeassistant/components/roku/translations/it.json index 100d9992472..3c11aa4d8ae 100644 --- a/homeassistant/components/roku/translations/it.json +++ b/homeassistant/components/roku/translations/it.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "unknown": "Errore imprevisto" }, "error": { diff --git a/homeassistant/components/roku/translations/ru.json b/homeassistant/components/roku/translations/ru.json index f7f36f41b27..c3ae135ed76 100644 --- a/homeassistant/components/roku/translations/ru.json +++ b/homeassistant/components/roku/translations/ru.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { diff --git a/homeassistant/components/roku/translations/zh-Hant.json b/homeassistant/components/roku/translations/zh-Hant.json index 4b0566d66b0..429c03a991e 100644 --- a/homeassistant/components/roku/translations/zh-Hant.json +++ b/homeassistant/components/roku/translations/zh-Hant.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/roomba/translations/no.json b/homeassistant/components/roomba/translations/no.json index 2bfe9f774d1..67df735719c 100644 --- a/homeassistant/components/roomba/translations/no.json +++ b/homeassistant/components/roomba/translations/no.json @@ -25,7 +25,7 @@ "data": { "password": "Passord" }, - "description": "Passordet kunne ikke hentes automatisk fra enheten. F\u00f8lg trinnene som er beskrevet i dokumentasjonen p\u00e5: {auth_help_url}", + "description": "Passordet kan ikke hentes fra enheten automatisk. F\u00f8lg trinnene som er beskrevet i dokumentasjonen p\u00e5: {auth_help_url}", "title": "Skriv inn passord" }, "manual": { diff --git a/homeassistant/components/shelly/translations/it.json b/homeassistant/components/shelly/translations/it.json index 4d486a8f2fa..051cf88dc38 100644 --- a/homeassistant/components/shelly/translations/it.json +++ b/homeassistant/components/shelly/translations/it.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "confirm_discovery": { - "description": "Vuoi impostare {model} su {host} ?\n\n Prima della configurazione, i dispositivi alimentati a batteria devono essere riattivati premendo il pulsante sul dispositivo." + "description": "Vuoi impostare {model} su {host}? \n\nI dispositivi alimentati a batteria protetti da password devono essere riattivati prima di continuare con la configurazione.\nI dispositivi alimentati a batteria che non sono protetti da password verranno aggiunti quando il dispositivo si riattiver\u00e0, ora puoi riattivare manualmente il dispositivo utilizzando un pulsante su di esso o attendere il prossimo aggiornamento dei dati dal dispositivo." }, "credentials": { "data": { @@ -24,7 +24,7 @@ "data": { "host": "Host" }, - "description": "Prima della configurazione, i dispositivi alimentati a batteria devono essere riattivati premendo il pulsante sul dispositivo." + "description": "Prima della configurazione, i dispositivi alimentati a batteria devono essere riattivati, ora puoi riattivare il dispositivo utilizzando un pulsante su di esso." } } }, diff --git a/homeassistant/components/shelly/translations/no.json b/homeassistant/components/shelly/translations/no.json index 1606a1acbb1..90cfe3ca906 100644 --- a/homeassistant/components/shelly/translations/no.json +++ b/homeassistant/components/shelly/translations/no.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "confirm_discovery": { - "description": "Vil du konfigurere {model} p\u00e5 {host} ?\n\n F\u00f8r du setter opp, m\u00e5 batteridrevne enheter vekkes ved \u00e5 trykke p\u00e5 knappen p\u00e5 enheten." + "description": "Vil du konfigurere {model} p\u00e5 {host} ? \n\n Batteridrevne enheter som er passordbeskyttet, m\u00e5 vekkes f\u00f8r du fortsetter med konfigurasjonen.\n Batteridrevne enheter som ikke er passordbeskyttet, blir lagt til n\u00e5r enheten v\u00e5kner, du kan n\u00e5 vekke enheten manuelt med en knapp p\u00e5 den eller vente p\u00e5 neste dataoppdatering fra enheten." }, "credentials": { "data": { @@ -24,7 +24,7 @@ "data": { "host": "Vert" }, - "description": "F\u00f8r du setter opp, m\u00e5 batteridrevne enheter vekkes ved \u00e5 trykke p\u00e5 knappen p\u00e5 enheten." + "description": "F\u00f8r du setter opp, m\u00e5 batteridrevne enheter vekkes, du kan n\u00e5 vekke enheten med en knapp p\u00e5 den." } } }, diff --git a/homeassistant/components/soma/translations/no.json b/homeassistant/components/soma/translations/no.json index f9b64dc8483..a399f430329 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 genererer godkjennelses-URL.", "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/spotify/translations/no.json b/homeassistant/components/spotify/translations/no.json index 8e2ec3d36c0..54e3ca1f8b4 100644 --- a/homeassistant/components/spotify/translations/no.json +++ b/homeassistant/components/spotify/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "authorize_url_timeout": "Tidsavbrudd genererer godkjennelses-URL.", "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 godkjent samsvarer ikke med kontoen som trenger godkjenning p\u00e5 nytt" diff --git a/homeassistant/components/tellduslive/translations/no.json b/homeassistant/components/tellduslive/translations/no.json index 649de0f86e4..563359d266a 100644 --- a/homeassistant/components/tellduslive/translations/no.json +++ b/homeassistant/components/tellduslive/translations/no.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "Tjenesten er allerede konfigurert", - "authorize_url_fail": "Ukjent feil ved generering av godkjenningsadresse", + "authorize_url_fail": "Ukjent feil under generering av en autoriserings-URL.", "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", "unknown": "Uventet feil", - "unknown_authorize_url_generation": "Ukjent feil ved generering av godkjenningsadresse" + "unknown_authorize_url_generation": "Ukjent feil under generering av en autoriserings-URL." }, "error": { "invalid_auth": "Ugyldig godkjenning" diff --git a/homeassistant/components/toon/translations/no.json b/homeassistant/components/toon/translations/no.json index a64a64ab74e..41246c42f0e 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 godkjenningsadresse", + "authorize_url_fail": "Ukjent feil under generering av en autoriserings-URL.", "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 godkjenningsadresse" + "unknown_authorize_url_generation": "Ukjent feil under generering av en autoriserings-URL." }, "step": { "agreement": { diff --git a/homeassistant/components/traccar/translations/no.json b/homeassistant/components/traccar/translations/no.json index 38faa4dc1c1..e2051be22b6 100644 --- a/homeassistant/components/traccar/translations/no.json +++ b/homeassistant/components/traccar/translations/no.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "Home Assistant forekomsten din m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta webhook meldinger" }, "create_entry": { - "default": "Hvis du vil sende hendelser til Home Assistant, m\u00e5 du konfigurere webhook-funksjonen i Traccar.\n\nBruk f\u00f8lgende URL-adresse: `{webhook_url}`\n\nSe [dokumentasjonen]({docs_url}) for mer informasjon." + "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du konfigurere webhook-funksjonen i Traccar. \n\n Bruk f\u00f8lgende URL: \"{webhook_url}\" \n\n Se [dokumentasjonen] ({docs_url}) for mer informasjon." }, "step": { "user": { diff --git a/homeassistant/components/vera/translations/no.json b/homeassistant/components/vera/translations/no.json index 7ec6850a7c8..f1454be6799 100644 --- a/homeassistant/components/vera/translations/no.json +++ b/homeassistant/components/vera/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cannot_connect": "Kunne ikke koble til kontrolleren med url {base_url}" + "cannot_connect": "Kan ikke koble til kontrolleren med URL-adressen {base_url}" }, "step": { "user": { @@ -10,7 +10,7 @@ "lights": "Vera bytter enhets ID-er for \u00e5 behandle som lys i Home Assistant", "vera_controller_url": "URL-adresse for kontroller" }, - "description": "Oppgi en Vera-kontroller-url nedenfor. Det skal se slik ut: http://192.168.1.161:3480.", + "description": "Gi en Vera-kontroller-URL nedenfor. Det skal se slik ut: http://192.168.1.161:3480.", "title": "Oppsett Vera-kontroller" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/no.json b/homeassistant/components/xiaomi_aqara/translations/no.json index 523e2da898c..5a46d66fcf0 100644 --- a/homeassistant/components/xiaomi_aqara/translations/no.json +++ b/homeassistant/components/xiaomi_aqara/translations/no.json @@ -18,7 +18,7 @@ "data": { "select_ip": "IP adresse" }, - "description": "Kj\u00f8r oppsettet igjen hvis du vil koble til tilleggsportaler", + "description": "Kj\u00f8r oppsettet p\u00e5 nytt hvis du vil koble til flere gatewayer", "title": "Velg Xiaomi Aqara Gateway som du \u00f8nsker \u00e5 koble til" }, "settings": { @@ -35,7 +35,7 @@ "interface": "Nettverksgrensesnittet som skal brukes", "mac": "MAC-adresse (valgfritt)" }, - "description": "Koble til Xiaomi Aqara Gateway, hvis IP- og MAC-adressene er tomme, brukes automatisk oppdagelse", + "description": "Koble til Xiaomi Aqara Gateway, hvis IP- og MAC-adressene blir tomme, brukes automatisk oppdagelse", "title": "" } } diff --git a/homeassistant/components/zwave/translations/no.json b/homeassistant/components/zwave/translations/no.json index ba875354f7f..ab5a405f975 100644 --- a/homeassistant/components/zwave/translations/no.json +++ b/homeassistant/components/zwave/translations/no.json @@ -13,7 +13,7 @@ "network_key": "Nettverksn\u00f8kkel (la v\u00e6re tom for automatisk oppretting)", "usb_path": "USB enhetsbane" }, - "description": "Se [www.home-assistant.io/docs/z-wave/installation/](https://www.home-assistant.io/docs/z-wave/installation/) for informasjon om konfigurasjon variablene", + "description": "Denne integrasjonen opprettholdes ikke lenger. For nye installasjoner, bruk Z-Wave JS i stedet. \n\n Se https://www.home-assistant.io/docs/z-wave/installation/ for informasjon om konfigurasjonsvariablene", "title": "Sett opp Z-Wave" } } From 7f8fa7feafc4d8b48071817f240f67427a764279 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Wed, 10 Feb 2021 21:20:40 -0500 Subject: [PATCH 347/796] Use core constants for logi_circle (#46359) --- homeassistant/components/logi_circle/__init__.py | 3 +-- homeassistant/components/logi_circle/config_flow.py | 5 ++--- homeassistant/components/logi_circle/const.py | 1 - 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index d3551765079..056783ef6da 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -11,6 +11,7 @@ from homeassistant import config_entries from homeassistant.components.camera import ATTR_FILENAME, CAMERA_SERVICE_SCHEMA from homeassistant.const import ( ATTR_MODE, + CONF_API_KEY, CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_MONITORED_CONDITIONS, @@ -22,7 +23,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from . import config_flow from .const import ( - CONF_API_KEY, CONF_REDIRECT_URI, DATA_LOGI, DEFAULT_CACHEDB, @@ -117,7 +117,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up Logi Circle from a config entry.""" - logi_circle = LogiCircle( client_id=entry.data[CONF_CLIENT_ID], client_secret=entry.data[CONF_CLIENT_SECRET], diff --git a/homeassistant/components/logi_circle/config_flow.py b/homeassistant/components/logi_circle/config_flow.py index 279c3052010..00fd0edc437 100644 --- a/homeassistant/components/logi_circle/config_flow.py +++ b/homeassistant/components/logi_circle/config_flow.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( + CONF_API_KEY, CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_SENSORS, @@ -17,7 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback -from .const import CONF_API_KEY, CONF_REDIRECT_URI, DEFAULT_CACHEDB, DOMAIN +from .const import CONF_REDIRECT_URI, DEFAULT_CACHEDB, DOMAIN _TIMEOUT = 15 # seconds @@ -120,7 +121,6 @@ class LogiCircleFlowHandler(config_entries.ConfigFlow): def _get_authorization_url(self): """Create temporary Circle session and generate authorization url.""" - flow = self.hass.data[DATA_FLOW_IMPL][self.flow_impl] client_id = flow[CONF_CLIENT_ID] client_secret = flow[CONF_CLIENT_SECRET] @@ -147,7 +147,6 @@ class LogiCircleFlowHandler(config_entries.ConfigFlow): async def _async_create_session(self, code): """Create Logi Circle session and entries.""" - flow = self.hass.data[DATA_FLOW_IMPL][DOMAIN] client_id = flow[CONF_CLIENT_ID] client_secret = flow[CONF_CLIENT_SECRET] diff --git a/homeassistant/components/logi_circle/const.py b/homeassistant/components/logi_circle/const.py index fb22338b2c7..92967d2eb84 100644 --- a/homeassistant/components/logi_circle/const.py +++ b/homeassistant/components/logi_circle/const.py @@ -4,7 +4,6 @@ from homeassistant.const import PERCENTAGE DOMAIN = "logi_circle" DATA_LOGI = DOMAIN -CONF_API_KEY = "api_key" CONF_REDIRECT_URI = "redirect_uri" DEFAULT_CACHEDB = ".logi_cache.pickle" From af2fa17e8ebb22c18955c7cedacd29c49d5dac24 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Wed, 10 Feb 2021 21:21:35 -0500 Subject: [PATCH 348/796] Use core constants for local_file (#46349) --- homeassistant/components/local_file/camera.py | 10 ++-------- homeassistant/components/local_file/const.py | 1 - 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/local_file/camera.py b/homeassistant/components/local_file/camera.py index 1d06efeb708..b0b84677183 100644 --- a/homeassistant/components/local_file/camera.py +++ b/homeassistant/components/local_file/camera.py @@ -10,16 +10,10 @@ from homeassistant.components.camera import ( PLATFORM_SCHEMA, Camera, ) -from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME +from homeassistant.const import ATTR_ENTITY_ID, CONF_FILE_PATH, CONF_NAME from homeassistant.helpers import config_validation as cv -from .const import ( - CONF_FILE_PATH, - DATA_LOCAL_FILE, - DEFAULT_NAME, - DOMAIN, - SERVICE_UPDATE_FILE_PATH, -) +from .const import DATA_LOCAL_FILE, DEFAULT_NAME, DOMAIN, SERVICE_UPDATE_FILE_PATH _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/local_file/const.py b/homeassistant/components/local_file/const.py index 5225a70daed..3ea98f89c0e 100644 --- a/homeassistant/components/local_file/const.py +++ b/homeassistant/components/local_file/const.py @@ -1,6 +1,5 @@ """Constants for the Local File Camera component.""" DOMAIN = "local_file" SERVICE_UPDATE_FILE_PATH = "update_file_path" -CONF_FILE_PATH = "file_path" DATA_LOCAL_FILE = "local_file_cameras" DEFAULT_NAME = "Local File" From 56adc9dadb43aa9045080e00647b5d5447027c9d Mon Sep 17 00:00:00 2001 From: tkdrob Date: Wed, 10 Feb 2021 21:22:32 -0500 Subject: [PATCH 349/796] Use core constants for lcn (#46348) --- homeassistant/components/lcn/binary_sensor.py | 4 ++-- homeassistant/components/lcn/climate.py | 8 ++++++-- homeassistant/components/lcn/const.py | 2 -- homeassistant/components/lcn/light.py | 2 -- homeassistant/components/lcn/scene.py | 3 +-- homeassistant/components/lcn/schemas.py | 4 ++-- homeassistant/components/lcn/sensor.py | 3 +-- homeassistant/components/lcn/switch.py | 1 - 8 files changed, 12 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/lcn/binary_sensor.py b/homeassistant/components/lcn/binary_sensor.py index 415668f5924..56a5ea6e646 100644 --- a/homeassistant/components/lcn/binary_sensor.py +++ b/homeassistant/components/lcn/binary_sensor.py @@ -2,10 +2,10 @@ import pypck from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.const import CONF_ADDRESS +from homeassistant.const import CONF_ADDRESS, CONF_SOURCE from . import LcnEntity -from .const import BINSENSOR_PORTS, CONF_CONNECTIONS, CONF_SOURCE, DATA_LCN, SETPOINTS +from .const import BINSENSOR_PORTS, CONF_CONNECTIONS, DATA_LCN, SETPOINTS from .helpers import get_connection diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index ece3994f651..e3269a51cd6 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -3,7 +3,12 @@ import pypck from homeassistant.components.climate import ClimateEntity, const -from homeassistant.const import ATTR_TEMPERATURE, CONF_ADDRESS, CONF_UNIT_OF_MEASUREMENT +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_ADDRESS, + CONF_SOURCE, + CONF_UNIT_OF_MEASUREMENT, +) from . import LcnEntity from .const import ( @@ -12,7 +17,6 @@ from .const import ( CONF_MAX_TEMP, CONF_MIN_TEMP, CONF_SETPOINT, - CONF_SOURCE, DATA_LCN, ) from .helpers import get_connection diff --git a/homeassistant/components/lcn/const.py b/homeassistant/components/lcn/const.py index 821a7102154..3dcac6fb55f 100644 --- a/homeassistant/components/lcn/const.py +++ b/homeassistant/components/lcn/const.py @@ -25,7 +25,6 @@ CONF_LOCKABLE = "lockable" CONF_VARIABLE = "variable" CONF_VALUE = "value" CONF_RELVARREF = "value_reference" -CONF_SOURCE = "source" CONF_SETPOINT = "setpoint" CONF_LED = "led" CONF_KEYS = "keys" @@ -40,7 +39,6 @@ CONF_MAX_TEMP = "max_temp" CONF_MIN_TEMP = "min_temp" CONF_SCENES = "scenes" CONF_REGISTER = "register" -CONF_SCENE = "scene" CONF_OUTPUTS = "outputs" CONF_REVERSE_TIME = "reverse_time" diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index 5242ed1cc59..8a76056ff46 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -166,7 +166,6 @@ class LcnRelayLight(LcnEntity, LightEntity): async def async_turn_on(self, **kwargs): """Turn the entity on.""" - states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 states[self.output.value] = pypck.lcn_defs.RelayStateModifier.ON if not await self.device_connection.control_relays(states): @@ -176,7 +175,6 @@ class LcnRelayLight(LcnEntity, LightEntity): async def async_turn_off(self, **kwargs): """Turn the entity off.""" - states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 states[self.output.value] = pypck.lcn_defs.RelayStateModifier.OFF if not await self.device_connection.control_relays(states): diff --git a/homeassistant/components/lcn/scene.py b/homeassistant/components/lcn/scene.py index ed211473e29..1c359607fb2 100644 --- a/homeassistant/components/lcn/scene.py +++ b/homeassistant/components/lcn/scene.py @@ -4,14 +4,13 @@ from typing import Any import pypck from homeassistant.components.scene import Scene -from homeassistant.const import CONF_ADDRESS +from homeassistant.const import CONF_ADDRESS, CONF_SCENE from . import LcnEntity from .const import ( CONF_CONNECTIONS, CONF_OUTPUTS, CONF_REGISTER, - CONF_SCENE, CONF_TRANSITION, DATA_LCN, OUTPUT_PORTS, diff --git a/homeassistant/components/lcn/schemas.py b/homeassistant/components/lcn/schemas.py index 1cc51f400da..5244bac3b6b 100644 --- a/homeassistant/components/lcn/schemas.py +++ b/homeassistant/components/lcn/schemas.py @@ -11,7 +11,9 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_PORT, + CONF_SCENE, CONF_SENSORS, + CONF_SOURCE, CONF_SWITCHES, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, @@ -32,11 +34,9 @@ from .const import ( CONF_OUTPUTS, CONF_REGISTER, CONF_REVERSE_TIME, - CONF_SCENE, CONF_SCENES, CONF_SETPOINT, CONF_SK_NUM_TRIES, - CONF_SOURCE, CONF_TRANSITION, DIM_MODES, DOMAIN, diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index 4d4be5e1259..11932dccea8 100644 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -1,12 +1,11 @@ """Support for LCN sensors.""" import pypck -from homeassistant.const import CONF_ADDRESS, CONF_UNIT_OF_MEASUREMENT +from homeassistant.const import CONF_ADDRESS, CONF_SOURCE, CONF_UNIT_OF_MEASUREMENT from . import LcnEntity from .const import ( CONF_CONNECTIONS, - CONF_SOURCE, DATA_LCN, LED_PORTS, S0_INPUTS, diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py index 6f9cc25db99..5fe624b04bf 100644 --- a/homeassistant/components/lcn/switch.py +++ b/homeassistant/components/lcn/switch.py @@ -117,7 +117,6 @@ class LcnRelaySwitch(LcnEntity, SwitchEntity): async def async_turn_off(self, **kwargs): """Turn the entity off.""" - states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 states[self.output.value] = pypck.lcn_defs.RelayStateModifier.OFF if not await self.device_connection.control_relays(states): From 3ffa42e56a0c0792292a2da5b37e6085d32bb824 Mon Sep 17 00:00:00 2001 From: Leonardo Figueiro Date: Thu, 11 Feb 2021 04:25:42 -0300 Subject: [PATCH 350/796] Update WiLight Cover Fan Light (#46366) --- homeassistant/components/wilight/__init__.py | 4 ++-- homeassistant/components/wilight/config_flow.py | 2 +- homeassistant/components/wilight/cover.py | 3 +-- homeassistant/components/wilight/fan.py | 9 +++++++-- homeassistant/components/wilight/light.py | 3 +-- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py index 0e08fec2c31..67433772551 100644 --- a/homeassistant/components/wilight/__init__.py +++ b/homeassistant/components/wilight/__init__.py @@ -1,8 +1,6 @@ """The WiLight integration.""" import asyncio -from pywilight.const import DOMAIN - from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -10,6 +8,8 @@ from homeassistant.helpers.entity import Entity from .parent_device import WiLightParent +DOMAIN = "wilight" + # List the platforms that you want to support. PLATFORMS = ["cover", "fan", "light"] diff --git a/homeassistant/components/wilight/config_flow.py b/homeassistant/components/wilight/config_flow.py index c3148a4d045..40643b4372f 100644 --- a/homeassistant/components/wilight/config_flow.py +++ b/homeassistant/components/wilight/config_flow.py @@ -7,7 +7,7 @@ from homeassistant.components import ssdp from homeassistant.config_entries import CONN_CLASS_LOCAL_PUSH, ConfigFlow from homeassistant.const import CONF_HOST -DOMAIN = "wilight" +from . import DOMAIN # pylint: disable=unused-import CONF_SERIAL_NUMBER = "serial_number" CONF_MODEL_NAME = "model_name" diff --git a/homeassistant/components/wilight/cover.py b/homeassistant/components/wilight/cover.py index bbe723b413a..93c9a8c4503 100644 --- a/homeassistant/components/wilight/cover.py +++ b/homeassistant/components/wilight/cover.py @@ -2,7 +2,6 @@ from pywilight.const import ( COVER_V1, - DOMAIN, ITEM_COVER, WL_CLOSE, WL_CLOSING, @@ -16,7 +15,7 @@ from homeassistant.components.cover import ATTR_POSITION, CoverEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from . import WiLightDevice +from . import DOMAIN, WiLightDevice async def async_setup_entry( diff --git a/homeassistant/components/wilight/fan.py b/homeassistant/components/wilight/fan.py index 948d99ac81d..ece79874ccf 100644 --- a/homeassistant/components/wilight/fan.py +++ b/homeassistant/components/wilight/fan.py @@ -1,7 +1,6 @@ """Support for WiLight Fan.""" from pywilight.const import ( - DOMAIN, FAN_V1, ITEM_FAN, WL_DIRECTION_FORWARD, @@ -25,7 +24,7 @@ from homeassistant.util.percentage import ( percentage_to_ordered_list_item, ) -from . import WiLightDevice +from . import DOMAIN, WiLightDevice ORDERED_NAMED_FAN_SPEEDS = [WL_SPEED_LOW, WL_SPEED_MEDIUM, WL_SPEED_HIGH] @@ -80,6 +79,9 @@ class WiLightFan(WiLightDevice, FanEntity): @property def percentage(self) -> str: """Return the current speed percentage.""" + if "direction" in self._status: + if self._status["direction"] == WL_DIRECTION_OFF: + return 0 wl_speed = self._status.get("speed") if wl_speed is None: return None @@ -111,6 +113,9 @@ class WiLightFan(WiLightDevice, FanEntity): if percentage == 0: await self._client.set_fan_direction(self._index, WL_DIRECTION_OFF) return + if "direction" in self._status: + if self._status["direction"] == WL_DIRECTION_OFF: + await self._client.set_fan_direction(self._index, self._direction) wl_speed = percentage_to_ordered_list_item(ORDERED_NAMED_FAN_SPEEDS, percentage) await self._client.set_fan_speed(self._index, wl_speed) diff --git a/homeassistant/components/wilight/light.py b/homeassistant/components/wilight/light.py index 2f3c9e3c5f2..0c7206be00c 100644 --- a/homeassistant/components/wilight/light.py +++ b/homeassistant/components/wilight/light.py @@ -1,7 +1,6 @@ """Support for WiLight lights.""" from pywilight.const import ( - DOMAIN, ITEM_LIGHT, LIGHT_COLOR, LIGHT_DIMMER, @@ -19,7 +18,7 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from . import WiLightDevice +from . import DOMAIN, WiLightDevice def entities_from_discovered_wilight(hass, api_device): From f549ec5ec949a1615aaa717fe60df5369a02ec13 Mon Sep 17 00:00:00 2001 From: Mike Keesey Date: Thu, 11 Feb 2021 00:50:27 -0700 Subject: [PATCH 351/796] Use activity ids for unique_id for Harmony switches (#46139) --- homeassistant/components/harmony/__init__.py | 33 +++++++++ homeassistant/components/harmony/data.py | 18 +++-- homeassistant/components/harmony/switch.py | 15 ++-- tests/components/harmony/conftest.py | 11 ++- tests/components/harmony/const.py | 5 ++ tests/components/harmony/test_init.py | 72 ++++++++++++++++++++ 6 files changed, 137 insertions(+), 17 deletions(-) create mode 100644 tests/components/harmony/test_init.py diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index 6ba63ee0f81..8445c7be937 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -1,16 +1,20 @@ """The Logitech Harmony Hub integration.""" import asyncio +import logging from homeassistant.components.remote import ATTR_ACTIVITY, ATTR_DELAY_SECS from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import DOMAIN, HARMONY_OPTIONS_UPDATE, PLATFORMS from .data import HarmonyData +_LOGGER = logging.getLogger(__name__) + async def async_setup(hass: HomeAssistant, config: dict): """Set up the Logitech Harmony Hub component.""" @@ -40,6 +44,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][entry.entry_id] = data + await _migrate_old_unique_ids(hass, entry.entry_id, data) + entry.add_update_listener(_update_listener) for component in PLATFORMS: @@ -50,6 +56,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return True +async def _migrate_old_unique_ids( + hass: HomeAssistant, entry_id: str, data: HarmonyData +): + names_to_ids = {activity["label"]: activity["id"] for activity in data.activities} + + @callback + def _async_migrator(entity_entry: entity_registry.RegistryEntry): + # Old format for switches was {remote_unique_id}-{activity_name} + # New format is activity_{activity_id} + parts = entity_entry.unique_id.split("-", 1) + if len(parts) > 1: # old format + activity_name = parts[1] + activity_id = names_to_ids.get(activity_name) + + if activity_id is not None: + _LOGGER.info( + "Migrating unique_id from [%s] to [%s]", + entity_entry.unique_id, + activity_id, + ) + return {"new_unique_id": f"activity_{activity_id}"} + + return None + + await entity_registry.async_migrate_entries(hass, entry_id, _async_migrator) + + @callback def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry): options = dict(entry.options) diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py index 6c3ad874fa9..8c1d137bc85 100644 --- a/homeassistant/components/harmony/data.py +++ b/homeassistant/components/harmony/data.py @@ -34,18 +34,22 @@ class HarmonyData(HarmonySubscriberMixin): ip_address=address, callbacks=ClientCallbackType(**callbacks) ) + @property + def activities(self): + """List of all non-poweroff activity objects.""" + activity_infos = self._client.config.get("activity", []) + return [ + info + for info in activity_infos + if info["label"] is not None and info["label"] != ACTIVITY_POWER_OFF + ] + @property def activity_names(self): """Names of all the remotes activities.""" - activity_infos = self._client.config.get("activity", []) + activity_infos = self.activities activities = [activity["label"] for activity in activity_infos] - # Remove both ways of representing PowerOff - if None in activities: - activities.remove(None) - if ACTIVITY_POWER_OFF in activities: - activities.remove(ACTIVITY_POWER_OFF) - return activities @property diff --git a/homeassistant/components/harmony/switch.py b/homeassistant/components/harmony/switch.py index 2832872c2ef..5aac145e749 100644 --- a/homeassistant/components/harmony/switch.py +++ b/homeassistant/components/harmony/switch.py @@ -15,12 +15,12 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, entry, async_add_entities): """Set up harmony activity switches.""" data = hass.data[DOMAIN][entry.entry_id] - activities = data.activity_names + activities = data.activities switches = [] for activity in activities: _LOGGER.debug("creating switch for activity: %s", activity) - name = f"{entry.data[CONF_NAME]} {activity}" + name = f"{entry.data[CONF_NAME]} {activity['label']}" switches.append(HarmonyActivitySwitch(name, activity, data)) async_add_entities(switches, True) @@ -29,11 +29,12 @@ async def async_setup_entry(hass, entry, async_add_entities): class HarmonyActivitySwitch(ConnectionStateMixin, SwitchEntity): """Switch representation of a Harmony activity.""" - def __init__(self, name: str, activity: str, data: HarmonyData): + def __init__(self, name: str, activity: dict, data: HarmonyData): """Initialize HarmonyActivitySwitch class.""" super().__init__() self._name = name - self._activity = activity + self._activity_name = activity["label"] + self._activity_id = activity["id"] self._data = data @property @@ -44,7 +45,7 @@ class HarmonyActivitySwitch(ConnectionStateMixin, SwitchEntity): @property def unique_id(self): """Return the unique id.""" - return f"{self._data.unique_id}-{self._activity}" + return f"activity_{self._activity_id}" @property def device_info(self): @@ -55,7 +56,7 @@ class HarmonyActivitySwitch(ConnectionStateMixin, SwitchEntity): def is_on(self): """Return if the current activity is the one for this switch.""" _, activity_name = self._data.current_activity - return activity_name == self._activity + return activity_name == self._activity_name @property def should_poll(self): @@ -69,7 +70,7 @@ class HarmonyActivitySwitch(ConnectionStateMixin, SwitchEntity): async def async_turn_on(self, **kwargs): """Start this activity.""" - await self._data.async_start_activity(self._activity) + await self._data.async_start_activity(self._activity_name) async def async_turn_off(self, **kwargs): """Stop this activity.""" diff --git a/tests/components/harmony/conftest.py b/tests/components/harmony/conftest.py index cde8c43fe89..6ca483b2588 100644 --- a/tests/components/harmony/conftest.py +++ b/tests/components/harmony/conftest.py @@ -7,21 +7,22 @@ import pytest from homeassistant.components.harmony.const import ACTIVITY_POWER_OFF -_LOGGER = logging.getLogger(__name__) +from .const import NILE_TV_ACTIVITY_ID, PLAY_MUSIC_ACTIVITY_ID, WATCH_TV_ACTIVITY_ID -WATCH_TV_ACTIVITY_ID = 123 -PLAY_MUSIC_ACTIVITY_ID = 456 +_LOGGER = logging.getLogger(__name__) ACTIVITIES_TO_IDS = { ACTIVITY_POWER_OFF: -1, "Watch TV": WATCH_TV_ACTIVITY_ID, "Play Music": PLAY_MUSIC_ACTIVITY_ID, + "Nile-TV": NILE_TV_ACTIVITY_ID, } IDS_TO_ACTIVITIES = { -1: ACTIVITY_POWER_OFF, WATCH_TV_ACTIVITY_ID: "Watch TV", PLAY_MUSIC_ACTIVITY_ID: "Play Music", + NILE_TV_ACTIVITY_ID: "Nile-TV", } TV_DEVICE_ID = 1234 @@ -111,6 +112,7 @@ class FakeHarmonyClient: return_value=[ {"name": "Watch TV", "id": WATCH_TV_ACTIVITY_ID}, {"name": "Play Music", "id": PLAY_MUSIC_ACTIVITY_ID}, + {"name": "Nile-TV", "id": NILE_TV_ACTIVITY_ID}, ] ) type(config).devices = PropertyMock( @@ -121,8 +123,11 @@ class FakeHarmonyClient: type(config).config = PropertyMock( return_value={ "activity": [ + {"id": 10000, "label": None}, + {"id": -1, "label": "PowerOff"}, {"id": WATCH_TV_ACTIVITY_ID, "label": "Watch TV"}, {"id": PLAY_MUSIC_ACTIVITY_ID, "label": "Play Music"}, + {"id": NILE_TV_ACTIVITY_ID, "label": "Nile-TV"}, ] } ) diff --git a/tests/components/harmony/const.py b/tests/components/harmony/const.py index 1911ea949af..488fe30dec3 100644 --- a/tests/components/harmony/const.py +++ b/tests/components/harmony/const.py @@ -4,3 +4,8 @@ HUB_NAME = "Guest Room" ENTITY_REMOTE = "remote.guest_room" ENTITY_WATCH_TV = "switch.guest_room_watch_tv" ENTITY_PLAY_MUSIC = "switch.guest_room_play_music" +ENTITY_NILE_TV = "switch.guest_room_nile_tv" + +WATCH_TV_ACTIVITY_ID = 123 +PLAY_MUSIC_ACTIVITY_ID = 456 +NILE_TV_ACTIVITY_ID = 789 diff --git a/tests/components/harmony/test_init.py b/tests/components/harmony/test_init.py new file mode 100644 index 00000000000..c63727f8738 --- /dev/null +++ b/tests/components/harmony/test_init.py @@ -0,0 +1,72 @@ +"""Test init of Logitch Harmony Hub integration.""" +from homeassistant.components.harmony.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.helpers import entity_registry +from homeassistant.setup import async_setup_component + +from .const import ( + ENTITY_NILE_TV, + ENTITY_PLAY_MUSIC, + ENTITY_WATCH_TV, + HUB_NAME, + NILE_TV_ACTIVITY_ID, + PLAY_MUSIC_ACTIVITY_ID, + WATCH_TV_ACTIVITY_ID, +) + +from tests.common import MockConfigEntry, mock_registry + + +async def test_unique_id_migration(mock_hc, hass, mock_write_config): + """Test migration of switch unique ids to stable ones.""" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} + ) + + entry.add_to_hass(hass) + mock_registry( + hass, + { + # old format + ENTITY_WATCH_TV: entity_registry.RegistryEntry( + entity_id=ENTITY_WATCH_TV, + unique_id="123443-Watch TV", + platform="harmony", + config_entry_id=entry.entry_id, + ), + # old format, activity name with - + ENTITY_NILE_TV: entity_registry.RegistryEntry( + entity_id=ENTITY_NILE_TV, + unique_id="123443-Nile-TV", + platform="harmony", + config_entry_id=entry.entry_id, + ), + # new format + ENTITY_PLAY_MUSIC: entity_registry.RegistryEntry( + entity_id=ENTITY_PLAY_MUSIC, + unique_id=f"activity_{PLAY_MUSIC_ACTIVITY_ID}", + platform="harmony", + config_entry_id=entry.entry_id, + ), + # old entity which no longer has a matching activity on the hub. skipped. + "switch.some_other_activity": entity_registry.RegistryEntry( + entity_id="switch.some_other_activity", + unique_id="123443-Some Other Activity", + platform="harmony", + config_entry_id=entry.entry_id, + ), + }, + ) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + ent_reg = await entity_registry.async_get_registry(hass) + + switch_tv = ent_reg.async_get(ENTITY_WATCH_TV) + assert switch_tv.unique_id == f"activity_{WATCH_TV_ACTIVITY_ID}" + + switch_nile = ent_reg.async_get(ENTITY_NILE_TV) + assert switch_nile.unique_id == f"activity_{NILE_TV_ACTIVITY_ID}" + + switch_music = ent_reg.async_get(ENTITY_PLAY_MUSIC) + assert switch_music.unique_id == f"activity_{PLAY_MUSIC_ACTIVITY_ID}" From 379f5455e58d691738f550b780c948da4db629d8 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Thu, 11 Feb 2021 04:13:18 -0500 Subject: [PATCH 352/796] Use core constants for lovelace (#46368) --- homeassistant/components/lovelace/__init__.py | 4 +--- homeassistant/components/lovelace/const.py | 4 +--- homeassistant/components/lovelace/resources.py | 3 +-- homeassistant/components/lovelace/system_health.py | 3 ++- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 99b00a92289..e673b2a470b 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.components import frontend from homeassistant.config import async_hass_config_yaml, async_process_component_config -from homeassistant.const import CONF_FILENAME +from homeassistant.const import CONF_FILENAME, CONF_MODE, CONF_RESOURCES from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection, config_validation as cv @@ -16,9 +16,7 @@ from homeassistant.loader import async_get_integration from . import dashboard, resources, websocket from .const import ( CONF_ICON, - CONF_MODE, CONF_REQUIRE_ADMIN, - CONF_RESOURCES, CONF_SHOW_IN_SIDEBAR, CONF_TITLE, CONF_URL_PATH, diff --git a/homeassistant/components/lovelace/const.py b/homeassistant/components/lovelace/const.py index e93649de451..6952a80a214 100644 --- a/homeassistant/components/lovelace/const.py +++ b/homeassistant/components/lovelace/const.py @@ -3,7 +3,7 @@ from typing import Any import voluptuous as vol -from homeassistant.const import CONF_ICON, CONF_TYPE, CONF_URL +from homeassistant.const import CONF_ICON, CONF_MODE, CONF_TYPE, CONF_URL from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.util import slugify @@ -13,13 +13,11 @@ EVENT_LOVELACE_UPDATED = "lovelace_updated" DEFAULT_ICON = "hass:view-dashboard" -CONF_MODE = "mode" MODE_YAML = "yaml" MODE_STORAGE = "storage" MODE_AUTO = "auto-gen" LOVELACE_CONFIG_FILE = "ui-lovelace.yaml" -CONF_RESOURCES = "resources" CONF_URL_PATH = "url_path" CONF_RESOURCE_TYPE_WS = "res_type" diff --git a/homeassistant/components/lovelace/resources.py b/homeassistant/components/lovelace/resources.py index 78a23540ed4..0a3e36892d5 100644 --- a/homeassistant/components/lovelace/resources.py +++ b/homeassistant/components/lovelace/resources.py @@ -5,14 +5,13 @@ import uuid import voluptuous as vol -from homeassistant.const import CONF_TYPE +from homeassistant.const import CONF_RESOURCES, CONF_TYPE from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection, storage from .const import ( CONF_RESOURCE_TYPE_WS, - CONF_RESOURCES, DOMAIN, RESOURCE_CREATE_FIELDS, RESOURCE_SCHEMA, diff --git a/homeassistant/components/lovelace/system_health.py b/homeassistant/components/lovelace/system_health.py index a148427c9bd..2f4cfc6af76 100644 --- a/homeassistant/components/lovelace/system_health.py +++ b/homeassistant/components/lovelace/system_health.py @@ -2,9 +2,10 @@ import asyncio from homeassistant.components import system_health +from homeassistant.const import CONF_MODE from homeassistant.core import HomeAssistant, callback -from .const import CONF_MODE, DOMAIN, MODE_AUTO, MODE_STORAGE, MODE_YAML +from .const import DOMAIN, MODE_AUTO, MODE_STORAGE, MODE_YAML @callback From 29d8b8a22fb1eb93fbd75dbdcde2d4c9dcf3c964 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Thu, 11 Feb 2021 04:19:39 -0500 Subject: [PATCH 353/796] Some code cleanups for ESPHome (#46367) --- homeassistant/components/esphome/__init__.py | 11 +++++------ homeassistant/components/esphome/config_flow.py | 9 ++++----- homeassistant/components/esphome/entry_data.py | 2 -- tests/components/esphome/test_config_flow.py | 12 ++++++------ 4 files changed, 15 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 0b3a7522845..23b4044fc9e 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -40,8 +40,7 @@ from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, HomeAssistantType # Import config flow so that it's added to the registry -from .config_flow import EsphomeFlowHandler # noqa: F401 -from .entry_data import DATA_KEY, RuntimeEntryData +from .entry_data import RuntimeEntryData DOMAIN = "esphome" _LOGGER = logging.getLogger(__name__) @@ -62,7 +61,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up the esphome component.""" - hass.data.setdefault(DATA_KEY, {}) + hass.data.setdefault(DOMAIN, {}) host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] @@ -84,7 +83,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool store = Store( hass, STORAGE_VERSION, f"esphome.{entry.entry_id}", encoder=JSONEncoder ) - entry_data = hass.data[DATA_KEY][entry.entry_id] = RuntimeEntryData( + entry_data = hass.data[DOMAIN][entry.entry_id] = RuntimeEntryData( client=cli, entry_id=entry.entry_id, store=store ) @@ -363,7 +362,7 @@ async def _cleanup_instance( hass: HomeAssistantType, entry: ConfigEntry ) -> RuntimeEntryData: """Cleanup the esphome client if it exists.""" - data: RuntimeEntryData = hass.data[DATA_KEY].pop(entry.entry_id) + data: RuntimeEntryData = hass.data[DOMAIN].pop(entry.entry_id) if data.reconnect_task is not None: data.reconnect_task.cancel() for disconnect_cb in data.disconnect_callbacks: @@ -545,7 +544,7 @@ class EsphomeBaseEntity(Entity): @property def _entry_data(self) -> RuntimeEntryData: - return self.hass.data[DATA_KEY][self._entry_id] + return self.hass.data[DOMAIN][self._entry_id] @property def _static_info(self) -> EntityInfo: diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 1d1fc421bb8..34b168cdd8c 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -11,9 +11,8 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .entry_data import DATA_KEY, RuntimeEntryData - -DOMAIN = "esphome" +from . import DOMAIN +from .entry_data import RuntimeEntryData class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): @@ -107,9 +106,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): ]: # Is this address or IP address already configured? already_configured = True - elif entry.entry_id in self.hass.data.get(DATA_KEY, {}): + elif entry.entry_id in self.hass.data.get(DOMAIN, {}): # Does a config entry with this name already exist? - data: RuntimeEntryData = self.hass.data[DATA_KEY][entry.entry_id] + data: RuntimeEntryData = self.hass.data[DOMAIN][entry.entry_id] # Node names are unique in the network if data.device_info is not None: diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 54da1ed5562..58b20d18e12 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -29,8 +29,6 @@ from homeassistant.helpers.typing import HomeAssistantType if TYPE_CHECKING: from . import APIClient -DATA_KEY = "esphome" - SAVE_DELAY = 120 # Mapping from ESPHome info type to HA platform diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index f3afce0d43b..233255c1a89 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from homeassistant.components.esphome import DATA_KEY +from homeassistant.components.esphome import DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -207,7 +207,7 @@ async def test_discovery_initiation(hass, mock_client): async def test_discovery_already_configured_hostname(hass, mock_client): """Test discovery aborts if already configured via hostname.""" entry = MockConfigEntry( - domain="esphome", + domain=DOMAIN, data={CONF_HOST: "test8266.local", CONF_PORT: 6053, CONF_PASSWORD: ""}, ) @@ -232,7 +232,7 @@ async def test_discovery_already_configured_hostname(hass, mock_client): async def test_discovery_already_configured_ip(hass, mock_client): """Test discovery aborts if already configured via static IP.""" entry = MockConfigEntry( - domain="esphome", + domain=DOMAIN, data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, ) @@ -257,14 +257,14 @@ async def test_discovery_already_configured_ip(hass, mock_client): async def test_discovery_already_configured_name(hass, mock_client): """Test discovery aborts if already configured via name.""" entry = MockConfigEntry( - domain="esphome", + domain=DOMAIN, data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, ) entry.add_to_hass(hass) mock_entry_data = MagicMock() mock_entry_data.device_info.name = "test8266" - hass.data[DATA_KEY] = {entry.entry_id: mock_entry_data} + hass.data[DOMAIN] = {entry.entry_id: mock_entry_data} service_info = { "host": "192.168.43.184", @@ -310,7 +310,7 @@ async def test_discovery_duplicate_data(hass, mock_client): async def test_discovery_updates_unique_id(hass, mock_client): """Test a duplicate discovery host aborts and updates existing entry.""" entry = MockConfigEntry( - domain="esphome", + domain=DOMAIN, data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, ) From f9f4c0aeede27261d5d5d1aa3b3b21821a92041c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Feb 2021 23:24:31 -1000 Subject: [PATCH 354/796] Fix explict return in tesla config flow (#46377) --- homeassistant/components/tesla/config_flow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py index 194ea71a3b7..8dce5e238ac 100644 --- a/homeassistant/components/tesla/config_flow.py +++ b/homeassistant/components/tesla/config_flow.py @@ -106,6 +106,7 @@ class TeslaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): for entry in self._async_current_entries(): if entry.data.get(CONF_USERNAME) == username: return entry + return None class OptionsFlowHandler(config_entries.OptionsFlow): From e013ad2413c0d94035b57b970451e9d7fef917ae Mon Sep 17 00:00:00 2001 From: tkdrob Date: Thu, 11 Feb 2021 04:25:43 -0500 Subject: [PATCH 355/796] Use core constants for microsoft (#46369) --- homeassistant/components/microsoft/tts.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index c349589ebdd..bbfe9b0379e 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -6,7 +6,7 @@ from pycsspeechtts import pycsspeechtts import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider -from homeassistant.const import CONF_API_KEY, CONF_TYPE, PERCENTAGE +from homeassistant.const import CONF_API_KEY, CONF_REGION, CONF_TYPE, PERCENTAGE import homeassistant.helpers.config_validation as cv CONF_GENDER = "gender" @@ -15,8 +15,6 @@ CONF_RATE = "rate" CONF_VOLUME = "volume" CONF_PITCH = "pitch" CONF_CONTOUR = "contour" -CONF_REGION = "region" - _LOGGER = logging.getLogger(__name__) SUPPORTED_LANGUAGES = [ From 1f5fb8f28aefebe0ae0cf42f0da5a901c2f994e8 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 11 Feb 2021 10:30:09 +0100 Subject: [PATCH 356/796] Raise ConditionError for template errors (#46245) --- homeassistant/helpers/condition.py | 3 +-- tests/helpers/test_condition.py | 13 ++++--------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 126513608c7..da4fdab07d7 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -475,8 +475,7 @@ def async_template( try: value: str = value_template.async_render(variables, parse_result=False) except TemplateError as ex: - _LOGGER.error("Error during template condition: %s", ex) - return False + raise ConditionError(f"Error in 'template' condition: {ex}") from ex return value.lower() == "true" diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 485c51a8bb7..dd1abdecb72 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -1,5 +1,5 @@ """Test the condition helper.""" -from logging import ERROR, WARNING +from logging import WARNING from unittest.mock import patch import pytest @@ -1041,19 +1041,14 @@ async def test_extract_devices(): ) -async def test_condition_template_error(hass, caplog): +async def test_condition_template_error(hass): """Test invalid template.""" - caplog.set_level(ERROR) - test = await condition.async_from_config( hass, {"condition": "template", "value_template": "{{ undefined.state }}"} ) - assert not test(hass) - assert len(caplog.records) == 1 - assert caplog.records[0].message.startswith( - "Error during template condition: UndefinedError:" - ) + with pytest.raises(ConditionError, match="template"): + test(hass) async def test_condition_template_invalid_results(hass): From 6015161dab78c0ac0d4707eb63656a547d4e6f9e Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 11 Feb 2021 11:40:03 +0200 Subject: [PATCH 357/796] Fix Shelly relay device set to light appliance type (#46181) --- homeassistant/components/shelly/light.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 0c91ddc1088..5422f3fff05 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -187,6 +187,11 @@ class ShellyLight(ShellyBlockEntity, LightEntity): async def async_turn_on(self, **kwargs) -> None: """Turn on light.""" + if self.block.type == "relay": + self.control_result = await self.block.set_state(turn="on") + self.async_write_ha_state() + return + params = {"turn": "on"} if ATTR_BRIGHTNESS in kwargs: tmp_brightness = int(kwargs[ATTR_BRIGHTNESS] / 255 * 100) From 1b61b5c10b4436a673f27b6c5104ef38f24615e1 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Thu, 11 Feb 2021 05:04:11 -0500 Subject: [PATCH 358/796] Clean up kira integration (#46292) --- homeassistant/components/kira/__init__.py | 2 +- homeassistant/components/kira/remote.py | 4 ++-- homeassistant/components/kira/sensor.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/kira/__init__.py b/homeassistant/components/kira/__init__.py index 8948fbd0b8f..732008e5780 100644 --- a/homeassistant/components/kira/__init__.py +++ b/homeassistant/components/kira/__init__.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, + CONF_REPEAT, CONF_SENSORS, CONF_TYPE, EVENT_HOMEASSISTANT_STOP, @@ -28,7 +29,6 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_HOST = "0.0.0.0" DEFAULT_PORT = 65432 -CONF_REPEAT = "repeat" CONF_REMOTES = "remotes" CONF_SENSOR = "sensor" CONF_REMOTE = "remote" diff --git a/homeassistant/components/kira/remote.py b/homeassistant/components/kira/remote.py index c9b51fd7ab7..52659426681 100644 --- a/homeassistant/components/kira/remote.py +++ b/homeassistant/components/kira/remote.py @@ -6,12 +6,12 @@ from homeassistant.components import remote from homeassistant.const import CONF_DEVICE, CONF_NAME from homeassistant.helpers.entity import Entity +from . import CONF_REMOTE + DOMAIN = "kira" _LOGGER = logging.getLogger(__name__) -CONF_REMOTE = "remote" - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Kira platform.""" diff --git a/homeassistant/components/kira/sensor.py b/homeassistant/components/kira/sensor.py index 71aeec63232..6656780d0e9 100644 --- a/homeassistant/components/kira/sensor.py +++ b/homeassistant/components/kira/sensor.py @@ -4,14 +4,14 @@ import logging from homeassistant.const import CONF_DEVICE, CONF_NAME, STATE_UNKNOWN from homeassistant.helpers.entity import Entity +from . import CONF_SENSOR + DOMAIN = "kira" _LOGGER = logging.getLogger(__name__) ICON = "mdi:remote" -CONF_SENSOR = "sensor" - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a Kira sensor.""" From b85ecc0bd27c8378eca9581bb5a007fb4ab7ae7f Mon Sep 17 00:00:00 2001 From: tkdrob Date: Thu, 11 Feb 2021 07:38:33 -0500 Subject: [PATCH 359/796] Use core constants for mqtt (#46389) --- homeassistant/components/mqtt/__init__.py | 4 +--- homeassistant/components/mqtt/alarm_control_panel.py | 1 - homeassistant/components/mqtt/binary_sensor.py | 2 -- homeassistant/components/mqtt/camera.py | 1 - homeassistant/components/mqtt/climate.py | 5 ++--- homeassistant/components/mqtt/config_flow.py | 2 +- homeassistant/components/mqtt/const.py | 1 - homeassistant/components/mqtt/cover.py | 1 - .../components/mqtt/device_tracker/schema_discovery.py | 1 - homeassistant/components/mqtt/fan.py | 1 - homeassistant/components/mqtt/light/__init__.py | 1 - homeassistant/components/mqtt/light/schema_json.py | 2 +- homeassistant/components/mqtt/light/schema_template.py | 2 +- homeassistant/components/mqtt/lock.py | 1 - homeassistant/components/mqtt/number.py | 3 --- homeassistant/components/mqtt/scene.py | 1 - homeassistant/components/mqtt/sensor.py | 1 - homeassistant/components/mqtt/switch.py | 1 - homeassistant/components/mqtt/tag.py | 1 - homeassistant/components/mqtt/vacuum/__init__.py | 1 - homeassistant/components/mqtt/vacuum/schema_legacy.py | 1 - 21 files changed, 6 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 098099f0a03..58c99aa7c00 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -19,6 +19,7 @@ from homeassistant import config_entries from homeassistant.components import websocket_api from homeassistant.const import ( CONF_CLIENT_ID, + CONF_DISCOVERY, CONF_PASSWORD, CONF_PAYLOAD, CONF_PORT, @@ -28,7 +29,6 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.const import CONF_UNIQUE_ID # noqa: F401 from homeassistant.core import CoreState, Event, HassJob, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template @@ -48,7 +48,6 @@ from .const import ( ATTR_TOPIC, CONF_BIRTH_MESSAGE, CONF_BROKER, - CONF_DISCOVERY, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, @@ -1053,7 +1052,6 @@ async def websocket_subscribe(hass, connection, msg): @callback def async_subscribe_connection_status(hass, connection_status_callback): """Subscribe to MQTT connection changes.""" - connection_status_callback_job = HassJob(connection_status_callback) async def connected(): diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 38fec57607e..2f2ba06d6d7 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -110,7 +110,6 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT alarm control panel dynamically through MQTT discovery.""" - setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index d965401cef4..b9fb297cd5c 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -78,7 +78,6 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT binary sensor dynamically through MQTT discovery.""" - setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) @@ -214,7 +213,6 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity): @callback def _value_is_expired(self, *_): """Triggered when value is expired.""" - self._expiration_trigger = None self._expired = True diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 21fcb9276dd..684e41214fe 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -52,7 +52,6 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT camera dynamically through MQTT discovery.""" - setup = functools.partial( _async_setup_entity, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 15c7c916eeb..ede39103791 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -38,6 +38,8 @@ from homeassistant.const import ( ATTR_TEMPERATURE, CONF_DEVICE, CONF_NAME, + CONF_PAYLOAD_OFF, + CONF_PAYLOAD_ON, CONF_TEMPERATURE_UNIT, CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, @@ -98,8 +100,6 @@ CONF_MODE_COMMAND_TOPIC = "mode_command_topic" CONF_MODE_LIST = "modes" CONF_MODE_STATE_TEMPLATE = "mode_state_template" CONF_MODE_STATE_TOPIC = "mode_state_topic" -CONF_PAYLOAD_OFF = "payload_off" -CONF_PAYLOAD_ON = "payload_on" CONF_POWER_COMMAND_TOPIC = "power_command_topic" CONF_POWER_STATE_TEMPLATE = "power_state_template" CONF_POWER_STATE_TOPIC = "power_state_topic" @@ -274,7 +274,6 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT climate device dynamically through MQTT discovery.""" - setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 5c4016437a6..c71541b58b0 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( + CONF_DISCOVERY, CONF_HOST, CONF_PASSWORD, CONF_PAYLOAD, @@ -22,7 +23,6 @@ from .const import ( ATTR_TOPIC, CONF_BIRTH_MESSAGE, CONF_BROKER, - CONF_DISCOVERY, CONF_WILL_MESSAGE, DATA_MQTT_CONFIG, DEFAULT_BIRTH, diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 3e56ab6caf9..6c334eca311 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -11,7 +11,6 @@ ATTR_TOPIC = "topic" CONF_BROKER = "broker" CONF_BIRTH_MESSAGE = "birth_message" -CONF_DISCOVERY = "discovery" CONF_QOS = ATTR_QOS CONF_RETAIN = ATTR_RETAIN CONF_STATE_TOPIC = "state_topic" diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 4b428027a4d..54ef6cfb539 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -207,7 +207,6 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT cover dynamically through MQTT discovery.""" - setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index 8b51b9fac0e..296f66889b3 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -59,7 +59,6 @@ PLATFORM_SCHEMA_DISCOVERY = ( async def async_setup_entry_from_discovery(hass, config_entry, async_add_entities): """Set up MQTT device tracker dynamically through MQTT discovery.""" - setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index f96180b0982..ef3ecffc0e0 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -122,7 +122,6 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT fan dynamically through MQTT discovery.""" - setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index e780332d093..72c438df812 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -43,7 +43,6 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT light dynamically through MQTT discovery.""" - setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 489b424f4eb..8ec5c29db62 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -28,6 +28,7 @@ from homeassistant.const import ( CONF_COLOR_TEMP, CONF_DEVICE, CONF_EFFECT, + CONF_HS, CONF_NAME, CONF_OPTIMISTIC, CONF_RGB, @@ -75,7 +76,6 @@ CONF_EFFECT_LIST = "effect_list" CONF_FLASH_TIME_LONG = "flash_time_long" CONF_FLASH_TIME_SHORT = "flash_time_short" -CONF_HS = "hs" CONF_MAX_MIREDS = "max_mireds" CONF_MIN_MIREDS = "min_mireds" diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index e696e99552e..1f4421205e0 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -24,6 +24,7 @@ from homeassistant.const import ( CONF_DEVICE, CONF_NAME, CONF_OPTIMISTIC, + CONF_STATE_TEMPLATE, CONF_UNIQUE_ID, STATE_OFF, STATE_ON, @@ -62,7 +63,6 @@ CONF_GREEN_TEMPLATE = "green_template" CONF_MAX_MIREDS = "max_mireds" CONF_MIN_MIREDS = "min_mireds" CONF_RED_TEMPLATE = "red_template" -CONF_STATE_TEMPLATE = "state_template" CONF_WHITE_VALUE_TEMPLATE = "white_value_template" PLATFORM_SCHEMA_TEMPLATE = ( diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index b08f8f8bb43..408bc07b9d9 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -84,7 +84,6 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT lock dynamically through MQTT discovery.""" - setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 159f466f7ae..969eb254072 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -67,7 +67,6 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT number dynamically through MQTT discovery.""" - setup = functools.partial( _async_setup_entity, async_add_entities, config_entry=config_entry ) @@ -110,7 +109,6 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): @log_messages(self.hass, self.entity_id) def message_received(msg): """Handle new MQTT messages.""" - try: if msg.payload.decode("utf-8").isnumeric(): self._current_number = int(msg.payload) @@ -149,7 +147,6 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): async def async_set_value(self, value: float) -> None: """Update the current value.""" - current_number = value if value.is_integer(): diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 908f4bafd30..061c7df3fdc 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -47,7 +47,6 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT scene dynamically through MQTT discovery.""" - setup = functools.partial( _async_setup_entity, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 3f79ab1bafe..78a05179210 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -72,7 +72,6 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT sensors dynamically through MQTT discovery.""" - setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index d6d476b680d..73c7d55f6b0 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -80,7 +80,6 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT switch dynamically through MQTT discovery.""" - setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index b691c5cf8ce..4960ff50fb5 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -45,7 +45,6 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( async def async_setup_entry(hass, config_entry): """Set up MQTT tag scan dynamically through MQTT discovery.""" - setup = functools.partial(async_setup_tag, hass, config_entry=config_entry) await async_setup_entry_helper(hass, "tag", setup, PLATFORM_SCHEMA) diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index e580e874993..4801c1ea994 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -35,7 +35,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT vacuum dynamically through MQTT discovery.""" - setup = functools.partial( _async_setup_entity, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index e7be64be6ae..577e6a70ccd 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -377,7 +377,6 @@ class MqttVacuum(MqttEntity, VacuumEntity): No need to check SUPPORT_BATTERY, this won't be called if battery_level is None. """ - return icon_for_battery_level( battery_level=self.battery_level, charging=self._charging ) From c75e63dc95035fb1e00774a7f55fd1c48ddb10a6 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Thu, 11 Feb 2021 07:58:16 -0500 Subject: [PATCH 360/796] Use core constants for modbus (#46388) --- homeassistant/components/modbus/__init__.py | 3 +-- homeassistant/components/modbus/binary_sensor.py | 3 +-- homeassistant/components/modbus/climate.py | 3 +-- homeassistant/components/modbus/const.py | 2 -- homeassistant/components/modbus/cover.py | 1 - homeassistant/components/modbus/switch.py | 2 -- tests/components/modbus/conftest.py | 2 -- tests/components/modbus/test_modbus_binary_sensor.py | 3 +-- tests/components/modbus/test_modbus_sensor.py | 3 +-- 9 files changed, 5 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 77e9b6f7ca9..c2a1a76840c 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_HOST, CONF_METHOD, CONF_NAME, + CONF_OFFSET, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SLAVE, @@ -46,7 +47,6 @@ from .const import ( CONF_INPUT_TYPE, CONF_MAX_TEMP, CONF_MIN_TEMP, - CONF_OFFSET, CONF_PARITY, CONF_PRECISION, CONF_REGISTER, @@ -257,7 +257,6 @@ class ModbusHub: def __init__(self, client_config): """Initialize the Modbus hub.""" - # generic configuration self._client = None self._lock = threading.Lock() diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index c9e9cc4196a..8e91945d073 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -10,13 +10,12 @@ from homeassistant.components.binary_sensor import ( PLATFORM_SCHEMA, BinarySensorEntity, ) -from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_SLAVE +from homeassistant.const import CONF_ADDRESS, CONF_DEVICE_CLASS, CONF_NAME, CONF_SLAVE from homeassistant.helpers import config_validation as cv from .const import ( CALL_TYPE_COIL, CALL_TYPE_DISCRETE, - CONF_ADDRESS, CONF_COILS, CONF_HUB, CONF_INPUT_TYPE, diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index b09a38f082e..c1b7b2e6bf4 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -15,6 +15,7 @@ from homeassistant.components.climate.const import ( from homeassistant.const import ( ATTR_TEMPERATURE, CONF_NAME, + CONF_OFFSET, CONF_SCAN_INTERVAL, CONF_SLAVE, CONF_STRUCTURE, @@ -39,7 +40,6 @@ from .const import ( CONF_DATA_TYPE, CONF_MAX_TEMP, CONF_MIN_TEMP, - CONF_OFFSET, CONF_PRECISION, CONF_SCALE, CONF_STEP, @@ -146,7 +146,6 @@ class ModbusThermostat(ClimateEntity): False if entity pushes its state to HA. """ - # Handle polling directly in this entity return False diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index e79b69bbb87..5e304165e42 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -13,7 +13,6 @@ CONF_REVERSE_ORDER = "reverse_order" CONF_SCALE = "scale" CONF_COUNT = "count" CONF_PRECISION = "precision" -CONF_OFFSET = "offset" CONF_COILS = "coils" # integration names @@ -51,7 +50,6 @@ DEFAULT_SCAN_INTERVAL = 15 # seconds # binary_sensor.py CONF_INPUTS = "inputs" CONF_INPUT_TYPE = "input_type" -CONF_ADDRESS = "address" # sensor.py # CONF_DATA_TYPE = "data_type" diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index ab16e5306f1..709c772564a 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -148,7 +148,6 @@ class ModbusCover(CoverEntity, RestoreEntity): False if entity pushes its state to HA. """ - # Handle polling directly in this entity return False diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 8fe1f886c3e..b1b07fb5a55 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -200,7 +200,6 @@ class ModbusRegisterSwitch(ModbusBaseSwitch, SwitchEntity): def turn_on(self, **kwargs): """Set switch on.""" - # Only holding register is writable if self._register_type == CALL_TYPE_REGISTER_HOLDING: self._write_register(self._command_on) @@ -209,7 +208,6 @@ class ModbusRegisterSwitch(ModbusBaseSwitch, SwitchEntity): def turn_off(self, **kwargs): """Set switch off.""" - # Only holding register is writable if self._register_type == CALL_TYPE_REGISTER_HOLDING: self._write_register(self._command_off) diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index e3a707b7fc9..a3e6078ea09 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -46,7 +46,6 @@ async def setup_base_test( scan_interval, ): """Run setup device for given config.""" - # Full sensor configuration config = { entity_domain: { @@ -79,7 +78,6 @@ async def run_base_read_test( now, ): """Run test for given config.""" - # Setup inputs for the sensor read_result = ReadResult(register_words) if register_type == CALL_TYPE_COIL: diff --git a/tests/components/modbus/test_modbus_binary_sensor.py b/tests/components/modbus/test_modbus_binary_sensor.py index 91374cde22d..3bc7223c865 100644 --- a/tests/components/modbus/test_modbus_binary_sensor.py +++ b/tests/components/modbus/test_modbus_binary_sensor.py @@ -7,11 +7,10 @@ from homeassistant.components.binary_sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.modbus.const import ( CALL_TYPE_COIL, CALL_TYPE_DISCRETE, - CONF_ADDRESS, CONF_INPUT_TYPE, CONF_INPUTS, ) -from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.const import CONF_ADDRESS, CONF_NAME, STATE_OFF, STATE_ON from .conftest import run_base_read_test, setup_base_test diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py index 68cdbffa462..3f7c0fc60df 100644 --- a/tests/components/modbus/test_modbus_sensor.py +++ b/tests/components/modbus/test_modbus_sensor.py @@ -8,7 +8,6 @@ from homeassistant.components.modbus.const import ( CALL_TYPE_REGISTER_INPUT, CONF_COUNT, CONF_DATA_TYPE, - CONF_OFFSET, CONF_PRECISION, CONF_REGISTER, CONF_REGISTER_TYPE, @@ -21,7 +20,7 @@ from homeassistant.components.modbus.const import ( DATA_TYPE_UINT, ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, CONF_OFFSET from .conftest import run_base_read_test, setup_base_test From b1a7bfee14a5d81ac9b92067c39571c705b1b5f2 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Thu, 11 Feb 2021 07:59:09 -0500 Subject: [PATCH 361/796] Clean up kira integration constants (#46390) --- homeassistant/components/kira/remote.py | 4 +--- homeassistant/components/kira/sensor.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/kira/remote.py b/homeassistant/components/kira/remote.py index 52659426681..9c02a3199e4 100644 --- a/homeassistant/components/kira/remote.py +++ b/homeassistant/components/kira/remote.py @@ -6,9 +6,7 @@ from homeassistant.components import remote from homeassistant.const import CONF_DEVICE, CONF_NAME from homeassistant.helpers.entity import Entity -from . import CONF_REMOTE - -DOMAIN = "kira" +from . import CONF_REMOTE, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/kira/sensor.py b/homeassistant/components/kira/sensor.py index 6656780d0e9..2d6322918c7 100644 --- a/homeassistant/components/kira/sensor.py +++ b/homeassistant/components/kira/sensor.py @@ -4,9 +4,7 @@ import logging from homeassistant.const import CONF_DEVICE, CONF_NAME, STATE_UNKNOWN from homeassistant.helpers.entity import Entity -from . import CONF_SENSOR - -DOMAIN = "kira" +from . import CONF_SENSOR, DOMAIN _LOGGER = logging.getLogger(__name__) From 5ce49c62b143a4b0d9913687e9a37a8e9fdee35d Mon Sep 17 00:00:00 2001 From: Steve Dwyer <6332150+kangaroomadman@users.noreply.github.com> Date: Thu, 11 Feb 2021 13:57:27 +0000 Subject: [PATCH 362/796] Allow MQTT template light floating point transition (#46385) Allow to use floating point values for the transition time of the MQTT template light. --- homeassistant/components/mqtt/light/schema_template.py | 4 ++-- tests/components/mqtt/test_light_template.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 1f4421205e0..665e1a30d99 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -383,7 +383,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): values["flash"] = kwargs.get(ATTR_FLASH) if ATTR_TRANSITION in kwargs: - values["transition"] = int(kwargs[ATTR_TRANSITION]) + values["transition"] = kwargs[ATTR_TRANSITION] mqtt.async_publish( self.hass, @@ -408,7 +408,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): self._state = False if ATTR_TRANSITION in kwargs: - values["transition"] = int(kwargs[ATTR_TRANSITION]) + values["transition"] = kwargs[ATTR_TRANSITION] mqtt.async_publish( self.hass, diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 733a39ce252..7b5d34edd69 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -677,7 +677,7 @@ async def test_transition(hass, mqtt_mock): "name": "test", "command_topic": "test_light_rgb/set", "command_on_template": "on,{{ transition }}", - "command_off_template": "off,{{ transition|d }}", + "command_off_template": "off,{{ transition|int|d }}", "qos": 1, } }, @@ -689,15 +689,15 @@ async def test_transition(hass, mqtt_mock): assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 40 - await common.async_turn_on(hass, "light.test", transition=10) + await common.async_turn_on(hass, "light.test", transition=10.0) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,10", 1, False + "test_light_rgb/set", "on,10.0", 1, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") assert state.state == STATE_ON - await common.async_turn_off(hass, "light.test", transition=20) + await common.async_turn_off(hass, "light.test", transition=20.0) mqtt_mock.async_publish.assert_called_once_with( "test_light_rgb/set", "off,20", 1, False ) From 888c9e120de997a4cb0419901b3a170f46286305 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 11 Feb 2021 17:29:17 +0100 Subject: [PATCH 363/796] Raise ConditionError for time errors (#46250) --- homeassistant/helpers/condition.py | 8 ++++++-- tests/helpers/test_condition.py | 7 +++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index da4fdab07d7..2f63498c9cd 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -518,7 +518,9 @@ def time( elif isinstance(after, str): after_entity = hass.states.get(after) if not after_entity: - return False + raise ConditionError( + f"Error in 'time' condition: The 'after' entity {after} is not available" + ) after = dt_util.dt.time( after_entity.attributes.get("hour", 23), after_entity.attributes.get("minute", 59), @@ -530,7 +532,9 @@ def time( elif isinstance(before, str): before_entity = hass.states.get(before) if not before_entity: - return False + raise ConditionError( + f"Error in 'time' condition: The 'before' entity {before} is not available" + ) before = dt_util.dt.time( before_entity.attributes.get("hour", 23), before_entity.attributes.get("minute", 59), diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index dd1abdecb72..1fc1e07da5d 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -334,8 +334,11 @@ async def test_time_using_input_datetime(hass): hass, after="input_datetime.pm", before="input_datetime.am" ) - assert not condition.time(hass, after="input_datetime.not_existing") - assert not condition.time(hass, before="input_datetime.not_existing") + with pytest.raises(ConditionError): + condition.time(hass, after="input_datetime.not_existing") + + with pytest.raises(ConditionError): + condition.time(hass, before="input_datetime.not_existing") async def test_if_numeric_state_raises_on_unavailable(hass, caplog): From ed31cc363b3a457ed9249f7cf0386f2ec86e1850 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 11 Feb 2021 17:36:19 +0100 Subject: [PATCH 364/796] Wait for registries to load at startup (#46265) * Wait for registries to load at startup * Don't decorate new functions with @bind_hass * Fix typing errors in zwave_js * Load registries in async_test_home_assistant * Tweak * Typo * Tweak * Explicitly silence mypy errors * Fix tests * Fix more tests * Fix test * Improve docstring * Wait for registries to load --- homeassistant/bootstrap.py | 11 ++++-- homeassistant/components/zwave_js/__init__.py | 8 ++-- homeassistant/helpers/area_registry.py | 37 +++++++++---------- homeassistant/helpers/device_registry.py | 28 ++++++++++---- homeassistant/helpers/entity_registry.py | 27 +++++++++++--- tests/common.py | 11 +++++- tests/components/discovery/test_init.py | 14 +++---- tests/components/template/test_sensor.py | 3 ++ tests/conftest.py | 14 ++++++- tests/helpers/test_area_registry.py | 21 ++--------- tests/helpers/test_device_registry.py | 31 ++++++---------- tests/helpers/test_entity_registry.py | 26 ++++--------- tests/test_bootstrap.py | 7 ++++ 13 files changed, 131 insertions(+), 107 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 0f5bda7fbf2..fd2d580a879 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -17,6 +17,7 @@ from homeassistant import config as conf_util, config_entries, core, loader from homeassistant.components import http from homeassistant.const import REQUIRED_NEXT_PYTHON_DATE, REQUIRED_NEXT_PYTHON_VER from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import area_registry, device_registry, entity_registry from homeassistant.helpers.typing import ConfigType from homeassistant.setup import ( DATA_SETUP, @@ -510,10 +511,12 @@ async def _async_set_up_integrations( stage_2_domains = domains_to_setup - logging_domains - debuggers - stage_1_domains - # Kick off loading the registries. They don't need to be awaited. - asyncio.create_task(hass.helpers.device_registry.async_get_registry()) - asyncio.create_task(hass.helpers.entity_registry.async_get_registry()) - asyncio.create_task(hass.helpers.area_registry.async_get_registry()) + # Load the registries + await asyncio.gather( + device_registry.async_load(hass), + entity_registry.async_load(hass), + area_registry.async_load(hass), + ) # Start setup if stage_1_domains: diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 01b8f4785c5..d5624551b27 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -131,8 +131,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # grab device in device registry attached to this node dev_id = get_device_id(client, node) device = dev_reg.async_get_device({dev_id}) - # note: removal of entity registry is handled by core - dev_reg.async_remove_device(device.id) + # note: removal of entity registry entry is handled by core + dev_reg.async_remove_device(device.id) # type: ignore @callback def async_on_value_notification(notification: ValueNotification) -> None: @@ -149,7 +149,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ATTR_NODE_ID: notification.node.node_id, ATTR_HOME_ID: client.driver.controller.home_id, ATTR_ENDPOINT: notification.endpoint, - ATTR_DEVICE_ID: device.id, + ATTR_DEVICE_ID: device.id, # type: ignore ATTR_COMMAND_CLASS: notification.command_class, ATTR_COMMAND_CLASS_NAME: notification.command_class_name, ATTR_LABEL: notification.metadata.label, @@ -170,7 +170,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ATTR_DOMAIN: DOMAIN, ATTR_NODE_ID: notification.node.node_id, ATTR_HOME_ID: client.driver.controller.home_id, - ATTR_DEVICE_ID: device.id, + ATTR_DEVICE_ID: device.id, # type: ignore ATTR_LABEL: notification.notification_label, ATTR_PARAMETERS: notification.parameters, }, diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index bdd231686e2..a41e748d1ad 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -1,5 +1,5 @@ """Provide a way to connect devices to one physical location.""" -from asyncio import Event, gather +from asyncio import gather from collections import OrderedDict from typing import Container, Dict, Iterable, List, MutableMapping, Optional, cast @@ -154,24 +154,23 @@ class AreaRegistry: return data +@callback +def async_get(hass: HomeAssistantType) -> AreaRegistry: + """Get area registry.""" + return cast(AreaRegistry, hass.data[DATA_REGISTRY]) + + +async def async_load(hass: HomeAssistantType) -> None: + """Load area registry.""" + assert DATA_REGISTRY not in hass.data + hass.data[DATA_REGISTRY] = AreaRegistry(hass) + await hass.data[DATA_REGISTRY].async_load() + + @bind_hass async def async_get_registry(hass: HomeAssistantType) -> AreaRegistry: - """Return area registry instance.""" - reg_or_evt = hass.data.get(DATA_REGISTRY) + """Get area registry. - if not reg_or_evt: - evt = hass.data[DATA_REGISTRY] = Event() - - reg = AreaRegistry(hass) - await reg.async_load() - - hass.data[DATA_REGISTRY] = reg - evt.set() - return reg - - if isinstance(reg_or_evt, Event): - evt = reg_or_evt - await evt.wait() - return cast(AreaRegistry, hass.data.get(DATA_REGISTRY)) - - return cast(AreaRegistry, reg_or_evt) + This is deprecated and will be removed in the future. Use async_get instead. + """ + return async_get(hass) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index c449d2ed4d0..0d62b2cab47 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -2,16 +2,16 @@ from collections import OrderedDict import logging import time -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union, cast import attr from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import Event, callback +from homeassistant.loader import bind_hass import homeassistant.util.uuid as uuid_util from .debounce import Debouncer -from .singleton import singleton from .typing import UNDEFINED, HomeAssistantType, UndefinedType # mypy: disallow_any_generics @@ -593,12 +593,26 @@ class DeviceRegistry: self._async_update_device(dev_id, area_id=None) -@singleton(DATA_REGISTRY) +@callback +def async_get(hass: HomeAssistantType) -> DeviceRegistry: + """Get device registry.""" + return cast(DeviceRegistry, hass.data[DATA_REGISTRY]) + + +async def async_load(hass: HomeAssistantType) -> None: + """Load device registry.""" + assert DATA_REGISTRY not in hass.data + hass.data[DATA_REGISTRY] = DeviceRegistry(hass) + await hass.data[DATA_REGISTRY].async_load() + + +@bind_hass async def async_get_registry(hass: HomeAssistantType) -> DeviceRegistry: - """Create entity registry.""" - reg = DeviceRegistry(hass) - await reg.async_load() - return reg + """Get device registry. + + This is deprecated and will be removed in the future. Use async_get instead. + """ + return async_get(hass) @callback diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 052e7398ba1..51985f7bae4 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -19,6 +19,7 @@ from typing import ( Optional, Tuple, Union, + cast, ) import attr @@ -35,10 +36,10 @@ from homeassistant.const import ( ) from homeassistant.core import Event, callback, split_entity_id, valid_entity_id from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED +from homeassistant.loader import bind_hass from homeassistant.util import slugify from homeassistant.util.yaml import load_yaml -from .singleton import singleton from .typing import UNDEFINED, HomeAssistantType, UndefinedType if TYPE_CHECKING: @@ -568,12 +569,26 @@ class EntityRegistry: self._add_index(entry) -@singleton(DATA_REGISTRY) +@callback +def async_get(hass: HomeAssistantType) -> EntityRegistry: + """Get entity registry.""" + return cast(EntityRegistry, hass.data[DATA_REGISTRY]) + + +async def async_load(hass: HomeAssistantType) -> None: + """Load entity registry.""" + assert DATA_REGISTRY not in hass.data + hass.data[DATA_REGISTRY] = EntityRegistry(hass) + await hass.data[DATA_REGISTRY].async_load() + + +@bind_hass async def async_get_registry(hass: HomeAssistantType) -> EntityRegistry: - """Create entity registry.""" - reg = EntityRegistry(hass) - await reg.async_load() - return reg + """Get entity registry. + + This is deprecated and will be removed in the future. Use async_get instead. + """ + return async_get(hass) @callback diff --git a/tests/common.py b/tests/common.py index ab5da25e38d..c07716dbfc9 100644 --- a/tests/common.py +++ b/tests/common.py @@ -146,7 +146,7 @@ def get_test_home_assistant(): # pylint: disable=protected-access -async def async_test_home_assistant(loop): +async def async_test_home_assistant(loop, load_registries=True): """Return a Home Assistant object pointing at test config dir.""" hass = ha.HomeAssistant() store = auth_store.AuthStore(hass) @@ -280,6 +280,15 @@ async def async_test_home_assistant(loop): hass.config_entries._entries = [] hass.config_entries._store._async_ensure_stop_listener = lambda: None + # Load the registries + if load_registries: + await asyncio.gather( + device_registry.async_load(hass), + entity_registry.async_load(hass), + area_registry.async_load(hass), + ) + await hass.async_block_till_done() + hass.state = ha.CoreState.running # Mock async_start diff --git a/tests/components/discovery/test_init.py b/tests/components/discovery/test_init.py index fd66e59ef21..2c1e41e8285 100644 --- a/tests/components/discovery/test_init.py +++ b/tests/components/discovery/test_init.py @@ -38,19 +38,17 @@ async def mock_discovery(hass, discoveries, config=BASE_CONFIG): """Mock discoveries.""" with patch("homeassistant.components.zeroconf.async_get_instance"), patch( "homeassistant.components.zeroconf.async_setup", return_value=True - ): - assert await async_setup_component(hass, "discovery", config) - await hass.async_block_till_done() - await hass.async_start() - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - with patch.object(discovery, "_discover", discoveries), patch( + ), patch.object(discovery, "_discover", discoveries), patch( "homeassistant.components.discovery.async_discover", return_value=mock_coro() ) as mock_discover, patch( "homeassistant.components.discovery.async_load_platform", return_value=mock_coro(), ) as mock_platform: + assert await async_setup_component(hass, "discovery", config) + await hass.async_block_till_done() + await hass.async_start() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() async_fire_time_changed(hass, utcnow()) # Work around an issue where our loop.call_soon not get caught await hass.async_block_till_done() diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 7f560fa0abb..9d014f86a36 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -3,6 +3,8 @@ from asyncio import Event from datetime import timedelta from unittest.mock import patch +import pytest + from homeassistant.bootstrap import async_from_config_dict from homeassistant.components import sensor from homeassistant.const import ( @@ -403,6 +405,7 @@ async def test_setup_valid_device_class(hass): assert "device_class" not in state.attributes +@pytest.mark.parametrize("load_registries", [False]) async def test_creating_sensor_loads_group(hass): """Test setting up template sensor loads group component first.""" order = [] diff --git a/tests/conftest.py b/tests/conftest.py index 6e3edbd73e8..3fc2dc748cb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -121,7 +121,17 @@ def hass_storage(): @pytest.fixture -def hass(loop, hass_storage, request): +def load_registries(): + """Fixture to control the loading of registries when setting up the hass fixture. + + To avoid loading the registries, tests can be marked with: + @pytest.mark.parametrize("load_registries", [False]) + """ + return True + + +@pytest.fixture +def hass(loop, load_registries, hass_storage, request): """Fixture to provide a test instance of Home Assistant.""" def exc_handle(loop, context): @@ -141,7 +151,7 @@ def hass(loop, hass_storage, request): orig_exception_handler(loop, context) exceptions = [] - hass = loop.run_until_complete(async_test_home_assistant(loop)) + hass = loop.run_until_complete(async_test_home_assistant(loop, load_registries)) orig_exception_handler = loop.get_exception_handler() loop.set_exception_handler(exc_handle) diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py index ec008dde7da..2b06202c862 100644 --- a/tests/helpers/test_area_registry.py +++ b/tests/helpers/test_area_registry.py @@ -1,7 +1,4 @@ """Tests for the Area Registry.""" -import asyncio -import unittest.mock - import pytest from homeassistant.core import callback @@ -164,6 +161,7 @@ async def test_load_area(hass, registry): assert list(registry.areas) == list(registry2.areas) +@pytest.mark.parametrize("load_registries", [False]) async def test_loading_area_from_storage(hass, hass_storage): """Test loading stored areas on start.""" hass_storage[area_registry.STORAGE_KEY] = { @@ -171,20 +169,7 @@ async def test_loading_area_from_storage(hass, hass_storage): "data": {"areas": [{"id": "12345A", "name": "mock"}]}, } - registry = await area_registry.async_get_registry(hass) + await area_registry.async_load(hass) + registry = area_registry.async_get(hass) assert len(registry.areas) == 1 - - -async def test_loading_race_condition(hass): - """Test only one storage load called when concurrent loading occurred .""" - with unittest.mock.patch( - "homeassistant.helpers.area_registry.AreaRegistry.async_load" - ) as mock_load: - results = await asyncio.gather( - area_registry.async_get_registry(hass), - area_registry.async_get_registry(hass), - ) - - mock_load.assert_called_once_with() - assert results[0] == results[1] diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 01959174335..a128f8aa390 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -1,5 +1,4 @@ """Tests for the Device Registry.""" -import asyncio import time from unittest.mock import patch @@ -135,6 +134,7 @@ async def test_multiple_config_entries(registry): assert entry2.config_entries == {"123", "456"} +@pytest.mark.parametrize("load_registries", [False]) async def test_loading_from_storage(hass, hass_storage): """Test loading stored devices on start.""" hass_storage[device_registry.STORAGE_KEY] = { @@ -167,7 +167,8 @@ async def test_loading_from_storage(hass, hass_storage): }, } - registry = await device_registry.async_get_registry(hass) + await device_registry.async_load(hass) + registry = device_registry.async_get(hass) assert len(registry.devices) == 1 assert len(registry.deleted_devices) == 1 @@ -687,20 +688,6 @@ async def test_update_remove_config_entries(hass, registry, update_events): assert update_events[4]["device_id"] == entry3.id -async def test_loading_race_condition(hass): - """Test only one storage load called when concurrent loading occurred .""" - with patch( - "homeassistant.helpers.device_registry.DeviceRegistry.async_load" - ) as mock_load: - results = await asyncio.gather( - device_registry.async_get_registry(hass), - device_registry.async_get_registry(hass), - ) - - mock_load.assert_called_once_with() - assert results[0] == results[1] - - async def test_update_sw_version(registry): """Verify that we can update software version of a device.""" entry = registry.async_get_or_create( @@ -798,10 +785,16 @@ async def test_cleanup_startup(hass): assert len(mock_call.mock_calls) == 1 +@pytest.mark.parametrize("load_registries", [False]) async def test_cleanup_entity_registry_change(hass): - """Test we run a cleanup when entity registry changes.""" - await device_registry.async_get_registry(hass) - ent_reg = await entity_registry.async_get_registry(hass) + """Test we run a cleanup when entity registry changes. + + Don't pre-load the registries as the debouncer will then not be waiting for + EVENT_ENTITY_REGISTRY_UPDATED events. + """ + await device_registry.async_load(hass) + await entity_registry.async_load(hass) + ent_reg = entity_registry.async_get(hass) with patch( "homeassistant.helpers.device_registry.Debouncer.async_call" diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index b176f7022d5..71cfb331591 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1,6 +1,4 @@ """Tests for the Entity Registry.""" -import asyncio -import unittest.mock from unittest.mock import patch import pytest @@ -219,6 +217,7 @@ def test_is_registered(registry): assert not registry.async_is_registered("light.non_existing") +@pytest.mark.parametrize("load_registries", [False]) async def test_loading_extra_values(hass, hass_storage): """Test we load extra data from the registry.""" hass_storage[entity_registry.STORAGE_KEY] = { @@ -258,7 +257,8 @@ async def test_loading_extra_values(hass, hass_storage): }, } - registry = await entity_registry.async_get_registry(hass) + await entity_registry.async_load(hass) + registry = entity_registry.async_get(hass) assert len(registry.entities) == 4 @@ -350,6 +350,7 @@ async def test_removing_area_id(registry): assert entry_w_area != entry_wo_area +@pytest.mark.parametrize("load_registries", [False]) async def test_migration(hass): """Test migration from old data to new.""" mock_config = MockConfigEntry(domain="test-platform", entry_id="test-config-id") @@ -366,7 +367,8 @@ async def test_migration(hass): with patch("os.path.isfile", return_value=True), patch("os.remove"), patch( "homeassistant.helpers.entity_registry.load_yaml", return_value=old_conf ): - registry = await entity_registry.async_get_registry(hass) + await entity_registry.async_load(hass) + registry = entity_registry.async_get(hass) assert registry.async_is_registered("light.kitchen") entry = registry.async_get_or_create( @@ -427,20 +429,6 @@ async def test_loading_invalid_entity_id(hass, hass_storage): assert valid_entity_id(entity_invalid_start.entity_id) -async def test_loading_race_condition(hass): - """Test only one storage load called when concurrent loading occurred .""" - with unittest.mock.patch( - "homeassistant.helpers.entity_registry.EntityRegistry.async_load" - ) as mock_load: - results = await asyncio.gather( - entity_registry.async_get_registry(hass), - entity_registry.async_get_registry(hass), - ) - - mock_load.assert_called_once_with() - assert results[0] == results[1] - - async def test_update_entity_unique_id(registry): """Test entity's unique_id is updated.""" mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") @@ -794,7 +782,7 @@ async def test_disable_device_disables_entities(hass, registry): async def test_disabled_entities_excluded_from_entity_list(hass, registry): - """Test that disabled entities are exclduded from async_entries_for_device.""" + """Test that disabled entities are excluded from async_entries_for_device.""" device_registry = mock_device_registry(hass) config_entry = MockConfigEntry(domain="light") diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index fc653c25d0b..c035f6f1d1d 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -71,6 +71,7 @@ async def test_load_hassio(hass): assert bootstrap._get_domains(hass, {}) == {"hassio"} +@pytest.mark.parametrize("load_registries", [False]) async def test_empty_setup(hass): """Test an empty set up loads the core.""" await bootstrap.async_from_config_dict({}, hass) @@ -91,6 +92,7 @@ async def test_core_failure_loads_safe_mode(hass, caplog): assert "group" not in hass.config.components +@pytest.mark.parametrize("load_registries", [False]) async def test_setting_up_config(hass): """Test we set up domains in config.""" await bootstrap._async_set_up_integrations( @@ -100,6 +102,7 @@ async def test_setting_up_config(hass): assert "group" in hass.config.components +@pytest.mark.parametrize("load_registries", [False]) async def test_setup_after_deps_all_present(hass): """Test after_dependencies when all present.""" order = [] @@ -144,6 +147,7 @@ async def test_setup_after_deps_all_present(hass): assert order == ["logger", "root", "first_dep", "second_dep"] +@pytest.mark.parametrize("load_registries", [False]) async def test_setup_after_deps_in_stage_1_ignored(hass): """Test after_dependencies are ignored in stage 1.""" # This test relies on this @@ -190,6 +194,7 @@ async def test_setup_after_deps_in_stage_1_ignored(hass): assert order == ["cloud", "an_after_dep", "normal_integration"] +@pytest.mark.parametrize("load_registries", [False]) async def test_setup_after_deps_via_platform(hass): """Test after_dependencies set up via platform.""" order = [] @@ -239,6 +244,7 @@ async def test_setup_after_deps_via_platform(hass): assert order == ["after_dep_of_platform_int", "platform_int"] +@pytest.mark.parametrize("load_registries", [False]) async def test_setup_after_deps_not_trigger_load(hass): """Test after_dependencies does not trigger loading it.""" order = [] @@ -277,6 +283,7 @@ async def test_setup_after_deps_not_trigger_load(hass): assert "second_dep" in hass.config.components +@pytest.mark.parametrize("load_registries", [False]) async def test_setup_after_deps_not_present(hass): """Test after_dependencies when referenced integration doesn't exist.""" order = [] From c95f401e2e951f1d6b18181930c797b310d3cfa5 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Thu, 11 Feb 2021 11:44:39 -0500 Subject: [PATCH 365/796] Use core constants for nissan_leaf (#46401) --- homeassistant/components/nissan_leaf/__init__.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 26689e5cb0a..6417926702d 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -7,7 +7,7 @@ import sys from pycarwings2 import CarwingsError, Session import voluptuous as vol -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, HTTP_OK +from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME, HTTP_OK from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform @@ -34,7 +34,6 @@ DATA_RANGE_AC_OFF = "range_ac_off" CONF_INTERVAL = "update_interval" CONF_CHARGING_INTERVAL = "update_interval_charging" CONF_CLIMATE_INTERVAL = "update_interval_climate" -CONF_REGION = "region" CONF_VALID_REGIONS = ["NNA", "NE", "NCI", "NMA", "NML"] CONF_FORCE_MILES = "force_miles" @@ -272,7 +271,6 @@ class LeafDataStore: async def async_refresh_data(self, now): """Refresh the leaf data and update the datastore.""" - if self.request_in_progress: _LOGGER.debug("Refresh currently in progress for %s", self.leaf.nickname) return @@ -336,7 +334,6 @@ class LeafDataStore: async def async_get_battery(self): """Request battery update from Nissan servers.""" - try: # Request battery update from the car _LOGGER.debug("Requesting battery update, %s", self.leaf.vin) @@ -388,7 +385,6 @@ class LeafDataStore: async def async_get_climate(self): """Request climate data from Nissan servers.""" - try: return await self.hass.async_add_executor_job( self.leaf.get_latest_hvac_status From fd177441b3cfddb22b893b26e91862307bbcc437 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Thu, 11 Feb 2021 11:45:26 -0500 Subject: [PATCH 366/796] Use core constants for nmap_tracker (#46402) --- homeassistant/components/nmap_tracker/device_tracker.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/nmap_tracker/device_tracker.py b/homeassistant/components/nmap_tracker/device_tracker.py index 0a8c177b08c..608f90d5421 100644 --- a/homeassistant/components/nmap_tracker/device_tracker.py +++ b/homeassistant/components/nmap_tracker/device_tracker.py @@ -12,13 +12,12 @@ from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import CONF_HOSTS +from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) -CONF_EXCLUDE = "exclude" # Interval in minutes to exclude devices from a scan while they are home CONF_HOME_INTERVAL = "home_interval" CONF_OPTIONS = "scan_options" From eb0d1bb673230b8bbece6019aa639dc55477160c Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 11 Feb 2021 18:55:17 +0100 Subject: [PATCH 367/796] Improve knx fan implementation (#46404) --- homeassistant/components/knx/fan.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index d5dfb25ccd4..de08b576edd 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -35,6 +35,8 @@ class KNXFan(KnxEntity, FanEntity): if self._device.mode == FanSpeedMode.Step: self._step_range = (1, device.max_step) + else: + self._step_range = None async def async_set_percentage(self, percentage: int) -> None: """Set the speed of the fan, as a percentage.""" From 70e23402a90d4e5ac5cb306bbfc7f02a3a619bb1 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Thu, 11 Feb 2021 13:56:50 -0500 Subject: [PATCH 368/796] Use core constants for ohmconnect (#46413) --- homeassistant/components/ohmconnect/sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/ohmconnect/sensor.py b/homeassistant/components/ohmconnect/sensor.py index 56a3cc06556..7c7331990ea 100644 --- a/homeassistant/components/ohmconnect/sensor.py +++ b/homeassistant/components/ohmconnect/sensor.py @@ -7,15 +7,13 @@ import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_ID, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -CONF_ID = "id" - DEFAULT_NAME = "OhmConnect Status" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) From 26e79163677645943ff009b2dbe5922365681c25 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 11 Feb 2021 20:18:03 +0100 Subject: [PATCH 369/796] Migrate mobile_app to RestoreEntity (#46391) --- .../components/mobile_app/__init__.py | 6 - .../components/mobile_app/binary_sensor.py | 52 ++-- .../components/mobile_app/config_flow.py | 6 +- homeassistant/components/mobile_app/const.py | 2 - homeassistant/components/mobile_app/entity.py | 55 ++-- .../components/mobile_app/helpers.py | 4 - homeassistant/components/mobile_app/sensor.py | 46 +-- .../components/mobile_app/webhook.py | 30 +- tests/components/mobile_app/conftest.py | 8 - .../mobile_app/test_binary_sensor.py | 271 ++++++++++++++++++ .../{test_entity.py => test_sensor.py} | 14 + tests/components/mobile_app/test_webhook.py | 2 +- 12 files changed, 395 insertions(+), 101 deletions(-) create mode 100644 tests/components/mobile_app/test_binary_sensor.py rename tests/components/mobile_app/{test_entity.py => test_sensor.py} (92%) diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 3bc95bf3e05..54fa3398ee2 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -17,11 +17,9 @@ from .const import ( ATTR_MODEL, ATTR_OS_VERSION, CONF_CLOUDHOOK_URL, - DATA_BINARY_SENSOR, DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, DATA_DEVICES, - DATA_SENSOR, DATA_STORE, DOMAIN, STORAGE_KEY, @@ -40,18 +38,14 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): app_config = await store.async_load() if app_config is None: app_config = { - DATA_BINARY_SENSOR: {}, DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: [], - DATA_SENSOR: {}, } hass.data[DOMAIN] = { - DATA_BINARY_SENSOR: app_config.get(DATA_BINARY_SENSOR, {}), DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: app_config.get(DATA_DELETED_IDS, []), DATA_DEVICES: {}, - DATA_SENSOR: app_config.get(DATA_SENSOR, {}), DATA_STORE: store, } diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py index ae8efc0c113..36897dd9f69 100644 --- a/homeassistant/components/mobile_app/binary_sensor.py +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -2,18 +2,25 @@ from functools import partial from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID, STATE_ON from homeassistant.core import callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( + ATTR_DEVICE_NAME, + ATTR_SENSOR_ATTRIBUTES, + ATTR_SENSOR_DEVICE_CLASS, + ATTR_SENSOR_ICON, + ATTR_SENSOR_NAME, ATTR_SENSOR_STATE, + ATTR_SENSOR_TYPE, ATTR_SENSOR_TYPE_BINARY_SENSOR as ENTITY_TYPE, ATTR_SENSOR_UNIQUE_ID, DATA_DEVICES, DOMAIN, ) -from .entity import MobileAppEntity, sensor_id +from .entity import MobileAppEntity, unique_id async def async_setup_entry(hass, config_entry, async_add_entities): @@ -22,13 +29,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities): webhook_id = config_entry.data[CONF_WEBHOOK_ID] - for config in hass.data[DOMAIN][ENTITY_TYPE].values(): - if config[CONF_WEBHOOK_ID] != webhook_id: + entity_registry = await er.async_get_registry(hass) + entries = er.async_entries_for_config_entry(entity_registry, config_entry.entry_id) + for entry in entries: + if entry.domain != ENTITY_TYPE or entry.disabled_by: continue - - device = hass.data[DOMAIN][DATA_DEVICES][webhook_id] - - entities.append(MobileAppBinarySensor(config, device, config_entry)) + config = { + ATTR_SENSOR_ATTRIBUTES: {}, + ATTR_SENSOR_DEVICE_CLASS: entry.device_class, + ATTR_SENSOR_ICON: entry.original_icon, + ATTR_SENSOR_NAME: entry.original_name, + ATTR_SENSOR_STATE: None, + ATTR_SENSOR_TYPE: entry.domain, + ATTR_SENSOR_UNIQUE_ID: entry.unique_id, + } + entities.append(MobileAppBinarySensor(config, entry.device_id, config_entry)) async_add_entities(entities) @@ -37,14 +52,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if data[CONF_WEBHOOK_ID] != webhook_id: return - unique_id = sensor_id(data[CONF_WEBHOOK_ID], data[ATTR_SENSOR_UNIQUE_ID]) - - entity = hass.data[DOMAIN][ENTITY_TYPE][unique_id] - - if "added" in entity: - return - - entity["added"] = True + data[CONF_UNIQUE_ID] = unique_id( + data[CONF_WEBHOOK_ID], data[ATTR_SENSOR_UNIQUE_ID] + ) + data[ + CONF_NAME + ] = f"{config_entry.data[ATTR_DEVICE_NAME]} {data[ATTR_SENSOR_NAME]}" device = hass.data[DOMAIN][DATA_DEVICES][data[CONF_WEBHOOK_ID]] @@ -64,3 +77,10 @@ class MobileAppBinarySensor(MobileAppEntity, BinarySensorEntity): def is_on(self): """Return the state of the binary sensor.""" return self._config[ATTR_SENSOR_STATE] + + @callback + def async_restore_last_state(self, last_state): + """Restore previous state.""" + + super().async_restore_last_state(last_state) + self._config[ATTR_SENSOR_STATE] = last_state.state == STATE_ON diff --git a/homeassistant/components/mobile_app/config_flow.py b/homeassistant/components/mobile_app/config_flow.py index 08fdecf364d..80b6c8db5e1 100644 --- a/homeassistant/components/mobile_app/config_flow.py +++ b/homeassistant/components/mobile_app/config_flow.py @@ -3,7 +3,7 @@ import uuid from homeassistant import config_entries from homeassistant.components import person -from homeassistant.helpers import entity_registry +from homeassistant.helpers import entity_registry as er from .const import ATTR_APP_ID, ATTR_DEVICE_ID, ATTR_DEVICE_NAME, CONF_USER_ID, DOMAIN @@ -36,8 +36,8 @@ class MobileAppFlowHandler(config_entries.ConfigFlow): user_input[ATTR_DEVICE_ID] = str(uuid.uuid4()).replace("-", "") # Register device tracker entity and add to person registering app - ent_reg = await entity_registry.async_get_registry(self.hass) - devt_entry = ent_reg.async_get_or_create( + entity_registry = await er.async_get_registry(self.hass) + devt_entry = entity_registry.async_get_or_create( "device_tracker", DOMAIN, user_input[ATTR_DEVICE_ID], diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index b35468a6fb3..b603e117c4c 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -9,11 +9,9 @@ CONF_REMOTE_UI_URL = "remote_ui_url" CONF_SECRET = "secret" CONF_USER_ID = "user_id" -DATA_BINARY_SENSOR = "binary_sensor" DATA_CONFIG_ENTRIES = "config_entries" DATA_DELETED_IDS = "deleted_ids" DATA_DEVICES = "devices" -DATA_SENSOR = "sensor" DATA_STORE = "store" DATA_NOTIFY = "notify" diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index 7a12f617740..748f680da5e 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -1,57 +1,70 @@ """A entity class for mobile_app.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.const import ATTR_ICON, CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.restore_state import RestoreEntity from .const import ( - ATTR_DEVICE_NAME, ATTR_SENSOR_ATTRIBUTES, ATTR_SENSOR_DEVICE_CLASS, ATTR_SENSOR_ICON, - ATTR_SENSOR_NAME, + ATTR_SENSOR_STATE, ATTR_SENSOR_TYPE, ATTR_SENSOR_UNIQUE_ID, - DOMAIN, SIGNAL_SENSOR_UPDATE, ) from .helpers import device_info -def sensor_id(webhook_id, unique_id): +def unique_id(webhook_id, sensor_unique_id): """Return a unique sensor ID.""" - return f"{webhook_id}_{unique_id}" + return f"{webhook_id}_{sensor_unique_id}" -class MobileAppEntity(Entity): +class MobileAppEntity(RestoreEntity): """Representation of an mobile app entity.""" def __init__(self, config: dict, device: DeviceEntry, entry: ConfigEntry): - """Initialize the sensor.""" + """Initialize the entity.""" self._config = config self._device = device self._entry = entry self._registration = entry.data - self._sensor_id = sensor_id( - self._registration[CONF_WEBHOOK_ID], config[ATTR_SENSOR_UNIQUE_ID] - ) + self._unique_id = config[CONF_UNIQUE_ID] self._entity_type = config[ATTR_SENSOR_TYPE] self.unsub_dispatcher = None - self._name = f"{entry.data[ATTR_DEVICE_NAME]} {config[ATTR_SENSOR_NAME]}" + self._name = config[CONF_NAME] async def async_added_to_hass(self): """Register callbacks.""" self.unsub_dispatcher = async_dispatcher_connect( self.hass, SIGNAL_SENSOR_UPDATE, self._handle_update ) + state = await self.async_get_last_state() + + if state is None: + return + + self.async_restore_last_state(state) async def async_will_remove_from_hass(self): """Disconnect dispatcher listener when removed.""" if self.unsub_dispatcher is not None: self.unsub_dispatcher() + @callback + def async_restore_last_state(self, last_state): + """Restore previous state.""" + self._config[ATTR_SENSOR_STATE] = last_state.state + self._config[ATTR_SENSOR_ATTRIBUTES] = { + **last_state.attributes, + **self._config[ATTR_SENSOR_ATTRIBUTES], + } + if ATTR_ICON in last_state.attributes: + self._config[ATTR_SENSOR_ICON] = last_state.attributes[ATTR_ICON] + @property def should_poll(self) -> bool: """Declare that this entity pushes its state to HA.""" @@ -80,27 +93,19 @@ class MobileAppEntity(Entity): @property def unique_id(self): """Return the unique ID of this sensor.""" - return self._sensor_id + return self._unique_id @property def device_info(self): """Return device registry information for this entity.""" return device_info(self._registration) - async def async_update(self): - """Get the latest state of the sensor.""" - data = self.hass.data[DOMAIN] - try: - self._config = data[self._entity_type][self._sensor_id] - except KeyError: - return - @callback def _handle_update(self, data): """Handle async event updates.""" - incoming_id = sensor_id(data[CONF_WEBHOOK_ID], data[ATTR_SENSOR_UNIQUE_ID]) - if incoming_id != self._sensor_id: + incoming_id = unique_id(data[CONF_WEBHOOK_ID], data[ATTR_SENSOR_UNIQUE_ID]) + if incoming_id != self._unique_id: return - self._config = data + self._config = {**self._config, **data} self.async_write_ha_state() diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 7c5cbd135ed..a9079be4f04 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -25,9 +25,7 @@ from .const import ( ATTR_SUPPORTS_ENCRYPTION, CONF_SECRET, CONF_USER_ID, - DATA_BINARY_SENSOR, DATA_DELETED_IDS, - DATA_SENSOR, DOMAIN, ) @@ -138,9 +136,7 @@ def safe_registration(registration: Dict) -> Dict: def savable_state(hass: HomeAssistantType) -> Dict: """Return a clean object containing things that should be saved.""" return { - DATA_BINARY_SENSOR: hass.data[DOMAIN][DATA_BINARY_SENSOR], DATA_DELETED_IDS: hass.data[DOMAIN][DATA_DELETED_IDS], - DATA_SENSOR: hass.data[DOMAIN][DATA_SENSOR], } diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index 11e07ed5e79..b09ef86453b 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -1,19 +1,26 @@ """Sensor platform for mobile_app.""" from functools import partial -from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID from homeassistant.core import callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( + ATTR_DEVICE_NAME, + ATTR_SENSOR_ATTRIBUTES, + ATTR_SENSOR_DEVICE_CLASS, + ATTR_SENSOR_ICON, + ATTR_SENSOR_NAME, ATTR_SENSOR_STATE, + ATTR_SENSOR_TYPE, ATTR_SENSOR_TYPE_SENSOR as ENTITY_TYPE, ATTR_SENSOR_UNIQUE_ID, ATTR_SENSOR_UOM, DATA_DEVICES, DOMAIN, ) -from .entity import MobileAppEntity, sensor_id +from .entity import MobileAppEntity, unique_id async def async_setup_entry(hass, config_entry, async_add_entities): @@ -22,13 +29,22 @@ async def async_setup_entry(hass, config_entry, async_add_entities): webhook_id = config_entry.data[CONF_WEBHOOK_ID] - for config in hass.data[DOMAIN][ENTITY_TYPE].values(): - if config[CONF_WEBHOOK_ID] != webhook_id: + entity_registry = await er.async_get_registry(hass) + entries = er.async_entries_for_config_entry(entity_registry, config_entry.entry_id) + for entry in entries: + if entry.domain != ENTITY_TYPE or entry.disabled_by: continue - - device = hass.data[DOMAIN][DATA_DEVICES][webhook_id] - - entities.append(MobileAppSensor(config, device, config_entry)) + config = { + ATTR_SENSOR_ATTRIBUTES: {}, + ATTR_SENSOR_DEVICE_CLASS: entry.device_class, + ATTR_SENSOR_ICON: entry.original_icon, + ATTR_SENSOR_NAME: entry.original_name, + ATTR_SENSOR_STATE: None, + ATTR_SENSOR_TYPE: entry.domain, + ATTR_SENSOR_UNIQUE_ID: entry.unique_id, + ATTR_SENSOR_UOM: entry.unit_of_measurement, + } + entities.append(MobileAppSensor(config, entry.device_id, config_entry)) async_add_entities(entities) @@ -37,14 +53,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if data[CONF_WEBHOOK_ID] != webhook_id: return - unique_id = sensor_id(data[CONF_WEBHOOK_ID], data[ATTR_SENSOR_UNIQUE_ID]) - - entity = hass.data[DOMAIN][ENTITY_TYPE][unique_id] - - if "added" in entity: - return - - entity["added"] = True + data[CONF_UNIQUE_ID] = unique_id( + data[CONF_WEBHOOK_ID], data[ATTR_SENSOR_UNIQUE_ID] + ) + data[ + CONF_NAME + ] = f"{config_entry.data[ATTR_DEVICE_NAME]} {data[ATTR_SENSOR_NAME]}" device = hass.data[DOMAIN][DATA_DEVICES][data[CONF_WEBHOOK_ID]] diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 043a555b6b7..3044f2df212 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -36,6 +36,7 @@ from homeassistant.exceptions import HomeAssistantError, ServiceNotFound from homeassistant.helpers import ( config_validation as cv, device_registry as dr, + entity_registry as er, template, ) from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -79,7 +80,6 @@ from .const import ( CONF_SECRET, DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, - DATA_STORE, DOMAIN, ERR_ENCRYPTION_ALREADY_ENABLED, ERR_ENCRYPTION_NOT_AVAILABLE, @@ -95,7 +95,6 @@ from .helpers import ( error_response, registration_context, safe_registration, - savable_state, supports_encryption, webhook_response, ) @@ -415,7 +414,10 @@ async def webhook_register_sensor(hass, config_entry, data): device_name = config_entry.data[ATTR_DEVICE_NAME] unique_store_key = f"{config_entry.data[CONF_WEBHOOK_ID]}_{unique_id}" - existing_sensor = unique_store_key in hass.data[DOMAIN][entity_type] + entity_registry = await er.async_get_registry(hass) + existing_sensor = entity_registry.async_get_entity_id( + entity_type, DOMAIN, unique_store_key + ) data[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID] @@ -424,16 +426,7 @@ async def webhook_register_sensor(hass, config_entry, data): _LOGGER.debug( "Re-register for %s of existing sensor %s", device_name, unique_id ) - entry = hass.data[DOMAIN][entity_type][unique_store_key] - data = {**entry, **data} - hass.data[DOMAIN][entity_type][unique_store_key] = data - - hass.data[DOMAIN][DATA_STORE].async_delay_save( - lambda: savable_state(hass), DELAY_SAVE - ) - - if existing_sensor: async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, data) else: register_signal = f"{DOMAIN}_{data[ATTR_SENSOR_TYPE]}_register" @@ -485,7 +478,10 @@ async def webhook_update_sensor_states(hass, config_entry, data): unique_store_key = f"{config_entry.data[CONF_WEBHOOK_ID]}_{unique_id}" - if unique_store_key not in hass.data[DOMAIN][entity_type]: + entity_registry = await er.async_get_registry(hass) + if not entity_registry.async_get_entity_id( + entity_type, DOMAIN, unique_store_key + ): _LOGGER.error( "Refusing to update %s non-registered sensor: %s", device_name, @@ -498,7 +494,7 @@ async def webhook_update_sensor_states(hass, config_entry, data): } continue - entry = hass.data[DOMAIN][entity_type][unique_store_key] + entry = {CONF_WEBHOOK_ID: config_entry.data[CONF_WEBHOOK_ID]} try: sensor = sensor_schema_full(sensor) @@ -518,16 +514,10 @@ async def webhook_update_sensor_states(hass, config_entry, data): new_state = {**entry, **sensor} - hass.data[DOMAIN][entity_type][unique_store_key] = new_state - async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, new_state) resp[unique_id] = {"success": True} - hass.data[DOMAIN][DATA_STORE].async_delay_save( - lambda: savable_state(hass), DELAY_SAVE - ) - return webhook_response(resp, registration=config_entry.data) diff --git a/tests/components/mobile_app/conftest.py b/tests/components/mobile_app/conftest.py index 7c611eb1010..db4843c126a 100644 --- a/tests/components/mobile_app/conftest.py +++ b/tests/components/mobile_app/conftest.py @@ -7,14 +7,6 @@ from homeassistant.setup import async_setup_component from .const import REGISTER, REGISTER_CLEARTEXT -from tests.common import mock_device_registry - - -@pytest.fixture -def registry(hass): - """Return a configured device registry.""" - return mock_device_registry(hass) - @pytest.fixture async def create_registrations(hass, authed_api_client): diff --git a/tests/components/mobile_app/test_binary_sensor.py b/tests/components/mobile_app/test_binary_sensor.py new file mode 100644 index 00000000000..5ada948a5d6 --- /dev/null +++ b/tests/components/mobile_app/test_binary_sensor.py @@ -0,0 +1,271 @@ +"""Entity tests for mobile_app.""" +from homeassistant.const import STATE_OFF +from homeassistant.helpers import device_registry + + +async def test_sensor(hass, create_registrations, webhook_client): + """Test that sensors can be registered and updated.""" + webhook_id = create_registrations[1]["webhook_id"] + webhook_url = f"/api/webhook/{webhook_id}" + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "register_sensor", + "data": { + "attributes": {"foo": "bar"}, + "device_class": "plug", + "icon": "mdi:power-plug", + "name": "Is Charging", + "state": True, + "type": "binary_sensor", + "unique_id": "is_charging", + }, + }, + ) + + assert reg_resp.status == 201 + + json = await reg_resp.json() + assert json == {"success": True} + await hass.async_block_till_done() + + entity = hass.states.get("binary_sensor.test_1_is_charging") + assert entity is not None + + assert entity.attributes["device_class"] == "plug" + assert entity.attributes["icon"] == "mdi:power-plug" + assert entity.attributes["foo"] == "bar" + assert entity.domain == "binary_sensor" + assert entity.name == "Test 1 Is Charging" + assert entity.state == "on" + + update_resp = await webhook_client.post( + webhook_url, + json={ + "type": "update_sensor_states", + "data": [ + { + "icon": "mdi:battery-unknown", + "state": False, + "type": "binary_sensor", + "unique_id": "is_charging", + }, + # This invalid data should not invalidate whole request + { + "type": "binary_sensor", + "unique_id": "invalid_state", + "invalid": "data", + }, + ], + }, + ) + + assert update_resp.status == 200 + + json = await update_resp.json() + assert json["invalid_state"]["success"] is False + + updated_entity = hass.states.get("binary_sensor.test_1_is_charging") + assert updated_entity.state == "off" + assert "foo" not in updated_entity.attributes + + dev_reg = await device_registry.async_get_registry(hass) + assert len(dev_reg.devices) == len(create_registrations) + + # Reload to verify state is restored + config_entry = hass.config_entries.async_entries("mobile_app")[1] + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + unloaded_entity = hass.states.get("binary_sensor.test_1_is_charging") + assert unloaded_entity.state == "unavailable" + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + restored_entity = hass.states.get("binary_sensor.test_1_is_charging") + assert restored_entity.state == updated_entity.state + assert restored_entity.attributes == updated_entity.attributes + + +async def test_sensor_must_register(hass, create_registrations, webhook_client): + """Test that sensors must be registered before updating.""" + webhook_id = create_registrations[1]["webhook_id"] + webhook_url = f"/api/webhook/{webhook_id}" + resp = await webhook_client.post( + webhook_url, + json={ + "type": "update_sensor_states", + "data": [ + {"state": True, "type": "binary_sensor", "unique_id": "battery_state"} + ], + }, + ) + + assert resp.status == 200 + + json = await resp.json() + assert json["battery_state"]["success"] is False + assert json["battery_state"]["error"]["code"] == "not_registered" + + +async def test_sensor_id_no_dupes(hass, create_registrations, webhook_client, caplog): + """Test that a duplicate unique ID in registration updates the sensor.""" + webhook_id = create_registrations[1]["webhook_id"] + webhook_url = f"/api/webhook/{webhook_id}" + + payload = { + "type": "register_sensor", + "data": { + "attributes": {"foo": "bar"}, + "device_class": "plug", + "icon": "mdi:power-plug", + "name": "Is Charging", + "state": True, + "type": "binary_sensor", + "unique_id": "is_charging", + }, + } + + reg_resp = await webhook_client.post(webhook_url, json=payload) + + assert reg_resp.status == 201 + + reg_json = await reg_resp.json() + assert reg_json == {"success": True} + await hass.async_block_till_done() + + assert "Re-register" not in caplog.text + + entity = hass.states.get("binary_sensor.test_1_is_charging") + assert entity is not None + + assert entity.attributes["device_class"] == "plug" + assert entity.attributes["icon"] == "mdi:power-plug" + assert entity.attributes["foo"] == "bar" + assert entity.domain == "binary_sensor" + assert entity.name == "Test 1 Is Charging" + assert entity.state == "on" + + payload["data"]["state"] = False + dupe_resp = await webhook_client.post(webhook_url, json=payload) + + assert dupe_resp.status == 201 + dupe_reg_json = await dupe_resp.json() + assert dupe_reg_json == {"success": True} + await hass.async_block_till_done() + + assert "Re-register" in caplog.text + + entity = hass.states.get("binary_sensor.test_1_is_charging") + assert entity is not None + + assert entity.attributes["device_class"] == "plug" + assert entity.attributes["icon"] == "mdi:power-plug" + assert entity.attributes["foo"] == "bar" + assert entity.domain == "binary_sensor" + assert entity.name == "Test 1 Is Charging" + assert entity.state == "off" + + +async def test_register_sensor_no_state(hass, create_registrations, webhook_client): + """Test that sensors can be registered, when there is no (unknown) state.""" + webhook_id = create_registrations[1]["webhook_id"] + webhook_url = f"/api/webhook/{webhook_id}" + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "register_sensor", + "data": { + "name": "Is Charging", + "state": None, + "type": "binary_sensor", + "unique_id": "is_charging", + }, + }, + ) + + assert reg_resp.status == 201 + + json = await reg_resp.json() + assert json == {"success": True} + await hass.async_block_till_done() + + entity = hass.states.get("binary_sensor.test_1_is_charging") + assert entity is not None + + assert entity.domain == "binary_sensor" + assert entity.name == "Test 1 Is Charging" + assert entity.state == STATE_OFF # Binary sensor defaults to off + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "register_sensor", + "data": { + "name": "Backup Is Charging", + "type": "binary_sensor", + "unique_id": "backup_is_charging", + }, + }, + ) + + assert reg_resp.status == 201 + + json = await reg_resp.json() + assert json == {"success": True} + await hass.async_block_till_done() + + entity = hass.states.get("binary_sensor.test_1_backup_is_charging") + assert entity + + assert entity.domain == "binary_sensor" + assert entity.name == "Test 1 Backup Is Charging" + assert entity.state == STATE_OFF # Binary sensor defaults to off + + +async def test_update_sensor_no_state(hass, create_registrations, webhook_client): + """Test that sensors can be updated, when there is no (unknown) state.""" + webhook_id = create_registrations[1]["webhook_id"] + webhook_url = f"/api/webhook/{webhook_id}" + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "register_sensor", + "data": { + "name": "Is Charging", + "state": True, + "type": "binary_sensor", + "unique_id": "is_charging", + }, + }, + ) + + assert reg_resp.status == 201 + + json = await reg_resp.json() + assert json == {"success": True} + await hass.async_block_till_done() + + entity = hass.states.get("binary_sensor.test_1_is_charging") + assert entity is not None + assert entity.state == "on" + + update_resp = await webhook_client.post( + webhook_url, + json={ + "type": "update_sensor_states", + "data": [ + {"state": None, "type": "binary_sensor", "unique_id": "is_charging"} + ], + }, + ) + + assert update_resp.status == 200 + + json = await update_resp.json() + assert json == {"is_charging": {"success": True}} + + updated_entity = hass.states.get("binary_sensor.test_1_is_charging") + assert updated_entity.state == STATE_OFF # Binary sensor defaults to off diff --git a/tests/components/mobile_app/test_entity.py b/tests/components/mobile_app/test_sensor.py similarity index 92% rename from tests/components/mobile_app/test_entity.py rename to tests/components/mobile_app/test_sensor.py index ba121d766ac..0ba1cf3096d 100644 --- a/tests/components/mobile_app/test_entity.py +++ b/tests/components/mobile_app/test_sensor.py @@ -66,10 +66,24 @@ async def test_sensor(hass, create_registrations, webhook_client): updated_entity = hass.states.get("sensor.test_1_battery_state") assert updated_entity.state == "123" + assert "foo" not in updated_entity.attributes dev_reg = await device_registry.async_get_registry(hass) assert len(dev_reg.devices) == len(create_registrations) + # Reload to verify state is restored + config_entry = hass.config_entries.async_entries("mobile_app")[1] + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + unloaded_entity = hass.states.get("sensor.test_1_battery_state") + assert unloaded_entity.state == "unavailable" + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + restored_entity = hass.states.get("sensor.test_1_battery_state") + assert restored_entity.state == updated_entity.state + assert restored_entity.attributes == updated_entity.attributes + async def test_sensor_must_register(hass, create_registrations, webhook_client): """Test that sensors must be registered before updating.""" diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 831c8250d7a..a7dc675b7b7 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -109,7 +109,7 @@ async def test_webhook_handle_fire_event(hass, create_registrations, webhook_cli @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("test_event", store_event) From 14a64ea970493eb792ca6e48697b751623bf8252 Mon Sep 17 00:00:00 2001 From: Czapla <56671347+Antoni-Czaplicki@users.noreply.github.com> Date: Thu, 11 Feb 2021 20:46:58 +0100 Subject: [PATCH 370/796] Add generic_thermostat unique ID parameter (#46399) * Add generic_thermostat unique ID parameter * Add tests for unique id * Fix flake8 --- .../components/generic_thermostat/climate.py | 11 ++++++++ .../generic_thermostat/test_climate.py | 27 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 433e91104ad..5fbdf499146 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -23,6 +23,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, CONF_NAME, + CONF_UNIQUE_ID, EVENT_HOMEASSISTANT_START, PRECISION_HALVES, PRECISION_TENTHS, @@ -85,6 +86,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_PRECISION): vol.In( [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE] ), + vol.Optional(CONF_UNIQUE_ID): cv.string, } ) @@ -109,6 +111,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= away_temp = config.get(CONF_AWAY_TEMP) precision = config.get(CONF_PRECISION) unit = hass.config.units.temperature_unit + unique_id = config.get(CONF_UNIQUE_ID) async_add_entities( [ @@ -128,6 +131,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= away_temp, precision, unit, + unique_id, ) ] ) @@ -153,6 +157,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity): away_temp, precision, unit, + unique_id, ): """Initialize the thermostat.""" self._name = name @@ -177,6 +182,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity): self._max_temp = max_temp self._target_temp = target_temp self._unit = unit + self._unique_id = unique_id self._support_flags = SUPPORT_FLAGS if away_temp: self._support_flags = SUPPORT_FLAGS | SUPPORT_PRESET_MODE @@ -269,6 +275,11 @@ class GenericThermostat(ClimateEntity, RestoreEntity): """Return the name of the thermostat.""" return self._name + @property + def unique_id(self): + """Return the unique id of this thermostat.""" + return self._unique_id + @property def precision(self): """Return the precision of the system.""" diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 201ed0130ff..e6cdf962d24 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -159,6 +159,33 @@ async def test_heater_switch(hass, setup_comp_1): assert STATE_ON == hass.states.get(heater_switch).state +async def test_unique_id(hass, setup_comp_1): + """Test heater switching input_boolean.""" + unique_id = "some_unique_id" + _setup_sensor(hass, 18) + _setup_switch(hass, True) + assert await async_setup_component( + hass, + DOMAIN, + { + "climate": { + "platform": "generic_thermostat", + "name": "test", + "heater": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "unique_id": unique_id, + } + }, + ) + await hass.async_block_till_done() + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entry = entity_registry.async_get(ENTITY) + assert entry + assert entry.unique_id == unique_id + + def _setup_sensor(hass, temp): """Set up the test sensor.""" hass.states.async_set(ENT_SENSOR, temp) From 8dc06e612fc3e1fe5ea2b6cd9dc493f917b6d6ff Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 11 Feb 2021 21:37:53 +0100 Subject: [PATCH 371/796] Add config flow to philips_js (#45784) * Add config flow to philips_js * Adjust name of entry to contain serial * Use device id in event rather than entity id * Adjust turn on text * Deprecate all fields * Be somewhat more explicit in typing * Switch to direct coordinator access * Refactor the pluggable action * Adjust tests a bit * Minor adjustment * More adjustments * Add missing await in update coordinator * Be more lenient to lack of system info * Use constant for trigger type and simplify * Apply suggestions from code review Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- .coveragerc | 1 + .../components/philips_js/__init__.py | 132 +++++++++++- .../components/philips_js/config_flow.py | 90 +++++++++ homeassistant/components/philips_js/const.py | 4 + .../components/philips_js/device_trigger.py | 65 ++++++ .../components/philips_js/manifest.json | 11 +- .../components/philips_js/media_player.py | 189 ++++++++++-------- .../components/philips_js/strings.json | 24 +++ .../philips_js/translations/en.json | 24 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + tests/components/philips_js/__init__.py | 25 +++ tests/components/philips_js/conftest.py | 62 ++++++ .../components/philips_js/test_config_flow.py | 105 ++++++++++ .../philips_js/test_device_trigger.py | 69 +++++++ 16 files changed, 721 insertions(+), 86 deletions(-) create mode 100644 homeassistant/components/philips_js/config_flow.py create mode 100644 homeassistant/components/philips_js/const.py create mode 100644 homeassistant/components/philips_js/device_trigger.py create mode 100644 homeassistant/components/philips_js/strings.json create mode 100644 homeassistant/components/philips_js/translations/en.json create mode 100644 tests/components/philips_js/__init__.py create mode 100644 tests/components/philips_js/conftest.py create mode 100644 tests/components/philips_js/test_config_flow.py create mode 100644 tests/components/philips_js/test_device_trigger.py diff --git a/.coveragerc b/.coveragerc index cd1d6a9f6d3..c17f3d1057d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -704,6 +704,7 @@ omit = homeassistant/components/pandora/media_player.py homeassistant/components/pcal9535a/* homeassistant/components/pencom/switch.py + homeassistant/components/philips_js/__init__.py homeassistant/components/philips_js/media_player.py homeassistant/components/pi_hole/sensor.py homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 4b011c9f207..bfb99898ab2 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -1 +1,131 @@ -"""The philips_js component.""" +"""The Philips TV integration.""" +import asyncio +from datetime import timedelta +import logging +from typing import Any, Callable, Dict, Optional + +from haphilipsjs import ConnectionFailure, PhilipsTV + +from homeassistant.components.automation import AutomationActionType +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_VERSION, CONF_HOST +from homeassistant.core import Context, HassJob, HomeAssistant, callback +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +PLATFORMS = ["media_player"] + +LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Philips TV component.""" + hass.data[DOMAIN] = {} + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Philips TV from a config entry.""" + + tvapi = PhilipsTV(entry.data[CONF_HOST], entry.data[CONF_API_VERSION]) + + coordinator = PhilipsTVDataUpdateCoordinator(hass, tvapi) + + await coordinator.async_refresh() + hass.data[DOMAIN][entry.entry_id] = coordinator + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class PluggableAction: + """A pluggable action handler.""" + + _actions: Dict[Any, AutomationActionType] = {} + + def __init__(self, update: Callable[[], None]): + """Initialize.""" + self._update = update + + def __bool__(self): + """Return if we have something attached.""" + return bool(self._actions) + + @callback + def async_attach(self, action: AutomationActionType, variables: Dict[str, Any]): + """Attach a device trigger for turn on.""" + + @callback + def _remove(): + del self._actions[_remove] + self._update() + + job = HassJob(action) + + self._actions[_remove] = (job, variables) + self._update() + + return _remove + + async def async_run( + self, hass: HomeAssistantType, context: Optional[Context] = None + ): + """Run all turn on triggers.""" + for job, variables in self._actions.values(): + hass.async_run_hass_job(job, variables, context) + + +class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): + """Coordinator to update data.""" + + api: PhilipsTV + + def __init__(self, hass, api: PhilipsTV) -> None: + """Set up the coordinator.""" + self.api = api + + def _update_listeners(): + for update_callback in self._listeners: + update_callback() + + self.turn_on = PluggableAction(_update_listeners) + + async def _async_update(): + try: + await self.hass.async_add_executor_job(self.api.update) + except ConnectionFailure: + pass + + super().__init__( + hass, + LOGGER, + name=DOMAIN, + update_method=_async_update, + update_interval=timedelta(seconds=30), + request_refresh_debouncer=Debouncer( + hass, LOGGER, cooldown=2.0, immediate=False + ), + ) diff --git a/homeassistant/components/philips_js/config_flow.py b/homeassistant/components/philips_js/config_flow.py new file mode 100644 index 00000000000..71bc34688b4 --- /dev/null +++ b/homeassistant/components/philips_js/config_flow.py @@ -0,0 +1,90 @@ +"""Config flow for Philips TV integration.""" +import logging +from typing import Any, Dict, Optional, TypedDict + +from haphilipsjs import ConnectionFailure, PhilipsTV +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_API_VERSION, CONF_HOST + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +class FlowUserDict(TypedDict): + """Data for user step.""" + + host: str + api_version: int + + +async def validate_input(hass: core.HomeAssistant, data: FlowUserDict): + """Validate the user input allows us to connect.""" + hub = PhilipsTV(data[CONF_HOST], data[CONF_API_VERSION]) + + await hass.async_add_executor_job(hub.getSystem) + + if hub.system is None: + raise ConnectionFailure + + return hub.system + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Philips TV.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + _default = {} + + async def async_step_import(self, conf: Dict[str, Any]): + """Import a configuration from config.yaml.""" + for entry in self._async_current_entries(): + if entry.data[CONF_HOST] == conf[CONF_HOST]: + return self.async_abort(reason="already_configured") + + return await self.async_step_user( + { + CONF_HOST: conf[CONF_HOST], + CONF_API_VERSION: conf[CONF_API_VERSION], + } + ) + + async def async_step_user(self, user_input: Optional[FlowUserDict] = None): + """Handle the initial step.""" + errors = {} + if user_input: + self._default = user_input + try: + system = await validate_input(self.hass, user_input) + except ConnectionFailure: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(system["serialnumber"]) + self._abort_if_unique_id_configured(updates=user_input) + + data = {**user_input, "system": system} + + return self.async_create_entry( + title=f"{system['name']} ({system['serialnumber']})", data=data + ) + + schema = vol.Schema( + { + vol.Required(CONF_HOST, default=self._default.get(CONF_HOST)): str, + vol.Required( + CONF_API_VERSION, default=self._default.get(CONF_API_VERSION) + ): vol.In([1, 6]), + } + ) + return self.async_show_form(step_id="user", data_schema=schema, errors=errors) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/philips_js/const.py b/homeassistant/components/philips_js/const.py new file mode 100644 index 00000000000..893766b0083 --- /dev/null +++ b/homeassistant/components/philips_js/const.py @@ -0,0 +1,4 @@ +"""The Philips TV constants.""" + +DOMAIN = "philips_js" +CONF_SYSTEM = "system" diff --git a/homeassistant/components/philips_js/device_trigger.py b/homeassistant/components/philips_js/device_trigger.py new file mode 100644 index 00000000000..ec1da0635db --- /dev/null +++ b/homeassistant/components/philips_js/device_trigger.py @@ -0,0 +1,65 @@ +"""Provides device automations for control of device.""" +from typing import List + +import voluptuous as vol + +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.device_registry import DeviceRegistry, async_get_registry +from homeassistant.helpers.typing import ConfigType + +from . import PhilipsTVDataUpdateCoordinator +from .const import DOMAIN + +TRIGGER_TYPE_TURN_ON = "turn_on" + +TRIGGER_TYPES = {TRIGGER_TYPE_TURN_ON} +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + } +) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for device.""" + triggers = [] + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: TRIGGER_TYPE_TURN_ON, + } + ) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + registry: DeviceRegistry = await async_get_registry(hass) + if config[CONF_TYPE] == TRIGGER_TYPE_TURN_ON: + variables = { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": config[CONF_DEVICE_ID], + "description": f"philips_js '{config[CONF_TYPE]}' event", + } + } + + device = registry.async_get(config[CONF_DEVICE_ID]) + for config_entry_id in device.config_entries: + coordinator: PhilipsTVDataUpdateCoordinator = hass.data[DOMAIN].get( + config_entry_id + ) + if coordinator: + return coordinator.turn_on.async_attach(action, variables) diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index 74473827424..e41aa348732 100644 --- a/homeassistant/components/philips_js/manifest.json +++ b/homeassistant/components/philips_js/manifest.json @@ -2,6 +2,11 @@ "domain": "philips_js", "name": "Philips TV", "documentation": "https://www.home-assistant.io/integrations/philips_js", - "requirements": ["ha-philipsjs==0.0.8"], - "codeowners": ["@elupus"] -} + "requirements": [ + "ha-philipsjs==0.1.0" + ], + "codeowners": [ + "@elupus" + ], + "config_flow": true +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 7ccec14406a..2e263ea2891 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -1,11 +1,11 @@ """Media Player component to integrate TVs exposing the Joint Space API.""" -from datetime import timedelta -import logging +from typing import Any, Dict -from haphilipsjs import PhilipsTV import voluptuous as vol +from homeassistant import config_entries from homeassistant.components.media_player import ( + DEVICE_CLASS_TV, PLATFORM_SCHEMA, BrowseMedia, MediaPlayerEntity, @@ -27,6 +27,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.philips_js import PhilipsTVDataUpdateCoordinator from homeassistant.const import ( CONF_API_VERSION, CONF_HOST, @@ -34,11 +35,13 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import call_later, track_time_interval -from homeassistant.helpers.script import Script +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import CoordinatorEntity -_LOGGER = logging.getLogger(__name__) +from . import LOGGER as _LOGGER +from .const import CONF_SYSTEM, DOMAIN SUPPORT_PHILIPS_JS = ( SUPPORT_TURN_OFF @@ -54,24 +57,25 @@ SUPPORT_PHILIPS_JS = ( CONF_ON_ACTION = "turn_on_action" -DEFAULT_NAME = "Philips TV" DEFAULT_API_VERSION = "1" -DEFAULT_SCAN_INTERVAL = 30 - -DELAY_ACTION_DEFAULT = 2.0 -DELAY_ACTION_ON = 10.0 PREFIX_SEPARATOR = ": " PREFIX_SOURCE = "Input" PREFIX_CHANNEL = "Channel" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_API_VERSION, default=DEFAULT_API_VERSION): cv.string, - vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, - } +PLATFORM_SCHEMA = vol.All( + cv.deprecated(CONF_HOST), + cv.deprecated(CONF_NAME), + cv.deprecated(CONF_API_VERSION), + cv.deprecated(CONF_ON_ACTION), + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Remove(CONF_NAME): cv.string, + vol.Optional(CONF_API_VERSION, default=DEFAULT_API_VERSION): cv.string, + vol.Remove(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, + } + ), ) @@ -81,70 +85,69 @@ def _inverted(data): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Philips TV platform.""" - name = config.get(CONF_NAME) - host = config.get(CONF_HOST) - api_version = config.get(CONF_API_VERSION) - turn_on_action = config.get(CONF_ON_ACTION) - - tvapi = PhilipsTV(host, api_version) - domain = __name__.split(".")[-2] - on_script = Script(hass, turn_on_action, name, domain) if turn_on_action else None - - add_entities([PhilipsTVMediaPlayer(tvapi, name, on_script)]) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=config, + ) + ) -class PhilipsTVMediaPlayer(MediaPlayerEntity): +async def async_setup_entry( + hass: HomeAssistantType, + config_entry: config_entries.ConfigEntry, + async_add_entities, +): + """Set up the configuration entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + [ + PhilipsTVMediaPlayer( + coordinator, + config_entry.data[CONF_SYSTEM], + config_entry.unique_id or config_entry.entry_id, + ) + ] + ) + + +class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): """Representation of a Philips TV exposing the JointSpace API.""" - def __init__(self, tv: PhilipsTV, name: str, on_script: Script): + def __init__( + self, + coordinator: PhilipsTVDataUpdateCoordinator, + system: Dict[str, Any], + unique_id: str, + ): """Initialize the Philips TV.""" - self._tv = tv - self._name = name + self._tv = coordinator.api + self._coordinator = coordinator self._sources = {} self._channels = {} - self._on_script = on_script self._supports = SUPPORT_PHILIPS_JS - if self._on_script: - self._supports |= SUPPORT_TURN_ON - self._update_task = None + self._system = system + self._unique_id = unique_id + super().__init__(coordinator) + self._update_from_coordinator() - def _update_soon(self, delay): + def _update_soon(self): """Reschedule update task.""" - if self._update_task: - self._update_task() - self._update_task = None - - self.schedule_update_ha_state(force_refresh=False) - - def update_forced(event_time): - self.schedule_update_ha_state(force_refresh=True) - - def update_and_restart(event_time): - update_forced(event_time) - self._update_task = track_time_interval( - self.hass, update_forced, timedelta(seconds=DEFAULT_SCAN_INTERVAL) - ) - - call_later(self.hass, delay, update_and_restart) - - async def async_added_to_hass(self): - """Start running updates once we are added to hass.""" - await self.hass.async_add_executor_job(self._update_soon, 0) + self.hass.add_job(self.coordinator.async_request_refresh) @property def name(self): """Return the device name.""" - return self._name - - @property - def should_poll(self): - """Device should be polled.""" - return False + return self._system["name"] @property def supported_features(self): """Flag media player features that are supported.""" - return self._supports + supports = self._supports + if self._coordinator.turn_on: + supports |= SUPPORT_TURN_ON + return supports @property def state(self): @@ -178,7 +181,7 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity): source_id = _inverted(self._sources).get(source) if source_id: self._tv.setSource(source_id) - self._update_soon(DELAY_ACTION_DEFAULT) + self._update_soon() @property def volume_level(self): @@ -190,47 +193,45 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity): """Boolean if volume is currently muted.""" return self._tv.muted - def turn_on(self): + async def async_turn_on(self): """Turn on the device.""" - if self._on_script: - self._on_script.run(context=self._context) - self._update_soon(DELAY_ACTION_ON) + await self._coordinator.turn_on.async_run(self.hass, self._context) def turn_off(self): """Turn off the device.""" self._tv.sendKey("Standby") self._tv.on = False - self._update_soon(DELAY_ACTION_DEFAULT) + self._update_soon() def volume_up(self): """Send volume up command.""" self._tv.sendKey("VolumeUp") - self._update_soon(DELAY_ACTION_DEFAULT) + self._update_soon() def volume_down(self): """Send volume down command.""" self._tv.sendKey("VolumeDown") - self._update_soon(DELAY_ACTION_DEFAULT) + self._update_soon() def mute_volume(self, mute): """Send mute command.""" self._tv.setVolume(None, mute) - self._update_soon(DELAY_ACTION_DEFAULT) + self._update_soon() def set_volume_level(self, volume): """Set volume level, range 0..1.""" self._tv.setVolume(volume, self._tv.muted) - self._update_soon(DELAY_ACTION_DEFAULT) + self._update_soon() def media_previous_track(self): """Send rewind command.""" self._tv.sendKey("Previous") - self._update_soon(DELAY_ACTION_DEFAULT) + self._update_soon() def media_next_track(self): """Send fast forward command.""" self._tv.sendKey("Next") - self._update_soon(DELAY_ACTION_DEFAULT) + self._update_soon() @property def media_channel(self): @@ -267,6 +268,29 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity): """Return the state attributes.""" return {"channel_list": list(self._channels.values())} + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_TV + + @property + def unique_id(self): + """Return unique identifier if known.""" + return self._unique_id + + @property + def device_info(self): + """Return a device description for device registry.""" + return { + "name": self._system["name"], + "identifiers": { + (DOMAIN, self._unique_id), + }, + "model": self._system.get("model"), + "manufacturer": "Philips", + "sw_version": self._system.get("softwareversion"), + } + def play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" _LOGGER.debug("Call play media type <%s>, Id <%s>", media_type, media_id) @@ -275,7 +299,7 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity): channel_id = _inverted(self._channels).get(media_id) if channel_id: self._tv.setChannel(channel_id) - self._update_soon(DELAY_ACTION_DEFAULT) + self._update_soon() else: _LOGGER.error("Unable to find channel <%s>", media_id) else: @@ -308,10 +332,7 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity): ], ) - def update(self): - """Get the latest data and update device state.""" - self._tv.update() - + def _update_from_coordinator(self): self._sources = { srcid: source.get("name") or f"Source {srcid}" for srcid, source in (self._tv.sources or {}).items() @@ -321,3 +342,9 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity): chid: channel.get("name") or f"Channel {chid}" for chid, channel in (self._tv.channels or {}).items() } + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_from_coordinator() + super()._handle_coordinator_update() diff --git a/homeassistant/components/philips_js/strings.json b/homeassistant/components/philips_js/strings.json new file mode 100644 index 00000000000..2267315501f --- /dev/null +++ b/homeassistant/components/philips_js/strings.json @@ -0,0 +1,24 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "api_version": "API Version" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "Device is requested to turn on" + } + } +} diff --git a/homeassistant/components/philips_js/translations/en.json b/homeassistant/components/philips_js/translations/en.json new file mode 100644 index 00000000000..ca580159dab --- /dev/null +++ b/homeassistant/components/philips_js/translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "api_version": "API Version" + } + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "Device is requested to turn on" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 6366a3eb887..06e2516633e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -159,6 +159,7 @@ FLOWS = [ "owntracks", "ozw", "panasonic_viera", + "philips_js", "pi_hole", "plaato", "plex", diff --git a/requirements_all.txt b/requirements_all.txt index 12ade1a446b..4dbee8b0a65 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -720,7 +720,7 @@ guppy3==3.1.0 ha-ffmpeg==3.0.2 # homeassistant.components.philips_js -ha-philipsjs==0.0.8 +ha-philipsjs==0.1.0 # homeassistant.components.habitica habitipy==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f5a629a771f..88e8dffff1c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -380,6 +380,9 @@ guppy3==3.1.0 # homeassistant.components.ffmpeg ha-ffmpeg==3.0.2 +# homeassistant.components.philips_js +ha-philipsjs==0.1.0 + # homeassistant.components.hangouts hangups==0.4.11 diff --git a/tests/components/philips_js/__init__.py b/tests/components/philips_js/__init__.py new file mode 100644 index 00000000000..1c96a6d4e55 --- /dev/null +++ b/tests/components/philips_js/__init__.py @@ -0,0 +1,25 @@ +"""Tests for the Philips TV integration.""" + +MOCK_SERIAL_NO = "1234567890" +MOCK_NAME = "Philips TV" + +MOCK_SYSTEM = { + "menulanguage": "English", + "name": MOCK_NAME, + "country": "Sweden", + "serialnumber": MOCK_SERIAL_NO, + "softwareversion": "abcd", + "model": "modelname", +} + +MOCK_USERINPUT = { + "host": "1.1.1.1", + "api_version": 1, +} + +MOCK_CONFIG = { + **MOCK_USERINPUT, + "system": MOCK_SYSTEM, +} + +MOCK_ENTITY_ID = "media_player.philips_tv" diff --git a/tests/components/philips_js/conftest.py b/tests/components/philips_js/conftest.py new file mode 100644 index 00000000000..1f20cd821a5 --- /dev/null +++ b/tests/components/philips_js/conftest.py @@ -0,0 +1,62 @@ +"""Standard setup for tests.""" +from unittest.mock import Mock, patch + +from pytest import fixture + +from homeassistant import setup +from homeassistant.components.philips_js.const import DOMAIN + +from . import MOCK_CONFIG, MOCK_ENTITY_ID, MOCK_NAME, MOCK_SERIAL_NO, MOCK_SYSTEM + +from tests.common import MockConfigEntry, mock_device_registry + + +@fixture(autouse=True) +async def setup_notification(hass): + """Configure notification system.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + +@fixture(autouse=True) +def mock_tv(): + """Disable component actual use.""" + tv = Mock(autospec="philips_js.PhilipsTV") + tv.sources = {} + tv.channels = {} + tv.system = MOCK_SYSTEM + + with patch( + "homeassistant.components.philips_js.config_flow.PhilipsTV", return_value=tv + ), patch("homeassistant.components.philips_js.PhilipsTV", return_value=tv): + yield tv + + +@fixture +async def mock_config_entry(hass): + """Get standard player.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, title=MOCK_NAME) + config_entry.add_to_hass(hass) + return config_entry + + +@fixture +def mock_device_reg(hass): + """Get standard device.""" + return mock_device_registry(hass) + + +@fixture +async def mock_entity(hass, mock_device_reg, mock_config_entry): + """Get standard player.""" + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + yield MOCK_ENTITY_ID + + +@fixture +def mock_device(hass, mock_device_reg, mock_entity, mock_config_entry): + """Get standard device.""" + return mock_device_reg.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={(DOMAIN, MOCK_SERIAL_NO)}, + ) diff --git a/tests/components/philips_js/test_config_flow.py b/tests/components/philips_js/test_config_flow.py new file mode 100644 index 00000000000..75caff78891 --- /dev/null +++ b/tests/components/philips_js/test_config_flow.py @@ -0,0 +1,105 @@ +"""Test the Philips TV config flow.""" +from unittest.mock import patch + +from pytest import fixture + +from homeassistant import config_entries +from homeassistant.components.philips_js.const import DOMAIN + +from . import MOCK_CONFIG, MOCK_USERINPUT + + +@fixture(autouse=True) +def mock_setup(): + """Disable component setup.""" + with patch( + "homeassistant.components.philips_js.async_setup", return_value=True + ) as mock_setup: + yield mock_setup + + +@fixture(autouse=True) +def mock_setup_entry(): + """Disable component setup.""" + with patch( + "homeassistant.components.philips_js.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +async def test_import(hass, mock_setup, mock_setup_entry): + """Test we get an item on import.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_USERINPUT, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "Philips TV (1234567890)" + assert result["data"] == MOCK_CONFIG + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_exist(hass, mock_config_entry): + """Test we get an item on import.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_USERINPUT, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_form(hass, mock_setup, mock_setup_entry): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USERINPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Philips TV (1234567890)" + assert result2["data"] == MOCK_CONFIG + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass, mock_tv): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_tv.system = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_USERINPUT + ) + + assert result["type"] == "form" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_form_unexpected_error(hass, mock_tv): + """Test we handle unexpected exceptions.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_tv.getSystem.side_effect = Exception("Unexpected exception") + result = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_USERINPUT + ) + + assert result["type"] == "form" + assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/philips_js/test_device_trigger.py b/tests/components/philips_js/test_device_trigger.py new file mode 100644 index 00000000000..43c7c424cf9 --- /dev/null +++ b/tests/components/philips_js/test_device_trigger.py @@ -0,0 +1,69 @@ +"""The tests for Philips TV device triggers.""" +import pytest + +import homeassistant.components.automation as automation +from homeassistant.components.philips_js.const import DOMAIN +from homeassistant.setup import async_setup_component + +from tests.common import ( + assert_lists_same, + async_get_device_automations, + async_mock_service, +) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, mock_device): + """Test we get the expected triggers.""" + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "turn_on", + "device_id": mock_device.id, + }, + ] + triggers = await async_get_device_automations(hass, "trigger", mock_device.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_if_fires_on_turn_on_request(hass, calls, mock_entity, mock_device): + """Test for turn_on and turn_off triggers firing.""" + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": mock_device.id, + "type": "turn_on", + }, + "action": { + "service": "test.automation", + "data_template": {"some": "{{ trigger.device_id }}"}, + }, + } + ] + }, + ) + + await hass.services.async_call( + "media_player", + "turn_on", + {"entity_id": mock_entity}, + blocking=True, + ) + + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == mock_device.id From 34a491f826e4891f62eb368370c2ad1a26e77da2 Mon Sep 17 00:00:00 2001 From: chriss158 Date: Fri, 12 Feb 2021 00:17:49 +0100 Subject: [PATCH 372/796] Install libpcap-dev for devcontainer (#46106) --- Dockerfile.dev | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile.dev b/Dockerfile.dev index 0d86c1a7eec..09f8f155930 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -14,6 +14,7 @@ RUN \ libswscale-dev \ libswresample-dev \ libavfilter-dev \ + libpcap-dev \ git \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* From ee04473e857431fc66424bc4e56d0cfe9851d4ff Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 12 Feb 2021 00:02:46 +0000 Subject: [PATCH 373/796] [ci skip] Translation update --- .../components/foscam/translations/pl.json | 2 + .../media_player/translations/pl.json | 7 ++ .../media_player/translations/tr.json | 4 + .../components/mysensors/translations/pl.json | 79 +++++++++++++++++++ .../philips_js/translations/en.json | 6 +- .../philips_js/translations/pl.json | 24 ++++++ .../components/powerwall/translations/ca.json | 8 +- .../components/powerwall/translations/et.json | 1 + .../components/powerwall/translations/no.json | 8 +- .../components/powerwall/translations/pl.json | 8 +- .../components/powerwall/translations/ru.json | 8 +- .../powerwall/translations/zh-Hant.json | 8 +- .../components/roku/translations/no.json | 1 + .../components/roku/translations/pl.json | 1 + .../components/shelly/translations/pl.json | 2 +- .../shelly/translations/zh-Hant.json | 2 +- .../components/tesla/translations/ca.json | 4 + .../components/tesla/translations/no.json | 4 + .../components/tesla/translations/pl.json | 4 + .../components/tesla/translations/ru.json | 4 + .../tesla/translations/zh-Hant.json | 4 + 21 files changed, 174 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/mysensors/translations/pl.json create mode 100644 homeassistant/components/philips_js/translations/pl.json diff --git a/homeassistant/components/foscam/translations/pl.json b/homeassistant/components/foscam/translations/pl.json index ef0bcda2b3a..d7494e22063 100644 --- a/homeassistant/components/foscam/translations/pl.json +++ b/homeassistant/components/foscam/translations/pl.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_response": "Nieprawid\u0142owa odpowied\u017a z urz\u0105dzenia", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { @@ -14,6 +15,7 @@ "host": "Nazwa hosta lub adres IP", "password": "Has\u0142o", "port": "Port", + "rtsp_port": "Port RTSP", "stream": "Strumie\u0144", "username": "Nazwa u\u017cytkownika" } diff --git a/homeassistant/components/media_player/translations/pl.json b/homeassistant/components/media_player/translations/pl.json index 23ba46f9339..2a70661d788 100644 --- a/homeassistant/components/media_player/translations/pl.json +++ b/homeassistant/components/media_player/translations/pl.json @@ -6,6 +6,13 @@ "is_on": "odtwarzacz {entity_name} jest w\u0142\u0105czony", "is_paused": "odtwarzanie medi\u00f3w na {entity_name} jest wstrzymane", "is_playing": "{entity_name} odtwarza media" + }, + "trigger_type": { + "idle": "odtwarzacz {entity_name} stanie si\u0119 bezczynny", + "paused": "odtwarzacz {entity_name} zostanie wstrzymany", + "playing": "odtwarzacz {entity_name} rozpocznie odtwarzanie", + "turned_off": "odtwarzacz {entity_name} zostanie wy\u0142\u0105czony", + "turned_on": "odtwarzacz {entity_name} zostanie w\u0142\u0105czony" } }, "state": { diff --git a/homeassistant/components/media_player/translations/tr.json b/homeassistant/components/media_player/translations/tr.json index 1f46c6a8bc7..f7b9be9da53 100644 --- a/homeassistant/components/media_player/translations/tr.json +++ b/homeassistant/components/media_player/translations/tr.json @@ -3,6 +3,10 @@ "condition_type": { "is_idle": "{entity_name} bo\u015fta", "is_off": "{entity_name} kapal\u0131" + }, + "trigger_type": { + "playing": "{entity_name} oynamaya ba\u015flar", + "turned_off": "{entity_name} kapat\u0131ld\u0131" } }, "state": { diff --git a/homeassistant/components/mysensors/translations/pl.json b/homeassistant/components/mysensors/translations/pl.json new file mode 100644 index 00000000000..fa67ffe4030 --- /dev/null +++ b/homeassistant/components/mysensors/translations/pl.json @@ -0,0 +1,79 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "duplicate_persistence_file": "Plik danych z sensora jest ju\u017c w u\u017cyciu", + "duplicate_topic": "Temat jest ju\u017c w u\u017cyciu", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_device": "Nieprawid\u0142owe urz\u0105dzenie", + "invalid_ip": "Nieprawid\u0142owy adres IP", + "invalid_persistence_file": "Nieprawid\u0142owy plik danych z sensora", + "invalid_port": "Nieprawid\u0142owy numer portu", + "invalid_publish_topic": "Nieprawid\u0142owy temat \"publish\"", + "invalid_serial": "Nieprawid\u0142owy port szeregowy", + "invalid_subscribe_topic": "Nieprawid\u0142owy temat \"subscribe\"", + "invalid_version": "Nieprawid\u0142owa wersja MySensors", + "not_a_number": "Prosz\u0119 wpisa\u0107 numer", + "port_out_of_range": "Numer portu musi by\u0107 pomi\u0119dzy 1 a 65535", + "same_topic": "Tematy \"subscribe\" i \"publish\" s\u0105 takie same", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "error": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "duplicate_persistence_file": "Plik danych z sensora jest ju\u017c w u\u017cyciu", + "duplicate_topic": "Temat jest ju\u017c w u\u017cyciu", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_device": "Nieprawid\u0142owe urz\u0105dzenie", + "invalid_ip": "Nieprawid\u0142owy adres IP", + "invalid_persistence_file": "Nieprawid\u0142owy plik danych z sensora", + "invalid_port": "Nieprawid\u0142owy numer portu", + "invalid_publish_topic": "Nieprawid\u0142owy temat \"publish\"", + "invalid_serial": "Nieprawid\u0142owy port szeregowy", + "invalid_subscribe_topic": "Nieprawid\u0142owy temat \"subscribe\"", + "invalid_version": "Nieprawid\u0142owa wersja MySensors", + "not_a_number": "Prosz\u0119 wpisa\u0107 numer", + "port_out_of_range": "Numer portu musi by\u0107 pomi\u0119dzy 1 a 65535", + "same_topic": "Tematy \"subscribe\" i \"publish\" s\u0105 takie same", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "gw_mqtt": { + "data": { + "persistence_file": "plik danych z sensora (pozostaw puste. aby wygenerowa\u0107 automatycznie)", + "retain": "flaga \"retain\" dla mqtt", + "topic_in_prefix": "prefix tematu wej\u015bciowego (topic_in_prefix)", + "topic_out_prefix": "prefix tematu wyj\u015bciowego (topic_out_prefix)", + "version": "Wersja MySensors" + }, + "description": "Konfiguracja bramki MQTT" + }, + "gw_serial": { + "data": { + "baud_rate": "szybko\u015b\u0107 transmisji (baud rate)", + "device": "Port szeregowy", + "persistence_file": "plik danych z sensora (pozostaw puste. aby wygenerowa\u0107 automatycznie)", + "version": "Wersja MySensors" + }, + "description": "Konfiguracja bramki szeregowej" + }, + "gw_tcp": { + "data": { + "device": "Adres IP bramki", + "persistence_file": "plik danych z sensora (pozostaw puste. aby wygenerowa\u0107 automatycznie)", + "tcp_port": "port", + "version": "Wersja MySensors" + }, + "description": "Konfiguracja bramki LAN" + }, + "user": { + "data": { + "gateway_type": "Typ bramki" + }, + "description": "Wybierz metod\u0119 po\u0142\u0105czenia z bramk\u0105" + } + } + }, + "title": "MySensors" +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/en.json b/homeassistant/components/philips_js/translations/en.json index ca580159dab..249fe5a892d 100644 --- a/homeassistant/components/philips_js/translations/en.json +++ b/homeassistant/components/philips_js/translations/en.json @@ -10,8 +10,8 @@ "step": { "user": { "data": { - "host": "Host", - "api_version": "API Version" + "api_version": "API Version", + "host": "Host" } } } @@ -21,4 +21,4 @@ "turn_on": "Device is requested to turn on" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/pl.json b/homeassistant/components/philips_js/translations/pl.json new file mode 100644 index 00000000000..27c088350c4 --- /dev/null +++ b/homeassistant/components/philips_js/translations/pl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "api_version": "Wersja API", + "host": "Nazwa hosta lub adres IP" + } + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "Urz\u0105dzenie zostanie poproszone o w\u0142\u0105czenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/ca.json b/homeassistant/components/powerwall/translations/ca.json index 4b176fff686..38a86f05d11 100644 --- a/homeassistant/components/powerwall/translations/ca.json +++ b/homeassistant/components/powerwall/translations/ca.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" + "already_configured": "El dispositiu ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat", "wrong_version": "El teu Powerwall utilitza una versi\u00f3 de programari no compatible. L'hauries d'actualitzar o informar d'aquest problema perqu\u00e8 sigui solucionat." }, @@ -12,8 +14,10 @@ "step": { "user": { "data": { - "ip_address": "Adre\u00e7a IP" + "ip_address": "Adre\u00e7a IP", + "password": "Contrasenya" }, + "description": "La contrasenya normalment s\u00f3n els darrers cinc car\u00e0cters del n\u00famero de s\u00e8rie de la pasarel\u00b7la de control i es pot trobar a l'aplicaci\u00f3 de Tesla; tamb\u00e9 pot consistir en els darrers 5 car\u00e0cters de la contrasenya que es troba a l'interior de la tapa de la pasarel\u00b7la de control 2.", "title": "Connexi\u00f3 amb el Powerwall" } } diff --git a/homeassistant/components/powerwall/translations/et.json b/homeassistant/components/powerwall/translations/et.json index b10dca9b08b..eaa70dc0a22 100644 --- a/homeassistant/components/powerwall/translations/et.json +++ b/homeassistant/components/powerwall/translations/et.json @@ -14,6 +14,7 @@ "data": { "ip_address": "IP aadress" }, + "description": "Parool on tavaliselt Backup Gateway seerianumbri viimased 5 t\u00e4hem\u00e4rki ja selle leiad Tesla rakendusest v\u00f5i Backup Gateway 2 luugilt leitud parooli viimased 5 m\u00e4rki.", "title": "Powerwalliga \u00fchendamine" } } diff --git a/homeassistant/components/powerwall/translations/no.json b/homeassistant/components/powerwall/translations/no.json index cdc04a006ad..13609f911a4 100644 --- a/homeassistant/components/powerwall/translations/no.json +++ b/homeassistant/components/powerwall/translations/no.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil", "wrong_version": "Powerwall bruker en programvareversjon som ikke st\u00f8ttes. Vennligst vurder \u00e5 oppgradere eller rapportere dette problemet, s\u00e5 det kan l\u00f8ses." }, @@ -12,8 +14,10 @@ "step": { "user": { "data": { - "ip_address": "IP adresse" + "ip_address": "IP adresse", + "password": "Passord" }, + "description": "Passordet er vanligvis de siste 5 tegnene i serienummeret for Backup Gateway, og finnes i Telsa-appen. eller de siste 5 tegnene i passordet som er funnet inne i d\u00f8ren til Backup Gateway 2.", "title": "Koble til powerwall" } } diff --git a/homeassistant/components/powerwall/translations/pl.json b/homeassistant/components/powerwall/translations/pl.json index dfd4fa21a37..059aab2c014 100644 --- a/homeassistant/components/powerwall/translations/pl.json +++ b/homeassistant/components/powerwall/translations/pl.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d", "wrong_version": "Powerwall u\u017cywa wersji oprogramowania, kt\u00f3ra nie jest obs\u0142ugiwana. Rozwa\u017c uaktualnienie lub zg\u0142oszenie tego problemu, aby mo\u017cna go by\u0142o rozwi\u0105za\u0107." }, @@ -12,8 +14,10 @@ "step": { "user": { "data": { - "ip_address": "Adres IP" + "ip_address": "Adres IP", + "password": "Has\u0142o" }, + "description": "Has\u0142o to zazwyczaj 5 ostatnich znak\u00f3w numeru seryjnego Backup Gateway i mo\u017cna je znale\u017a\u0107 w aplikacji Telsa; lub ostatnie 5 znak\u00f3w has\u0142a na wewn\u0119trznej stronie drzwiczek Backup Gateway 2.", "title": "Po\u0142\u0105czenie z Powerwall" } } diff --git a/homeassistant/components/powerwall/translations/ru.json b/homeassistant/components/powerwall/translations/ru.json index faabf2d0ede..2d8246cc14f 100644 --- a/homeassistant/components/powerwall/translations/ru.json +++ b/homeassistant/components/powerwall/translations/ru.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + "already_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.", + "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.", + "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.", "wrong_version": "\u0412\u0430\u0448 powerwall \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0432\u0435\u0440\u0441\u0438\u044e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u043e\u0433\u043e \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u0441\u043e\u043e\u0431\u0449\u0438\u0442\u0435 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0435, \u0447\u0442\u043e\u0431\u044b \u0435\u0435 \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0440\u0435\u0448\u0438\u0442\u044c." }, @@ -12,8 +14,10 @@ "step": { "user": { "data": { - "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441" + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, + "description": "\u041f\u0430\u0440\u043e\u043b\u044c \u043e\u0431\u044b\u0447\u043d\u043e \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0441\u043e\u0431\u043e\u0439 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0435 5 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432 \u0441\u0435\u0440\u0438\u0439\u043d\u043e\u0433\u043e \u043d\u043e\u043c\u0435\u0440\u0430 \u0434\u043b\u044f Backup Gateway, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438 \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 Telsa; \u0438\u043b\u0438 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0435 5 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432 \u043f\u0430\u0440\u043e\u043b\u044f, \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u043e\u0433\u043e \u0432\u043d\u0443\u0442\u0440\u0438 Backup Gateway 2.", "title": "Tesla Powerwall" } } diff --git a/homeassistant/components/powerwall/translations/zh-Hant.json b/homeassistant/components/powerwall/translations/zh-Hant.json index ec0d2e278b6..44e79e935cd 100644 --- a/homeassistant/components/powerwall/translations/zh-Hant.json +++ b/homeassistant/components/powerwall/translations/zh-Hant.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4", "wrong_version": "\u4e0d\u652f\u63f4\u60a8\u6240\u4f7f\u7528\u7684 Powerwall \u7248\u672c\u3002\u8acb\u8003\u616e\u9032\u884c\u5347\u7d1a\u6216\u56de\u5831\u6b64\u554f\u984c\u3001\u4ee5\u671f\u554f\u984c\u53ef\u4ee5\u7372\u5f97\u89e3\u6c7a\u3002" }, @@ -12,8 +14,10 @@ "step": { "user": { "data": { - "ip_address": "IP \u4f4d\u5740" + "ip_address": "IP \u4f4d\u5740", + "password": "\u5bc6\u78bc" }, + "description": "\u5bc6\u78bc\u901a\u5e38\u70ba\u81f3\u5c11\u5099\u4efd\u9598\u9053\u5668\u5e8f\u865f\u7684\u6700\u5f8c\u4e94\u78bc\uff0c\u4e26\u4e14\u80fd\u5920\u65bc Telsa App \u4e2d\n\u627e\u5230\u3002\u6216\u8005\u70ba\u5099\u4efd\u9598\u9053\u5668 2 \u9580\u5167\u5074\u627e\u5230\u7684\u5bc6\u78bc\u6700\u5f8c\u4e94\u78bc\u3002", "title": "\u9023\u7dda\u81f3 Powerwall" } } diff --git a/homeassistant/components/roku/translations/no.json b/homeassistant/components/roku/translations/no.json index dd4ce418141..e7dc663b8f8 100644 --- a/homeassistant/components/roku/translations/no.json +++ b/homeassistant/components/roku/translations/no.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "unknown": "Uventet feil" }, "error": { diff --git a/homeassistant/components/roku/translations/pl.json b/homeassistant/components/roku/translations/pl.json index 1d193acc0ff..1a570c64347 100644 --- a/homeassistant/components/roku/translations/pl.json +++ b/homeassistant/components/roku/translations/pl.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { diff --git a/homeassistant/components/shelly/translations/pl.json b/homeassistant/components/shelly/translations/pl.json index cd8ffac7138..a6ca567d91a 100644 --- a/homeassistant/components/shelly/translations/pl.json +++ b/homeassistant/components/shelly/translations/pl.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "confirm_discovery": { - "description": "Czy chcesz skonfigurowa\u0107 {model} ({host})?\n\nPrzed skonfigurowaniem urz\u0105dzenia zasilane bateryjnie nale\u017cy, wybudzi\u0107 naciskaj\u0105c przycisk na urz\u0105dzeniu." + "description": "Czy chcesz skonfigurowa\u0107 {model} ({host})?\n\nUrz\u0105dzenia zasilane bateryjnie, z ustawionym has\u0142em, nale\u017cy wybudzi\u0107 przed konfiguracj\u0105.\nUrz\u0105dzenia zasilane bateryjnie, bez ustawionego has\u0142a, zostan\u0105 dodane gdy urz\u0105dzenie si\u0119 wybudzi. Mo\u017cesz r\u0119cznie wybudzi\u0107 urz\u0105dzenie jego przyciskiem lub poczeka\u0107 na aktualizacj\u0119 danych z urz\u0105dzenia." }, "credentials": { "data": { diff --git a/homeassistant/components/shelly/translations/zh-Hant.json b/homeassistant/components/shelly/translations/zh-Hant.json index 8f315208135..abc0b627423 100644 --- a/homeassistant/components/shelly/translations/zh-Hant.json +++ b/homeassistant/components/shelly/translations/zh-Hant.json @@ -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\u88dd\u7f6e\u4e0a\u7684\u6309\u9215\u4ee5\u559a\u9192\u96fb\u6c60\u4f9b\u96fb\u88dd\u7f6e\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\n\u4e0d\u5177\u5bc6\u78bc\u4fdd\u8b77\u7684\u96fb\u6c60\u4f9b\u96fb\u88dd\u7f6e\uff0c\u53ef\u4ee5\u65bc\u559a\u9192\u5f8c\u65b0\u589e\u3002\u53ef\u4ee5\u4f7f\u7528\u88dd\u7f6e\u4e0a\u7684\u6309\u9215\u6216\u7b49\u5f85\u88dd\u7f6e\u4e0b\u4e00\u6b21\u8cc7\u6599\u66f4\u65b0\u6642\u9032\u884c\u624b\u52d5\u559a\u9192\u3002" }, "credentials": { "data": { diff --git a/homeassistant/components/tesla/translations/ca.json b/homeassistant/components/tesla/translations/ca.json index 4d0583af408..2a51c0297ae 100644 --- a/homeassistant/components/tesla/translations/ca.json +++ b/homeassistant/components/tesla/translations/ca.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "El compte ja ha estat configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, "error": { "already_configured": "El compte ja ha estat configurat", "cannot_connect": "Ha fallat la connexi\u00f3", diff --git a/homeassistant/components/tesla/translations/no.json b/homeassistant/components/tesla/translations/no.json index 36cceb97f9f..ce706640636 100644 --- a/homeassistant/components/tesla/translations/no.json +++ b/homeassistant/components/tesla/translations/no.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, "error": { "already_configured": "Kontoen er allerede konfigurert", "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/tesla/translations/pl.json b/homeassistant/components/tesla/translations/pl.json index dc4144d0f6a..7ec634cd56c 100644 --- a/homeassistant/components/tesla/translations/pl.json +++ b/homeassistant/components/tesla/translations/pl.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, "error": { "already_configured": "Konto jest ju\u017c skonfigurowane", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", diff --git a/homeassistant/components/tesla/translations/ru.json b/homeassistant/components/tesla/translations/ru.json index 8fe167d8631..7429b8ffa53 100644 --- a/homeassistant/components/tesla/translations/ru.json +++ b/homeassistant/components/tesla/translations/ru.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, "error": { "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "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/tesla/translations/zh-Hant.json b/homeassistant/components/tesla/translations/zh-Hant.json index 235c9036637..d9b7fd4ef79 100644 --- a/homeassistant/components/tesla/translations/zh-Hant.json +++ b/homeassistant/components/tesla/translations/zh-Hant.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, "error": { "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", From a67b598971eb5142751dd6642697257b82aa31aa Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 12 Feb 2021 02:35:29 +0100 Subject: [PATCH 374/796] Correct errors found on post merge review in philips_js (#46428) * Correct missed review changes * Adjust return value for device trigger * Drop cannot connect * Always assume there is a unique id * No need to yield * Update homeassistant/components/philips_js/media_player.py Co-authored-by: Martin Hjelmare * Move typing to init * Adjust typing instead of returning lambda * Explicity return None * Coerce into int Co-authored-by: Martin Hjelmare --- .../components/philips_js/__init__.py | 19 ++++++++----------- .../components/philips_js/config_flow.py | 6 +----- .../components/philips_js/device_trigger.py | 6 ++++-- .../components/philips_js/media_player.py | 10 ++++++---- tests/components/philips_js/conftest.py | 2 +- 5 files changed, 20 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index bfb99898ab2..11e84b6cd82 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -64,11 +64,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): class PluggableAction: """A pluggable action handler.""" - _actions: Dict[Any, AutomationActionType] = {} - def __init__(self, update: Callable[[], None]): """Initialize.""" self._update = update + self._actions: Dict[Any, AutomationActionType] = {} def __bool__(self): """Return if we have something attached.""" @@ -101,8 +100,6 @@ class PluggableAction: class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): """Coordinator to update data.""" - api: PhilipsTV - def __init__(self, hass, api: PhilipsTV) -> None: """Set up the coordinator.""" self.api = api @@ -113,19 +110,19 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): self.turn_on = PluggableAction(_update_listeners) - async def _async_update(): - try: - await self.hass.async_add_executor_job(self.api.update) - except ConnectionFailure: - pass - super().__init__( hass, LOGGER, name=DOMAIN, - update_method=_async_update, update_interval=timedelta(seconds=30), request_refresh_debouncer=Debouncer( hass, LOGGER, cooldown=2.0, immediate=False ), ) + + async def _async_update_data(self): + """Fetch the latest data from the source.""" + try: + await self.hass.async_add_executor_job(self.api.update) + except ConnectionFailure: + pass diff --git a/homeassistant/components/philips_js/config_flow.py b/homeassistant/components/philips_js/config_flow.py index 71bc34688b4..523918daa7c 100644 --- a/homeassistant/components/philips_js/config_flow.py +++ b/homeassistant/components/philips_js/config_flow.py @@ -5,7 +5,7 @@ from typing import Any, Dict, Optional, TypedDict from haphilipsjs import ConnectionFailure, PhilipsTV import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant import config_entries, core from homeassistant.const import CONF_API_VERSION, CONF_HOST from .const import DOMAIN # pylint:disable=unused-import @@ -84,7 +84,3 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } ) return self.async_show_form(step_id="user", data_schema=schema, errors=errors) - - -class CannotConnect(exceptions.HomeAssistantError): - """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/philips_js/device_trigger.py b/homeassistant/components/philips_js/device_trigger.py index ec1da0635db..2a60a1664bc 100644 --- a/homeassistant/components/philips_js/device_trigger.py +++ b/homeassistant/components/philips_js/device_trigger.py @@ -1,5 +1,5 @@ """Provides device automations for control of device.""" -from typing import List +from typing import List, Optional import voluptuous as vol @@ -43,7 +43,7 @@ async def async_attach_trigger( config: ConfigType, action: AutomationActionType, automation_info: dict, -) -> CALLBACK_TYPE: +) -> Optional[CALLBACK_TYPE]: """Attach a trigger.""" registry: DeviceRegistry = await async_get_registry(hass) if config[CONF_TYPE] == TRIGGER_TYPE_TURN_ON: @@ -63,3 +63,5 @@ async def async_attach_trigger( ) if coordinator: return coordinator.turn_on.async_attach(action, variables) + + return None diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 2e263ea2891..20ef6ed9c0f 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -57,7 +57,7 @@ SUPPORT_PHILIPS_JS = ( CONF_ON_ACTION = "turn_on_action" -DEFAULT_API_VERSION = "1" +DEFAULT_API_VERSION = 1 PREFIX_SEPARATOR = ": " PREFIX_SOURCE = "Input" @@ -72,7 +72,9 @@ PLATFORM_SCHEMA = vol.All( { vol.Required(CONF_HOST): cv.string, vol.Remove(CONF_NAME): cv.string, - vol.Optional(CONF_API_VERSION, default=DEFAULT_API_VERSION): cv.string, + vol.Optional(CONF_API_VERSION, default=DEFAULT_API_VERSION): vol.Coerce( + int + ), vol.Remove(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, } ), @@ -83,7 +85,7 @@ def _inverted(data): return {v: k for k, v in data.items()} -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 Philips TV platform.""" hass.async_create_task( hass.config_entries.flow.async_init( @@ -106,7 +108,7 @@ async def async_setup_entry( PhilipsTVMediaPlayer( coordinator, config_entry.data[CONF_SYSTEM], - config_entry.unique_id or config_entry.entry_id, + config_entry.unique_id, ) ] ) diff --git a/tests/components/philips_js/conftest.py b/tests/components/philips_js/conftest.py index 1f20cd821a5..549ad77fb06 100644 --- a/tests/components/philips_js/conftest.py +++ b/tests/components/philips_js/conftest.py @@ -50,7 +50,7 @@ async def mock_entity(hass, mock_device_reg, mock_config_entry): """Get standard player.""" assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - yield MOCK_ENTITY_ID + return MOCK_ENTITY_ID @fixture From 910c034613a43a30f3796637cbae681a049c5d44 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Fri, 12 Feb 2021 03:28:11 -0500 Subject: [PATCH 375/796] Use core constants for recollect_waste (#46416) --- homeassistant/components/recollect_waste/sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index d66c2aae0e4..66ced51b77f 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -6,7 +6,7 @@ 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, CONF_FRIENDLY_NAME +from homeassistant.const import ATTR_ATTRIBUTION, CONF_FRIENDLY_NAME, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.update_coordinator import ( @@ -25,8 +25,6 @@ DEFAULT_ATTRIBUTION = "Pickup data provided by ReCollect Waste" DEFAULT_NAME = "recollect_waste" DEFAULT_ICON = "mdi:trash-can-outline" -CONF_NAME = "name" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_PLACE_ID): cv.string, From 9b7c39d20ba8e22652695fc145f5a0a4cc041022 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 12 Feb 2021 10:58:20 +0100 Subject: [PATCH 376/796] Postponed evaluation of annotations in core (#46434) * Postponed evaluation of annotations in core * Remove unneeded future --- homeassistant/auth/__init__.py | 6 ++-- homeassistant/auth/mfa_modules/__init__.py | 4 ++- homeassistant/auth/providers/__init__.py | 4 ++- homeassistant/auth/providers/homeassistant.py | 4 ++- homeassistant/config_entries.py | 8 +++-- homeassistant/data_entry_flow.py | 4 ++- homeassistant/helpers/check_config.py | 4 ++- homeassistant/helpers/entity_platform.py | 12 ++++---- homeassistant/helpers/intent.py | 14 +++++---- homeassistant/helpers/restore_state.py | 8 +++-- homeassistant/helpers/service.py | 8 +++-- homeassistant/helpers/significant_change.py | 4 ++- homeassistant/helpers/sun.py | 4 ++- homeassistant/helpers/template.py | 8 +++-- homeassistant/loader.py | 30 ++++++++++--------- homeassistant/util/aiohttp.py | 4 +-- homeassistant/util/yaml/objects.py | 4 ++- script/translations/lokalise.py | 4 ++- 18 files changed, 84 insertions(+), 50 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 531e36ff0b3..7d6f94dda85 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -1,4 +1,6 @@ """Provide an authentication layer for Home Assistant.""" +from __future__ import annotations + import asyncio from collections import OrderedDict from datetime import timedelta @@ -36,7 +38,7 @@ async def auth_manager_from_config( hass: HomeAssistant, provider_configs: List[Dict[str, Any]], module_configs: List[Dict[str, Any]], -) -> "AuthManager": +) -> AuthManager: """Initialize an auth manager from config. CORE_CONFIG_SCHEMA will make sure do duplicated auth providers or @@ -76,7 +78,7 @@ async def auth_manager_from_config( class AuthManagerFlowManager(data_entry_flow.FlowManager): """Manage authentication flows.""" - def __init__(self, hass: HomeAssistant, auth_manager: "AuthManager"): + def __init__(self, hass: HomeAssistant, auth_manager: AuthManager): """Init auth manager flows.""" super().__init__(hass) self.auth_manager = auth_manager diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index 6e4b189bf74..f29f5f8fcc2 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -1,4 +1,6 @@ """Pluggable auth modules for Home Assistant.""" +from __future__ import annotations + import importlib import logging import types @@ -66,7 +68,7 @@ class MultiFactorAuthModule: """Return a voluptuous schema to define mfa auth module's input.""" raise NotImplementedError - async def async_setup_flow(self, user_id: str) -> "SetupFlow": + async def async_setup_flow(self, user_id: str) -> SetupFlow: """Return a data entry flow handler for setup module. Mfa module should extend SetupFlow diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index e766083edc3..2afe1333c6a 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -1,4 +1,6 @@ """Auth providers for Home Assistant.""" +from __future__ import annotations + import importlib import logging import types @@ -92,7 +94,7 @@ class AuthProvider: # Implement by extending class - async def async_login_flow(self, context: Optional[Dict]) -> "LoginFlow": + async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow: """Return the data flow for logging in with auth provider. Auth provider should extend LoginFlow and return an instance. diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index 70e2f5403cd..c66ffa7332e 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -1,4 +1,6 @@ """Home Assistant auth provider.""" +from __future__ import annotations + import asyncio import base64 from collections import OrderedDict @@ -31,7 +33,7 @@ CONFIG_SCHEMA = vol.All(AUTH_PROVIDER_SCHEMA, _disallow_id) @callback -def async_get_provider(hass: HomeAssistant) -> "HassAuthProvider": +def async_get_provider(hass: HomeAssistant) -> HassAuthProvider: """Get the provider.""" for prv in hass.auth.auth_providers: if prv.type == "homeassistant": diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 122b6f15e41..e38136e33ca 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1,4 +1,6 @@ """Manage config entries in Home Assistant.""" +from __future__ import annotations + import asyncio import functools import logging @@ -526,7 +528,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): async def async_create_flow( self, handler_key: Any, *, context: Optional[Dict] = None, data: Any = None - ) -> "ConfigFlow": + ) -> ConfigFlow: """Create a flow for specified handler. Handler key is the domain of the component that we want to set up. @@ -890,7 +892,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): @staticmethod @callback - def async_get_options_flow(config_entry: ConfigEntry) -> "OptionsFlow": + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: """Get the options flow for this handler.""" raise data_entry_flow.UnknownHandler @@ -1074,7 +1076,7 @@ class OptionsFlowManager(data_entry_flow.FlowManager): *, context: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None, - ) -> "OptionsFlow": + ) -> OptionsFlow: """Create an options flow for a config entry. Entry_id and flow.handler is the same thing to map entry with flow. diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index c5b67ff16e8..85609556217 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -1,4 +1,6 @@ """Classes to help gather user submissions.""" +from __future__ import annotations + import abc import asyncio from typing import Any, Dict, List, Optional, cast @@ -75,7 +77,7 @@ class FlowManager(abc.ABC): *, context: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None, - ) -> "FlowHandler": + ) -> FlowHandler: """Create a flow for specified handler. Handler key is the domain of the component that we want to set up. diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index 97445b8cee2..7b7b53d3c0f 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -1,4 +1,6 @@ """Helper to check the configuration file.""" +from __future__ import annotations + from collections import OrderedDict import logging import os @@ -49,7 +51,7 @@ class HomeAssistantConfig(OrderedDict): message: str, domain: Optional[str] = None, config: Optional[ConfigType] = None, - ) -> "HomeAssistantConfig": + ) -> HomeAssistantConfig: """Add a single error.""" self.errors.append(CheckConfigError(str(message), domain, config)) return self diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 26fec28c047..509508405a4 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -1,4 +1,6 @@ """Class to manage the entities for a single platform.""" +from __future__ import annotations + import asyncio from contextvars import ContextVar from datetime import datetime, timedelta @@ -245,7 +247,7 @@ class EntityPlatform: warn_task.cancel() def _schedule_add_entities( - self, new_entities: Iterable["Entity"], update_before_add: bool = False + self, new_entities: Iterable[Entity], update_before_add: bool = False ) -> None: """Schedule adding entities for a single platform, synchronously.""" run_callback_threadsafe( @@ -257,7 +259,7 @@ class EntityPlatform: @callback def _async_schedule_add_entities( - self, new_entities: Iterable["Entity"], update_before_add: bool = False + self, new_entities: Iterable[Entity], update_before_add: bool = False ) -> None: """Schedule adding entities for a single platform async.""" task = self.hass.async_create_task( @@ -268,7 +270,7 @@ class EntityPlatform: self._tasks.append(task) def add_entities( - self, new_entities: Iterable["Entity"], update_before_add: bool = False + self, new_entities: Iterable[Entity], update_before_add: bool = False ) -> None: """Add entities for a single platform.""" # That avoid deadlocks @@ -284,7 +286,7 @@ class EntityPlatform: ).result() async def async_add_entities( - self, new_entities: Iterable["Entity"], update_before_add: bool = False + self, new_entities: Iterable[Entity], update_before_add: bool = False ) -> None: """Add entities for a single platform async. @@ -547,7 +549,7 @@ class EntityPlatform: async def async_extract_from_service( self, service_call: ServiceCall, expand_group: bool = True - ) -> List["Entity"]: + ) -> List[Entity]: """Extract all known and available entities from a service call. Will return an empty list if entities specified but unknown. diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index f8c8b2c6d8c..1c5d56ccbd1 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -1,4 +1,6 @@ """Module to coordinate user intentions.""" +from __future__ import annotations + import logging import re from typing import Any, Callable, Dict, Iterable, Optional @@ -29,7 +31,7 @@ SPEECH_TYPE_SSML = "ssml" @callback @bind_hass -def async_register(hass: HomeAssistantType, handler: "IntentHandler") -> None: +def async_register(hass: HomeAssistantType, handler: IntentHandler) -> None: """Register an intent with Home Assistant.""" intents = hass.data.get(DATA_KEY) if intents is None: @@ -53,7 +55,7 @@ async def async_handle( slots: Optional[_SlotsType] = None, text_input: Optional[str] = None, context: Optional[Context] = None, -) -> "IntentResponse": +) -> IntentResponse: """Handle an intent.""" handler: IntentHandler = hass.data.get(DATA_KEY, {}).get(intent_type) @@ -131,7 +133,7 @@ class IntentHandler: platforms: Optional[Iterable[str]] = [] @callback - def async_can_handle(self, intent_obj: "Intent") -> bool: + def async_can_handle(self, intent_obj: Intent) -> bool: """Test if an intent can be handled.""" return self.platforms is None or intent_obj.platform in self.platforms @@ -152,7 +154,7 @@ class IntentHandler: return self._slot_schema(slots) # type: ignore - async def async_handle(self, intent_obj: "Intent") -> "IntentResponse": + async def async_handle(self, intent_obj: Intent) -> IntentResponse: """Handle the intent.""" raise NotImplementedError() @@ -195,7 +197,7 @@ class ServiceIntentHandler(IntentHandler): self.service = service self.speech = speech - async def async_handle(self, intent_obj: "Intent") -> "IntentResponse": + async def async_handle(self, intent_obj: Intent) -> IntentResponse: """Handle the hass intent.""" hass = intent_obj.hass slots = self.async_validate_slots(intent_obj.slots) @@ -236,7 +238,7 @@ class Intent: self.context = context @callback - def create_response(self) -> "IntentResponse": + def create_response(self) -> IntentResponse: """Create a response.""" return IntentResponse(self) diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 97069913c80..4f738887ce3 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -1,4 +1,6 @@ """Support for restoring entity states on startup.""" +from __future__ import annotations + import asyncio from datetime import datetime, timedelta import logging @@ -48,7 +50,7 @@ class StoredState: return {"state": self.state.as_dict(), "last_seen": self.last_seen} @classmethod - def from_dict(cls, json_dict: Dict) -> "StoredState": + def from_dict(cls, json_dict: Dict) -> StoredState: """Initialize a stored state from a dict.""" last_seen = json_dict["last_seen"] @@ -62,11 +64,11 @@ class RestoreStateData: """Helper class for managing the helper saved data.""" @classmethod - async def async_get_instance(cls, hass: HomeAssistant) -> "RestoreStateData": + async def async_get_instance(cls, hass: HomeAssistant) -> RestoreStateData: """Get the singleton instance of this data helper.""" @singleton(DATA_RESTORE_STATE_TASK) - async def load_instance(hass: HomeAssistant) -> "RestoreStateData": + async def load_instance(hass: HomeAssistant) -> RestoreStateData: """Get the singleton instance of this data helper.""" data = cls(hass) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index b2fa97d51cc..afc354dae56 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -1,4 +1,6 @@ """Service calling related helpers.""" +from __future__ import annotations + import asyncio import dataclasses from functools import partial, wraps @@ -230,10 +232,10 @@ def extract_entity_ids( @bind_hass async def async_extract_entities( hass: HomeAssistantType, - entities: Iterable["Entity"], + entities: Iterable[Entity], service_call: ha.ServiceCall, expand_group: bool = True, -) -> List["Entity"]: +) -> List[Entity]: """Extract a list of entity objects from a service call. Will convert group entity ids to the entity ids it represents. @@ -634,7 +636,7 @@ async def entity_service_call( async def _handle_entity_call( hass: HomeAssistantType, - entity: "Entity", + entity: Entity, func: Union[str, Callable[..., Any]], data: Union[Dict, ha.ServiceCall], context: ha.Context, diff --git a/homeassistant/helpers/significant_change.py b/homeassistant/helpers/significant_change.py index 694acfcf2bd..a7be57693ba 100644 --- a/homeassistant/helpers/significant_change.py +++ b/homeassistant/helpers/significant_change.py @@ -26,6 +26,8 @@ The following cases will never be passed to your function: - if either state is unknown/unavailable - state adding/removing """ +from __future__ import annotations + from types import MappingProxyType from typing import Any, Callable, Dict, Optional, Tuple, Union @@ -65,7 +67,7 @@ async def create_checker( hass: HomeAssistant, _domain: str, extra_significant_check: Optional[ExtraCheckTypeFunc] = None, -) -> "SignificantlyChangedChecker": +) -> SignificantlyChangedChecker: """Create a significantly changed checker for a domain.""" await _initialize(hass) return SignificantlyChangedChecker(hass, extra_significant_check) diff --git a/homeassistant/helpers/sun.py b/homeassistant/helpers/sun.py index a2385ba397c..2b82e19b8ce 100644 --- a/homeassistant/helpers/sun.py +++ b/homeassistant/helpers/sun.py @@ -1,4 +1,6 @@ """Helpers for sun events.""" +from __future__ import annotations + import datetime from typing import TYPE_CHECKING, Optional, Union @@ -17,7 +19,7 @@ DATA_LOCATION_CACHE = "astral_location_cache" @callback @bind_hass -def get_astral_location(hass: HomeAssistantType) -> "astral.Location": +def get_astral_location(hass: HomeAssistantType) -> astral.Location: """Get an astral location for the current Home Assistant configuration.""" from astral import Location # pylint: disable=import-outside-toplevel diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 9da0cbc09eb..200d678719a 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1,4 +1,6 @@ """Template helper methods for rendering strings with Home Assistant data.""" +from __future__ import annotations + from ast import literal_eval import asyncio import base64 @@ -155,7 +157,7 @@ class TupleWrapper(tuple, ResultWrapper): def __new__( cls, value: tuple, *, render_result: Optional[str] = None - ) -> "TupleWrapper": + ) -> TupleWrapper: """Create a new tuple class.""" return super().__new__(cls, tuple(value)) @@ -297,7 +299,7 @@ class Template: self._limited = None @property - def _env(self) -> "TemplateEnvironment": + def _env(self) -> TemplateEnvironment: if self.hass is None or self._limited: return _NO_HASS_ENV ret: Optional[TemplateEnvironment] = self.hass.data.get(_ENVIRONMENT) @@ -530,7 +532,7 @@ class Template: ) return value if error_value is _SENTINEL else error_value - def _ensure_compiled(self, limited: bool = False) -> "Template": + def _ensure_compiled(self, limited: bool = False) -> Template: """Bind a template to a specific hass instance.""" self.ensure_valid() diff --git a/homeassistant/loader.py b/homeassistant/loader.py index de02db524a7..152a3d88b80 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -4,6 +4,8 @@ The methods for loading Home Assistant integrations. This module has quite some complex parts. I have tried to add as much documentation as possible to keep it understandable. """ +from __future__ import annotations + import asyncio import functools as ft import importlib @@ -114,7 +116,7 @@ def manifest_from_legacy_module(domain: str, module: ModuleType) -> Manifest: async def _async_get_custom_components( hass: "HomeAssistant", -) -> Dict[str, "Integration"]: +) -> Dict[str, Integration]: """Return list of custom integrations.""" if hass.config.safe_mode: return {} @@ -155,7 +157,7 @@ async def _async_get_custom_components( async def async_get_custom_components( hass: "HomeAssistant", -) -> Dict[str, "Integration"]: +) -> Dict[str, Integration]: """Return cached list of custom integrations.""" reg_or_evt = hass.data.get(DATA_CUSTOM_COMPONENTS) @@ -175,7 +177,7 @@ async def async_get_custom_components( return cast(Dict[str, "Integration"], reg_or_evt) -async def async_get_config_flows(hass: "HomeAssistant") -> Set[str]: +async def async_get_config_flows(hass: HomeAssistant) -> Set[str]: """Return cached list of config flows.""" # pylint: disable=import-outside-toplevel from homeassistant.generated.config_flows import FLOWS @@ -195,7 +197,7 @@ async def async_get_config_flows(hass: "HomeAssistant") -> Set[str]: return flows -async def async_get_zeroconf(hass: "HomeAssistant") -> Dict[str, List[Dict[str, str]]]: +async def async_get_zeroconf(hass: HomeAssistant) -> Dict[str, List[Dict[str, str]]]: """Return cached list of zeroconf types.""" zeroconf: Dict[str, List[Dict[str, str]]] = ZEROCONF.copy() @@ -218,7 +220,7 @@ async def async_get_zeroconf(hass: "HomeAssistant") -> Dict[str, List[Dict[str, return zeroconf -async def async_get_dhcp(hass: "HomeAssistant") -> List[Dict[str, str]]: +async def async_get_dhcp(hass: HomeAssistant) -> List[Dict[str, str]]: """Return cached list of dhcp types.""" dhcp: List[Dict[str, str]] = DHCP.copy() @@ -232,7 +234,7 @@ async def async_get_dhcp(hass: "HomeAssistant") -> List[Dict[str, str]]: return dhcp -async def async_get_homekit(hass: "HomeAssistant") -> Dict[str, str]: +async def async_get_homekit(hass: HomeAssistant) -> Dict[str, str]: """Return cached list of homekit models.""" homekit: Dict[str, str] = HOMEKIT.copy() @@ -251,7 +253,7 @@ async def async_get_homekit(hass: "HomeAssistant") -> Dict[str, str]: return homekit -async def async_get_ssdp(hass: "HomeAssistant") -> Dict[str, List[Dict[str, str]]]: +async def async_get_ssdp(hass: HomeAssistant) -> Dict[str, List[Dict[str, str]]]: """Return cached list of ssdp mappings.""" ssdp: Dict[str, List[Dict[str, str]]] = SSDP.copy() @@ -266,7 +268,7 @@ async def async_get_ssdp(hass: "HomeAssistant") -> Dict[str, List[Dict[str, str] return ssdp -async def async_get_mqtt(hass: "HomeAssistant") -> Dict[str, List[str]]: +async def async_get_mqtt(hass: HomeAssistant) -> Dict[str, List[str]]: """Return cached list of MQTT mappings.""" mqtt: Dict[str, List[str]] = MQTT.copy() @@ -287,7 +289,7 @@ class Integration: @classmethod def resolve_from_root( cls, hass: "HomeAssistant", root_module: ModuleType, domain: str - ) -> "Optional[Integration]": + ) -> Optional[Integration]: """Resolve an integration from a root module.""" for base in root_module.__path__: # type: ignore manifest_path = pathlib.Path(base) / domain / "manifest.json" @@ -312,7 +314,7 @@ class Integration: @classmethod def resolve_legacy( cls, hass: "HomeAssistant", domain: str - ) -> "Optional[Integration]": + ) -> Optional[Integration]: """Resolve legacy component. Will create a stub manifest. @@ -671,7 +673,7 @@ class ModuleWrapper: class Components: """Helper to load components.""" - def __init__(self, hass: "HomeAssistant") -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize the Components class.""" self._hass = hass @@ -697,7 +699,7 @@ class Components: class Helpers: """Helper to load helpers.""" - def __init__(self, hass: "HomeAssistant") -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize the Helpers class.""" self._hass = hass @@ -758,7 +760,7 @@ async def _async_component_dependencies( return loaded -def _async_mount_config_dir(hass: "HomeAssistant") -> bool: +def _async_mount_config_dir(hass: HomeAssistant) -> bool: """Mount config dir in order to load custom_component. Async friendly but not a coroutine. @@ -771,7 +773,7 @@ def _async_mount_config_dir(hass: "HomeAssistant") -> bool: return True -def _lookup_path(hass: "HomeAssistant") -> List[str]: +def _lookup_path(hass: HomeAssistant) -> List[str]: """Return the lookup paths for legacy lookups.""" if hass.config.safe_mode: return [PACKAGE_BUILTIN] diff --git a/homeassistant/util/aiohttp.py b/homeassistant/util/aiohttp.py index 36cdc0f25e2..f2c761282bc 100644 --- a/homeassistant/util/aiohttp.py +++ b/homeassistant/util/aiohttp.py @@ -48,7 +48,7 @@ class MockRequest: self.mock_source = mock_source @property - def query(self) -> "MultiDict[str]": + def query(self) -> MultiDict[str]: """Return a dictionary with the query variables.""" return MultiDict(parse_qsl(self.query_string, keep_blank_values=True)) @@ -66,7 +66,7 @@ class MockRequest: """Return the body as JSON.""" return json.loads(self._text) - async def post(self) -> "MultiDict[str]": + async def post(self) -> MultiDict[str]: """Return POST parameters.""" return MultiDict(parse_qsl(self._text, keep_blank_values=True)) diff --git a/homeassistant/util/yaml/objects.py b/homeassistant/util/yaml/objects.py index 0e46820e0db..2d318a9def0 100644 --- a/homeassistant/util/yaml/objects.py +++ b/homeassistant/util/yaml/objects.py @@ -1,4 +1,6 @@ """Custom yaml object types.""" +from __future__ import annotations + from dataclasses import dataclass import yaml @@ -19,6 +21,6 @@ class Input: name: str @classmethod - def from_node(cls, loader: yaml.Loader, node: yaml.nodes.Node) -> "Input": + def from_node(cls, loader: yaml.Loader, node: yaml.nodes.Node) -> Input: """Create a new placeholder from a node.""" return cls(node.value) diff --git a/script/translations/lokalise.py b/script/translations/lokalise.py index 69860b49e45..a23291169f4 100644 --- a/script/translations/lokalise.py +++ b/script/translations/lokalise.py @@ -1,4 +1,6 @@ """API for Lokalise.""" +from __future__ import annotations + from pprint import pprint import requests @@ -6,7 +8,7 @@ import requests from .util import get_lokalise_token -def get_api(project_id, debug=False) -> "Lokalise": +def get_api(project_id, debug=False) -> Lokalise: """Get Lokalise API.""" return Lokalise(project_id, get_lokalise_token(), debug) From 0d2f5cf7ed8f5d636386bb0be930ed90320c5188 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Fri, 12 Feb 2021 05:42:34 -0500 Subject: [PATCH 377/796] Use core constants for plugwise (#46414) --- homeassistant/components/plugwise/config_flow.py | 8 +------- homeassistant/components/plugwise/const.py | 1 - homeassistant/components/plugwise/gateway.py | 1 - 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index e0d22627737..247e0802eae 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -19,12 +19,7 @@ from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import DiscoveryInfoType -from .const import ( # pylint:disable=unused-import - DEFAULT_PORT, - DEFAULT_SCAN_INTERVAL, - DOMAIN, - ZEROCONF_MAP, -) +from .const import DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN, ZEROCONF_MAP _LOGGER = logging.getLogger(__name__) @@ -152,7 +147,6 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle the initial step.""" - # PLACEHOLDER USB vs Gateway Logic return await self.async_step_user_gateway() diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index c6ef43af602..fb8911d6fc7 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -22,7 +22,6 @@ DEFAULT_SCAN_INTERVAL = {"power": 10, "stretch": 60, "thermostat": 60} DEFAULT_TIMEOUT = 60 # Configuration directives -CONF_BASE = "base" CONF_GAS = "gas" CONF_MAX_TEMP = "max_temp" CONF_MIN_TEMP = "min_temp" diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index a0bf23986bd..c14395319d4 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -203,7 +203,6 @@ class SmileGateway(CoordinatorEntity): @property def device_info(self) -> Dict[str, any]: """Return the device information.""" - device_information = { "identifiers": {(DOMAIN, self._dev_id)}, "name": self._entity_name, From 190a9f66cb1d46b384d1211d83e65e7626413fe7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 12 Feb 2021 11:43:44 +0100 Subject: [PATCH 378/796] Improve MQTT timeout print (#46398) --- homeassistant/components/mqtt/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 58c99aa7c00..29f7e24da00 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -929,7 +929,9 @@ class MQTT: try: await asyncio.wait_for(self._pending_operations[mid].wait(), TIMEOUT_ACK) except asyncio.TimeoutError: - _LOGGER.error("Timed out waiting for mid %s", mid) + _LOGGER.warning( + "No ACK from MQTT server in %s seconds (mid: %s)", TIMEOUT_ACK, mid + ) finally: del self._pending_operations[mid] From 74f5f8976f289e5e40fbc70d7517ed61c186174d Mon Sep 17 00:00:00 2001 From: tkdrob Date: Fri, 12 Feb 2021 06:15:30 -0500 Subject: [PATCH 379/796] Use core constants for rpi_gpio (#46442) --- homeassistant/components/rpi_gpio/binary_sensor.py | 1 - homeassistant/components/rpi_gpio/cover.py | 4 +--- homeassistant/components/rpi_gpio/switch.py | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/rpi_gpio/binary_sensor.py b/homeassistant/components/rpi_gpio/binary_sensor.py index a2461f52db9..36d7ae50f32 100644 --- a/homeassistant/components/rpi_gpio/binary_sensor.py +++ b/homeassistant/components/rpi_gpio/binary_sensor.py @@ -32,7 +32,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Raspberry PI GPIO devices.""" - setup_reload_service(hass, DOMAIN, PLATFORMS) pull_mode = config.get(CONF_PULL_MODE) diff --git a/homeassistant/components/rpi_gpio/cover.py b/homeassistant/components/rpi_gpio/cover.py index 032796fe55b..15eae3b4b07 100644 --- a/homeassistant/components/rpi_gpio/cover.py +++ b/homeassistant/components/rpi_gpio/cover.py @@ -5,13 +5,12 @@ import voluptuous as vol from homeassistant.components import rpi_gpio from homeassistant.components.cover import PLATFORM_SCHEMA, CoverEntity -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_COVERS, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import setup_reload_service from . import DOMAIN, PLATFORMS -CONF_COVERS = "covers" CONF_RELAY_PIN = "relay_pin" CONF_RELAY_TIME = "relay_time" CONF_STATE_PIN = "state_pin" @@ -49,7 +48,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RPi cover platform.""" - setup_reload_service(hass, DOMAIN, PLATFORMS) relay_time = config.get(CONF_RELAY_TIME) diff --git a/homeassistant/components/rpi_gpio/switch.py b/homeassistant/components/rpi_gpio/switch.py index d556d8f0354..3fba7b4b2cb 100644 --- a/homeassistant/components/rpi_gpio/switch.py +++ b/homeassistant/components/rpi_gpio/switch.py @@ -28,7 +28,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Raspberry PI GPIO devices.""" - setup_reload_service(hass, DOMAIN, PLATFORMS) invert_logic = config.get(CONF_INVERT_LOGIC) From b7dd9bf58f465237cffe1c75355eb09ad2cbc951 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 12 Feb 2021 13:29:11 +0100 Subject: [PATCH 380/796] Enhance platform discovery for zwave_js (#46355) --- .../components/zwave_js/discovery.py | 380 +++++++++++++----- 1 file changed, 282 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index be6d9b698d4..62d45a5ae5e 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -14,12 +14,14 @@ from homeassistant.core import callback class ZwaveDiscoveryInfo: """Info discovered from (primary) ZWave Value to create entity.""" - node: ZwaveNode # node to which the value(s) belongs - primary_value: ZwaveValue # the value object itself for primary value - platform: str # the home assistant platform for which an entity should be created - platform_hint: Optional[ - str - ] = "" # hint for the platform about this discovered entity + # node to which the value(s) belongs + node: ZwaveNode + # the value object itself for primary value + primary_value: ZwaveValue + # the home assistant platform for which an entity should be created + platform: str + # hint for the platform about this discovered entity + platform_hint: Optional[str] = "" @property def value_id(self) -> str: @@ -28,24 +30,14 @@ class ZwaveDiscoveryInfo: @dataclass -class ZWaveDiscoverySchema: - """Z-Wave discovery schema. +class ZWaveValueDiscoverySchema: + """Z-Wave Value discovery schema. - The (primary) value for an entity must match these conditions. + The Z-Wave Value must match these conditions. Use the Z-Wave specifications to find out the values for these parameters: https://github.com/zwave-js/node-zwave-js/tree/master/specs """ - # specify the hass platform for which this scheme applies (e.g. light, sensor) - platform: str - # [optional] hint for platform - hint: Optional[str] = None - # [optional] the node's basic device class must match ANY of these values - device_class_basic: Optional[Set[str]] = None - # [optional] the node's generic device class must match ANY of these values - device_class_generic: Optional[Set[str]] = None - # [optional] the node's specific device class must match ANY of these values - device_class_specific: Optional[Set[str]] = None # [optional] the value's command class must match ANY of these values command_class: Optional[Set[int]] = None # [optional] the value's endpoint must match ANY of these values @@ -56,9 +48,121 @@ class ZWaveDiscoverySchema: type: Optional[Set[str]] = None +@dataclass +class ZWaveDiscoverySchema: + """Z-Wave discovery schema. + + The Z-Wave node and it's (primary) value for an entity must match these conditions. + Use the Z-Wave specifications to find out the values for these parameters: + https://github.com/zwave-js/node-zwave-js/tree/master/specs + """ + + # specify the hass platform for which this scheme applies (e.g. light, sensor) + platform: str + # primary value belonging to this discovery scheme + primary_value: ZWaveValueDiscoverySchema + # [optional] hint for platform + hint: Optional[str] = None + # [optional] the node's manufacturer_id must match ANY of these values + manufacturer_id: Optional[Set[int]] = None + # [optional] the node's product_id must match ANY of these values + product_id: Optional[Set[int]] = None + # [optional] the node's product_type must match ANY of these values + product_type: Optional[Set[int]] = None + # [optional] the node's firmware_version must match ANY of these values + firmware_version: Optional[Set[str]] = None + # [optional] the node's basic device class must match ANY of these values + device_class_basic: Optional[Set[str]] = None + # [optional] the node's generic device class must match ANY of these values + device_class_generic: Optional[Set[str]] = None + # [optional] the node's specific device class must match ANY of these values + device_class_specific: Optional[Set[str]] = None + # [optional] additional values that ALL need to be present on the node for this scheme to pass + required_values: Optional[Set[ZWaveValueDiscoverySchema]] = None + # [optional] bool to specify if this primary value may be discovered by multiple platforms + allow_multi: bool = False + + # For device class mapping see: # https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/deviceClasses.json DISCOVERY_SCHEMAS = [ + # ====== START OF DEVICE SPECIFIC MAPPING SCHEMAS ======= + # Honeywell 39358 In-Wall Fan Control using switch multilevel CC + ZWaveDiscoverySchema( + platform="fan", + manufacturer_id={0x0039}, + product_id={0x3131}, + product_type={0x4944}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, + ), + ), + # GE/Jasco fan controllers using switch multilevel CC + ZWaveDiscoverySchema( + platform="fan", + manufacturer_id={0x0063}, + product_id={0x3034, 0x3131, 0x3138}, + product_type={0x4944}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, + ), + ), + # Leviton ZW4SF fan controllers using switch multilevel CC + ZWaveDiscoverySchema( + platform="fan", + manufacturer_id={0x001D}, + product_id={0x0002}, + product_type={0x0038}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, + ), + ), + # Fibaro Shutter Fibaro FGS222 + ZWaveDiscoverySchema( + platform="cover", + hint="fibaro_fgs222", + manufacturer_id={0x010F}, + product_id={0x1000}, + product_type={0x0302}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, + ), + ), + # Qubino flush shutter + ZWaveDiscoverySchema( + platform="cover", + hint="fibaro_fgs222", + manufacturer_id={0x0159}, + product_id={0x0052}, + product_type={0x0003}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, + ), + ), + # Graber/Bali/Spring Fashion Covers + ZWaveDiscoverySchema( + platform="cover", + hint="fibaro_fgs222", + manufacturer_id={0x026E}, + product_id={0x5A31}, + product_type={0x4353}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, + ), + ), + # ====== START OF GENERIC MAPPING SCHEMAS ======= # locks ZWaveDiscoverySchema( platform="lock", @@ -69,12 +173,14 @@ DISCOVERY_SCHEMAS = [ "Secure Keypad Door Lock", "Secure Lockbox", }, - command_class={ - CommandClass.LOCK, - CommandClass.DOOR_LOCK, - }, - property={"currentMode", "locked"}, - type={"number", "boolean"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={ + CommandClass.LOCK, + CommandClass.DOOR_LOCK, + }, + property={"currentMode", "locked"}, + type={"number", "boolean"}, + ), ), # door lock door status ZWaveDiscoverySchema( @@ -87,12 +193,14 @@ DISCOVERY_SCHEMAS = [ "Secure Keypad Door Lock", "Secure Lockbox", }, - command_class={ - CommandClass.LOCK, - CommandClass.DOOR_LOCK, - }, - property={"doorStatus"}, - type={"any"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={ + CommandClass.LOCK, + CommandClass.DOOR_LOCK, + }, + property={"doorStatus"}, + type={"any"}, + ), ), # climate ZWaveDiscoverySchema( @@ -102,10 +210,14 @@ DISCOVERY_SCHEMAS = [ "Setback Thermostat", "Thermostat General", "Thermostat General V2", + "General Thermostat", + "General Thermostat V2", }, - command_class={CommandClass.THERMOSTAT_MODE}, - property={"mode"}, - type={"number"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.THERMOSTAT_MODE}, + property={"mode"}, + type={"number"}, + ), ), # climate # setpoint thermostats @@ -115,9 +227,11 @@ DISCOVERY_SCHEMAS = [ device_class_specific={ "Setpoint Thermostat", }, - command_class={CommandClass.THERMOSTAT_SETPOINT}, - property={"setpoint"}, - type={"number"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.THERMOSTAT_SETPOINT}, + property={"setpoint"}, + type={"number"}, + ), ), # lights # primary value is the currentValue (brightness) @@ -132,85 +246,104 @@ DISCOVERY_SCHEMAS = [ "Multilevel Scene Switch", "Unused", }, - command_class={CommandClass.SWITCH_MULTILEVEL}, - property={"currentValue"}, - type={"number"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, + ), ), # binary sensors ZWaveDiscoverySchema( platform="binary_sensor", hint="boolean", - command_class={ - CommandClass.SENSOR_BINARY, - CommandClass.BATTERY, - CommandClass.SENSOR_ALARM, - }, - type={"boolean"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={ + CommandClass.SENSOR_BINARY, + CommandClass.BATTERY, + CommandClass.SENSOR_ALARM, + }, + type={"boolean"}, + ), ), ZWaveDiscoverySchema( platform="binary_sensor", hint="notification", - command_class={ - CommandClass.NOTIFICATION, - }, - type={"number"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={ + CommandClass.NOTIFICATION, + }, + type={"number"}, + ), + allow_multi=True, ), # generic text sensors ZWaveDiscoverySchema( platform="sensor", hint="string_sensor", - command_class={ - CommandClass.SENSOR_ALARM, - CommandClass.INDICATOR, - }, - type={"string"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={ + CommandClass.SENSOR_ALARM, + CommandClass.INDICATOR, + }, + type={"string"}, + ), ), # generic numeric sensors ZWaveDiscoverySchema( platform="sensor", hint="numeric_sensor", - command_class={ - CommandClass.SENSOR_MULTILEVEL, - CommandClass.SENSOR_ALARM, - CommandClass.INDICATOR, - CommandClass.BATTERY, - }, - type={"number"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={ + CommandClass.SENSOR_MULTILEVEL, + CommandClass.SENSOR_ALARM, + CommandClass.INDICATOR, + CommandClass.BATTERY, + }, + type={"number"}, + ), ), # numeric sensors for Meter CC ZWaveDiscoverySchema( platform="sensor", hint="numeric_sensor", - command_class={ - CommandClass.METER, - }, - type={"number"}, - property={"value"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={ + CommandClass.METER, + }, + type={"number"}, + property={"value"}, + ), ), # special list sensors (Notification CC) ZWaveDiscoverySchema( platform="sensor", hint="list_sensor", - command_class={ - CommandClass.NOTIFICATION, - }, - type={"number"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={ + CommandClass.NOTIFICATION, + }, + type={"number"}, + ), + allow_multi=True, ), # sensor for basic CC ZWaveDiscoverySchema( platform="sensor", hint="numeric_sensor", - command_class={ - CommandClass.BASIC, - }, - type={"number"}, - property={"currentValue"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={ + CommandClass.BASIC, + }, + type={"number"}, + property={"currentValue"}, + ), ), # binary switches ZWaveDiscoverySchema( platform="switch", - command_class={CommandClass.SWITCH_BINARY}, - property={"currentValue"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_BINARY}, property={"currentValue"} + ), ), # cover ZWaveDiscoverySchema( @@ -223,9 +356,11 @@ DISCOVERY_SCHEMAS = [ "Motor Control Class C", "Multiposition Motor", }, - command_class={CommandClass.SWITCH_MULTILEVEL}, - property={"currentValue"}, - type={"number"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, + ), ), # fan ZWaveDiscoverySchema( @@ -233,9 +368,11 @@ DISCOVERY_SCHEMAS = [ hint="fan", device_class_generic={"Multilevel Switch"}, device_class_specific={"Fan Switch"}, - command_class={CommandClass.SWITCH_MULTILEVEL}, - property={"currentValue"}, - type={"number"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, + ), ), ] @@ -243,8 +380,33 @@ DISCOVERY_SCHEMAS = [ @callback def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None, None]: """Run discovery on ZWave node and return matching (primary) values.""" + # pylint: disable=too-many-nested-blocks for value in node.values.values(): for schema in DISCOVERY_SCHEMAS: + # check manufacturer_id + if ( + schema.manufacturer_id is not None + and value.node.manufacturer_id not in schema.manufacturer_id + ): + continue + # check product_id + if ( + schema.product_id is not None + and value.node.product_id not in schema.product_id + ): + continue + # check product_type + if ( + schema.product_type is not None + and value.node.product_type not in schema.product_type + ): + continue + # check firmware_version + if ( + schema.firmware_version is not None + and value.node.firmware_version not in schema.firmware_version + ): + continue # check device_class_basic if ( schema.device_class_basic is not None @@ -263,21 +425,19 @@ def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None and value.node.device_class.specific not in schema.device_class_specific ): continue - # check command_class - if ( - schema.command_class is not None - and value.command_class not in schema.command_class - ): - continue - # check endpoint - if schema.endpoint is not None and value.endpoint not in schema.endpoint: - continue - # check property - if schema.property is not None and value.property_ not in schema.property: - continue - # check metadata_type - if schema.type is not None and value.metadata.type not in schema.type: + # check primary value + if not check_value(value, schema.primary_value): continue + # check additional required values + if schema.required_values is not None: + required_values_present = True + for val_scheme in schema.required_values: + for val in node.values.values(): + if not check_value(val, val_scheme): + required_values_present = False + break + if not required_values_present: + continue # all checks passed, this value belongs to an entity yield ZwaveDiscoveryInfo( node=value.node, @@ -285,3 +445,27 @@ def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None platform=schema.platform, platform_hint=schema.hint, ) + if not schema.allow_multi: + # break out of loop, this value may not be discovered by other schemas/platforms + break + + +@callback +def check_value(value: ZwaveValue, schema: ZWaveValueDiscoverySchema) -> bool: + """Check if value matches scheme.""" + # check command_class + if ( + schema.command_class is not None + and value.command_class not in schema.command_class + ): + return False + # check endpoint + if schema.endpoint is not None and value.endpoint not in schema.endpoint: + return False + # check property + if schema.property is not None and value.property_ not in schema.property: + return False + # check metadata_type + if schema.type is not None and value.metadata.type not in schema.type: + return False + return True From 479ff92acbc9ec4db09b7a300d2beb67f00ab746 Mon Sep 17 00:00:00 2001 From: Robert Kingston Date: Fri, 12 Feb 2021 23:31:36 +1100 Subject: [PATCH 381/796] Fix cmus remote disconnections (#40284) Co-authored-by: Martin Hjelmare --- homeassistant/components/cmus/media_player.py | 77 +++++++++++++------ 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/cmus/media_player.py b/homeassistant/components/cmus/media_player.py index 73a55fda8e3..49c10ab92a5 100644 --- a/homeassistant/components/cmus/media_player.py +++ b/homeassistant/components/cmus/media_player.py @@ -64,37 +64,66 @@ def setup_platform(hass, config, add_entities, discover_info=None): port = config[CONF_PORT] name = config[CONF_NAME] - try: - cmus_remote = CmusDevice(host, password, port, name) - except exceptions.InvalidPassword: - _LOGGER.error("The provided password was rejected by cmus") - return False - add_entities([cmus_remote], True) + cmus_remote = CmusRemote(server=host, port=port, password=password) + cmus_remote.connect() + + if cmus_remote.cmus is None: + return + + add_entities([CmusDevice(device=cmus_remote, name=name, server=host)], True) + + +class CmusRemote: + """Representation of a cmus connection.""" + + def __init__(self, server, port, password): + """Initialize the cmus remote.""" + + self._server = server + self._port = port + self._password = password + self.cmus = None + + def connect(self): + """Connect to the cmus server.""" + + try: + self.cmus = remote.PyCmus( + server=self._server, port=self._port, password=self._password + ) + except exceptions.InvalidPassword: + _LOGGER.error("The provided password was rejected by cmus") class CmusDevice(MediaPlayerEntity): """Representation of a running cmus.""" # pylint: disable=no-member - def __init__(self, server, password, port, name): + def __init__(self, device, name, server): """Initialize the CMUS device.""" + self._remote = device if server: - self.cmus = remote.PyCmus(server=server, password=password, port=port) auto_name = f"cmus-{server}" else: - self.cmus = remote.PyCmus() auto_name = "cmus-local" self._name = name or auto_name self.status = {} def update(self): """Get the latest data and update the state.""" - status = self.cmus.get_status_dict() - if not status: - _LOGGER.warning("Received no status from cmus") + try: + status = self._remote.cmus.get_status_dict() + except BrokenPipeError: + self._remote.connect() + except exceptions.ConfigurationError: + _LOGGER.warning("A configuration error occurred") + self._remote.connect() else: self.status = status + return + + _LOGGER.warning("Received no status from cmus") @property def name(self): @@ -168,15 +197,15 @@ class CmusDevice(MediaPlayerEntity): def turn_off(self): """Service to send the CMUS the command to stop playing.""" - self.cmus.player_stop() + self._remote.cmus.player_stop() def turn_on(self): """Service to send the CMUS the command to start playing.""" - self.cmus.player_play() + self._remote.cmus.player_play() def set_volume_level(self, volume): """Set volume level, range 0..1.""" - self.cmus.set_volume(int(volume * 100)) + self._remote.cmus.set_volume(int(volume * 100)) def volume_up(self): """Set the volume up.""" @@ -188,7 +217,7 @@ class CmusDevice(MediaPlayerEntity): current_volume = left if current_volume <= 100: - self.cmus.set_volume(int(current_volume) + 5) + self._remote.cmus.set_volume(int(current_volume) + 5) def volume_down(self): """Set the volume down.""" @@ -200,12 +229,12 @@ class CmusDevice(MediaPlayerEntity): current_volume = left if current_volume <= 100: - self.cmus.set_volume(int(current_volume) - 5) + self._remote.cmus.set_volume(int(current_volume) - 5) def play_media(self, media_type, media_id, **kwargs): """Send the play command.""" if media_type in [MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST]: - self.cmus.player_play_file(media_id) + self._remote.cmus.player_play_file(media_id) else: _LOGGER.error( "Invalid media type %s. Only %s and %s are supported", @@ -216,24 +245,24 @@ class CmusDevice(MediaPlayerEntity): def media_pause(self): """Send the pause command.""" - self.cmus.player_pause() + self._remote.cmus.player_pause() def media_next_track(self): """Send next track command.""" - self.cmus.player_next() + self._remote.cmus.player_next() def media_previous_track(self): """Send next track command.""" - self.cmus.player_prev() + self._remote.cmus.player_prev() def media_seek(self, position): """Send seek command.""" - self.cmus.seek(position) + self._remote.cmus.seek(position) def media_play(self): """Send the play command.""" - self.cmus.player_play() + self._remote.cmus.player_play() def media_stop(self): """Send the stop command.""" - self.cmus.stop() + self._remote.cmus.stop() From a8beae3c517210823d2ef901196a885d321d5e6e Mon Sep 17 00:00:00 2001 From: David Dix Date: Fri, 12 Feb 2021 13:58:01 +0000 Subject: [PATCH 382/796] Add apple tv remote delay command (#46301) Co-authored-by: Martin Hjelmare --- homeassistant/components/apple_tv/remote.py | 23 ++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/apple_tv/remote.py b/homeassistant/components/apple_tv/remote.py index a76c4c6a208..3d88bddcbc9 100644 --- a/homeassistant/components/apple_tv/remote.py +++ b/homeassistant/components/apple_tv/remote.py @@ -1,8 +1,14 @@ """Remote control support for Apple TV.""" +import asyncio import logging -from homeassistant.components.remote import RemoteEntity +from homeassistant.components.remote import ( + ATTR_DELAY_SECS, + ATTR_NUM_REPEATS, + DEFAULT_DELAY_SECS, + RemoteEntity, +) from homeassistant.const import CONF_NAME from . import AppleTVEntity @@ -43,12 +49,19 @@ class AppleTVRemote(AppleTVEntity, RemoteEntity): async def async_send_command(self, command, **kwargs): """Send a command to one device.""" + num_repeats = kwargs[ATTR_NUM_REPEATS] + delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) + if not self.is_on: _LOGGER.error("Unable to send commands, not connected to %s", self._name) return - for single_command in command: - if not hasattr(self.atv.remote_control, single_command): - continue + for _ in range(num_repeats): + for single_command in command: + attr_value = getattr(self.atv.remote_control, single_command, None) + if not attr_value: + raise ValueError("Command not found. Exiting sequence") - await getattr(self.atv.remote_control, single_command)() + _LOGGER.info("Sending command %s", single_command) + await attr_value() + await asyncio.sleep(delay) From c3b460920e76a45ae50f2883545e5c8359d15217 Mon Sep 17 00:00:00 2001 From: Christophe Painchaud Date: Fri, 12 Feb 2021 15:58:59 +0100 Subject: [PATCH 383/796] Enable TCP KEEPALIVE to RFLink for dead connection detection (#46438) RFLink compoment when used over TCP protocol suffers a major issue : it doesn't know when connection is timeout or lost because there is no keepalive mechanism so it can stay disconnected forever. I wrote a small patch for the underlying 'python-rflink' library which will enable TCP KEEPPAlive. On HASSIO side it will just add an optional argument in yml file which will propagate to python-rflink caller. --- homeassistant/components/rflink/__init__.py | 26 +++++++++++++++++++ homeassistant/components/rflink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index 116b2464213..6bedea3ecd9 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -44,12 +44,14 @@ CONF_IGNORE_DEVICES = "ignore_devices" CONF_RECONNECT_INTERVAL = "reconnect_interval" CONF_SIGNAL_REPETITIONS = "signal_repetitions" CONF_WAIT_FOR_ACK = "wait_for_ack" +CONF_KEEPALIVE_IDLE = "tcp_keepalive_idle_timer" DATA_DEVICE_REGISTER = "rflink_device_register" DATA_ENTITY_LOOKUP = "rflink_entity_lookup" DATA_ENTITY_GROUP_LOOKUP = "rflink_entity_group_only_lookup" DEFAULT_RECONNECT_INTERVAL = 10 DEFAULT_SIGNAL_REPETITIONS = 1 +DEFAULT_TCP_KEEPALIVE_IDLE_TIMER = 3600 CONNECTION_TIMEOUT = 10 EVENT_BUTTON_PRESSED = "button_pressed" @@ -85,6 +87,9 @@ CONFIG_SCHEMA = vol.Schema( vol.Required(CONF_PORT): vol.Any(cv.port, cv.string), vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_WAIT_FOR_ACK, default=True): cv.boolean, + vol.Optional( + CONF_KEEPALIVE_IDLE, default=DEFAULT_TCP_KEEPALIVE_IDLE_TIMER + ): int, vol.Optional( CONF_RECONNECT_INTERVAL, default=DEFAULT_RECONNECT_INTERVAL ): int, @@ -199,6 +204,26 @@ async def async_setup(hass, config): # TCP port when host configured, otherwise serial port port = config[DOMAIN][CONF_PORT] + # TCP KEEPALIVE will be enabled if value > 0 + keepalive_idle_timer = config[DOMAIN][CONF_KEEPALIVE_IDLE] + if keepalive_idle_timer < 0: + _LOGGER.error( + "A bogus TCP Keepalive IDLE timer was provided (%d secs), " + "default value will be used. " + "Recommended values: 60-3600 (seconds)", + keepalive_idle_timer, + ) + keepalive_idle_timer = DEFAULT_TCP_KEEPALIVE_IDLE_TIMER + elif keepalive_idle_timer == 0: + keepalive_idle_timer = None + elif keepalive_idle_timer <= 30: + _LOGGER.warning( + "A very short TCP Keepalive IDLE timer was provided (%d secs), " + "and may produce unexpected disconnections from RFlink device." + " Recommended values: 60-3600 (seconds)", + keepalive_idle_timer, + ) + @callback def reconnect(exc=None): """Schedule reconnect after connection has been unexpectedly lost.""" @@ -223,6 +248,7 @@ async def async_setup(hass, config): connection = create_rflink_connection( port=port, host=host, + keepalive=keepalive_idle_timer, event_callback=event_callback, disconnect_callback=reconnect, loop=hass.loop, diff --git a/homeassistant/components/rflink/manifest.json b/homeassistant/components/rflink/manifest.json index cdcfe97c219..f3854a139f2 100644 --- a/homeassistant/components/rflink/manifest.json +++ b/homeassistant/components/rflink/manifest.json @@ -2,6 +2,6 @@ "domain": "rflink", "name": "RFLink", "documentation": "https://www.home-assistant.io/integrations/rflink", - "requirements": ["rflink==0.0.55"], + "requirements": ["rflink==0.0.58"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 4dbee8b0a65..f13cd23f0c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1943,7 +1943,7 @@ restrictedpython==5.1 rfk101py==0.0.1 # homeassistant.components.rflink -rflink==0.0.55 +rflink==0.0.58 # homeassistant.components.ring ring_doorbell==0.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 88e8dffff1c..95642b36891 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -989,7 +989,7 @@ regenmaschine==3.0.0 restrictedpython==5.1 # homeassistant.components.rflink -rflink==0.0.55 +rflink==0.0.58 # homeassistant.components.ring ring_doorbell==0.6.2 From f929aa222fb513ebb75d830361661d4cb104801c Mon Sep 17 00:00:00 2001 From: tkdrob Date: Fri, 12 Feb 2021 10:09:36 -0500 Subject: [PATCH 384/796] Use core constants for roomba (#46441) --- homeassistant/components/roomba/__init__.py | 13 ++----------- homeassistant/components/roomba/config_flow.py | 4 +--- homeassistant/components/roomba/const.py | 2 -- tests/components/roomba/test_config_flow.py | 9 ++------- 4 files changed, 5 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py index 63deead7307..658c230c3a7 100644 --- a/homeassistant/components/roomba/__init__.py +++ b/homeassistant/components/roomba/__init__.py @@ -6,18 +6,9 @@ import async_timeout from roombapy import Roomba, RoombaConnectionError from homeassistant import exceptions -from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.const import CONF_DELAY, CONF_HOST, CONF_NAME, CONF_PASSWORD -from .const import ( - BLID, - COMPONENTS, - CONF_BLID, - CONF_CONTINUOUS, - CONF_DELAY, - CONF_NAME, - DOMAIN, - ROOMBA_SESSION, -) +from .const import BLID, COMPONENTS, CONF_BLID, CONF_CONTINUOUS, DOMAIN, ROOMBA_SESSION _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index 4b0b76b44c9..b1f2b290a54 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -9,15 +9,13 @@ import voluptuous as vol from homeassistant import config_entries, core from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS -from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.const import CONF_DELAY, CONF_HOST, CONF_NAME, CONF_PASSWORD from homeassistant.core import callback from . import CannotConnect, async_connect_or_timeout, async_disconnect_or_timeout from .const import ( CONF_BLID, CONF_CONTINUOUS, - CONF_DELAY, - CONF_NAME, DEFAULT_CONTINUOUS, DEFAULT_DELAY, ROOMBA_SESSION, diff --git a/homeassistant/components/roomba/const.py b/homeassistant/components/roomba/const.py index 06684e63bdc..2ffb34eb7c8 100644 --- a/homeassistant/components/roomba/const.py +++ b/homeassistant/components/roomba/const.py @@ -3,8 +3,6 @@ DOMAIN = "roomba" COMPONENTS = ["sensor", "binary_sensor", "vacuum"] CONF_CERT = "certificate" CONF_CONTINUOUS = "continuous" -CONF_DELAY = "delay" -CONF_NAME = "name" CONF_BLID = "blid" DEFAULT_CERT = "/etc/ssl/certs/ca-certificates.crt" DEFAULT_CONTINUOUS = True diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index bf8e674950f..b597717e4a8 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -6,13 +6,8 @@ from roombapy.roomba import RoombaInfo from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS -from homeassistant.components.roomba.const import ( - CONF_BLID, - CONF_CONTINUOUS, - CONF_DELAY, - DOMAIN, -) -from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.components.roomba.const import CONF_BLID, CONF_CONTINUOUS, DOMAIN +from homeassistant.const import CONF_DELAY, CONF_HOST, CONF_PASSWORD from tests.common import MockConfigEntry From 8418489345b5053e53b59635c594b8e17a4d25df Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 12 Feb 2021 16:33:18 +0100 Subject: [PATCH 385/796] Allow Modbus "old" config or discovery_info as configuration (#46445) --- .coveragerc | 1 + homeassistant/components/modbus/__init__.py | 192 +--------------- homeassistant/components/modbus/climate.py | 2 +- homeassistant/components/modbus/const.py | 4 +- homeassistant/components/modbus/cover.py | 2 +- homeassistant/components/modbus/modbus.py | 229 ++++++++++++++++++++ homeassistant/components/modbus/switch.py | 2 +- 7 files changed, 239 insertions(+), 193 deletions(-) create mode 100644 homeassistant/components/modbus/modbus.py diff --git a/.coveragerc b/.coveragerc index c17f3d1057d..6043d3d45f0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -563,6 +563,7 @@ omit = homeassistant/components/mochad/* homeassistant/components/modbus/climate.py homeassistant/components/modbus/cover.py + homeassistant/components/modbus/modbus.py homeassistant/components/modbus/switch.py homeassistant/components/modbus/sensor.py homeassistant/components/modem_callerid/sensor.py diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index c2a1a76840c..fe6811a35d9 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -1,9 +1,6 @@ """Support for Modbus.""" import logging -import threading -from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient -from pymodbus.transaction import ModbusRtuFramer import voluptuous as vol from homeassistant.components.cover import ( @@ -24,10 +21,8 @@ from homeassistant.const import ( CONF_STRUCTURE, CONF_TIMEOUT, CONF_TYPE, - EVENT_HOMEASSISTANT_STOP, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform from .const import ( ATTR_ADDRESS, @@ -71,9 +66,8 @@ from .const import ( DEFAULT_STRUCTURE_PREFIX, DEFAULT_TEMP_UNIT, MODBUS_DOMAIN as DOMAIN, - SERVICE_WRITE_COIL, - SERVICE_WRITE_REGISTER, ) +from .modbus import modbus_setup _LOGGER = logging.getLogger(__name__) @@ -193,186 +187,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up Modbus component.""" - hass.data[DOMAIN] = hub_collect = {} - - for conf_hub in config[DOMAIN]: - hub_collect[conf_hub[CONF_NAME]] = ModbusHub(conf_hub) - - # load platforms - for component, conf_key in ( - ("climate", CONF_CLIMATES), - ("cover", CONF_COVERS), - ): - if conf_key in conf_hub: - load_platform(hass, component, DOMAIN, conf_hub, config) - - def stop_modbus(event): - """Stop Modbus service.""" - for client in hub_collect.values(): - client.close() - - def write_register(service): - """Write Modbus registers.""" - unit = int(float(service.data[ATTR_UNIT])) - address = int(float(service.data[ATTR_ADDRESS])) - value = service.data[ATTR_VALUE] - client_name = service.data[ATTR_HUB] - if isinstance(value, list): - hub_collect[client_name].write_registers( - unit, address, [int(float(i)) for i in value] - ) - else: - hub_collect[client_name].write_register(unit, address, int(float(value))) - - def write_coil(service): - """Write Modbus coil.""" - unit = service.data[ATTR_UNIT] - address = service.data[ATTR_ADDRESS] - state = service.data[ATTR_STATE] - client_name = service.data[ATTR_HUB] - hub_collect[client_name].write_coil(unit, address, state) - - # do not wait for EVENT_HOMEASSISTANT_START, activate pymodbus now - for client in hub_collect.values(): - client.setup() - - # register function to gracefully stop modbus - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus) - - # Register services for modbus - hass.services.register( - DOMAIN, - SERVICE_WRITE_REGISTER, - write_register, - schema=SERVICE_WRITE_REGISTER_SCHEMA, + return modbus_setup( + hass, config, SERVICE_WRITE_REGISTER_SCHEMA, SERVICE_WRITE_COIL_SCHEMA ) - hass.services.register( - DOMAIN, SERVICE_WRITE_COIL, write_coil, schema=SERVICE_WRITE_COIL_SCHEMA - ) - return True - - -class ModbusHub: - """Thread safe wrapper class for pymodbus.""" - - def __init__(self, client_config): - """Initialize the Modbus hub.""" - # generic configuration - self._client = None - self._lock = threading.Lock() - self._config_name = client_config[CONF_NAME] - self._config_type = client_config[CONF_TYPE] - self._config_port = client_config[CONF_PORT] - self._config_timeout = client_config[CONF_TIMEOUT] - self._config_delay = 0 - - if self._config_type == "serial": - # serial configuration - self._config_method = client_config[CONF_METHOD] - self._config_baudrate = client_config[CONF_BAUDRATE] - self._config_stopbits = client_config[CONF_STOPBITS] - self._config_bytesize = client_config[CONF_BYTESIZE] - self._config_parity = client_config[CONF_PARITY] - else: - # network configuration - self._config_host = client_config[CONF_HOST] - self._config_delay = client_config[CONF_DELAY] - if self._config_delay > 0: - _LOGGER.warning( - "Parameter delay is accepted but not used in this version" - ) - - @property - def name(self): - """Return the name of this hub.""" - return self._config_name - - def setup(self): - """Set up pymodbus client.""" - if self._config_type == "serial": - self._client = ModbusSerialClient( - method=self._config_method, - port=self._config_port, - baudrate=self._config_baudrate, - stopbits=self._config_stopbits, - bytesize=self._config_bytesize, - parity=self._config_parity, - timeout=self._config_timeout, - retry_on_empty=True, - ) - elif self._config_type == "rtuovertcp": - self._client = ModbusTcpClient( - host=self._config_host, - port=self._config_port, - framer=ModbusRtuFramer, - timeout=self._config_timeout, - ) - elif self._config_type == "tcp": - self._client = ModbusTcpClient( - host=self._config_host, - port=self._config_port, - timeout=self._config_timeout, - ) - elif self._config_type == "udp": - self._client = ModbusUdpClient( - host=self._config_host, - port=self._config_port, - timeout=self._config_timeout, - ) - else: - assert False - - # Connect device - self.connect() - - def close(self): - """Disconnect client.""" - with self._lock: - self._client.close() - - def connect(self): - """Connect client.""" - with self._lock: - self._client.connect() - - def read_coils(self, unit, address, count): - """Read coils.""" - with self._lock: - kwargs = {"unit": unit} if unit else {} - return self._client.read_coils(address, count, **kwargs) - - def read_discrete_inputs(self, unit, address, count): - """Read discrete inputs.""" - with self._lock: - kwargs = {"unit": unit} if unit else {} - return self._client.read_discrete_inputs(address, count, **kwargs) - - def read_input_registers(self, unit, address, count): - """Read input registers.""" - with self._lock: - kwargs = {"unit": unit} if unit else {} - return self._client.read_input_registers(address, count, **kwargs) - - def read_holding_registers(self, unit, address, count): - """Read holding registers.""" - with self._lock: - kwargs = {"unit": unit} if unit else {} - return self._client.read_holding_registers(address, count, **kwargs) - - def write_coil(self, unit, address, value): - """Write coil.""" - with self._lock: - kwargs = {"unit": unit} if unit else {} - self._client.write_coil(address, value, **kwargs) - - def write_register(self, unit, address, value): - """Write register.""" - with self._lock: - kwargs = {"unit": unit} if unit else {} - self._client.write_register(address, value, **kwargs) - - def write_registers(self, unit, address, values): - """Write registers.""" - with self._lock: - kwargs = {"unit": unit} if unit else {} - self._client.write_registers(address, values, **kwargs) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index c1b7b2e6bf4..45cfbf5eb57 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -29,7 +29,6 @@ from homeassistant.helpers.typing import ( HomeAssistantType, ) -from . import ModbusHub from .const import ( CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, @@ -49,6 +48,7 @@ from .const import ( DEFAULT_STRUCT_FORMAT, MODBUS_DOMAIN, ) +from .modbus import ModbusHub _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 5e304165e42..d3193cc004c 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -16,7 +16,7 @@ CONF_PRECISION = "precision" CONF_COILS = "coils" # integration names -DEFAULT_HUB = "default" +DEFAULT_HUB = "modbus_hub" MODBUS_DOMAIN = "modbus" # data types @@ -67,6 +67,7 @@ CONF_VERIFY_STATE = "verify_state" # climate.py CONF_CLIMATES = "climates" +CONF_CLIMATE = "climate" CONF_TARGET_TEMP = "target_temp_register" CONF_CURRENT_TEMP = "current_temp_register" CONF_CURRENT_TEMP_REGISTER_TYPE = "current_temp_register_type" @@ -80,6 +81,7 @@ DEFAULT_STRUCTURE_PREFIX = ">f" DEFAULT_TEMP_UNIT = "C" # cover.py +CONF_COVER = "cover" CONF_STATE_OPEN = "state_open" CONF_STATE_CLOSED = "state_closed" CONF_STATE_OPENING = "state_opening" diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index 709c772564a..09a465a2cdd 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -21,7 +21,6 @@ from homeassistant.helpers.typing import ( HomeAssistantType, ) -from . import ModbusHub from .const import ( CALL_TYPE_COIL, CALL_TYPE_REGISTER_HOLDING, @@ -35,6 +34,7 @@ from .const import ( CONF_STATUS_REGISTER_TYPE, MODBUS_DOMAIN, ) +from .modbus import ModbusHub async def async_setup_platform( diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py new file mode 100644 index 00000000000..21c6caa6fcc --- /dev/null +++ b/homeassistant/components/modbus/modbus.py @@ -0,0 +1,229 @@ +"""Support for Modbus.""" +import logging +import threading + +from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient +from pymodbus.transaction import ModbusRtuFramer + +from homeassistant.const import ( + ATTR_STATE, + CONF_COVERS, + CONF_DELAY, + CONF_HOST, + CONF_METHOD, + CONF_NAME, + CONF_PORT, + CONF_TIMEOUT, + CONF_TYPE, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.helpers.discovery import load_platform + +from .const import ( + ATTR_ADDRESS, + ATTR_HUB, + ATTR_UNIT, + ATTR_VALUE, + CONF_BAUDRATE, + CONF_BYTESIZE, + CONF_CLIMATE, + CONF_CLIMATES, + CONF_COVER, + CONF_PARITY, + CONF_STOPBITS, + MODBUS_DOMAIN as DOMAIN, + SERVICE_WRITE_COIL, + SERVICE_WRITE_REGISTER, +) + +_LOGGER = logging.getLogger(__name__) + + +def modbus_setup( + hass, config, service_write_register_schema, service_write_coil_schema +): + """Set up Modbus component.""" + hass.data[DOMAIN] = hub_collect = {} + + for conf_hub in config[DOMAIN]: + hub_collect[conf_hub[CONF_NAME]] = ModbusHub(conf_hub) + + # modbus needs to be activated before components are loaded + # to avoid a racing problem + hub_collect[conf_hub[CONF_NAME]].setup() + + # load platforms + for component, conf_key in ( + (CONF_CLIMATE, CONF_CLIMATES), + (CONF_COVER, CONF_COVERS), + ): + if conf_key in conf_hub: + load_platform(hass, component, DOMAIN, conf_hub, config) + + def stop_modbus(event): + """Stop Modbus service.""" + for client in hub_collect.values(): + client.close() + + def write_register(service): + """Write Modbus registers.""" + unit = int(float(service.data[ATTR_UNIT])) + address = int(float(service.data[ATTR_ADDRESS])) + value = service.data[ATTR_VALUE] + client_name = service.data[ATTR_HUB] + if isinstance(value, list): + hub_collect[client_name].write_registers( + unit, address, [int(float(i)) for i in value] + ) + else: + hub_collect[client_name].write_register(unit, address, int(float(value))) + + def write_coil(service): + """Write Modbus coil.""" + unit = service.data[ATTR_UNIT] + address = service.data[ATTR_ADDRESS] + state = service.data[ATTR_STATE] + client_name = service.data[ATTR_HUB] + hub_collect[client_name].write_coil(unit, address, state) + + # register function to gracefully stop modbus + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus) + + # Register services for modbus + hass.services.register( + DOMAIN, + SERVICE_WRITE_REGISTER, + write_register, + schema=service_write_register_schema, + ) + hass.services.register( + DOMAIN, SERVICE_WRITE_COIL, write_coil, schema=service_write_coil_schema + ) + return True + + +class ModbusHub: + """Thread safe wrapper class for pymodbus.""" + + def __init__(self, client_config): + """Initialize the Modbus hub.""" + + # generic configuration + self._client = None + self._lock = threading.Lock() + self._config_name = client_config[CONF_NAME] + self._config_type = client_config[CONF_TYPE] + self._config_port = client_config[CONF_PORT] + self._config_timeout = client_config[CONF_TIMEOUT] + self._config_delay = 0 + + if self._config_type == "serial": + # serial configuration + self._config_method = client_config[CONF_METHOD] + self._config_baudrate = client_config[CONF_BAUDRATE] + self._config_stopbits = client_config[CONF_STOPBITS] + self._config_bytesize = client_config[CONF_BYTESIZE] + self._config_parity = client_config[CONF_PARITY] + else: + # network configuration + self._config_host = client_config[CONF_HOST] + self._config_delay = client_config[CONF_DELAY] + if self._config_delay > 0: + _LOGGER.warning( + "Parameter delay is accepted but not used in this version" + ) + + @property + def name(self): + """Return the name of this hub.""" + return self._config_name + + def setup(self): + """Set up pymodbus client.""" + if self._config_type == "serial": + self._client = ModbusSerialClient( + method=self._config_method, + port=self._config_port, + baudrate=self._config_baudrate, + stopbits=self._config_stopbits, + bytesize=self._config_bytesize, + parity=self._config_parity, + timeout=self._config_timeout, + retry_on_empty=True, + ) + elif self._config_type == "rtuovertcp": + self._client = ModbusTcpClient( + host=self._config_host, + port=self._config_port, + framer=ModbusRtuFramer, + timeout=self._config_timeout, + ) + elif self._config_type == "tcp": + self._client = ModbusTcpClient( + host=self._config_host, + port=self._config_port, + timeout=self._config_timeout, + ) + elif self._config_type == "udp": + self._client = ModbusUdpClient( + host=self._config_host, + port=self._config_port, + timeout=self._config_timeout, + ) + else: + assert False + + # Connect device + self.connect() + + def close(self): + """Disconnect client.""" + with self._lock: + self._client.close() + + def connect(self): + """Connect client.""" + with self._lock: + self._client.connect() + + def read_coils(self, unit, address, count): + """Read coils.""" + with self._lock: + kwargs = {"unit": unit} if unit else {} + return self._client.read_coils(address, count, **kwargs) + + def read_discrete_inputs(self, unit, address, count): + """Read discrete inputs.""" + with self._lock: + kwargs = {"unit": unit} if unit else {} + return self._client.read_discrete_inputs(address, count, **kwargs) + + def read_input_registers(self, unit, address, count): + """Read input registers.""" + with self._lock: + kwargs = {"unit": unit} if unit else {} + return self._client.read_input_registers(address, count, **kwargs) + + def read_holding_registers(self, unit, address, count): + """Read holding registers.""" + with self._lock: + kwargs = {"unit": unit} if unit else {} + return self._client.read_holding_registers(address, count, **kwargs) + + def write_coil(self, unit, address, value): + """Write coil.""" + with self._lock: + kwargs = {"unit": unit} if unit else {} + self._client.write_coil(address, value, **kwargs) + + def write_register(self, unit, address, value): + """Write register.""" + with self._lock: + kwargs = {"unit": unit} if unit else {} + self._client.write_register(address, value, **kwargs) + + def write_registers(self, unit, address, values): + """Write registers.""" + with self._lock: + kwargs = {"unit": unit} if unit else {} + self._client.write_registers(address, values, **kwargs) diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index b1b07fb5a55..36fbef08428 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -20,7 +20,6 @@ from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import ModbusHub from .const import ( CALL_TYPE_COIL, CALL_TYPE_REGISTER_HOLDING, @@ -37,6 +36,7 @@ from .const import ( DEFAULT_HUB, MODBUS_DOMAIN, ) +from .modbus import ModbusHub _LOGGER = logging.getLogger(__name__) From f1714dd541c863a0a42844582ba00e80b2ed40fb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 12 Feb 2021 17:00:35 +0100 Subject: [PATCH 386/796] Make some Area and EntityRegistry member functions callbacks (#46433) --- homeassistant/components/config/area_registry.py | 2 +- homeassistant/helpers/area_registry.py | 11 +++++------ homeassistant/helpers/entity_registry.py | 6 ++++-- tests/helpers/test_area_registry.py | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/config/area_registry.py b/homeassistant/components/config/area_registry.py index 81daf35339e..f40ed7834e3 100644 --- a/homeassistant/components/config/area_registry.py +++ b/homeassistant/components/config/area_registry.py @@ -90,7 +90,7 @@ async def websocket_delete_area(hass, connection, msg): registry = await async_get_registry(hass) try: - await registry.async_delete(msg["area_id"]) + registry.async_delete(msg["area_id"]) except KeyError: connection.send_message( websocket_api.error_message( diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index a41e748d1ad..562e832cc19 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -1,11 +1,11 @@ """Provide a way to connect devices to one physical location.""" -from asyncio import gather from collections import OrderedDict from typing import Container, Dict, Iterable, List, MutableMapping, Optional, cast import attr from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.loader import bind_hass from homeassistant.util import slugify @@ -72,12 +72,11 @@ class AreaRegistry: ) return area - async def async_delete(self, area_id: str) -> None: + @callback + def async_delete(self, area_id: str) -> None: """Delete area.""" - device_registry, entity_registry = await gather( - self.hass.helpers.device_registry.async_get_registry(), - self.hass.helpers.entity_registry.async_get_registry(), - ) + device_registry = dr.async_get(self.hass) + entity_registry = er.async_get(self.hass) device_registry.async_clear_area_id(area_id) entity_registry.async_clear_area_id(area_id) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 51985f7bae4..418c3f90304 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -35,6 +35,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import Event, callback, split_entity_id, valid_entity_id +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.loader import bind_hass from homeassistant.util import slugify @@ -313,7 +314,8 @@ class EntityRegistry: ) self.async_schedule_save() - async def async_device_modified(self, event: Event) -> None: + @callback + def async_device_modified(self, event: Event) -> None: """Handle the removal or update of a device. Remove entities from the registry that are associated to a device when @@ -333,7 +335,7 @@ class EntityRegistry: if event.data["action"] != "update": return - device_registry = await self.hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(self.hass) device = device_registry.async_get(event.data["device_id"]) # The device may be deleted already if the event handling is late diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py index 2b06202c862..0bfa5e597d2 100644 --- a/tests/helpers/test_area_registry.py +++ b/tests/helpers/test_area_registry.py @@ -81,7 +81,7 @@ async def test_delete_area(hass, registry, update_events): """Make sure that we can delete an area.""" area = registry.async_create("mock") - await registry.async_delete(area.id) + registry.async_delete(area.id) assert not registry.areas From 362a1cd9bd5d17bb8e3caec84f3272e425fc9b49 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 12 Feb 2021 17:59:08 +0100 Subject: [PATCH 387/796] Upgrade sentry-sdk to 0.20.1 (#46456) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index da5294b9258..090d19eb2fc 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,6 +3,6 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==0.19.5"], + "requirements": ["sentry-sdk==0.20.1"], "codeowners": ["@dcramer", "@frenck"] } diff --git a/requirements_all.txt b/requirements_all.txt index f13cd23f0c7..f6716f52511 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2013,7 +2013,7 @@ sense-hat==2.2.0 sense_energy==0.8.1 # homeassistant.components.sentry -sentry-sdk==0.19.5 +sentry-sdk==0.20.1 # homeassistant.components.sharkiq sharkiqpy==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 95642b36891..748fd695137 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1023,7 +1023,7 @@ scapy==2.4.4 sense_energy==0.8.1 # homeassistant.components.sentry -sentry-sdk==0.19.5 +sentry-sdk==0.20.1 # homeassistant.components.sharkiq sharkiqpy==0.1.8 From 061d9c5293d1e049ee3d407aafa718891822f2dc Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 12 Feb 2021 18:11:35 +0100 Subject: [PATCH 388/796] Bump brother library to version 0.2.1 (#46421) --- homeassistant/components/brother/__init__.py | 42 ++++++++----------- .../components/brother/config_flow.py | 10 +++-- homeassistant/components/brother/const.py | 4 ++ .../components/brother/manifest.json | 2 +- homeassistant/components/brother/sensor.py | 3 +- homeassistant/components/brother/utils.py | 30 +++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 63 insertions(+), 32 deletions(-) create mode 100644 homeassistant/components/brother/utils.py diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py index 8c5cdb2d7ed..d7cf906a87c 100644 --- a/homeassistant/components/brother/__init__.py +++ b/homeassistant/components/brother/__init__.py @@ -6,12 +6,13 @@ import logging from brother import Brother, SnmpError, UnsupportedModel from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_TYPE, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_HOST, CONF_TYPE from homeassistant.core import Config, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DOMAIN +from .const import DATA_CONFIG_ENTRY, DOMAIN, SNMP +from .utils import get_snmp_engine PLATFORMS = ["sensor"] @@ -30,15 +31,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): host = entry.data[CONF_HOST] kind = entry.data[CONF_TYPE] - coordinator = BrotherDataUpdateCoordinator(hass, host=host, kind=kind) + snmp_engine = get_snmp_engine(hass) + + coordinator = BrotherDataUpdateCoordinator( + hass, host=host, kind=kind, snmp_engine=snmp_engine + ) await coordinator.async_refresh() if not coordinator.last_update_success: - coordinator.shutdown() raise ConfigEntryNotReady hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = coordinator + hass.data[DOMAIN].setdefault(DATA_CONFIG_ENTRY, {}) + hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = coordinator + hass.data[DOMAIN][SNMP] = snmp_engine for component in PLATFORMS: hass.async_create_task( @@ -59,7 +65,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) ) if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id).shutdown() + hass.data[DOMAIN][DATA_CONFIG_ENTRY].pop(entry.entry_id) + if not hass.data[DOMAIN][DATA_CONFIG_ENTRY]: + hass.data[DOMAIN].pop(SNMP) + hass.data[DOMAIN].pop(DATA_CONFIG_ENTRY) return unload_ok @@ -67,12 +76,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): class BrotherDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching Brother data from the printer.""" - def __init__(self, hass, host, kind): + def __init__(self, hass, host, kind, snmp_engine): """Initialize.""" - self.brother = Brother(host, kind=kind) - self._unsub_stop = hass.bus.async_listen( - EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop - ) + self.brother = Brother(host, kind=kind, snmp_engine=snmp_engine) super().__init__( hass, @@ -83,22 +89,8 @@ class BrotherDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self): """Update data via library.""" - # Race condition on shutdown. Stop all the fetches. - if self._unsub_stop is None: - return None - try: await self.brother.async_update() except (ConnectionError, SnmpError, UnsupportedModel) as error: raise UpdateFailed(error) from error return self.brother.data - - def shutdown(self): - """Shutdown the Brother coordinator.""" - self._unsub_stop() - self._unsub_stop = None - self.brother.shutdown() - - def _handle_ha_stop(self, _): - """Handle Home Assistant stopping.""" - self.shutdown() diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index aa9d7ce53a3..6a9d2ca6746 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -9,6 +9,7 @@ from homeassistant import config_entries, exceptions from homeassistant.const import CONF_HOST, CONF_TYPE from .const import DOMAIN, PRINTER_TYPES # pylint:disable=unused-import +from .utils import get_snmp_engine DATA_SCHEMA = vol.Schema( { @@ -48,9 +49,10 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not host_valid(user_input[CONF_HOST]): raise InvalidHost() - brother = Brother(user_input[CONF_HOST]) + snmp_engine = get_snmp_engine(self.hass) + + brother = Brother(user_input[CONF_HOST], snmp_engine=snmp_engine) await brother.async_update() - brother.shutdown() await self.async_set_unique_id(brother.serial.lower()) self._abort_if_unique_id_configured() @@ -83,7 +85,9 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # Hostname is format: brother.local. self.host = discovery_info["hostname"].rstrip(".") - self.brother = Brother(self.host) + snmp_engine = get_snmp_engine(self.hass) + + self.brother = Brother(self.host, snmp_engine=snmp_engine) try: await self.brother.async_update() except (ConnectionError, SnmpError, UnsupportedModel): diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index 5aecde16327..5ae459c79aa 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -41,12 +41,16 @@ ATTR_YELLOW_DRUM_REMAINING_PAGES = "yellow_drum_remaining_pages" ATTR_YELLOW_INK_REMAINING = "yellow_ink_remaining" ATTR_YELLOW_TONER_REMAINING = "yellow_toner_remaining" +DATA_CONFIG_ENTRY = "config_entry" + DOMAIN = "brother" UNIT_PAGES = "p" PRINTER_TYPES = ["laser", "ink"] +SNMP = "snmp" + SENSOR_TYPES = { ATTR_STATUS: { ATTR_ICON: "mdi:printer", diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index 52286cd2c68..15828e5f05a 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -3,7 +3,7 @@ "name": "Brother Printer", "documentation": "https://www.home-assistant.io/integrations/brother", "codeowners": ["@bieniu"], - "requirements": ["brother==0.2.0"], + "requirements": ["brother==0.2.1"], "zeroconf": [{ "type": "_printer._tcp.local.", "name": "brother*" }], "config_flow": true, "quality_scale": "platinum" diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 40e2deae67d..a379d9b4154 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -24,6 +24,7 @@ from .const import ( ATTR_YELLOW_DRUM_COUNTER, ATTR_YELLOW_DRUM_REMAINING_LIFE, ATTR_YELLOW_DRUM_REMAINING_PAGES, + DATA_CONFIG_ENTRY, DOMAIN, SENSOR_TYPES, ) @@ -37,7 +38,7 @@ ATTR_SERIAL = "serial" async def async_setup_entry(hass, config_entry, async_add_entities): """Add Brother entities from a config_entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id] sensors = [] diff --git a/homeassistant/components/brother/utils.py b/homeassistant/components/brother/utils.py new file mode 100644 index 00000000000..3a53f4c04a2 --- /dev/null +++ b/homeassistant/components/brother/utils.py @@ -0,0 +1,30 @@ +"""Brother helpers functions.""" +import logging + +import pysnmp.hlapi.asyncio as hlapi +from pysnmp.hlapi.asyncio.cmdgen import lcd + +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import callback +from homeassistant.helpers import singleton + +from .const import DOMAIN, SNMP + +_LOGGER = logging.getLogger(__name__) + + +@singleton.singleton("snmp_engine") +def get_snmp_engine(hass): + """Get SNMP engine.""" + _LOGGER.debug("Creating SNMP engine") + snmp_engine = hlapi.SnmpEngine() + + @callback + def shutdown_listener(ev): + if hass.data.get(DOMAIN): + _LOGGER.debug("Unconfiguring SNMP engine") + lcd.unconfigure(hass.data[DOMAIN][SNMP], None) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_listener) + + return snmp_engine diff --git a/requirements_all.txt b/requirements_all.txt index f6716f52511..763d35a8cae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -380,7 +380,7 @@ bravia-tv==1.0.8 broadlink==0.16.0 # homeassistant.components.brother -brother==0.2.0 +brother==0.2.1 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 748fd695137..3d57d3d2302 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -210,7 +210,7 @@ bravia-tv==1.0.8 broadlink==0.16.0 # homeassistant.components.brother -brother==0.2.0 +brother==0.2.1 # homeassistant.components.bsblan bsblan==0.4.0 From dd8d4471ec5ca42de70f2035a2d2f23c7a757459 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 12 Feb 2021 18:54:00 +0100 Subject: [PATCH 389/796] Postponed evaluation of annotations for integrations (#46455) --- homeassistant/components/bmw_connected_drive/__init__.py | 4 +++- homeassistant/components/cast/helpers.py | 4 +++- homeassistant/components/counter/__init__.py | 4 +++- homeassistant/components/esphome/sensor.py | 4 ++-- homeassistant/components/huawei_lte/config_flow.py | 3 ++- homeassistant/components/huawei_lte/notify.py | 3 ++- homeassistant/components/input_boolean/__init__.py | 4 +++- homeassistant/components/input_datetime/__init__.py | 4 +++- homeassistant/components/input_number/__init__.py | 4 +++- homeassistant/components/input_select/__init__.py | 4 +++- homeassistant/components/input_text/__init__.py | 4 +++- homeassistant/components/light/__init__.py | 4 +++- homeassistant/components/media_player/__init__.py | 4 +++- homeassistant/components/media_source/models.py | 6 ++++-- homeassistant/components/timer/__init__.py | 4 +++- homeassistant/components/transmission/__init__.py | 4 +++- homeassistant/components/zha/core/channels/__init__.py | 8 +++++--- homeassistant/components/zone/__init__.py | 4 +++- 18 files changed, 54 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index ac25636a066..9d794ace5be 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -1,4 +1,6 @@ """Reads vehicle status from BMW connected drive portal.""" +from __future__ import annotations + import asyncio import logging @@ -195,7 +197,7 @@ async def update_listener(hass, config_entry): await hass.config_entries.async_reload(config_entry.entry_id) -def setup_account(entry: ConfigEntry, hass, name: str) -> "BMWConnectedDriveAccount": +def setup_account(entry: ConfigEntry, hass, name: str) -> BMWConnectedDriveAccount: """Set up a new BMWConnectedDriveAccount based on the config.""" username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index e7db380406b..b8742ec2b5e 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -1,4 +1,6 @@ """Helpers to deal with Cast devices.""" +from __future__ import annotations + from typing import Optional import attr @@ -57,7 +59,7 @@ class ChromecastInfo: return None return CAST_MANUFACTURERS.get(self.model_name.lower(), "Google Inc.") - def fill_out_missing_chromecast_info(self) -> "ChromecastInfo": + def fill_out_missing_chromecast_info(self) -> ChromecastInfo: """Return a new ChromecastInfo object with missing attributes filled in. Uses blocking HTTP / HTTPS. diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index d23c90bcb93..868a74cc7b7 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -1,4 +1,6 @@ """Component to count within automations.""" +from __future__ import annotations + import logging from typing import Dict, Optional @@ -179,7 +181,7 @@ class Counter(RestoreEntity): self.editable: bool = True @classmethod - def from_yaml(cls, config: Dict) -> "Counter": + def from_yaml(cls, config: Dict) -> Counter: """Create counter instance from yaml config.""" counter = cls(config) counter.editable = False diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index fbf3925953b..a5cc321cb08 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -78,11 +78,11 @@ class EsphomeTextSensor(EsphomeEntity): """A text sensor implementation for ESPHome.""" @property - def _static_info(self) -> "TextSensorInfo": + def _static_info(self) -> TextSensorInfo: return super()._static_info @property - def _state(self) -> Optional["TextSensorState"]: + def _state(self) -> Optional[TextSensorState]: return super()._state @property diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index ba8baedcaf7..350ad5bca0d 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the Huawei LTE platform.""" +from __future__ import annotations from collections import OrderedDict import logging @@ -48,7 +49,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @callback def async_get_options_flow( config_entry: config_entries.ConfigEntry, - ) -> "OptionsFlowHandler": + ) -> OptionsFlowHandler: """Get options flow.""" return OptionsFlowHandler(config_entry) diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index 5659e66ea98..ef354fefaf3 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -1,4 +1,5 @@ """Support for Huawei LTE router notifications.""" +from __future__ import annotations import logging import time @@ -21,7 +22,7 @@ async def async_get_service( hass: HomeAssistantType, config: Dict[str, Any], discovery_info: Optional[Dict[str, Any]] = None, -) -> Optional["HuaweiLteSmsNotificationService"]: +) -> Optional[HuaweiLteSmsNotificationService]: """Get the notification service.""" if discovery_info is None: return None diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index 1b996722c01..fbfe4cd0454 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -1,4 +1,6 @@ """Support to keep track of user controlled booleans for within automation.""" +from __future__ import annotations + import logging import typing @@ -150,7 +152,7 @@ class InputBoolean(ToggleEntity, RestoreEntity): self._state = config.get(CONF_INITIAL) @classmethod - def from_yaml(cls, config: typing.Dict) -> "InputBoolean": + def from_yaml(cls, config: typing.Dict) -> InputBoolean: """Return entity instance initialized from yaml storage.""" input_bool = cls(config) input_bool.entity_id = f"{DOMAIN}.{config[CONF_ID]}" diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 9589fe9a7ea..adefa36639a 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -1,4 +1,6 @@ """Support to select a date and/or a time.""" +from __future__ import annotations + import datetime as py_datetime import logging import typing @@ -228,7 +230,7 @@ class InputDatetime(RestoreEntity): ) @classmethod - def from_yaml(cls, config: typing.Dict) -> "InputDatetime": + def from_yaml(cls, config: typing.Dict) -> InputDatetime: """Return entity instance initialized from yaml storage.""" input_dt = cls(config) input_dt.entity_id = f"{DOMAIN}.{config[CONF_ID]}" diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index 5cad0f49c88..b68e6fff45d 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -1,4 +1,6 @@ """Support to set a numeric value from a slider or text box.""" +from __future__ import annotations + import logging import typing @@ -202,7 +204,7 @@ class InputNumber(RestoreEntity): self._current_value = config.get(CONF_INITIAL) @classmethod - def from_yaml(cls, config: typing.Dict) -> "InputNumber": + def from_yaml(cls, config: typing.Dict) -> InputNumber: """Return entity instance initialized from yaml storage.""" input_num = cls(config) input_num.entity_id = f"{DOMAIN}.{config[CONF_ID]}" diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index a390d8e1901..f6831dc3e88 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -1,4 +1,6 @@ """Support to select an option from a list.""" +from __future__ import annotations + import logging import typing @@ -207,7 +209,7 @@ class InputSelect(RestoreEntity): self._current_option = config.get(CONF_INITIAL) @classmethod - def from_yaml(cls, config: typing.Dict) -> "InputSelect": + def from_yaml(cls, config: typing.Dict) -> InputSelect: """Return entity instance initialized from yaml storage.""" input_select = cls(config) input_select.entity_id = f"{DOMAIN}.{config[CONF_ID]}" diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 76eb51eedd5..3f8c1d6a13e 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -1,4 +1,6 @@ """Support to enter a value into a text box.""" +from __future__ import annotations + import logging import typing @@ -196,7 +198,7 @@ class InputText(RestoreEntity): self._current_value = config.get(CONF_INITIAL) @classmethod - def from_yaml(cls, config: typing.Dict) -> "InputText": + def from_yaml(cls, config: typing.Dict) -> InputText: """Return entity instance initialized from yaml storage.""" input_text = cls(config) input_text.entity_id = f"{DOMAIN}.{config[CONF_ID]}" diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index c46b7568b59..55476c754f2 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -1,4 +1,6 @@ """Provides functionality to interact with lights.""" +from __future__ import annotations + import csv import dataclasses from datetime import timedelta @@ -327,7 +329,7 @@ class Profile: ) @classmethod - def from_csv_row(cls, csv_row: List[str]) -> "Profile": + def from_csv_row(cls, csv_row: List[str]) -> Profile: """Create profile from a CSV row tuple.""" return cls(*cls.SCHEMA(csv_row)) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index d670acb7af9..87ecff7a54c 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -1,4 +1,6 @@ """Component to interface with various media players.""" +from __future__ import annotations + import asyncio import base64 import collections @@ -851,7 +853,7 @@ class MediaPlayerEntity(Entity): self, media_content_type: Optional[str] = None, media_content_id: Optional[str] = None, - ) -> "BrowseMedia": + ) -> BrowseMedia: """Return a BrowseMedia instance. The BrowseMedia instance will be used by the diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index e16ecbe578e..98b817344d9 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -1,4 +1,6 @@ """Media Source models.""" +from __future__ import annotations + from abc import ABC from dataclasses import dataclass from typing import List, Optional, Tuple @@ -82,12 +84,12 @@ class MediaSourceItem: return await self.async_media_source().async_resolve_media(self) @callback - def async_media_source(self) -> "MediaSource": + def async_media_source(self) -> MediaSource: """Return media source that owns this item.""" return self.hass.data[DOMAIN][self.domain] @classmethod - def from_uri(cls, hass: HomeAssistant, uri: str) -> "MediaSourceItem": + def from_uri(cls, hass: HomeAssistant, uri: str) -> MediaSourceItem: """Create an item from a uri.""" match = URI_SCHEME_REGEX.match(uri) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index b123bbadf7d..b0ff60bbcae 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -1,4 +1,6 @@ """Support for Timers.""" +from __future__ import annotations + from datetime import datetime, timedelta import logging from typing import Dict, Optional @@ -198,7 +200,7 @@ class Timer(RestoreEntity): self._listener = None @classmethod - def from_yaml(cls, config: Dict) -> "Timer": + def from_yaml(cls, config: Dict) -> Timer: """Return entity instance initialized from yaml storage.""" timer = cls(config) timer.entity_id = ENTITY_ID_FORMAT.format(config[CONF_ID]) diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 76d9aedd8d5..5a37cc4d771 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -1,4 +1,6 @@ """Support for the Transmission BitTorrent client API.""" +from __future__ import annotations + from datetime import timedelta import logging from typing import List @@ -176,7 +178,7 @@ class TransmissionClient: self.unsub_timer = None @property - def api(self) -> "TransmissionData": + def api(self) -> TransmissionData: """Return the TransmissionData object.""" return self._tm_data diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 1bd8a52b6e6..852d576c035 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -1,4 +1,6 @@ """Channels module for Zigbee Home Automation.""" +from __future__ import annotations + import asyncio from typing import Any, Dict, List, Optional, Tuple, Union @@ -47,7 +49,7 @@ class Channels: self._zha_device = zha_device @property - def pools(self) -> List["ChannelPool"]: + def pools(self) -> List[ChannelPool]: """Return channel pools list.""" return self._pools @@ -102,7 +104,7 @@ class Channels: } @classmethod - def new(cls, zha_device: zha_typing.ZhaDeviceType) -> "Channels": + def new(cls, zha_device: zha_typing.ZhaDeviceType) -> Channels: """Create new instance.""" channels = cls(zha_device) for ep_id in sorted(zha_device.device.endpoints): @@ -263,7 +265,7 @@ class ChannelPool: ) @classmethod - def new(cls, channels: Channels, ep_id: int) -> "ChannelPool": + def new(cls, channels: Channels, ep_id: int) -> ChannelPool: """Create new channels for an endpoint.""" pool = cls(channels, ep_id) pool.add_all_channels() diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 1eef9636e36..e1d48cbe1ff 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -1,4 +1,6 @@ """Support for the definition of zones.""" +from __future__ import annotations + import logging from typing import Any, Dict, Optional, cast @@ -285,7 +287,7 @@ class Zone(entity.Entity): self._generate_attrs() @classmethod - def from_yaml(cls, config: Dict) -> "Zone": + def from_yaml(cls, config: Dict) -> Zone: """Return entity instance initialized from yaml storage.""" zone = cls(config) zone.editable = False From bc8a52038b77fc3629a046699aac4a8bf5bf24cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 12 Feb 2021 08:45:19 -1000 Subject: [PATCH 390/796] Fix homekit migration not being awaited (#46460) --- homeassistant/components/homekit/__init__.py | 2 +- tests/components/homekit/test_homekit.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 568804ef081..34044742703 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -240,7 +240,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if CONF_ENTRY_INDEX in conf and conf[CONF_ENTRY_INDEX] == 0: _LOGGER.debug("Migrating legacy HomeKit data for %s", name) - hass.async_add_executor_job( + await hass.async_add_executor_job( migrate_filesystem_state_data_for_primary_imported_entry_id, hass, entry.entry_id, diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index f0d6a8b365f..1fff55db195 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -51,7 +51,7 @@ from homeassistant.const import ( CONF_PORT, DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, - EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, PERCENTAGE, SERVICE_RELOAD, @@ -118,7 +118,7 @@ async def test_setup_min(hass, mock_zeroconf): # Test auto start enabled mock_homekit.reset_mock() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() mock_homekit().async_start.assert_called() @@ -155,7 +155,7 @@ async def test_setup_auto_start_disabled(hass, mock_zeroconf): # Test auto_start disabled homekit.reset_mock() homekit.async_start.reset_mock() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert homekit.async_start.called is False @@ -951,7 +951,7 @@ async def test_setup_imported(hass, mock_zeroconf): # Test auto start enabled mock_homekit.reset_mock() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() mock_homekit().async_start.assert_called() @@ -1005,7 +1005,7 @@ async def test_yaml_updates_update_config_entry_for_name(hass, mock_zeroconf): # Test auto start enabled mock_homekit.reset_mock() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() mock_homekit().async_start.assert_called() From da4cb6d2946f884a3899975159563fda7f85f55c Mon Sep 17 00:00:00 2001 From: tkdrob Date: Fri, 12 Feb 2021 17:25:15 -0500 Subject: [PATCH 391/796] Use core constants for somfy (#46466) --- homeassistant/components/somfy/__init__.py | 4 ++-- homeassistant/components/somfy/climate.py | 1 - homeassistant/components/somfy/const.py | 1 - homeassistant/components/somfy/cover.py | 5 ++--- homeassistant/components/somfy/sensor.py | 1 - homeassistant/components/somfy/switch.py | 1 - 6 files changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index 75475e52f06..ac32b9d5379 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.somfy import config_flow from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_OPTIMISTIC from homeassistant.core import callback from homeassistant.helpers import ( config_entry_oauth2_flow, @@ -25,7 +25,7 @@ from homeassistant.helpers.update_coordinator import ( ) from . import api -from .const import API, CONF_OPTIMISTIC, COORDINATOR, DOMAIN +from .const import API, COORDINATOR, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/somfy/climate.py b/homeassistant/components/somfy/climate.py index 00a2738f4fe..99d6dca06ee 100644 --- a/homeassistant/components/somfy/climate.py +++ b/homeassistant/components/somfy/climate.py @@ -48,7 +48,6 @@ 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.""" - domain_data = hass.data[DOMAIN] coordinator = domain_data[COORDINATOR] api = domain_data[API] diff --git a/homeassistant/components/somfy/const.py b/homeassistant/components/somfy/const.py index aca93be66cb..128d6eb76bb 100644 --- a/homeassistant/components/somfy/const.py +++ b/homeassistant/components/somfy/const.py @@ -3,4 +3,3 @@ DOMAIN = "somfy" COORDINATOR = "coordinator" API = "api" -CONF_OPTIMISTIC = "optimistic" diff --git a/homeassistant/components/somfy/cover.py b/homeassistant/components/somfy/cover.py index e7308558127..d227bc31227 100644 --- a/homeassistant/components/somfy/cover.py +++ b/homeassistant/components/somfy/cover.py @@ -18,11 +18,11 @@ from homeassistant.components.cover import ( SUPPORT_STOP_TILT, CoverEntity, ) -from homeassistant.const import STATE_CLOSED, STATE_OPEN +from homeassistant.const import CONF_OPTIMISTIC, STATE_CLOSED, STATE_OPEN from homeassistant.helpers.restore_state import RestoreEntity from . import SomfyEntity -from .const import API, CONF_OPTIMISTIC, COORDINATOR, DOMAIN +from .const import API, COORDINATOR, DOMAIN BLIND_DEVICE_CATEGORIES = {Category.INTERIOR_BLIND.value, Category.EXTERIOR_BLIND.value} SHUTTER_DEVICE_CATEGORIES = {Category.EXTERIOR_BLIND.value} @@ -35,7 +35,6 @@ SUPPORTED_CATEGORIES = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Somfy cover platform.""" - domain_data = hass.data[DOMAIN] coordinator = domain_data[COORDINATOR] api = domain_data[API] diff --git a/homeassistant/components/somfy/sensor.py b/homeassistant/components/somfy/sensor.py index 1becc929adc..996a95348a4 100644 --- a/homeassistant/components/somfy/sensor.py +++ b/homeassistant/components/somfy/sensor.py @@ -13,7 +13,6 @@ SUPPORTED_CATEGORIES = {Category.HVAC.value} async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Somfy sensor platform.""" - domain_data = hass.data[DOMAIN] coordinator = domain_data[COORDINATOR] api = domain_data[API] diff --git a/homeassistant/components/somfy/switch.py b/homeassistant/components/somfy/switch.py index 14328953367..66eef99d6b5 100644 --- a/homeassistant/components/somfy/switch.py +++ b/homeassistant/components/somfy/switch.py @@ -10,7 +10,6 @@ from .const import API, COORDINATOR, DOMAIN async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Somfy switch platform.""" - domain_data = hass.data[DOMAIN] coordinator = domain_data[COORDINATOR] api = domain_data[API] From ae45d7dade7667f51873f8e09eff80896f5e6b23 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Fri, 12 Feb 2021 17:32:56 -0500 Subject: [PATCH 392/796] Use core constants for rflink (#46440) --- homeassistant/components/rflink/__init__.py | 5 ++--- homeassistant/components/rflink/binary_sensor.py | 9 +++++++-- homeassistant/components/rflink/cover.py | 3 +-- homeassistant/components/rflink/light.py | 3 +-- homeassistant/components/rflink/sensor.py | 5 ++--- homeassistant/components/rflink/switch.py | 3 +-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index 6bedea3ecd9..3cff3beed3c 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -10,7 +10,9 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_STATE, CONF_COMMAND, + CONF_DEVICE_ID, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, @@ -29,15 +31,12 @@ from homeassistant.helpers.restore_state import RestoreEntity _LOGGER = logging.getLogger(__name__) ATTR_EVENT = "event" -ATTR_STATE = "state" CONF_ALIASES = "aliases" CONF_GROUP_ALIASES = "group_aliases" CONF_GROUP = "group" CONF_NOGROUP_ALIASES = "nogroup_aliases" CONF_DEVICE_DEFAULTS = "device_defaults" -CONF_DEVICE_ID = "device_id" -CONF_DEVICES = "devices" CONF_AUTOMATIC_ADD = "automatic_add" CONF_FIRE_EVENT = "fire_event" CONF_IGNORE_DEVICES = "ignore_devices" diff --git a/homeassistant/components/rflink/binary_sensor.py b/homeassistant/components/rflink/binary_sensor.py index dd16343898d..77a8a522f65 100644 --- a/homeassistant/components/rflink/binary_sensor.py +++ b/homeassistant/components/rflink/binary_sensor.py @@ -6,11 +6,16 @@ from homeassistant.components.binary_sensor import ( PLATFORM_SCHEMA, BinarySensorEntity, ) -from homeassistant.const import CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, CONF_NAME +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_DEVICES, + CONF_FORCE_UPDATE, + CONF_NAME, +) import homeassistant.helpers.config_validation as cv import homeassistant.helpers.event as evt -from . import CONF_ALIASES, CONF_DEVICES, RflinkDevice +from . import CONF_ALIASES, RflinkDevice CONF_OFF_DELAY = "off_delay" DEFAULT_FORCE_UPDATE = False diff --git a/homeassistant/components/rflink/cover.py b/homeassistant/components/rflink/cover.py index 5eacce3afa8..2e6837d21ea 100644 --- a/homeassistant/components/rflink/cover.py +++ b/homeassistant/components/rflink/cover.py @@ -4,14 +4,13 @@ import logging import voluptuous as vol from homeassistant.components.cover import PLATFORM_SCHEMA, CoverEntity -from homeassistant.const import CONF_NAME, CONF_TYPE, STATE_OPEN +from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_TYPE, STATE_OPEN import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity from . import ( CONF_ALIASES, CONF_DEVICE_DEFAULTS, - CONF_DEVICES, CONF_FIRE_EVENT, CONF_GROUP, CONF_GROUP_ALIASES, diff --git a/homeassistant/components/rflink/light.py b/homeassistant/components/rflink/light.py index 6d63e12378d..fe74c979396 100644 --- a/homeassistant/components/rflink/light.py +++ b/homeassistant/components/rflink/light.py @@ -9,14 +9,13 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, LightEntity, ) -from homeassistant.const import CONF_NAME, CONF_TYPE +from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_TYPE import homeassistant.helpers.config_validation as cv from . import ( CONF_ALIASES, CONF_AUTOMATIC_ADD, CONF_DEVICE_DEFAULTS, - CONF_DEVICES, CONF_FIRE_EVENT, CONF_GROUP, CONF_GROUP_ALIASES, diff --git a/homeassistant/components/rflink/sensor.py b/homeassistant/components/rflink/sensor.py index 2c27477e6c6..1a616c2ed90 100644 --- a/homeassistant/components/rflink/sensor.py +++ b/homeassistant/components/rflink/sensor.py @@ -5,7 +5,9 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + CONF_DEVICES, CONF_NAME, + CONF_SENSOR_TYPE, CONF_UNIT_OF_MEASUREMENT, ) import homeassistant.helpers.config_validation as cv @@ -14,7 +16,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import ( CONF_ALIASES, CONF_AUTOMATIC_ADD, - CONF_DEVICES, DATA_DEVICE_REGISTER, DATA_ENTITY_LOOKUP, EVENT_KEY_ID, @@ -32,8 +33,6 @@ SENSOR_ICONS = { "temperature": "mdi:thermometer", } -CONF_SENSOR_TYPE = "sensor_type" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_AUTOMATIC_ADD, default=True): cv.boolean, diff --git a/homeassistant/components/rflink/switch.py b/homeassistant/components/rflink/switch.py index 77e1f821bad..8f84286a616 100644 --- a/homeassistant/components/rflink/switch.py +++ b/homeassistant/components/rflink/switch.py @@ -2,13 +2,12 @@ import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv from . import ( CONF_ALIASES, CONF_DEVICE_DEFAULTS, - CONF_DEVICES, CONF_FIRE_EVENT, CONF_GROUP, CONF_GROUP_ALIASES, From 8bacfcec50cf523a788010e519c6f28d7c770e3e Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 13 Feb 2021 00:03:13 +0000 Subject: [PATCH 393/796] [ci skip] Translation update --- .../components/foscam/translations/es.json | 2 + .../components/foscam/translations/nl.json | 26 ++++++++ .../huisbaasje/translations/nl.json | 21 ++++++ .../components/mazda/translations/nl.json | 14 ++++ .../media_player/translations/nl.json | 7 ++ .../components/mysensors/translations/nl.json | 66 +++++++++++++++++++ .../components/number/translations/nl.json | 8 +++ .../philips_js/translations/es.json | 16 +++++ .../philips_js/translations/et.json | 16 +++++ .../philips_js/translations/no.json | 24 +++++++ .../philips_js/translations/ru.json | 24 +++++++ .../philips_js/translations/zh-Hant.json | 24 +++++++ .../components/pi_hole/translations/nl.json | 6 ++ .../components/powerwall/translations/es.json | 1 + .../components/powerwall/translations/nl.json | 1 + .../components/roku/translations/nl.json | 8 +++ .../components/roomba/translations/nl.json | 24 +++++++ .../somfy_mylink/translations/nl.json | 5 ++ .../components/unifi/translations/nl.json | 4 +- .../components/zwave_js/translations/nl.json | 56 ++++++++++++++++ 20 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/foscam/translations/nl.json create mode 100644 homeassistant/components/huisbaasje/translations/nl.json create mode 100644 homeassistant/components/mazda/translations/nl.json create mode 100644 homeassistant/components/mysensors/translations/nl.json create mode 100644 homeassistant/components/number/translations/nl.json create mode 100644 homeassistant/components/philips_js/translations/es.json create mode 100644 homeassistant/components/philips_js/translations/et.json create mode 100644 homeassistant/components/philips_js/translations/no.json create mode 100644 homeassistant/components/philips_js/translations/ru.json create mode 100644 homeassistant/components/philips_js/translations/zh-Hant.json create mode 100644 homeassistant/components/somfy_mylink/translations/nl.json create mode 100644 homeassistant/components/zwave_js/translations/nl.json diff --git a/homeassistant/components/foscam/translations/es.json b/homeassistant/components/foscam/translations/es.json index 27f7ac36489..7e8b7c1427d 100644 --- a/homeassistant/components/foscam/translations/es.json +++ b/homeassistant/components/foscam/translations/es.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "invalid_response": "Respuesta no v\u00e1lida del dispositivo", "unknown": "Error inesperado" }, "step": { @@ -14,6 +15,7 @@ "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto", + "rtsp_port": "Puerto RTSP", "stream": "Stream", "username": "Usuario" } diff --git a/homeassistant/components/foscam/translations/nl.json b/homeassistant/components/foscam/translations/nl.json new file mode 100644 index 00000000000..9bea23ad702 --- /dev/null +++ b/homeassistant/components/foscam/translations/nl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "invalid_response": "Ongeldig antwoord van het apparaat", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord", + "port": "Poort", + "rtsp_port": "RTSP-poort", + "stream": "Stream", + "username": "Gebruikersnaam" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/nl.json b/homeassistant/components/huisbaasje/translations/nl.json new file mode 100644 index 00000000000..8cb09793af8 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "connection_exception": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unauthenticated_exception": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/nl.json b/homeassistant/components/mazda/translations/nl.json new file mode 100644 index 00000000000..c820f481b9d --- /dev/null +++ b/homeassistant/components/mazda/translations/nl.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "account_locked": "Account vergrendeld. Probeer het later nog eens.", + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/translations/nl.json b/homeassistant/components/media_player/translations/nl.json index 5e690f35f8a..37c1d6b4d9e 100644 --- a/homeassistant/components/media_player/translations/nl.json +++ b/homeassistant/components/media_player/translations/nl.json @@ -6,6 +6,13 @@ "is_on": "{entity_name} is ingeschakeld", "is_paused": "{entity_name} is gepauzeerd", "is_playing": "{entity_name} wordt afgespeeld" + }, + "trigger_type": { + "idle": "{entity_name} wordt inactief", + "paused": "{entity_name} is gepauzeerd", + "playing": "{entity_name} begint te spelen", + "turned_off": "{entity_name} uitgeschakeld", + "turned_on": "{entity_name} ingeschakeld" } }, "state": { diff --git a/homeassistant/components/mysensors/translations/nl.json b/homeassistant/components/mysensors/translations/nl.json new file mode 100644 index 00000000000..ebbcbf9a36e --- /dev/null +++ b/homeassistant/components/mysensors/translations/nl.json @@ -0,0 +1,66 @@ +{ + "config": { + "abort": { + "not_a_number": "Voer een nummer in", + "port_out_of_range": "Poortnummer moet minimaal 1 en maximaal 65535 zijn", + "same_topic": "De topics abonneren en publiceren zijn hetzelfde", + "unknown": "Onverwachte fout" + }, + "error": { + "already_configured": "Apparaat is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken", + "duplicate_persistence_file": "Persistentiebestand al in gebruik", + "duplicate_topic": "Topic is al in gebruik", + "invalid_auth": "Ongeldige authenticatie", + "invalid_device": "Ongeldig apparaat", + "invalid_ip": "Ongeldig IP-adres", + "invalid_persistence_file": "Ongeldig persistentiebestand", + "invalid_port": "Ongeldig poortnummer", + "invalid_publish_topic": "ongeldig publiceer topic", + "invalid_serial": "Ongeldige seri\u00eble poort", + "invalid_subscribe_topic": "Ongeldig abonneer topic", + "invalid_version": "Ongeldige MySensors-versie", + "not_a_number": "Voer een nummer in", + "port_out_of_range": "Poortnummer moet minimaal 1 en maximaal 65535 zijn", + "same_topic": "De topics abonneren en publiceren zijn hetzelfde", + "unknown": "Onverwachte fout" + }, + "step": { + "gw_mqtt": { + "data": { + "persistence_file": "persistentiebestand (leeg laten om automatisch te genereren)", + "retain": "mqtt behouden", + "topic_in_prefix": "prefix voor inkomende topics (topic_in_prefix)", + "topic_out_prefix": "prefix voor uitgaande topics (topic_out_prefix)", + "version": "MySensors-versie" + }, + "description": "MQTT-gateway instellen" + }, + "gw_serial": { + "data": { + "baud_rate": "baudrate", + "device": "Seri\u00eble poort", + "persistence_file": "persistentiebestand (leeg laten om automatisch te genereren)", + "version": "MySensors-versie" + }, + "description": "Seri\u00eble gateway setup" + }, + "gw_tcp": { + "data": { + "device": "IP-adres van de gateway", + "persistence_file": "persistentiebestand (leeg laten om automatisch te genereren)", + "tcp_port": "Poort", + "version": "MySensors-versie" + }, + "description": "Ethernet gateway instellen" + }, + "user": { + "data": { + "gateway_type": "Gateway type" + }, + "description": "Kies de verbindingsmethode met de gateway" + } + } + }, + "title": "MySensors" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/nl.json b/homeassistant/components/number/translations/nl.json new file mode 100644 index 00000000000..f9a1c6b60a9 --- /dev/null +++ b/homeassistant/components/number/translations/nl.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "Stel waarde in voor {entity_name}" + } + }, + "title": "Nummer" +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/es.json b/homeassistant/components/philips_js/translations/es.json new file mode 100644 index 00000000000..3d4beaa8752 --- /dev/null +++ b/homeassistant/components/philips_js/translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_version": "Versi\u00f3n del API" + } + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "Se solicita al dispositivo que se encienda" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/et.json b/homeassistant/components/philips_js/translations/et.json new file mode 100644 index 00000000000..ef5e3e0ffce --- /dev/null +++ b/homeassistant/components/philips_js/translations/et.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_version": "API versioon" + } + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "Seadmel palutakse sisse l\u00fclituda" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/no.json b/homeassistant/components/philips_js/translations/no.json new file mode 100644 index 00000000000..dadf15fb67a --- /dev/null +++ b/homeassistant/components/philips_js/translations/no.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "api_version": "API-versjon", + "host": "Vert" + } + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "Enheten blir bedt om \u00e5 sl\u00e5 p\u00e5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/ru.json b/homeassistant/components/philips_js/translations/ru.json new file mode 100644 index 00000000000..9306ecf7a29 --- /dev/null +++ b/homeassistant/components/philips_js/translations/ru.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "api_version": "\u0412\u0435\u0440\u0441\u0438\u044f API", + "host": "\u0425\u043e\u0441\u0442" + } + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/zh-Hant.json b/homeassistant/components/philips_js/translations/zh-Hant.json new file mode 100644 index 00000000000..af161b6b16b --- /dev/null +++ b/homeassistant/components/philips_js/translations/zh-Hant.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "api_version": "API \u7248\u672c", + "host": "\u4e3b\u6a5f\u7aef" + } + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "\u88dd\u7f6e\u5fc5\u9808\u70ba\u958b\u555f\u72c0\u614b" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/nl.json b/homeassistant/components/pi_hole/translations/nl.json index 24da024acae..156a248a80b 100644 --- a/homeassistant/components/pi_hole/translations/nl.json +++ b/homeassistant/components/pi_hole/translations/nl.json @@ -7,6 +7,11 @@ "cannot_connect": "Kon niet verbinden" }, "step": { + "api_key": { + "data": { + "api_key": "API-sleutel" + } + }, "user": { "data": { "api_key": "API-sleutel", @@ -15,6 +20,7 @@ "name": "Naam", "port": "Poort", "ssl": "Maakt gebruik van een SSL-certificaat", + "statistics_only": "Alleen statistieken", "verify_ssl": "SSL-certificaat verifi\u00ebren" } } diff --git a/homeassistant/components/powerwall/translations/es.json b/homeassistant/components/powerwall/translations/es.json index 373bf29f8ba..81e3edab387 100644 --- a/homeassistant/components/powerwall/translations/es.json +++ b/homeassistant/components/powerwall/translations/es.json @@ -14,6 +14,7 @@ "data": { "ip_address": "Direcci\u00f3n IP" }, + "description": "La contrase\u00f1a suele ser los \u00faltimos 5 caracteres del n\u00famero de serie del Backup Gateway y se puede encontrar en la aplicaci\u00f3n Telsa; o los \u00faltimos 5 caracteres de la contrase\u00f1a que se encuentran dentro de la puerta del Backup Gateway 2.", "title": "Conectarse al powerwall" } } diff --git a/homeassistant/components/powerwall/translations/nl.json b/homeassistant/components/powerwall/translations/nl.json index f77cc864813..779da2086eb 100644 --- a/homeassistant/components/powerwall/translations/nl.json +++ b/homeassistant/components/powerwall/translations/nl.json @@ -8,6 +8,7 @@ "unknown": "Onverwachte fout", "wrong_version": "Uw powerwall gebruikt een softwareversie die niet wordt ondersteund. Overweeg om dit probleem te upgraden of te melden, zodat het kan worden opgelost." }, + "flow_title": "Tesla Powerwall ({ip_adres})", "step": { "user": { "data": { diff --git a/homeassistant/components/roku/translations/nl.json b/homeassistant/components/roku/translations/nl.json index 6b0927178fe..529b01b64c2 100644 --- a/homeassistant/components/roku/translations/nl.json +++ b/homeassistant/components/roku/translations/nl.json @@ -9,6 +9,14 @@ }, "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "data": { + "one": "Een", + "other": "Ander" + }, + "description": "Wilt u {naam} instellen?", + "title": "Roku" + }, "ssdp_confirm": { "data": { "one": "Leeg", diff --git a/homeassistant/components/roomba/translations/nl.json b/homeassistant/components/roomba/translations/nl.json index f5268bdf799..754ff2e51a8 100644 --- a/homeassistant/components/roomba/translations/nl.json +++ b/homeassistant/components/roomba/translations/nl.json @@ -1,9 +1,33 @@ { "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken", + "not_irobot_device": "Het gevonden apparaat is geen iRobot-apparaat" + }, "error": { "cannot_connect": "Verbinding mislukt, probeer het opnieuw" }, + "flow_title": "iRobot {naam} ({host})", "step": { + "init": { + "data": { + "host": "Host" + }, + "description": "Kies een Roomba of Braava.", + "title": "Automatisch verbinding maken met het apparaat" + }, + "link": { + "description": "Houd de Home-knop op {naam} ingedrukt totdat het apparaat een geluid genereert (ongeveer twee seconden).", + "title": "Wachtwoord opvragen" + }, + "link_manual": { + "data": { + "password": "Wachtwoord" + }, + "description": "Het wachtwoord kon niet automatisch van het apparaat worden opgehaald. Volg de stappen zoals beschreven in de documentatie op: {auth_help_url}", + "title": "Voer wachtwoord in" + }, "user": { "data": { "blid": "BLID", diff --git a/homeassistant/components/somfy_mylink/translations/nl.json b/homeassistant/components/somfy_mylink/translations/nl.json new file mode 100644 index 00000000000..208c032227a --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/nl.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "Somfy MyLink {mac} ( {ip} )" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/nl.json b/homeassistant/components/unifi/translations/nl.json index 4e9aa16a245..96ffc0c0ace 100644 --- a/homeassistant/components/unifi/translations/nl.json +++ b/homeassistant/components/unifi/translations/nl.json @@ -1,13 +1,15 @@ { "config": { "abort": { - "already_configured": "Controller site is al geconfigureerd" + "already_configured": "Controller site is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "faulty_credentials": "Foutieve gebruikersgegevens", "service_unavailable": "Geen service beschikbaar", "unknown_client_mac": "Geen client beschikbaar op dat MAC-adres" }, + "flow_title": "UniFi Netwerk {site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/zwave_js/translations/nl.json b/homeassistant/components/zwave_js/translations/nl.json new file mode 100644 index 00000000000..74b4db46de1 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/nl.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "addon_get_discovery_info_failed": "Ophalen van ontdekkingsinformatie voor Z-Wave JS-add-on is mislukt.", + "addon_info_failed": "Ophalen van Z-Wave JS add-on-info is mislukt.", + "addon_install_failed": "Kan de Z-Wave JS add-on niet installeren.", + "addon_missing_discovery_info": "De Z-Wave JS addon mist ontdekkings informatie", + "addon_set_config_failed": "Instellen van de Z-Wave JS-configuratie is mislukt.", + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", + "cannot_connect": "Kan geen verbinding maken" + }, + "error": { + "addon_start_failed": "Het is niet gelukt om de Z-Wave JS add-on te starten. Controleer de configuratie.", + "cannot_connect": "Kan geen verbinding maken", + "invalid_ws_url": "Ongeldige websocket URL", + "unknown": "Onverwachte fout" + }, + "progress": { + "install_addon": "Een ogenblik geduld terwijl de installatie van de Z-Wave JS add-on is voltooid. Dit kan enkele minuten duren." + }, + "step": { + "hassio_confirm": { + "title": "Z-Wave JS integratie instellen met de Z-Wave JS add-on" + }, + "install_addon": { + "title": "De Z-Wave JS add-on installatie is gestart" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Gebruik de Z-Wave JS Supervisor add-on" + }, + "description": "Wilt u de Z-Wave JS Supervisor add-on gebruiken?", + "title": "Selecteer verbindingsmethode" + }, + "start_addon": { + "data": { + "network_key": "Netwerksleutel", + "usb_path": "USB-apparaatpad" + }, + "title": "Voer de Z-Wave JS add-on configuratie in" + }, + "user": { + "data": { + "url": "URL" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file From 1a8cdba9af1680c0456735142479c3c7fef43bff Mon Sep 17 00:00:00 2001 From: On Freund Date: Sat, 13 Feb 2021 13:03:49 +0200 Subject: [PATCH 394/796] Gracefully handle missing A/V info in Onkyo integration (#46228) * Gracefully handle missing A/V info * Do not attempt to query A/V info if unsupported * Rename _parse_onkyo_tuple --- .../components/onkyo/media_player.py | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index ac8cfa5e4b6..7ac9b5fdfc6 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -118,15 +118,20 @@ ONKYO_SELECT_OUTPUT_SCHEMA = vol.Schema( SERVICE_SELECT_HDMI_OUTPUT = "onkyo_select_hdmi_output" -def _parse_onkyo_tuple(tup): - """Parse a tuple returned from the eiscp library.""" - if len(tup) < 2: +def _parse_onkyo_payload(payload): + """Parse a payload returned from the eiscp library.""" + if isinstance(payload, bool): + # command not supported by the device + return False + + if len(payload) < 2: + # no value return None - if isinstance(tup[1], str): - return tup[1].split(",") + if isinstance(payload[1], str): + return payload[1].split(",") - return tup[1] + return payload[1] def _tuple_get(tup, index, default=None): @@ -267,6 +272,8 @@ class OnkyoDevice(MediaPlayerEntity): self._reverse_mapping = {value: key for key, value in sources.items()} self._attributes = {} self._hdmi_out_supported = True + self._audio_info_supported = True + self._video_info_supported = True def command(self, command): """Run an eiscp command and catch connection errors.""" @@ -309,12 +316,14 @@ class OnkyoDevice(MediaPlayerEntity): else: hdmi_out_raw = [] preset_raw = self.command("preset query") - audio_information_raw = self.command("audio-information query") - video_information_raw = self.command("video-information query") + if self._audio_info_supported: + audio_information_raw = self.command("audio-information query") + if self._video_info_supported: + video_information_raw = self.command("video-information query") if not (volume_raw and mute_raw and current_source_raw): return - sources = _parse_onkyo_tuple(current_source_raw) + sources = _parse_onkyo_payload(current_source_raw) for source in sources: if source in self._source_mapping: @@ -441,7 +450,11 @@ class OnkyoDevice(MediaPlayerEntity): self.command(f"hdmi-output-selector={output}") def _parse_audio_information(self, audio_information_raw): - values = _parse_onkyo_tuple(audio_information_raw) + values = _parse_onkyo_payload(audio_information_raw) + if values is False: + self._audio_info_supported = False + return + if values: info = { "format": _tuple_get(values, 1), @@ -456,7 +469,11 @@ class OnkyoDevice(MediaPlayerEntity): self._attributes.pop(ATTR_AUDIO_INFORMATION, None) def _parse_video_information(self, video_information_raw): - values = _parse_onkyo_tuple(video_information_raw) + values = _parse_onkyo_payload(video_information_raw) + if values is False: + self._video_info_supported = False + return + if values: info = { "input_resolution": _tuple_get(values, 1), From 2ecac6550f6ea7c6769a021501190fc15e74c638 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sat, 13 Feb 2021 06:06:20 -0500 Subject: [PATCH 395/796] Use core constants for dynalite (#46044) --- homeassistant/components/dynalite/__init__.py | 4 +--- homeassistant/components/dynalite/const.py | 1 - homeassistant/components/dynalite/convert_config.py | 10 ++++++++-- homeassistant/components/dynalite/light.py | 1 - homeassistant/components/dynalite/switch.py | 1 - tests/components/dynalite/test_init.py | 4 ++-- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py index c131ebec3da..e52ec5946b6 100644 --- a/homeassistant/components/dynalite/__init__.py +++ b/homeassistant/components/dynalite/__init__.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.cover import DEVICE_CLASSES_SCHEMA from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE +from homeassistant.const import CONF_DEFAULT, CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv @@ -29,7 +29,6 @@ from .const import ( CONF_CHANNEL, CONF_CHANNEL_COVER, CONF_CLOSE_PRESET, - CONF_DEFAULT, CONF_DEVICE_CLASS, CONF_DURATION, CONF_FADE, @@ -181,7 +180,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool: """Set up the Dynalite platform.""" - conf = config.get(DOMAIN) LOGGER.debug("Setting up dynalite component config = %s", conf) diff --git a/homeassistant/components/dynalite/const.py b/homeassistant/components/dynalite/const.py index 4159c98f073..3f1e201f3fd 100644 --- a/homeassistant/components/dynalite/const.py +++ b/homeassistant/components/dynalite/const.py @@ -19,7 +19,6 @@ CONF_BRIDGES = "bridges" CONF_CHANNEL = "channel" CONF_CHANNEL_COVER = "channel_cover" CONF_CLOSE_PRESET = "close" -CONF_DEFAULT = "default" CONF_DEVICE_CLASS = "class" CONF_DURATION = "duration" CONF_FADE = "fade" diff --git a/homeassistant/components/dynalite/convert_config.py b/homeassistant/components/dynalite/convert_config.py index b84450c807d..6a85147a2e0 100644 --- a/homeassistant/components/dynalite/convert_config.py +++ b/homeassistant/components/dynalite/convert_config.py @@ -4,7 +4,14 @@ from typing import Any, Dict from dynalite_devices_lib import const as dyn_const -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_ROOM, CONF_TYPE +from homeassistant.const import ( + CONF_DEFAULT, + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_ROOM, + CONF_TYPE, +) from .const import ( ACTIVE_INIT, @@ -16,7 +23,6 @@ from .const import ( CONF_CHANNEL, CONF_CHANNEL_COVER, CONF_CLOSE_PRESET, - CONF_DEFAULT, CONF_DEVICE_CLASS, CONF_DURATION, CONF_FADE, diff --git a/homeassistant/components/dynalite/light.py b/homeassistant/components/dynalite/light.py index 5e7069ab50b..bb9569358be 100644 --- a/homeassistant/components/dynalite/light.py +++ b/homeassistant/components/dynalite/light.py @@ -12,7 +12,6 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ) -> None: """Record the async_add_entities function to add them later when received from Dynalite.""" - async_setup_entry_base( hass, config_entry, async_add_entities, "light", DynaliteLight ) diff --git a/homeassistant/components/dynalite/switch.py b/homeassistant/components/dynalite/switch.py index d106d976d68..a482228183c 100644 --- a/homeassistant/components/dynalite/switch.py +++ b/homeassistant/components/dynalite/switch.py @@ -12,7 +12,6 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ) -> None: """Record the async_add_entities function to add them later when received from Dynalite.""" - async_setup_entry_base( hass, config_entry, async_add_entities, "switch", DynaliteSwitch ) diff --git a/tests/components/dynalite/test_init.py b/tests/components/dynalite/test_init.py index d231f82d2f8..eab88fb18ca 100644 --- a/tests/components/dynalite/test_init.py +++ b/tests/components/dynalite/test_init.py @@ -7,7 +7,7 @@ import pytest from voluptuous import MultipleInvalid import homeassistant.components.dynalite.const as dynalite -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_ROOM +from homeassistant.const import CONF_DEFAULT, CONF_HOST, CONF_NAME, CONF_PORT, CONF_ROOM from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -54,7 +54,7 @@ async def test_async_setup(hass): dynalite.CONF_TEMPLATE: dynalite.CONF_TIME_COVER, }, }, - dynalite.CONF_DEFAULT: {dynalite.CONF_FADE: 2.3}, + CONF_DEFAULT: {dynalite.CONF_FADE: 2.3}, dynalite.CONF_ACTIVE: dynalite.ACTIVE_INIT, dynalite.CONF_PRESET: { "5": {CONF_NAME: "pres5", dynalite.CONF_FADE: 4.5} From 820a260252bba4fd8df80d0ea688d510700079f7 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sat, 13 Feb 2021 06:07:42 -0500 Subject: [PATCH 396/796] Use core constants for homeassistant triggers (#46472) --- homeassistant/components/homeassistant/triggers/event.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index 7665ee1b4d7..2bc42c3d063 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -1,14 +1,13 @@ """Offer event listening automation rules.""" import voluptuous as vol -from homeassistant.const import CONF_PLATFORM +from homeassistant.const import CONF_EVENT_DATA, CONF_PLATFORM from homeassistant.core import HassJob, callback from homeassistant.helpers import config_validation as cv, template # mypy: allow-untyped-defs CONF_EVENT_TYPE = "event_type" -CONF_EVENT_DATA = "event_data" CONF_EVENT_CONTEXT = "context" TRIGGER_SCHEMA = vol.Schema( From b8584cab5d65042a357e9dcc478e183545112a85 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 13 Feb 2021 12:27:54 +0100 Subject: [PATCH 397/796] Remove unnecessary gethostbyname() from Shelly integration (#46483) --- homeassistant/components/shelly/__init__.py | 5 +---- homeassistant/components/shelly/config_flow.py | 4 +--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 537caf9707f..d4423dc3a88 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -2,7 +2,6 @@ import asyncio from datetime import timedelta import logging -from socket import gethostbyname import aioshelly import async_timeout @@ -57,10 +56,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): temperature_unit = "C" if hass.config.units.is_metric else "F" - ip_address = await hass.async_add_executor_job(gethostbyname, entry.data[CONF_HOST]) - options = aioshelly.ConnectionOptions( - ip_address, + entry.data[CONF_HOST], entry.data.get(CONF_USERNAME), entry.data.get(CONF_PASSWORD), temperature_unit, diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 026021a992f..b3a9b068ac4 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Shelly integration.""" import asyncio import logging -from socket import gethostbyname import aiohttp import aioshelly @@ -33,10 +32,9 @@ async def validate_input(hass: core.HomeAssistant, host, data): Data has the keys from DATA_SCHEMA with values provided by the user. """ - ip_address = await hass.async_add_executor_job(gethostbyname, host) options = aioshelly.ConnectionOptions( - ip_address, data.get(CONF_USERNAME), data.get(CONF_PASSWORD) + host, data.get(CONF_USERNAME), data.get(CONF_PASSWORD) ) coap_context = await get_coap_context(hass) From 621c8e700bcdbe1cd831e7959fc51334de57cf10 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sat, 13 Feb 2021 06:33:13 -0500 Subject: [PATCH 398/796] Use core constants for starline (#46471) --- homeassistant/components/starline/__init__.py | 2 +- homeassistant/components/starline/const.py | 1 - homeassistant/components/starline/lock.py | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/starline/__init__.py b/homeassistant/components/starline/__init__.py index 392dbff9e03..3025a7b4c11 100644 --- a/homeassistant/components/starline/__init__.py +++ b/homeassistant/components/starline/__init__.py @@ -2,12 +2,12 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_SCAN_INTERVAL from homeassistant.core import Config, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from .account import StarlineAccount from .const import ( - CONF_SCAN_INTERVAL, CONF_SCAN_OBD_INTERVAL, DEFAULT_SCAN_INTERVAL, DEFAULT_SCAN_OBD_INTERVAL, diff --git a/homeassistant/components/starline/const.py b/homeassistant/components/starline/const.py index 89ea0873aa1..488a5cb9e0f 100644 --- a/homeassistant/components/starline/const.py +++ b/homeassistant/components/starline/const.py @@ -11,7 +11,6 @@ CONF_APP_SECRET = "app_secret" CONF_MFA_CODE = "mfa_code" CONF_CAPTCHA_CODE = "captcha_code" -CONF_SCAN_INTERVAL = "scan_interval" DEFAULT_SCAN_INTERVAL = 180 # in seconds CONF_SCAN_OBD_INTERVAL = "scan_obd_interval" DEFAULT_SCAN_OBD_INTERVAL = 10800 # 3 hours in seconds diff --git a/homeassistant/components/starline/lock.py b/homeassistant/components/starline/lock.py index 56cd8686186..0b158451fb3 100644 --- a/homeassistant/components/starline/lock.py +++ b/homeassistant/components/starline/lock.py @@ -8,7 +8,6 @@ from .entity import StarlineEntity async def async_setup_entry(hass, entry, async_add_entities): """Set up the StarLine lock.""" - account: StarlineAccount = hass.data[DOMAIN][entry.entry_id] entities = [] for device in account.api.devices.values(): From 13b881acfca9725f76a9be81827c5b0d61576c80 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sat, 13 Feb 2021 07:07:11 -0500 Subject: [PATCH 399/796] Use core constants for simplepush (#46465) --- homeassistant/components/simplepush/notify.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/simplepush/notify.py b/homeassistant/components/simplepush/notify.py index 1d101534157..5a83dec69f0 100644 --- a/homeassistant/components/simplepush/notify.py +++ b/homeassistant/components/simplepush/notify.py @@ -8,13 +8,12 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) -from homeassistant.const import CONF_PASSWORD +from homeassistant.const import CONF_EVENT, CONF_PASSWORD import homeassistant.helpers.config_validation as cv ATTR_ENCRYPTED = "encrypted" CONF_DEVICE_KEY = "device_key" -CONF_EVENT = "event" CONF_SALT = "salt" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -44,7 +43,6 @@ class SimplePushNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a Simplepush user.""" - title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) if self._password: From 1244fb4152680560b38f565b548c40e3db6b9f91 Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Sat, 13 Feb 2021 13:19:38 +0100 Subject: [PATCH 400/796] Bump dsmr_parser to 0.28, configure keep_alive_interval (#46464) --- homeassistant/components/dsmr/manifest.json | 2 +- homeassistant/components/dsmr/sensor.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json index c3f6aa4dea3..c442130bb9f 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.25"], + "requirements": ["dsmr_parser==0.28"], "codeowners": ["@Robbie1221"], "config_flow": false } diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 897fcd4e77b..aea12a863f0 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -215,6 +215,7 @@ async def async_setup_entry( config[CONF_DSMR_VERSION], update_entities_telegram, loop=hass.loop, + keep_alive_interval=60, ) else: reader_factory = partial( diff --git a/requirements_all.txt b/requirements_all.txt index 763d35a8cae..401009e9fef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -502,7 +502,7 @@ doorbirdpy==2.1.0 dovado==0.4.1 # homeassistant.components.dsmr -dsmr_parser==0.25 +dsmr_parser==0.28 # homeassistant.components.dwd_weather_warnings dwdwfsapi==1.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d57d3d2302..3fa94bd3117 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -272,7 +272,7 @@ distro==1.5.0 doorbirdpy==2.1.0 # homeassistant.components.dsmr -dsmr_parser==0.25 +dsmr_parser==0.28 # homeassistant.components.dynalite dynalite_devices==0.1.46 From bc1daf1802d2ef3ec6afe64882eb21ecf28baa49 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 13 Feb 2021 13:21:37 +0100 Subject: [PATCH 401/796] None optional hass typing in FlowHandler (#46462) Co-authored-by: Martin Hjelmare --- homeassistant/auth/mfa_modules/totp.py | 2 +- homeassistant/components/apple_tv/config_flow.py | 4 ---- .../components/arcam_fmj/config_flow.py | 4 ++-- homeassistant/components/axis/config_flow.py | 2 -- .../components/azure_devops/config_flow.py | 1 - homeassistant/components/bond/config_flow.py | 1 - .../components/broadlink/config_flow.py | 1 - homeassistant/components/brother/config_flow.py | 2 -- .../components/cert_expiry/config_flow.py | 4 +--- .../components/cloudflare/config_flow.py | 1 - homeassistant/components/deconz/config_flow.py | 2 -- homeassistant/components/denonavr/config_flow.py | 1 - homeassistant/components/directv/config_flow.py | 1 - homeassistant/components/doorbird/config_flow.py | 1 - homeassistant/components/elgato/config_flow.py | 3 --- homeassistant/components/esphome/config_flow.py | 2 -- .../components/forked_daapd/config_flow.py | 1 - homeassistant/components/fritzbox/config_flow.py | 2 -- .../fritzbox_callmonitor/config_flow.py | 4 +--- homeassistant/components/guardian/config_flow.py | 1 - homeassistant/components/harmony/config_flow.py | 1 - .../components/homekit_controller/config_flow.py | 2 -- .../components/huawei_lte/config_flow.py | 11 ++--------- homeassistant/components/hyperion/config_flow.py | 5 ----- homeassistant/components/ipp/config_flow.py | 1 - homeassistant/components/isy994/config_flow.py | 1 - homeassistant/components/kodi/config_flow.py | 1 - .../components/konnected/config_flow.py | 2 -- .../components/lutron_caseta/config_flow.py | 3 --- homeassistant/components/nut/config_flow.py | 1 - .../components/ovo_energy/config_flow.py | 1 - homeassistant/components/plex/config_flow.py | 7 ++----- homeassistant/components/plugwise/config_flow.py | 1 - .../components/powerwall/config_flow.py | 1 - homeassistant/components/roku/config_flow.py | 3 --- homeassistant/components/roomba/config_flow.py | 2 -- .../components/samsungtv/config_flow.py | 2 -- homeassistant/components/shelly/config_flow.py | 1 - homeassistant/components/smappee/config_flow.py | 3 --- homeassistant/components/sms/config_flow.py | 2 +- .../components/somfy_mylink/config_flow.py | 1 - homeassistant/components/songpal/config_flow.py | 1 - homeassistant/components/spotify/config_flow.py | 2 -- .../components/squeezebox/config_flow.py | 1 - homeassistant/components/syncthru/config_flow.py | 4 +--- .../components/synology_dsm/config_flow.py | 1 - homeassistant/components/toon/config_flow.py | 6 +----- homeassistant/components/unifi/config_flow.py | 2 -- homeassistant/components/upnp/config_flow.py | 1 - homeassistant/components/vizio/config_flow.py | 7 ------- homeassistant/components/wilight/config_flow.py | 1 - homeassistant/components/withings/config_flow.py | 3 --- homeassistant/components/wled/config_flow.py | 5 ----- .../components/xiaomi_aqara/config_flow.py | 1 - .../components/xiaomi_miio/config_flow.py | 1 - homeassistant/components/zwave_js/config_flow.py | 11 ----------- homeassistant/config_entries.py | 8 -------- homeassistant/data_entry_flow.py | 16 ++++++++++------ 58 files changed, 22 insertions(+), 141 deletions(-) diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 359f79ce6ce..4a6faef96c0 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -198,7 +198,7 @@ class TotpSetupFlow(SetupFlow): errors: Dict[str, str] = {} if user_input: - verified = await self.hass.async_add_executor_job( # type: ignore + verified = await self.hass.async_add_executor_job( pyotp.TOTP(self._ota_secret).verify, user_input["code"] ) if verified: diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py index 9c2f25b6d53..ef0a0cfe59e 100644 --- a/homeassistant/components/apple_tv/config_flow.py +++ b/homeassistant/components/apple_tv/config_flow.py @@ -101,10 +101,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(info[CONF_IDENTIFIER]) self.target_device = info[CONF_IDENTIFIER] - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = {"name": info[CONF_NAME]} - - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["identifier"] = self.unique_id return await self.async_step_reconfigure() @@ -170,7 +167,6 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(identifier) self._abort_if_unique_id_configured() - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["identifier"] = self.unique_id self.context["title_placeholders"] = {"name": name} self.target_device = identifier diff --git a/homeassistant/components/arcam_fmj/config_flow.py b/homeassistant/components/arcam_fmj/config_flow.py index d270af9295b..31735a0a037 100644 --- a/homeassistant/components/arcam_fmj/config_flow.py +++ b/homeassistant/components/arcam_fmj/config_flow.py @@ -71,7 +71,7 @@ class ArcamFmjFlowHandler(config_entries.ConfigFlow): async def async_step_confirm(self, user_input=None): """Handle user-confirmation of discovered node.""" - context = self.context # pylint: disable=no-member + context = self.context placeholders = { "host": context[CONF_HOST], } @@ -94,7 +94,7 @@ class ArcamFmjFlowHandler(config_entries.ConfigFlow): await self._async_set_unique_id_and_update(host, port, uuid) - context = self.context # pylint: disable=no-member + context = self.context context[CONF_HOST] = host context[CONF_PORT] = DEFAULT_PORT return await self.async_step_confirm() diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index d99c5329e32..c65f663f2b9 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -138,7 +138,6 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): async def async_step_reauth(self, device_config: dict): """Trigger a reauthentication flow.""" - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { CONF_NAME: device_config[CONF_NAME], CONF_HOST: device_config[CONF_HOST], @@ -204,7 +203,6 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): } ) - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { CONF_NAME: device[CONF_NAME], CONF_HOST: device[CONF_HOST], diff --git a/homeassistant/components/azure_devops/config_flow.py b/homeassistant/components/azure_devops/config_flow.py index e1e7d833926..d7d6f2868c3 100644 --- a/homeassistant/components/azure_devops/config_flow.py +++ b/homeassistant/components/azure_devops/config_flow.py @@ -95,7 +95,6 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): self._project = user_input[CONF_PROJECT] self._pat = user_input[CONF_PAT] - # pylint: disable=no-member self.context["title_placeholders"] = { "project_url": f"{self._organization}/{self._project}", } diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 2004da0c81e..9298961269e 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -73,7 +73,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_HOST: host, CONF_BOND_ID: bond_id, } - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context.update({"title_placeholders": self._discovered}) return await self.async_step_confirm() diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py index a2e770d6c4f..a309e4eb603 100644 --- a/homeassistant/components/broadlink/config_flow.py +++ b/homeassistant/components/broadlink/config_flow.py @@ -57,7 +57,6 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) self.device = device - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { "name": device.name, "model": device.model, diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index 6a9d2ca6746..49f1c0ed1a3 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -97,7 +97,6 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(self.brother.serial.lower()) self._abort_if_unique_id_configured() - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context.update( { "title_placeholders": { @@ -112,7 +111,6 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initiated by zeroconf.""" if user_input is not None: title = f"{self.brother.model} {self.brother.serial}" - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 return self.async_create_entry( title=title, data={CONF_HOST: self.host, CONF_TYPE: user_input[CONF_TYPE]}, diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py index 7953a7bb8cf..282c87b25c5 100644 --- a/homeassistant/components/cert_expiry/config_flow.py +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -63,9 +63,7 @@ class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=title, data={CONF_HOST: host, CONF_PORT: port}, ) - if ( # pylint: disable=no-member - self.context["source"] == config_entries.SOURCE_IMPORT - ): + if self.context["source"] == config_entries.SOURCE_IMPORT: _LOGGER.error("Config import failed for %s", user_input[CONF_HOST]) return self.async_abort(reason="import_failed") else: diff --git a/homeassistant/components/cloudflare/config_flow.py b/homeassistant/components/cloudflare/config_flow.py index c96e6455ce9..066fff9f704 100644 --- a/homeassistant/components/cloudflare/config_flow.py +++ b/homeassistant/components/cloudflare/config_flow.py @@ -97,7 +97,6 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN): if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") - assert self.hass persistent_notification.async_dismiss(self.hass, "cloudflare_setup") errors = {} diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index bc14af9ff11..d1ea3826e2f 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -176,7 +176,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth(self, config: dict): """Trigger a reauthentication flow.""" - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = {CONF_HOST: config[CONF_HOST]} self.deconz_config = { @@ -207,7 +206,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): updates={CONF_HOST: parsed_url.hostname, CONF_PORT: parsed_url.port} ) - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = {"host": parsed_url.hostname} self.deconz_config = { diff --git a/homeassistant/components/denonavr/config_flow.py b/homeassistant/components/denonavr/config_flow.py index 21c8da01783..ec0ab4fb177 100644 --- a/homeassistant/components/denonavr/config_flow.py +++ b/homeassistant/components/denonavr/config_flow.py @@ -225,7 +225,6 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured({CONF_HOST: self.host}) - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context.update( { "title_placeholders": { diff --git a/homeassistant/components/directv/config_flow.py b/homeassistant/components/directv/config_flow.py index cae0e62b1be..fed13c63dc8 100644 --- a/homeassistant/components/directv/config_flow.py +++ b/homeassistant/components/directv/config_flow.py @@ -79,7 +79,6 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): if discovery_info.get(ATTR_UPNP_SERIAL): receiver_id = discovery_info[ATTR_UPNP_SERIAL][4:] # strips off RID- - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context.update({"title_placeholders": {"name": host}}) self.discovery_info.update( diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 8e3f661254d..4bbd7f8dc86 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -103,7 +103,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if friendly_hostname.endswith(chop_ending): friendly_hostname = friendly_hostname[: -len(chop_ending)] - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { CONF_NAME: friendly_hostname, CONF_HOST: discovery_info[CONF_HOST], diff --git a/homeassistant/components/elgato/config_flow.py b/homeassistant/components/elgato/config_flow.py index a6f25b8827c..60cc08dc9b3 100644 --- a/homeassistant/components/elgato/config_flow.py +++ b/homeassistant/components/elgato/config_flow.py @@ -63,7 +63,6 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(info.serial_number) self._abort_if_unique_id_configured(updates={CONF_HOST: user_input[CONF_HOST]}) - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context.update( { CONF_HOST: user_input[CONF_HOST], @@ -76,7 +75,6 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): # Prepare configuration flow return self._show_confirm_dialog() - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 async def async_step_zeroconf_confirm( self, user_input: ConfigType = None ) -> Dict[str, Any]: @@ -119,7 +117,6 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): def _show_confirm_dialog(self) -> Dict[str, Any]: """Show the confirm dialog to the user.""" - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 serial_number = self.context.get(CONF_SERIAL_NUMBER) return self.async_show_form( step_id="zeroconf_confirm", diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 34b168cdd8c..a84aa2959ea 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -48,12 +48,10 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): @property def _name(self): - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 return self.context.get(CONF_NAME) @_name.setter def _name(self, value): - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context[CONF_NAME] = value self.context["title_placeholders"] = {"name": self._name} diff --git a/homeassistant/components/forked_daapd/config_flow.py b/homeassistant/components/forked_daapd/config_flow.py index 285f1382644..adc6e9b7b35 100644 --- a/homeassistant/components/forked_daapd/config_flow.py +++ b/homeassistant/components/forked_daapd/config_flow.py @@ -188,6 +188,5 @@ class ForkedDaapdFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_NAME: discovery_info["properties"]["Machine Name"], } self.discovery_schema = vol.Schema(fill_in_schema_dict(zeroconf_data)) - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context.update({"title_placeholders": zeroconf_data}) return await self.async_step_user() diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index f54211aa8a2..904081ef99f 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -43,8 +43,6 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - def __init__(self): """Initialize flow.""" self._entry = None diff --git a/homeassistant/components/fritzbox_callmonitor/config_flow.py b/homeassistant/components/fritzbox_callmonitor/config_flow.py index ab296c84121..a08450e20a1 100644 --- a/homeassistant/components/fritzbox_callmonitor/config_flow.py +++ b/homeassistant/components/fritzbox_callmonitor/config_flow.py @@ -165,9 +165,7 @@ class FritzBoxCallMonitorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if result != RESULT_SUCCESS: return self.async_abort(reason=result) - if ( # pylint: disable=no-member - self.context["source"] == config_entries.SOURCE_IMPORT - ): + if self.context["source"] == config_entries.SOURCE_IMPORT: self._phonebook_id = user_input[CONF_PHONEBOOK] self._phonebook_name = user_input[CONF_NAME] diff --git a/homeassistant/components/guardian/config_flow.py b/homeassistant/components/guardian/config_flow.py index 760cf960e43..a9286467afc 100644 --- a/homeassistant/components/guardian/config_flow.py +++ b/homeassistant/components/guardian/config_flow.py @@ -88,7 +88,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): pin = async_get_pin_from_discovery_hostname(discovery_info["hostname"]) await self._async_set_unique_id(pin) - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context[CONF_IP_ADDRESS] = discovery_info["host"] if any( diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index e01febbef43..899edeb8a91 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -89,7 +89,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self._host_already_configured(parsed_url.hostname): return self.async_abort(reason="already_configured") - # pylint: disable=no-member self.context["title_placeholders"] = {"name": friendly_name} self.harmony_config = { diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index e046a131a6b..38a41617c6a 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -253,7 +253,6 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): await self.async_set_unique_id(normalize_hkid(hkid)) self._abort_if_unique_id_configured() - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["hkid"] = hkid if paired: @@ -392,7 +391,6 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): @callback def _async_step_pair_show_form(self, errors=None): - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 placeholders = {"name": self.name} self.context["title_placeholders"] = {"name": self.name} diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index 350ad5bca0d..e38b873a5bb 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -70,10 +70,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_URL, default=user_input.get( CONF_URL, - # https://github.com/PyCQA/pylint/issues/3167 - self.context.get( # pylint: disable=no-member - CONF_URL, "" - ), + self.context.get(CONF_URL, ""), ), ), str, @@ -192,7 +189,6 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): title = info.get("DeviceName") return title or DEFAULT_DEVICE_NAME - assert self.hass is not None try: conn = await self.hass.async_add_executor_job(try_connect, user_input) except LoginErrorUsernameWrongException: @@ -218,7 +214,6 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input=user_input, errors=errors ) - # pylint: disable=no-member title = self.context.get("title_placeholders", {}).get( CONF_NAME ) or await self.hass.async_add_executor_job(get_router_title, conn) @@ -238,8 +233,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if "mobile" not in discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, "").lower(): return self.async_abort(reason="not_huawei_lte") - # https://github.com/PyCQA/pylint/issues/3167 - url = self.context[CONF_URL] = url_normalize( # pylint: disable=no-member + url = self.context[CONF_URL] = url_normalize( discovery_info.get( ssdp.ATTR_UPNP_PRESENTATION_URL, f"http://{urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname}/", @@ -255,7 +249,6 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if self._already_configured(user_input): return self.async_abort(reason="already_configured") - # pylint: disable=no-member self.context["title_placeholders"] = { CONF_NAME: discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) } diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py index f4528b0efbe..642bc0e93fd 100644 --- a/homeassistant/components/hyperion/config_flow.py +++ b/homeassistant/components/hyperion/config_flow.py @@ -270,7 +270,6 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): auth_resp = await hyperion_client.async_request_token( comment=DEFAULT_ORIGIN, id=auth_id ) - assert self.hass await self.hass.config_entries.flow.async_configure( flow_id=self.flow_id, user_input=auth_resp ) @@ -344,7 +343,6 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): # Start a task in the background requesting a new token. The next step will # wait on the response (which includes the user needing to visit the Hyperion # UI to approve the request for a new token). - assert self.hass assert self._auth_id is not None self._request_token_task = self.hass.async_create_task( self._request_token_task_func(self._auth_id) @@ -414,9 +412,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): 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 @@ -426,7 +422,6 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 return self.async_create_entry( title=f"{self._data[CONF_HOST]}:{self._data[CONF_PORT]}", data=self._data ) diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index feed7e7b528..3815dcf8f69 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -106,7 +106,6 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): tls = zctype == "_ipps._tcp.local." base_path = discovery_info["properties"].get("rp", "ipp/print") - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context.update({"title_placeholders": {"name": name}}) self.discovery_info.update( diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index 6049b8c6ec3..3d52687bced 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -168,7 +168,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_HOST: url, } - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = self.discovered_conf return await self.async_step_user() diff --git a/homeassistant/components/kodi/config_flow.py b/homeassistant/components/kodi/config_flow.py index c48e4564f92..69460a57570 100644 --- a/homeassistant/components/kodi/config_flow.py +++ b/homeassistant/components/kodi/config_flow.py @@ -119,7 +119,6 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } ) - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context.update({"title_placeholders": {CONF_NAME: self._name}}) try: diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py index dc15e7a86c4..219148e37cf 100644 --- a/homeassistant/components/konnected/config_flow.py +++ b/homeassistant/components/konnected/config_flow.py @@ -169,8 +169,6 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # class variable to store/share discovered host information discovered_hosts = {} - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - def __init__(self): """Initialize the Konnected flow.""" self.data = {} diff --git a/homeassistant/components/lutron_caseta/config_flow.py b/homeassistant/components/lutron_caseta/config_flow.py index ab9865f999a..6cd30a78f0c 100644 --- a/homeassistant/components/lutron_caseta/config_flow.py +++ b/homeassistant/components/lutron_caseta/config_flow.py @@ -77,7 +77,6 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured({CONF_HOST: host}) self.data[CONF_HOST] = host - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { CONF_NAME: self.bridge_id, CONF_HOST: host, @@ -201,8 +200,6 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import_failed(self, user_input=None): """Make failed import surfaced to user.""" - - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = {CONF_NAME: self.data[CONF_HOST]} if user_input is None: diff --git a/homeassistant/components/nut/config_flow.py b/homeassistant/components/nut/config_flow.py index 8a868d7bb39..7407958cdc0 100644 --- a/homeassistant/components/nut/config_flow.py +++ b/homeassistant/components/nut/config_flow.py @@ -129,7 +129,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Prepare configuration for a discovered nut device.""" self.discovery_info = discovery_info await self._async_handle_discovery_without_unique_id() - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { CONF_PORT: discovery_info.get(CONF_PORT, DEFAULT_PORT), CONF_HOST: discovery_info[CONF_HOST], diff --git a/homeassistant/components/ovo_energy/config_flow.py b/homeassistant/components/ovo_energy/config_flow.py index f395415d89e..0b2f7aac2d0 100644 --- a/homeassistant/components/ovo_energy/config_flow.py +++ b/homeassistant/components/ovo_energy/config_flow.py @@ -62,7 +62,6 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): if user_input and user_input.get(CONF_USERNAME): self.username = user_input[CONF_USERNAME] - # pylint: disable=no-member self.context["title_placeholders"] = {CONF_USERNAME: self.username} if user_input is not None and user_input.get(CONF_PASSWORD) is not None: diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index f177412e7ec..e52e4597bf9 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -230,10 +230,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } entry = await self.async_set_unique_id(server_id) - if ( - self.context[CONF_SOURCE] # pylint: disable=no-member - == config_entries.SOURCE_REAUTH - ): + if self.context[CONF_SOURCE] == config_entries.SOURCE_REAUTH: self.hass.config_entries.async_update_entry(entry, data=data) _LOGGER.debug("Updated config entry for %s", plex_server.friendly_name) await self.hass.config_entries.async_reload(entry.entry_id) @@ -280,7 +277,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() host = f"{discovery_info['from'][0]}:{discovery_info['data']['Port']}" name = discovery_info["data"]["Name"] - self.context["title_placeholders"] = { # pylint: disable=no-member + self.context["title_placeholders"] = { "host": host, "name": name, } diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index 247e0802eae..e17c85a7978 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -97,7 +97,6 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _version = _properties.get("version", "n/a") _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}" - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { CONF_HOST: discovery_info[CONF_HOST], CONF_PORT: discovery_info.get(CONF_PORT, DEFAULT_PORT), diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index b649b160085..eb804df3420 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -65,7 +65,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="already_configured") self.ip_address = dhcp_discovery[IP_ADDRESS] - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = {CONF_IP_ADDRESS: self.ip_address} return await self.async_step_user() diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py index f8e9034292c..b086d7a9311 100644 --- a/homeassistant/components/roku/config_flow.py +++ b/homeassistant/components/roku/config_flow.py @@ -109,7 +109,6 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): 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"]}) @@ -126,7 +125,6 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(serial_number) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context.update({"title_placeholders": {"name": name}}) self.discovery_info.update({CONF_HOST: host, CONF_NAME: name}) @@ -146,7 +144,6 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): 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="discovery_confirm", diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index b1f2b290a54..787382ed8b5 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -91,7 +91,6 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.host = dhcp_discovery[IP_ADDRESS] self.blid = blid - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = {"host": self.host, "name": self.blid} return await self.async_step_user() @@ -133,7 +132,6 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } if self.host and self.host in self.discovered_robots: # From discovery - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { "host": self.host, "name": self.discovered_robots[self.host].robot_name, diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 7a7fa26f922..73d0d0b5e93 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -51,8 +51,6 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - def __init__(self): """Initialize flow.""" self._host = None diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index b3a9b068ac4..cd74b83a62a 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -166,7 +166,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except HTTP_CONNECT_ERRORS: return self.async_abort(reason="cannot_connect") - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { "name": zeroconf_info.get("name", "").split(".")[0] } diff --git a/homeassistant/components/smappee/config_flow.py b/homeassistant/components/smappee/config_flow.py index c6d208626e4..450874b3f35 100644 --- a/homeassistant/components/smappee/config_flow.py +++ b/homeassistant/components/smappee/config_flow.py @@ -56,7 +56,6 @@ class SmappeeFlowHandler( if self.is_cloud_device_already_added(): return self.async_abort(reason="already_configured_device") - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context.update( { CONF_IP_ADDRESS: discovery_info["host"], @@ -76,7 +75,6 @@ class SmappeeFlowHandler( return self.async_abort(reason="already_configured_device") if user_input is None: - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 serialnumber = self.context.get(CONF_SERIALNUMBER) return self.async_show_form( step_id="zeroconf_confirm", @@ -84,7 +82,6 @@ class SmappeeFlowHandler( errors=errors, ) - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 ip_address = self.context.get(CONF_IP_ADDRESS) serial_number = self.context.get(CONF_SERIALNUMBER) diff --git a/homeassistant/components/sms/config_flow.py b/homeassistant/components/sms/config_flow.py index 52f3a403ed1..01c1d182c93 100644 --- a/homeassistant/components/sms/config_flow.py +++ b/homeassistant/components/sms/config_flow.py @@ -27,7 +27,7 @@ async def get_imei_from_config(hass: core.HomeAssistant, data): raise CannotConnect try: imei = await gateway.get_imei_async() - except gammu.GSMError as err: # pylint: disable=no-member + except gammu.GSMError as err: raise CannotConnect from err finally: await gateway.terminate_async() diff --git a/homeassistant/components/somfy_mylink/config_flow.py b/homeassistant/components/somfy_mylink/config_flow.py index ce69d265b55..b6d647b9a3b 100644 --- a/homeassistant/components/somfy_mylink/config_flow.py +++ b/homeassistant/components/somfy_mylink/config_flow.py @@ -72,7 +72,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.host = dhcp_discovery[HOSTNAME] self.mac = formatted_mac self.ip_address = dhcp_discovery[IP_ADDRESS] - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = {"ip": self.ip_address, "mac": self.mac} return await self.async_step_user() diff --git a/homeassistant/components/songpal/config_flow.py b/homeassistant/components/songpal/config_flow.py index 9acbedd11c7..aaa9302cac2 100644 --- a/homeassistant/components/songpal/config_flow.py +++ b/homeassistant/components/songpal/config_flow.py @@ -114,7 +114,6 @@ class SongpalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if "videoScreen" in service_types: return self.async_abort(reason="not_songpal_device") - # pylint: disable=no-member self.context["title_placeholders"] = { CONF_NAME: friendly_name, CONF_HOST: parsed_url.hostname, diff --git a/homeassistant/components/spotify/config_flow.py b/homeassistant/components/spotify/config_flow.py index ac6e101d4fe..afad75f0f39 100644 --- a/homeassistant/components/spotify/config_flow.py +++ b/homeassistant/components/spotify/config_flow.py @@ -63,7 +63,6 @@ class SpotifyFlowHandler( if entry: self.entry = entry - assert self.hass persistent_notification.async_create( self.hass, f"Spotify integration for account {entry['id']} needs to be re-authenticated. Please go to the integrations page to re-configure it.", @@ -85,7 +84,6 @@ class SpotifyFlowHandler( errors={}, ) - assert self.hass persistent_notification.async_dismiss(self.hass, "spotify_reauth") return await self.async_step_pick_implementation( diff --git a/homeassistant/components/squeezebox/config_flow.py b/homeassistant/components/squeezebox/config_flow.py index f5ed6073104..9edff5f9a2a 100644 --- a/homeassistant/components/squeezebox/config_flow.py +++ b/homeassistant/components/squeezebox/config_flow.py @@ -182,7 +182,6 @@ class SqueezeboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # update schema with suggested values from discovery self.data_schema = _base_schema(discovery_info) - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context.update({"title_placeholders": {"host": discovery_info[CONF_HOST]}}) return await self.async_step_edit() diff --git a/homeassistant/components/syncthru/config_flow.py b/homeassistant/components/syncthru/config_flow.py index cbdd46b4a6a..83f044d8ebc 100644 --- a/homeassistant/components/syncthru/config_flow.py +++ b/homeassistant/components/syncthru/config_flow.py @@ -63,9 +63,7 @@ class SyncThruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.name = re.sub(r"\s+\([\d.]+\)\s*$", "", self.name) # https://github.com/PyCQA/pylint/issues/3167 - self.context["title_placeholders"] = { # pylint: disable=no-member - CONF_NAME: self.name - } + self.context["title_placeholders"] = {CONF_NAME: self.name} return await self.async_step_confirm() async def async_step_confirm(self, user_input=None): diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 5a1ab53b3f7..f4638a5ec73 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -208,7 +208,6 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_NAME: friendly_name, CONF_HOST: parsed_url.hostname, } - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = self.discovered_conf return await self.async_step_user() diff --git a/homeassistant/components/toon/config_flow.py b/homeassistant/components/toon/config_flow.py index d1de68ef0b8..1e1739e85df 100644 --- a/homeassistant/components/toon/config_flow.py +++ b/homeassistant/components/toon/config_flow.py @@ -56,7 +56,6 @@ class ToonFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): """ if config is not None and CONF_MIGRATE in config: - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context.update({CONF_MIGRATE: config[CONF_MIGRATE]}) else: await self._async_handle_discovery_without_unique_id() @@ -87,10 +86,7 @@ class ToonFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): return await self._create_entry(self.agreements[agreement_index]) async def _create_entry(self, agreement: Agreement) -> Dict[str, Any]: - if ( # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - CONF_MIGRATE in self.context - ): - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + if CONF_MIGRATE in self.context: await self.hass.config_entries.async_remove(self.context[CONF_MIGRATE]) await self.async_set_unique_id(agreement.agreement_id) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 8e83f53d198..5a0a4969f09 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -195,7 +195,6 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): """Trigger a reauthentication flow.""" self.reauth_config_entry = config_entry - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { CONF_HOST: config_entry.data[CONF_HOST], CONF_SITE_ID: config_entry.title, @@ -229,7 +228,6 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): await self.async_set_unique_id(mac_address) self._abort_if_unique_id_configured(updates=self.config) - # pylint: disable=no-member self.context["title_placeholders"] = { CONF_HOST: self.config[CONF_HOST], CONF_SITE_ID: DEFAULT_SITE_ID, diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 41c56dddb29..d3811b7e18b 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -183,7 +183,6 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._discoveries = [discovery] # Ensure user recognizable. - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { "name": discovery[DISCOVERY_NAME], } diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 40f71adda12..3f57cdb81fa 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -206,7 +206,6 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: Dict[str, Any] = None ) -> Dict[str, Any]: """Handle a flow initialized by the user.""" - assert self.hass errors = {} if user_input is not None: @@ -232,7 +231,6 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors[CONF_HOST] = "existing_config_entry_found" if not errors: - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 if self._must_show_form and self.context["source"] == SOURCE_ZEROCONF: # Discovery should always display the config form before trying to # create entry so that user can update default config options @@ -251,7 +249,6 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not errors: return await self._create_entry(user_input) - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 elif self._must_show_form and self.context["source"] == SOURCE_IMPORT: # Import should always display the config form if CONF_ACCESS_TOKEN # wasn't included but is needed so that the user can choose to update @@ -271,7 +268,6 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): schema = self._user_schema or _get_config_schema() - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 if errors and self.context["source"] == SOURCE_IMPORT: # Log an error message if import config flow fails since otherwise failure is silent _LOGGER.error( @@ -346,8 +342,6 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: Optional[DiscoveryInfoType] = None ) -> Dict[str, Any]: """Handle zeroconf discovery.""" - assert self.hass - # If host already has port, no need to add it again if ":" not in discovery_info[CONF_HOST]: discovery_info[ @@ -432,7 +426,6 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._data[CONF_ACCESS_TOKEN] = pair_data.auth_token self._must_show_form = True - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 if self.context["source"] == SOURCE_IMPORT: # If user is pairing via config import, show different message return await self.async_step_pairing_complete_import() diff --git a/homeassistant/components/wilight/config_flow.py b/homeassistant/components/wilight/config_flow.py index 40643b4372f..32c66df65a7 100644 --- a/homeassistant/components/wilight/config_flow.py +++ b/homeassistant/components/wilight/config_flow.py @@ -84,7 +84,6 @@ class WiLightFlowHandler(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(self._serial_number) self._abort_if_unique_id_configured(updates={CONF_HOST: self._host}) - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = {"name": self._title} return await self.async_step_confirm() diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index d04327808de..ddf51741c62 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -48,7 +48,6 @@ class WithingsFlowHandler( async def async_step_profile(self, data: dict) -> dict: """Prompt the user to select a user profile.""" errors = {} - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 reauth_profile = ( self.context.get(const.PROFILE) if self.context.get("source") == "reauth" @@ -81,14 +80,12 @@ class WithingsFlowHandler( if data is not None: return await self.async_step_user() - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 placeholders = {const.PROFILE: self.context["profile"]} self.context.update({"title_placeholders": placeholders}) return self.async_show_form( step_id="reauth", - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 description_placeholders=placeholders, ) diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index 4d0f6bf1606..5915447f2f5 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -39,7 +39,6 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): host = user_input["hostname"].rstrip(".") name, _ = host.rsplit(".") - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context.update( { CONF_HOST: user_input["host"], @@ -62,7 +61,6 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): self, user_input: Optional[ConfigType] = None, prepare: bool = False ) -> Dict[str, Any]: """Config flow handler for WLED.""" - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 source = self.context.get("source") # Request user input, unless we are preparing discovery flow @@ -72,7 +70,6 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): return self._show_setup_form() if source == SOURCE_ZEROCONF: - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 user_input[CONF_HOST] = self.context.get(CONF_HOST) user_input[CONF_MAC] = self.context.get(CONF_MAC) @@ -93,7 +90,6 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): title = user_input[CONF_HOST] if source == SOURCE_ZEROCONF: - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 title = self.context.get(CONF_NAME) if prepare: @@ -114,7 +110,6 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): def _show_confirm_dialog(self, errors: Optional[Dict] = None) -> Dict[str, Any]: """Show the confirm dialog to the user.""" - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 name = self.context.get(CONF_NAME) return self.async_show_form( step_id="zeroconf_confirm", diff --git a/homeassistant/components/xiaomi_aqara/config_flow.py b/homeassistant/components/xiaomi_aqara/config_flow.py index 6bf1aa4f4ee..46d4852abae 100644 --- a/homeassistant/components/xiaomi_aqara/config_flow.py +++ b/homeassistant/components/xiaomi_aqara/config_flow.py @@ -181,7 +181,6 @@ class XiaomiAqaraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): {CONF_HOST: self.host, CONF_MAC: mac_address} ) - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context.update({"title_placeholders": {"name": self.host}}) return await self.async_step_user() diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 6ebb50cd7ce..6ed5f422f7c 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -67,7 +67,6 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured({CONF_HOST: self.host}) - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context.update( {"title_placeholders": {"name": f"Gateway {self.host}"}} ) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 5faaa02d03d..b18e28419dd 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -84,7 +84,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: """Handle the initial step.""" - assert self.hass # typing if self.hass.components.hassio.is_hassio(): return await self.async_step_on_supervisor() @@ -101,7 +100,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} - assert self.hass # typing try: version_info = await validate_input(self.hass, user_input) except InvalidInput as err: @@ -128,7 +126,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): This flow is triggered by the Z-Wave JS add-on. """ - assert self.hass self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}" try: version_info = await async_get_version_info(self.hass, self.ws_address) @@ -182,7 +179,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}" if not self.unique_id: - assert self.hass try: version_info = await async_get_version_info( self.hass, self.ws_address @@ -208,14 +204,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: """Install Z-Wave JS add-on.""" - assert self.hass if not self.install_task: self.install_task = self.hass.async_create_task(self._async_install_addon()) return self.async_show_progress( step_id="install_addon", progress_action="install_addon" ) - assert self.hass try: await self.install_task except self.hass.components.hassio.HassioAPIError as err: @@ -253,7 +247,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if new_addon_config != self.addon_config: await self._async_set_addon_config(new_addon_config) - assert self.hass try: await self.hass.components.hassio.async_start_addon("core_zwave_js") except self.hass.components.hassio.HassioAPIError as err: @@ -299,7 +292,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_get_addon_info(self) -> dict: """Return and cache Z-Wave JS add-on info.""" - assert self.hass try: addon_info: dict = await self.hass.components.hassio.async_get_addon_info( "core_zwave_js" @@ -327,7 +319,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_set_addon_config(self, config: dict) -> None: """Set Z-Wave JS add-on config.""" - assert self.hass options = {"options": config} try: await self.hass.components.hassio.async_set_addon_options( @@ -339,7 +330,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_install_addon(self) -> None: """Install the Z-Wave JS add-on.""" - assert self.hass try: await self.hass.components.hassio.async_install_addon("core_zwave_js") finally: @@ -350,7 +340,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_get_addon_discovery_info(self) -> dict: """Return add-on discovery info.""" - assert self.hass try: discovery_info: dict = ( await self.hass.components.hassio.async_get_addon_discovery_info( diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index e38136e33ca..bbc1479524a 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -903,7 +903,6 @@ class ConfigFlow(data_entry_flow.FlowHandler): reload_on_update: bool = True, ) -> None: """Abort if the unique ID is already configured.""" - assert self.hass if self.unique_id is None: return @@ -945,7 +944,6 @@ class ConfigFlow(data_entry_flow.FlowHandler): self.context["unique_id"] = unique_id # pylint: disable=no-member # Abort discoveries done using the default discovery unique id - assert self.hass is not None if unique_id != DEFAULT_DISCOVERY_UNIQUE_ID: for progress in self._async_in_progress(): if progress["context"].get("unique_id") == DEFAULT_DISCOVERY_UNIQUE_ID: @@ -963,7 +961,6 @@ class ConfigFlow(data_entry_flow.FlowHandler): If the flow is user initiated, filter out ignored entries unless include_ignore is True. """ - assert self.hass is not None config_entries = self.hass.config_entries.async_entries(self.handler) if include_ignore or self.source != SOURCE_USER: @@ -974,7 +971,6 @@ class ConfigFlow(data_entry_flow.FlowHandler): @callback def _async_current_ids(self, include_ignore: bool = True) -> Set[Optional[str]]: """Return current unique IDs.""" - assert self.hass is not None return { entry.unique_id for entry in self.hass.config_entries.async_entries(self.handler) @@ -984,7 +980,6 @@ class ConfigFlow(data_entry_flow.FlowHandler): @callback def _async_in_progress(self) -> List[Dict]: """Return other in progress flows for current domain.""" - assert self.hass is not None return [ flw for flw in self.hass.config_entries.flow.async_progress() @@ -1027,7 +1022,6 @@ class ConfigFlow(data_entry_flow.FlowHandler): self._abort_if_unique_id_configured() # Abort if any other flow for this handler is already in progress - assert self.hass is not None if self._async_in_progress(): raise data_entry_flow.AbortFlow("already_in_progress") @@ -1043,8 +1037,6 @@ class ConfigFlow(data_entry_flow.FlowHandler): self, *, reason: str, description_placeholders: Optional[Dict] = None ) -> Dict[str, Any]: """Abort the config flow.""" - assert self.hass - # Remove reauth notification if no reauth flows are in progress if self.source == SOURCE_REAUTH and not any( ent["context"]["source"] == SOURCE_REAUTH diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 85609556217..e8235c9a23c 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -3,7 +3,8 @@ from __future__ import annotations import abc import asyncio -from typing import Any, Dict, List, Optional, cast +from types import MappingProxyType +from typing import Any, Dict, List, Optional import uuid import voluptuous as vol @@ -264,11 +265,14 @@ class FlowHandler: """Handle the configuration flow of a component.""" # Set by flow manager - flow_id: str = None # type: ignore - hass: Optional[HomeAssistant] = None - handler: Optional[str] = None cur_step: Optional[Dict[str, str]] = None - context: Dict + # Ignore types, pylint workaround: https://github.com/PyCQA/pylint/issues/3167 + flow_id: str = None # type: ignore + hass: HomeAssistant = None # type: ignore + handler: str = None # type: ignore + # Pylint workaround: https://github.com/PyCQA/pylint/issues/3167 + # Ensure the attribute has a subscriptable, but immutable, default value. + context: Dict = MappingProxyType({}) # type: ignore # Set by _async_create_flow callback init_step = "init" @@ -339,7 +343,7 @@ class FlowHandler: ) -> Dict[str, Any]: """Abort the config flow.""" return _create_abort_data( - self.flow_id, cast(str, self.handler), reason, description_placeholders + self.flow_id, self.handler, reason, description_placeholders ) @callback From 52c5bc0a9977c6a2b9b2b33b3461aaf8af464c51 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 13 Feb 2021 14:23:40 +0200 Subject: [PATCH 402/796] Remove deprecated Synology integration (#46482) --- .coveragerc | 1 - homeassistant/components/synology/__init__.py | 1 - homeassistant/components/synology/camera.py | 143 ------------------ .../components/synology/manifest.json | 7 - requirements_all.txt | 3 - 5 files changed, 155 deletions(-) delete mode 100644 homeassistant/components/synology/__init__.py delete mode 100644 homeassistant/components/synology/camera.py delete mode 100644 homeassistant/components/synology/manifest.json diff --git a/.coveragerc b/.coveragerc index 6043d3d45f0..6339e09a034 100644 --- a/.coveragerc +++ b/.coveragerc @@ -899,7 +899,6 @@ omit = homeassistant/components/switcher_kis/switch.py homeassistant/components/switchmate/switch.py homeassistant/components/syncthru/sensor.py - homeassistant/components/synology/camera.py homeassistant/components/synology_chat/notify.py homeassistant/components/synology_dsm/__init__.py homeassistant/components/synology_dsm/binary_sensor.py diff --git a/homeassistant/components/synology/__init__.py b/homeassistant/components/synology/__init__.py deleted file mode 100644 index 0ab4b45e298..00000000000 --- a/homeassistant/components/synology/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The synology component.""" diff --git a/homeassistant/components/synology/camera.py b/homeassistant/components/synology/camera.py deleted file mode 100644 index 4417f72918d..00000000000 --- a/homeassistant/components/synology/camera.py +++ /dev/null @@ -1,143 +0,0 @@ -"""Support for Synology Surveillance Station Cameras.""" -from functools import partial -import logging - -import requests -from synology.surveillance_station import SurveillanceStation -import voluptuous as vol - -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera -from homeassistant.const import ( - CONF_NAME, - CONF_PASSWORD, - CONF_TIMEOUT, - CONF_URL, - CONF_USERNAME, - CONF_VERIFY_SSL, - CONF_WHITELIST, -) -from homeassistant.helpers.aiohttp_client import ( - async_aiohttp_proxy_web, - async_get_clientsession, -) -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "Synology Camera" -DEFAULT_TIMEOUT = 5 - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_URL): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list, - vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, - } -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up a Synology IP Camera.""" - _LOGGER.warning( - "The Synology integration is deprecated." - " Please use the Synology DSM integration" - " (https://www.home-assistant.io/integrations/synology_dsm/) instead." - " This integration will be removed in version 0.118.0." - ) - - verify_ssl = config.get(CONF_VERIFY_SSL) - timeout = config.get(CONF_TIMEOUT) - - try: - surveillance = await hass.async_add_executor_job( - partial( - SurveillanceStation, - config.get(CONF_URL), - config.get(CONF_USERNAME), - config.get(CONF_PASSWORD), - verify_ssl=verify_ssl, - timeout=timeout, - ) - ) - except (requests.exceptions.RequestException, ValueError): - _LOGGER.exception("Error when initializing SurveillanceStation") - return False - - cameras = surveillance.get_all_cameras() - - # add cameras - devices = [] - for camera in cameras: - if not config[CONF_WHITELIST] or camera.name in config[CONF_WHITELIST]: - device = SynologyCamera(surveillance, camera.camera_id, verify_ssl) - devices.append(device) - - async_add_entities(devices) - - -class SynologyCamera(Camera): - """An implementation of a Synology NAS based IP camera.""" - - def __init__(self, surveillance, camera_id, verify_ssl): - """Initialize a Synology Surveillance Station camera.""" - super().__init__() - self._surveillance = surveillance - self._camera_id = camera_id - self._verify_ssl = verify_ssl - self._camera = self._surveillance.get_camera(camera_id) - self._motion_setting = self._surveillance.get_motion_setting(camera_id) - self.is_streaming = self._camera.is_enabled - - def camera_image(self): - """Return bytes of camera image.""" - return self._surveillance.get_camera_image(self._camera_id) - - async def handle_async_mjpeg_stream(self, request): - """Return a MJPEG stream image response directly from the camera.""" - streaming_url = self._camera.video_stream_url - - websession = async_get_clientsession(self.hass, self._verify_ssl) - stream_coro = websession.get(streaming_url) - - return await async_aiohttp_proxy_web(self.hass, request, stream_coro) - - @property - def name(self): - """Return the name of this device.""" - return self._camera.name - - @property - def is_recording(self): - """Return true if the device is recording.""" - return self._camera.is_recording - - @property - def should_poll(self): - """Update the recording state periodically.""" - return True - - def update(self): - """Update the status of the camera.""" - self._surveillance.update() - self._camera = self._surveillance.get_camera(self._camera.camera_id) - self._motion_setting = self._surveillance.get_motion_setting( - self._camera.camera_id - ) - self.is_streaming = self._camera.is_enabled - - @property - def motion_detection_enabled(self): - """Return the camera motion detection status.""" - return self._motion_setting.is_enabled - - def enable_motion_detection(self): - """Enable motion detection in the camera.""" - self._surveillance.enable_motion_detection(self._camera_id) - - def disable_motion_detection(self): - """Disable motion detection in camera.""" - self._surveillance.disable_motion_detection(self._camera_id) diff --git a/homeassistant/components/synology/manifest.json b/homeassistant/components/synology/manifest.json deleted file mode 100644 index a29dccc2a78..00000000000 --- a/homeassistant/components/synology/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "synology", - "name": "Synology", - "documentation": "https://www.home-assistant.io/integrations/synology", - "requirements": ["py-synology==0.2.0"], - "codeowners": [] -} diff --git a/requirements_all.txt b/requirements_all.txt index 401009e9fef..9e7a75e8905 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1212,9 +1212,6 @@ py-nightscout==1.2.2 # homeassistant.components.schluter py-schluter==0.1.7 -# homeassistant.components.synology -py-synology==0.2.0 - # homeassistant.components.zabbix py-zabbix==1.1.7 From 6f261a09b04687f7d52a54a3a9a2c7ddc7595317 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 13 Feb 2021 15:07:55 +0200 Subject: [PATCH 403/796] Remove deprecated xfinity integration (#46484) --- .coveragerc | 1 - CODEOWNERS | 1 - homeassistant/components/xfinity/__init__.py | 1 - .../components/xfinity/device_tracker.py | 64 ------------------- .../components/xfinity/manifest.json | 7 -- requirements_all.txt | 3 - 6 files changed, 77 deletions(-) delete mode 100644 homeassistant/components/xfinity/__init__.py delete mode 100644 homeassistant/components/xfinity/device_tracker.py delete mode 100644 homeassistant/components/xfinity/manifest.json diff --git a/.coveragerc b/.coveragerc index 6339e09a034..b5427435636 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1065,7 +1065,6 @@ omit = homeassistant/components/xbox/sensor.py homeassistant/components/xbox_live/sensor.py homeassistant/components/xeoma/camera.py - homeassistant/components/xfinity/device_tracker.py homeassistant/components/xiaomi/camera.py homeassistant/components/xiaomi_aqara/__init__.py homeassistant/components/xiaomi_aqara/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 0e33076105f..3d7ff5f79a5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -525,7 +525,6 @@ homeassistant/components/workday/* @fabaff homeassistant/components/worldclock/* @fabaff homeassistant/components/xbox/* @hunterjm homeassistant/components/xbox_live/* @MartinHjelmare -homeassistant/components/xfinity/* @cisasteelersfan homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi homeassistant/components/xiaomi_miio/* @rytilahti @syssi @starkillerOG homeassistant/components/xiaomi_tv/* @simse diff --git a/homeassistant/components/xfinity/__init__.py b/homeassistant/components/xfinity/__init__.py deleted file mode 100644 index 22e37eccde9..00000000000 --- a/homeassistant/components/xfinity/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The xfinity component.""" diff --git a/homeassistant/components/xfinity/device_tracker.py b/homeassistant/components/xfinity/device_tracker.py deleted file mode 100644 index 832c8bb1d5d..00000000000 --- a/homeassistant/components/xfinity/device_tracker.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Support for device tracking via Xfinity Gateways.""" -import logging - -from requests.exceptions import RequestException -import voluptuous as vol -from xfinity_gateway import XfinityGateway - -from homeassistant.components.device_tracker import ( - DOMAIN, - PLATFORM_SCHEMA, - DeviceScanner, -) -from homeassistant.const import CONF_HOST -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_HOST = "10.0.0.1" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string} -) - - -def get_scanner(hass, config): - """Validate the configuration and return an Xfinity Gateway scanner.""" - _LOGGER.warning( - "The Xfinity Gateway has been deprecated and will be removed from " - "Home Assistant in version 0.109. Please remove it from your " - "configuration. " - ) - - gateway = XfinityGateway(config[DOMAIN][CONF_HOST]) - scanner = None - try: - gateway.scan_devices() - scanner = XfinityDeviceScanner(gateway) - except (RequestException, ValueError): - _LOGGER.error( - "Error communicating with Xfinity Gateway. Check host: %s", gateway.host - ) - - return scanner - - -class XfinityDeviceScanner(DeviceScanner): - """This class queries an Xfinity Gateway.""" - - def __init__(self, gateway): - """Initialize the scanner.""" - self.gateway = gateway - - def scan_devices(self): - """Scan for new devices and return a list of found MACs.""" - connected_devices = [] - try: - connected_devices = self.gateway.scan_devices() - except (RequestException, ValueError): - _LOGGER.error("Unable to scan devices. Check connection to gateway") - return connected_devices - - def get_device_name(self, device): - """Return the name of the given device or None if we don't know.""" - return self.gateway.get_device_name(device) diff --git a/homeassistant/components/xfinity/manifest.json b/homeassistant/components/xfinity/manifest.json deleted file mode 100644 index 999b77dfb59..00000000000 --- a/homeassistant/components/xfinity/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "xfinity", - "name": "Xfinity Gateway", - "documentation": "https://www.home-assistant.io/integrations/xfinity", - "requirements": ["xfinity-gateway==0.0.4"], - "codeowners": ["@cisasteelersfan"] -} diff --git a/requirements_all.txt b/requirements_all.txt index 9e7a75e8905..dbad80603d4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2313,9 +2313,6 @@ xbox-webapi==2.0.8 # homeassistant.components.xbox_live xboxapi==2.0.1 -# homeassistant.components.xfinity -xfinity-gateway==0.0.4 - # homeassistant.components.knx xknx==0.16.3 From f38b06ed6d18842e2921ae3bbec56ccad5f16972 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Sat, 13 Feb 2021 19:17:06 +0100 Subject: [PATCH 404/796] Add Asuswrt Config Flow and Scanner Entities (#46468) * Add Asuswrt config flow (#43948) * Add AsusWrt Scanner Entity (#44759) * Add Scanner Entity - device tracker entity changed from "DeviceScanner" to "ScannerEntity" - sensors recoded to use "router" class - config entry review to allow multiple entity (for future use) * Force checks * Removed new option and change sensors * Update test_sensor.py * Requested changes * Removed router unique-id * Update last_activity attr only when available * Add Options for AsusWRT Scanner Entity (#44808) * Add Asuswrt config flow (#43948) * Add AsusWrt Scanner Entity (#44759) * Add Scanner Entity - device tracker entity changed from "DeviceScanner" to "ScannerEntity" - sensors recoded to use "router" class - config entry review to allow multiple entity (for future use) * Force checks * Removed new option and change sensors * Update test_sensor.py * Requested changes * Removed router unique-id * Update last_activity attr only when available * Add Options for Scanner Entity * Fix isort * Removed "Track New" option * Add Options for Scanner Entity * Fix isort * Removed "Track New" option * Add test for all the options in the config flow --- .coveragerc | 2 + homeassistant/components/asuswrt/__init__.py | 232 +++++++++----- .../components/asuswrt/config_flow.py | 238 ++++++++++++++ homeassistant/components/asuswrt/const.py | 24 ++ .../components/asuswrt/device_tracker.py | 171 +++++++--- .../components/asuswrt/manifest.json | 1 + homeassistant/components/asuswrt/router.py | 274 ++++++++++++++++ homeassistant/components/asuswrt/sensor.py | 98 +++++- homeassistant/components/asuswrt/strings.json | 45 +++ .../components/asuswrt/translations/en.json | 45 +++ homeassistant/generated/config_flows.py | 1 + tests/components/asuswrt/test_config_flow.py | 296 ++++++++++++++++++ .../components/asuswrt/test_device_tracker.py | 119 ------- tests/components/asuswrt/test_sensor.py | 175 ++++++++--- 14 files changed, 1406 insertions(+), 315 deletions(-) create mode 100644 homeassistant/components/asuswrt/config_flow.py create mode 100644 homeassistant/components/asuswrt/const.py create mode 100644 homeassistant/components/asuswrt/router.py create mode 100644 homeassistant/components/asuswrt/strings.json create mode 100644 homeassistant/components/asuswrt/translations/en.json create mode 100644 tests/components/asuswrt/test_config_flow.py delete mode 100644 tests/components/asuswrt/test_device_tracker.py diff --git a/.coveragerc b/.coveragerc index b5427435636..3bf3fa10947 100644 --- a/.coveragerc +++ b/.coveragerc @@ -67,6 +67,8 @@ omit = homeassistant/components/arwn/sensor.py homeassistant/components/asterisk_cdr/mailbox.py homeassistant/components/asterisk_mbox/* + homeassistant/components/asuswrt/__init__.py + homeassistant/components/asuswrt/router.py homeassistant/components/aten_pe/* homeassistant/components/atome/* homeassistant/components/aurora/__init__.py diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index 9cd47d803de..d2eb47fa2d2 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -1,9 +1,10 @@ """Support for ASUSWRT devices.""" +import asyncio import logging -from aioasuswrt.asuswrt import AsusWrt import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_MODE, @@ -12,108 +13,165 @@ from homeassistant.const import ( CONF_PROTOCOL, CONF_SENSORS, CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.discovery import async_load_platform -from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ( + CONF_DNSMASQ, + CONF_INTERFACE, + CONF_REQUIRE_IP, + CONF_SSH_KEY, + DATA_ASUSWRT, + DEFAULT_DNSMASQ, + DEFAULT_INTERFACE, + DEFAULT_SSH_PORT, + DOMAIN, + MODE_AP, + MODE_ROUTER, + PROTOCOL_SSH, + PROTOCOL_TELNET, + SENSOR_TYPES, +) +from .router import AsusWrtRouter + +PLATFORMS = ["device_tracker", "sensor"] + +CONF_PUB_KEY = "pub_key" +SECRET_GROUP = "Password or SSH Key" _LOGGER = logging.getLogger(__name__) -CONF_DNSMASQ = "dnsmasq" -CONF_INTERFACE = "interface" -CONF_PUB_KEY = "pub_key" -CONF_REQUIRE_IP = "require_ip" -CONF_SSH_KEY = "ssh_key" - -DOMAIN = "asuswrt" -DATA_ASUSWRT = DOMAIN - -DEFAULT_SSH_PORT = 22 -DEFAULT_INTERFACE = "eth0" -DEFAULT_DNSMASQ = "/var/lib/misc" - -FIRST_RETRY_TIME = 60 -MAX_RETRY_TIME = 900 - -SECRET_GROUP = "Password or SSH Key" -SENSOR_TYPES = ["devices", "upload_speed", "download_speed", "download", "upload"] - CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_PROTOCOL, default="ssh"): vol.In(["ssh", "telnet"]), - vol.Optional(CONF_MODE, default="router"): vol.In(["router", "ap"]), - vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port, - vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean, - vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string, - vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile, - vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile, - vol.Optional(CONF_SENSORS): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - vol.Optional(CONF_INTERFACE, default=DEFAULT_INTERFACE): cv.string, - vol.Optional(CONF_DNSMASQ, default=DEFAULT_DNSMASQ): cv.string, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_PROTOCOL, default=PROTOCOL_SSH): vol.In( + [PROTOCOL_SSH, PROTOCOL_TELNET] + ), + vol.Optional(CONF_MODE, default=MODE_ROUTER): vol.In( + [MODE_ROUTER, MODE_AP] + ), + vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port, + vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean, + vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string, + vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile, + vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile, + vol.Optional(CONF_SENSORS): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ), + vol.Optional(CONF_INTERFACE, default=DEFAULT_INTERFACE): cv.string, + vol.Optional(CONF_DNSMASQ, default=DEFAULT_DNSMASQ): cv.string, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) -async def async_setup(hass, config, retry_delay=FIRST_RETRY_TIME): - """Set up the asuswrt component.""" - conf = config[DOMAIN] - - api = AsusWrt( - conf[CONF_HOST], - conf[CONF_PORT], - conf[CONF_PROTOCOL] == "telnet", - conf[CONF_USERNAME], - conf.get(CONF_PASSWORD, ""), - conf.get("ssh_key", conf.get("pub_key", "")), - conf[CONF_MODE], - conf[CONF_REQUIRE_IP], - interface=conf[CONF_INTERFACE], - dnsmasq=conf[CONF_DNSMASQ], - ) - - try: - await api.connection.async_connect() - except OSError as ex: - _LOGGER.warning( - "Error [%s] connecting %s to %s. Will retry in %s seconds...", - str(ex), - DOMAIN, - conf[CONF_HOST], - retry_delay, - ) - - async def retry_setup(now): - """Retry setup if a error happens on asuswrt API.""" - await async_setup( - hass, config, retry_delay=min(2 * retry_delay, MAX_RETRY_TIME) - ) - - async_call_later(hass, retry_delay, retry_setup) - +async def async_setup(hass, config): + """Set up the AsusWrt integration.""" + conf = config.get(DOMAIN) + if conf is None: return True - if not api.is_connected: - _LOGGER.error("Error connecting %s to %s", DOMAIN, conf[CONF_HOST]) - return False + # save the options from config yaml + options = {} + mode = conf.get(CONF_MODE, MODE_ROUTER) + for name, value in conf.items(): + if name in ([CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP]): + if name == CONF_REQUIRE_IP and mode != MODE_AP: + continue + options[name] = value + hass.data[DOMAIN] = {"yaml_options": options} - hass.data[DATA_ASUSWRT] = api + # check if already configured + domains_list = hass.config_entries.async_domains() + if DOMAIN in domains_list: + return True + + # remove not required config keys + pub_key = conf.pop(CONF_PUB_KEY, "") + if pub_key: + conf[CONF_SSH_KEY] = pub_key + + conf.pop(CONF_REQUIRE_IP, True) + conf.pop(CONF_SENSORS, {}) + conf.pop(CONF_INTERFACE, "") + conf.pop(CONF_DNSMASQ, "") hass.async_create_task( - async_load_platform( - hass, "sensor", DOMAIN, config[DOMAIN].get(CONF_SENSORS), config + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf ) ) - hass.async_create_task( - async_load_platform(hass, "device_tracker", DOMAIN, {}, config) - ) return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Set up AsusWrt platform.""" + + # import options from yaml if empty + yaml_options = hass.data.get(DOMAIN, {}).pop("yaml_options", {}) + if not entry.options and yaml_options: + hass.config_entries.async_update_entry(entry, options=yaml_options) + + router = AsusWrtRouter(hass, entry) + await router.setup() + + router.async_on_close(entry.add_update_listener(update_listener)) + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + async def async_close_connection(event): + """Close AsusWrt connection on HA Stop.""" + await router.close() + + stop_listener = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, async_close_connection + ) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + DATA_ASUSWRT: router, + "stop_listener": stop_listener, + } + + return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN][entry.entry_id]["stop_listener"]() + router = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT] + await router.close() + + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +async def update_listener(hass: HomeAssistantType, entry: ConfigEntry): + """Update when config_entry options update.""" + router = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT] + + if router.update_options(entry.options): + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/asuswrt/config_flow.py b/homeassistant/components/asuswrt/config_flow.py new file mode 100644 index 00000000000..303b3cc3822 --- /dev/null +++ b/homeassistant/components/asuswrt/config_flow.py @@ -0,0 +1,238 @@ +"""Config flow to configure the AsusWrt integration.""" +import logging +import os +import socket + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.device_tracker.const import ( + CONF_CONSIDER_HOME, + DEFAULT_CONSIDER_HOME, +) +from homeassistant.const import ( + CONF_HOST, + CONF_MODE, + CONF_PASSWORD, + CONF_PORT, + CONF_PROTOCOL, + CONF_USERNAME, +) +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv + +# pylint:disable=unused-import +from .const import ( + CONF_DNSMASQ, + CONF_INTERFACE, + CONF_REQUIRE_IP, + CONF_SSH_KEY, + CONF_TRACK_UNKNOWN, + DEFAULT_DNSMASQ, + DEFAULT_INTERFACE, + DEFAULT_SSH_PORT, + DEFAULT_TRACK_UNKNOWN, + DOMAIN, + MODE_AP, + MODE_ROUTER, + PROTOCOL_SSH, + PROTOCOL_TELNET, +) +from .router import get_api + +RESULT_CONN_ERROR = "cannot_connect" +RESULT_UNKNOWN = "unknown" +RESULT_SUCCESS = "success" + +_LOGGER = logging.getLogger(__name__) + + +def _is_file(value) -> bool: + """Validate that the value is an existing file.""" + file_in = os.path.expanduser(str(value)) + + if not os.path.isfile(file_in): + return False + if not os.access(file_in, os.R_OK): + return False + return True + + +def _get_ip(host): + """Get the ip address from the host name.""" + try: + return socket.gethostbyname(host) + except socket.gaierror: + return None + + +class AsusWrtFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize AsusWrt config flow.""" + self._host = None + + @callback + def _show_setup_form(self, user_input=None, errors=None): + """Show the setup form to the user.""" + + if user_input is None: + user_input = {} + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str, + vol.Required( + CONF_USERNAME, default=user_input.get(CONF_USERNAME, "") + ): str, + vol.Optional(CONF_PASSWORD): str, + vol.Optional(CONF_SSH_KEY): str, + vol.Required(CONF_PROTOCOL, default=PROTOCOL_SSH): vol.In( + {PROTOCOL_SSH: "SSH", PROTOCOL_TELNET: "Telnet"} + ), + vol.Required(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port, + vol.Required(CONF_MODE, default=MODE_ROUTER): vol.In( + {MODE_ROUTER: "Router", MODE_AP: "Access Point"} + ), + } + ), + errors=errors or {}, + ) + + async def _async_check_connection(self, user_input): + """Attempt to connect the AsusWrt router.""" + + api = get_api(user_input) + try: + await api.connection.async_connect() + + except OSError: + _LOGGER.error("Error connecting to the AsusWrt router at %s", self._host) + return RESULT_CONN_ERROR + + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Unknown error connecting with AsusWrt router at %s", self._host + ) + return RESULT_UNKNOWN + + if not api.is_connected: + _LOGGER.error("Error connecting to the AsusWrt router at %s", self._host) + return RESULT_CONN_ERROR + + conf_protocol = user_input[CONF_PROTOCOL] + if conf_protocol == PROTOCOL_TELNET: + await api.connection.disconnect() + return RESULT_SUCCESS + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if user_input is None: + return self._show_setup_form(user_input) + + errors = {} + self._host = user_input[CONF_HOST] + pwd = user_input.get(CONF_PASSWORD) + ssh = user_input.get(CONF_SSH_KEY) + + if not (pwd or ssh): + errors["base"] = "pwd_or_ssh" + elif ssh: + if pwd: + errors["base"] = "pwd_and_ssh" + else: + isfile = await self.hass.async_add_executor_job(_is_file, ssh) + if not isfile: + errors["base"] = "ssh_not_file" + + if not errors: + ip_address = await self.hass.async_add_executor_job(_get_ip, self._host) + if not ip_address: + errors["base"] = "invalid_host" + + if not errors: + result = await self._async_check_connection(user_input) + if result != RESULT_SUCCESS: + errors["base"] = result + + if errors: + return self._show_setup_form(user_input, errors) + + return self.async_create_entry( + title=self._host, + data=user_input, + ) + + async def async_step_import(self, user_input=None): + """Import a config entry.""" + return await self.async_step_user(user_input) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for AsusWrt.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + data_schema = vol.Schema( + { + vol.Optional( + CONF_CONSIDER_HOME, + default=self.config_entry.options.get( + CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds() + ), + ): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=900)), + vol.Optional( + CONF_TRACK_UNKNOWN, + default=self.config_entry.options.get( + CONF_TRACK_UNKNOWN, DEFAULT_TRACK_UNKNOWN + ), + ): bool, + vol.Required( + CONF_INTERFACE, + default=self.config_entry.options.get( + CONF_INTERFACE, DEFAULT_INTERFACE + ), + ): str, + vol.Required( + CONF_DNSMASQ, + default=self.config_entry.options.get( + CONF_DNSMASQ, DEFAULT_DNSMASQ + ), + ): str, + } + ) + + conf_mode = self.config_entry.data[CONF_MODE] + if conf_mode == MODE_AP: + data_schema = data_schema.extend( + { + vol.Optional( + CONF_REQUIRE_IP, + default=self.config_entry.options.get(CONF_REQUIRE_IP, True), + ): bool, + } + ) + + return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/asuswrt/const.py b/homeassistant/components/asuswrt/const.py new file mode 100644 index 00000000000..40752e81a08 --- /dev/null +++ b/homeassistant/components/asuswrt/const.py @@ -0,0 +1,24 @@ +"""AsusWrt component constants.""" +DOMAIN = "asuswrt" + +CONF_DNSMASQ = "dnsmasq" +CONF_INTERFACE = "interface" +CONF_REQUIRE_IP = "require_ip" +CONF_SSH_KEY = "ssh_key" +CONF_TRACK_UNKNOWN = "track_unknown" + +DATA_ASUSWRT = DOMAIN + +DEFAULT_DNSMASQ = "/var/lib/misc" +DEFAULT_INTERFACE = "eth0" +DEFAULT_SSH_PORT = 22 +DEFAULT_TRACK_UNKNOWN = False + +MODE_AP = "ap" +MODE_ROUTER = "router" + +PROTOCOL_SSH = "ssh" +PROTOCOL_TELNET = "telnet" + +# Sensor +SENSOR_TYPES = ["devices", "upload_speed", "download_speed", "download", "upload"] diff --git a/homeassistant/components/asuswrt/device_tracker.py b/homeassistant/components/asuswrt/device_tracker.py index a3545183d2e..85553674dba 100644 --- a/homeassistant/components/asuswrt/device_tracker.py +++ b/homeassistant/components/asuswrt/device_tracker.py @@ -1,64 +1,143 @@ """Support for ASUSWRT routers.""" import logging +from typing import Dict -from homeassistant.components.device_tracker import DeviceScanner +from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import callback +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType -from . import DATA_ASUSWRT +from .const import DATA_ASUSWRT, DOMAIN +from .router import AsusWrtRouter + +DEFAULT_DEVICE_NAME = "Unknown device" _LOGGER = logging.getLogger(__name__) -async def async_get_scanner(hass, config): - """Validate the configuration and return an ASUS-WRT scanner.""" - scanner = AsusWrtDeviceScanner(hass.data[DATA_ASUSWRT]) - await scanner.async_connect() - return scanner if scanner.success_init else None +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Set up device tracker for AsusWrt component.""" + router = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT] + tracked = set() + + @callback + def update_router(): + """Update the values of the router.""" + add_entities(router, async_add_entities, tracked) + + router.async_on_close( + async_dispatcher_connect(hass, router.signal_device_new, update_router) + ) + + update_router() -class AsusWrtDeviceScanner(DeviceScanner): - """This class queries a router running ASUSWRT firmware.""" +@callback +def add_entities(router, async_add_entities, tracked): + """Add new tracker entities from the router.""" + new_tracked = [] - # Eighth attribute needed for mode (AP mode vs router mode) - def __init__(self, api): - """Initialize the scanner.""" - self.last_results = {} - self.success_init = False - self.connection = api - self._connect_error = False + for mac, device in router.devices.items(): + if mac in tracked: + continue - async def async_connect(self): - """Initialize connection to the router.""" - # Test the router is accessible. - data = await self.connection.async_get_connected_devices() - self.success_init = data is not None + new_tracked.append(AsusWrtDevice(router, device)) + tracked.add(mac) - async def async_scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - await self.async_update_info() - return list(self.last_results) + if new_tracked: + async_add_entities(new_tracked) - async def async_get_device_name(self, device): - """Return the name of the given device or None if we don't know.""" - if device not in self.last_results: - return None - return self.last_results[device].name - async def async_update_info(self): - """Ensure the information from the ASUSWRT router is up to date. +class AsusWrtDevice(ScannerEntity): + """Representation of a AsusWrt device.""" - Return boolean if scanning successful. - """ - _LOGGER.debug("Checking Devices") + def __init__(self, router: AsusWrtRouter, device) -> None: + """Initialize a AsusWrt device.""" + self._router = router + self._mac = device.mac + self._name = device.name or DEFAULT_DEVICE_NAME + self._active = False + self._icon = None + self._attrs = {} - try: - self.last_results = await self.connection.async_get_connected_devices() - if self._connect_error: - self._connect_error = False - _LOGGER.info("Reconnected to ASUS router for device update") + @callback + def async_update_state(self) -> None: + """Update the AsusWrt device.""" + device = self._router.devices[self._mac] + self._active = device.is_connected - except OSError as err: - if not self._connect_error: - self._connect_error = True - _LOGGER.error( - "Error connecting to ASUS router for device update: %s", err - ) + self._attrs = { + "mac": device.mac, + "ip_address": device.ip_address, + } + if device.last_activity: + self._attrs["last_time_reachable"] = device.last_activity.isoformat( + timespec="seconds" + ) + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._mac + + @property + def name(self) -> str: + """Return the name.""" + return self._name + + @property + def is_connected(self): + """Return true if the device is connected to the network.""" + return self._active + + @property + def source_type(self) -> str: + """Return the source type.""" + return SOURCE_TYPE_ROUTER + + @property + def icon(self) -> str: + """Return the icon.""" + return self._icon + + @property + def device_state_attributes(self) -> Dict[str, any]: + """Return the attributes.""" + return self._attrs + + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return { + "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "AsusWRT Tracked device", + } + + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False + + @callback + def async_on_demand_update(self): + """Update state.""" + self.async_update_state() + self.async_write_ha_state() + + async def async_added_to_hass(self): + """Register state update callback.""" + self.async_update_state() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self._router.signal_device_update, + self.async_on_demand_update, + ) + ) diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json index 9afb7849f8c..744a05b9728 100644 --- a/homeassistant/components/asuswrt/manifest.json +++ b/homeassistant/components/asuswrt/manifest.json @@ -1,6 +1,7 @@ { "domain": "asuswrt", "name": "ASUSWRT", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/asuswrt", "requirements": ["aioasuswrt==1.3.1"], "codeowners": ["@kennedyshead"] diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py new file mode 100644 index 00000000000..11545919b43 --- /dev/null +++ b/homeassistant/components/asuswrt/router.py @@ -0,0 +1,274 @@ +"""Represent the AsusWrt router.""" +from datetime import datetime, timedelta +import logging +from typing import Any, Dict, Optional + +from aioasuswrt.asuswrt import AsusWrt + +from homeassistant.components.device_tracker.const import ( + CONF_CONSIDER_HOME, + DEFAULT_CONSIDER_HOME, + DOMAIN as TRACKER_DOMAIN, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_MODE, + CONF_PASSWORD, + CONF_PORT, + CONF_PROTOCOL, + CONF_USERNAME, +) +from homeassistant.core import CALLBACK_TYPE, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import dt as dt_util + +from .const import ( + CONF_DNSMASQ, + CONF_INTERFACE, + CONF_REQUIRE_IP, + CONF_SSH_KEY, + CONF_TRACK_UNKNOWN, + DEFAULT_DNSMASQ, + DEFAULT_INTERFACE, + DEFAULT_TRACK_UNKNOWN, + DOMAIN, + PROTOCOL_TELNET, +) + +CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP] +SCAN_INTERVAL = timedelta(seconds=30) + +_LOGGER = logging.getLogger(__name__) + + +class AsusWrtDevInfo: + """Representation of a AsusWrt device info.""" + + def __init__(self, mac, name=None): + """Initialize a AsusWrt device info.""" + self._mac = mac + self._name = name + self._ip_address = None + self._last_activity = None + self._connected = False + + def update(self, dev_info=None, consider_home=0): + """Update AsusWrt device info.""" + utc_point_in_time = dt_util.utcnow() + if dev_info: + if not self._name: + self._name = dev_info.name or self._mac.replace(":", "_") + self._ip_address = dev_info.ip + self._last_activity = utc_point_in_time + self._connected = True + + elif self._connected: + self._connected = ( + utc_point_in_time - self._last_activity + ).total_seconds() < consider_home + self._ip_address = None + + @property + def is_connected(self): + """Return connected status.""" + return self._connected + + @property + def mac(self): + """Return device mac address.""" + return self._mac + + @property + def name(self): + """Return device name.""" + return self._name + + @property + def ip_address(self): + """Return device ip address.""" + return self._ip_address + + @property + def last_activity(self): + """Return device last activity.""" + return self._last_activity + + +class AsusWrtRouter: + """Representation of a AsusWrt router.""" + + def __init__(self, hass: HomeAssistantType, entry: ConfigEntry) -> None: + """Initialize a AsusWrt router.""" + self.hass = hass + self._entry = entry + + self._api: AsusWrt = None + self._protocol = entry.data[CONF_PROTOCOL] + self._host = entry.data[CONF_HOST] + + self._devices: Dict[str, Any] = {} + self._connect_error = False + + self._on_close = [] + + self._options = { + CONF_DNSMASQ: DEFAULT_DNSMASQ, + CONF_INTERFACE: DEFAULT_INTERFACE, + CONF_REQUIRE_IP: True, + } + self._options.update(entry.options) + + async def setup(self) -> None: + """Set up a AsusWrt router.""" + self._api = get_api(self._entry.data, self._options) + + try: + await self._api.connection.async_connect() + except OSError as exp: + raise ConfigEntryNotReady from exp + + if not self._api.is_connected: + raise ConfigEntryNotReady + + # Load tracked entities from registry + entity_registry = await self.hass.helpers.entity_registry.async_get_registry() + track_entries = ( + self.hass.helpers.entity_registry.async_entries_for_config_entry( + entity_registry, self._entry.entry_id + ) + ) + for entry in track_entries: + if entry.domain == TRACKER_DOMAIN: + self._devices[entry.unique_id] = AsusWrtDevInfo( + entry.unique_id, entry.original_name + ) + + # Update devices + await self.update_devices() + + self.async_on_close( + async_track_time_interval(self.hass, self.update_all, SCAN_INTERVAL) + ) + + async def update_all(self, now: Optional[datetime] = None) -> None: + """Update all AsusWrt platforms.""" + await self.update_devices() + + async def update_devices(self) -> None: + """Update AsusWrt devices tracker.""" + new_device = False + _LOGGER.debug("Checking devices for ASUS router %s", self._host) + try: + wrt_devices = await self._api.async_get_connected_devices() + except OSError as exc: + if not self._connect_error: + self._connect_error = True + _LOGGER.error( + "Error connecting to ASUS router %s for device update: %s", + self._host, + exc, + ) + return + + if self._connect_error: + self._connect_error = False + _LOGGER.info("Reconnected to ASUS router %s", self._host) + + consider_home = self._options.get( + CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds() + ) + track_unknown = self._options.get(CONF_TRACK_UNKNOWN, DEFAULT_TRACK_UNKNOWN) + + for device_mac in self._devices: + dev_info = wrt_devices.get(device_mac) + self._devices[device_mac].update(dev_info, consider_home) + + for device_mac, dev_info in wrt_devices.items(): + if device_mac in self._devices: + continue + if not track_unknown and not dev_info.name: + continue + new_device = True + device = AsusWrtDevInfo(device_mac) + device.update(dev_info) + self._devices[device_mac] = device + + async_dispatcher_send(self.hass, self.signal_device_update) + if new_device: + async_dispatcher_send(self.hass, self.signal_device_new) + + async def close(self) -> None: + """Close the connection.""" + if self._api is not None: + if self._protocol == PROTOCOL_TELNET: + await self._api.connection.disconnect() + self._api = None + + for func in self._on_close: + func() + self._on_close.clear() + + @callback + def async_on_close(self, func: CALLBACK_TYPE) -> None: + """Add a function to call when router is closed.""" + self._on_close.append(func) + + def update_options(self, new_options: Dict) -> bool: + """Update router options.""" + req_reload = False + for name, new_opt in new_options.items(): + if name in (CONF_REQ_RELOAD): + old_opt = self._options.get(name) + if not old_opt or old_opt != new_opt: + req_reload = True + break + + self._options.update(new_options) + return req_reload + + @property + def signal_device_new(self) -> str: + """Event specific per AsusWrt entry to signal new device.""" + return f"{DOMAIN}-device-new" + + @property + def signal_device_update(self) -> str: + """Event specific per AsusWrt entry to signal updates in devices.""" + return f"{DOMAIN}-device-update" + + @property + def host(self) -> str: + """Return router hostname.""" + return self._host + + @property + def devices(self) -> Dict[str, Any]: + """Return devices.""" + return self._devices + + @property + def api(self) -> AsusWrt: + """Return router API.""" + return self._api + + +def get_api(conf: Dict, options: Optional[Dict] = None) -> AsusWrt: + """Get the AsusWrt API.""" + opt = options or {} + + return AsusWrt( + conf[CONF_HOST], + conf[CONF_PORT], + conf[CONF_PROTOCOL] == PROTOCOL_TELNET, + conf[CONF_USERNAME], + conf.get(CONF_PASSWORD, ""), + conf.get(CONF_SSH_KEY, ""), + conf[CONF_MODE], + opt.get(CONF_REQUIRE_IP, True), + interface=opt.get(CONF_INTERFACE, DEFAULT_INTERFACE), + dnsmasq=opt.get(CONF_DNSMASQ, DEFAULT_DNSMASQ), + ) diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index aa13bee81d0..2a39d339f06 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -6,13 +6,15 @@ from typing import Any, Dict, List, Optional from aioasuswrt.asuswrt import AsusWrt -from homeassistant.const import DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME, DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) -from . import DATA_ASUSWRT +from .const import DATA_ASUSWRT, DOMAIN, SENSOR_TYPES UPLOAD_ICON = "mdi:upload-network" DOWNLOAD_ICON = "mdi:download-network" @@ -35,6 +37,8 @@ class _SensorTypes(enum.Enum): return DATA_GIGABYTES if self in (_SensorTypes.UPLOAD_SPEED, _SensorTypes.DOWNLOAD_SPEED): return DATA_RATE_MEGABITS_PER_SECOND + if self == _SensorTypes.DEVICES: + return "devices" return None @property @@ -72,15 +76,26 @@ class _SensorTypes(enum.Enum): return self in (_SensorTypes.UPLOAD, _SensorTypes.DOWNLOAD) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the asuswrt sensors.""" - if discovery_info is None: - return +class _SensorInfo: + """Class handling sensor information.""" - api: AsusWrt = hass.data[DATA_ASUSWRT] + def __init__(self, sensor_type: _SensorTypes): + """Initialize the handler class.""" + self.type = sensor_type + self.enabled = False + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Set up the asuswrt sensors.""" + + router = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT] + api: AsusWrt = router.api + device_name = entry.data.get(CONF_NAME, "AsusWRT") # Let's discover the valid sensor types. - sensors = [_SensorTypes(x) for x in discovery_info] + sensors = [_SensorInfo(_SensorTypes(x)) for x in SENSOR_TYPES] data_handler = AsuswrtDataHandler(sensors, api) coordinator = DataUpdateCoordinator( @@ -93,34 +108,50 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) await coordinator.async_refresh() - async_add_entities([AsuswrtSensor(coordinator, x) for x in sensors]) + async_add_entities( + [AsuswrtSensor(coordinator, data_handler, device_name, x.type) for x in sensors] + ) class AsuswrtDataHandler: """Class handling the API updates.""" - def __init__(self, sensors: List[_SensorTypes], api: AsusWrt): + def __init__(self, sensors: List[_SensorInfo], api: AsusWrt): """Initialize the handler class.""" self._api = api self._sensors = sensors self._connected = True + def enable_sensor(self, sensor_type: _SensorTypes): + """Enable a specific sensor type.""" + for index, sensor in enumerate(self._sensors): + if sensor.type == sensor_type: + self._sensors[index].enabled = True + return + + def disable_sensor(self, sensor_type: _SensorTypes): + """Disable a specific sensor type.""" + for index, sensor in enumerate(self._sensors): + if sensor.type == sensor_type: + self._sensors[index].enabled = False + return + async def update_data(self) -> Dict[_SensorTypes, Any]: """Fetch the relevant data from the router.""" ret_dict: Dict[_SensorTypes, Any] = {} try: - if _SensorTypes.DEVICES in self._sensors: + if _SensorTypes.DEVICES in [x.type for x in self._sensors if x.enabled]: # Let's check the nr of devices. devices = await self._api.async_get_connected_devices() ret_dict[_SensorTypes.DEVICES] = len(devices) - if any(x.is_speed for x in self._sensors): + if any(x.type.is_speed for x in self._sensors if x.enabled): # Let's check the upload and download speed speed = await self._api.async_get_current_transfer_rates() ret_dict[_SensorTypes.DOWNLOAD_SPEED] = round(speed[0] / 125000, 2) ret_dict[_SensorTypes.UPLOAD_SPEED] = round(speed[1] / 125000, 2) - if any(x.is_size for x in self._sensors): + if any(x.type.is_size for x in self._sensors if x.enabled): rates = await self._api.async_get_bytes_total() ret_dict[_SensorTypes.DOWNLOAD] = round(rates[0] / 1000000000, 1) ret_dict[_SensorTypes.UPLOAD] = round(rates[1] / 1000000000, 1) @@ -142,9 +173,17 @@ class AsuswrtDataHandler: class AsuswrtSensor(CoordinatorEntity): """The asuswrt specific sensor class.""" - def __init__(self, coordinator: DataUpdateCoordinator, sensor_type: _SensorTypes): + def __init__( + self, + coordinator: DataUpdateCoordinator, + data_handler: AsuswrtDataHandler, + device_name: str, + sensor_type: _SensorTypes, + ): """Initialize the sensor class.""" super().__init__(coordinator) + self._handler = data_handler + self._device_name = device_name self._type = sensor_type @property @@ -164,5 +203,34 @@ class AsuswrtSensor(CoordinatorEntity): @property def unit_of_measurement(self) -> Optional[str]: - """Return the unit of measurement of this entity, if any.""" + """Return the unit.""" return self._type.unit_of_measurement + + @property + def unique_id(self) -> str: + """Return the unique_id of the sensor.""" + return f"{DOMAIN} {self._type.sensor_name}" + + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return { + "identifiers": {(DOMAIN, "AsusWRT")}, + "name": self._device_name, + "model": "Asus Router", + "manufacturer": "Asus", + } + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return False + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + self._handler.enable_sensor(self._type) + await super().async_added_to_hass() + + async def async_will_remove_from_hass(self): + """Call when entity is removed from hass.""" + self._handler.disable_sensor(self._type) diff --git a/homeassistant/components/asuswrt/strings.json b/homeassistant/components/asuswrt/strings.json new file mode 100644 index 00000000000..079ee35bf95 --- /dev/null +++ b/homeassistant/components/asuswrt/strings.json @@ -0,0 +1,45 @@ +{ + "config": { + "step": { + "user": { + "title": "AsusWRT", + "description": "Set required parameter to connect to your router", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "name": "[%key:common::config_flow::data::name%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "ssh_key": "Path to your SSH key file (instead of password)", + "protocol": "Communication protocol to use", + "port": "[%key:common::config_flow::data::port%]", + "mode": "[%key:common::config_flow::data::mode%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_host": "[%key:common::config_flow::error::invalid_host%]", + "pwd_and_ssh": "Only provide password or SSH key file", + "pwd_or_ssh": "Please provide password or SSH key file", + "ssh_not_file": "SSH key file not found", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + }, + "options": { + "step": { + "init": { + "title": "AsusWRT Options", + "data": { + "consider_home": "Seconds to wait before considering a device away", + "track_unknown": "Track unknown / unamed devices", + "interface": "The interface that you want statistics from (e.g. eth0,eth1 etc)", + "dnsmasq": "The location in the router of the dnsmasq.leases files", + "require_ip": "Devices must have IP (for access point mode)" + } + } + } + } +} diff --git a/homeassistant/components/asuswrt/translations/en.json b/homeassistant/components/asuswrt/translations/en.json new file mode 100644 index 00000000000..5ac87e277f4 --- /dev/null +++ b/homeassistant/components/asuswrt/translations/en.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_host": "Invalid hostname or IP address", + "pwd_and_ssh": "Only provide password or SSH key file", + "pwd_or_ssh": "Please provide password or SSH key file", + "ssh_not_file": "SSH key file not found", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "mode": "Mode", + "name": "Name", + "password": "Password", + "port": "Port", + "protocol": "Communication protocol to use", + "ssh_key": "Path to your SSH key file (instead of password)", + "username": "Username" + }, + "description": "Set required parameter to connect to your router", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Seconds to wait before considering a device away", + "dnsmasq": "The location in the router of the dnsmasq.leases files", + "interface": "The interface that you want statistics from (e.g. eth0,eth1 etc)", + "require_ip": "Devices must have IP (for access point mode)", + "track_unknown": "Track unknown / unamed devices" + }, + "title": "AsusWRT Options" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 06e2516633e..f5f550b3073 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -21,6 +21,7 @@ FLOWS = [ "ambient_station", "apple_tv", "arcam_fmj", + "asuswrt", "atag", "august", "aurora", diff --git a/tests/components/asuswrt/test_config_flow.py b/tests/components/asuswrt/test_config_flow.py new file mode 100644 index 00000000000..7faec5d336c --- /dev/null +++ b/tests/components/asuswrt/test_config_flow.py @@ -0,0 +1,296 @@ +"""Tests for the AsusWrt config flow.""" +from socket import gaierror +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.asuswrt.const import ( + CONF_DNSMASQ, + CONF_INTERFACE, + CONF_REQUIRE_IP, + CONF_SSH_KEY, + CONF_TRACK_UNKNOWN, + DOMAIN, +) +from homeassistant.components.device_tracker.const import CONF_CONSIDER_HOME +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import ( + CONF_HOST, + CONF_MODE, + CONF_PASSWORD, + CONF_PORT, + CONF_PROTOCOL, + CONF_USERNAME, +) + +from tests.common import MockConfigEntry + +HOST = "myrouter.asuswrt.com" +IP_ADDRESS = "192.168.1.1" +SSH_KEY = "1234" + +CONFIG_DATA = { + CONF_HOST: HOST, + CONF_PORT: 22, + CONF_PROTOCOL: "telnet", + CONF_USERNAME: "user", + CONF_PASSWORD: "pwd", + CONF_MODE: "ap", +} + + +@pytest.fixture(name="connect") +def mock_controller_connect(): + """Mock a successful connection.""" + with patch("homeassistant.components.asuswrt.router.AsusWrt") as service_mock: + service_mock.return_value.connection.async_connect = AsyncMock() + service_mock.return_value.is_connected = True + service_mock.return_value.connection.disconnect = AsyncMock() + yield service_mock + + +async def test_user(hass, connect): + """Test user config.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + # test with all provided + with patch( + "homeassistant.components.asuswrt.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.asuswrt.config_flow.socket.gethostbyname", + return_value=IP_ADDRESS, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=CONFIG_DATA, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == HOST + assert result["data"] == CONFIG_DATA + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import(hass, connect): + """Test import step.""" + with patch( + "homeassistant.components.asuswrt.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.asuswrt.config_flow.socket.gethostbyname", + return_value=IP_ADDRESS, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=CONFIG_DATA, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == HOST + assert result["data"] == CONFIG_DATA + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_ssh(hass, connect): + """Test import step with ssh file.""" + config_data = CONFIG_DATA.copy() + config_data.pop(CONF_PASSWORD) + config_data[CONF_SSH_KEY] = SSH_KEY + + with patch( + "homeassistant.components.asuswrt.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.asuswrt.config_flow.socket.gethostbyname", + return_value=IP_ADDRESS, + ), patch( + "homeassistant.components.asuswrt.config_flow.os.path.isfile", + return_value=True, + ), patch( + "homeassistant.components.asuswrt.config_flow.os.access", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config_data, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == HOST + assert result["data"] == config_data + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_error_no_password_ssh(hass): + """Test we abort if component is already setup.""" + config_data = CONFIG_DATA.copy() + config_data.pop(CONF_PASSWORD) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=config_data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "pwd_or_ssh"} + + +async def test_error_both_password_ssh(hass): + """Test we abort if component is already setup.""" + config_data = CONFIG_DATA.copy() + config_data[CONF_SSH_KEY] = SSH_KEY + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=config_data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "pwd_and_ssh"} + + +async def test_error_invalid_ssh(hass): + """Test we abort if component is already setup.""" + config_data = CONFIG_DATA.copy() + config_data.pop(CONF_PASSWORD) + config_data[CONF_SSH_KEY] = SSH_KEY + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=config_data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "ssh_not_file"} + + +async def test_error_invalid_host(hass): + """Test we abort if host name is invalid.""" + with patch( + "homeassistant.components.asuswrt.config_flow.socket.gethostbyname", + side_effect=gaierror, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=CONFIG_DATA, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_host"} + + +async def test_abort_if_already_setup(hass): + """Test we abort if component is already setup.""" + MockConfigEntry( + domain=DOMAIN, + data=CONFIG_DATA, + ).add_to_hass(hass) + + with patch( + "homeassistant.components.asuswrt.config_flow.socket.gethostbyname", + return_value=IP_ADDRESS, + ): + # Should fail, same HOST (flow) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=CONFIG_DATA, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" + + # Should fail, same HOST (import) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=CONFIG_DATA, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" + + +async def test_on_connect_failed(hass): + """Test when we have errors connecting the router.""" + flow_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + with patch("homeassistant.components.asuswrt.router.AsusWrt") as asus_wrt: + asus_wrt.return_value.connection.async_connect = AsyncMock() + asus_wrt.return_value.is_connected = False + result = await hass.config_entries.flow.async_configure( + flow_result["flow_id"], user_input=CONFIG_DATA + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + with patch("homeassistant.components.asuswrt.router.AsusWrt") as asus_wrt: + asus_wrt.return_value.connection.async_connect = AsyncMock(side_effect=OSError) + result = await hass.config_entries.flow.async_configure( + flow_result["flow_id"], user_input=CONFIG_DATA + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + with patch("homeassistant.components.asuswrt.router.AsusWrt") as asus_wrt: + asus_wrt.return_value.connection.async_connect = AsyncMock( + side_effect=TypeError + ) + result = await hass.config_entries.flow.async_configure( + flow_result["flow_id"], user_input=CONFIG_DATA + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "unknown"} + + +async def test_options_flow(hass): + """Test config flow options.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=CONFIG_DATA, + options={CONF_REQUIRE_IP: True}, + ) + config_entry.add_to_hass(hass) + + with patch("homeassistant.components.asuswrt.async_setup_entry", return_value=True): + 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"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_CONSIDER_HOME: 20, + CONF_TRACK_UNKNOWN: True, + CONF_INTERFACE: "aaa", + CONF_DNSMASQ: "bbb", + CONF_REQUIRE_IP: False, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options[CONF_CONSIDER_HOME] == 20 + assert config_entry.options[CONF_TRACK_UNKNOWN] is True + assert config_entry.options[CONF_INTERFACE] == "aaa" + assert config_entry.options[CONF_DNSMASQ] == "bbb" + assert config_entry.options[CONF_REQUIRE_IP] is False diff --git a/tests/components/asuswrt/test_device_tracker.py b/tests/components/asuswrt/test_device_tracker.py deleted file mode 100644 index 941b0c340d6..00000000000 --- a/tests/components/asuswrt/test_device_tracker.py +++ /dev/null @@ -1,119 +0,0 @@ -"""The tests for the ASUSWRT device tracker platform.""" - -from unittest.mock import AsyncMock, patch - -from homeassistant.components.asuswrt import ( - CONF_DNSMASQ, - CONF_INTERFACE, - DATA_ASUSWRT, - DOMAIN, -) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.setup import async_setup_component - - -async def test_password_or_pub_key_required(hass): - """Test creating an AsusWRT scanner without a pass or pubkey.""" - with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: - AsusWrt().connection.async_connect = AsyncMock() - AsusWrt().is_connected = False - result = await async_setup_component( - hass, DOMAIN, {DOMAIN: {CONF_HOST: "fake_host", CONF_USERNAME: "fake_user"}} - ) - assert not result - - -async def test_network_unreachable(hass): - """Test creating an AsusWRT scanner without a pass or pubkey.""" - with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: - AsusWrt().connection.async_connect = AsyncMock(side_effect=OSError) - AsusWrt().is_connected = False - result = await async_setup_component( - hass, DOMAIN, {DOMAIN: {CONF_HOST: "fake_host", CONF_USERNAME: "fake_user"}} - ) - assert result - assert hass.data.get(DATA_ASUSWRT) is None - - -async def test_get_scanner_with_password_no_pubkey(hass): - """Test creating an AsusWRT scanner with a password and no pubkey.""" - with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: - AsusWrt().connection.async_connect = AsyncMock() - AsusWrt().connection.async_get_connected_devices = AsyncMock(return_value={}) - result = await async_setup_component( - hass, - DOMAIN, - { - DOMAIN: { - CONF_HOST: "fake_host", - CONF_USERNAME: "fake_user", - CONF_PASSWORD: "4321", - CONF_DNSMASQ: "/", - } - }, - ) - assert result - assert hass.data[DATA_ASUSWRT] is not None - - -async def test_specify_non_directory_path_for_dnsmasq(hass): - """Test creating an AsusWRT scanner with a dnsmasq location which is not a valid directory.""" - with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: - AsusWrt().connection.async_connect = AsyncMock() - AsusWrt().is_connected = False - result = await async_setup_component( - hass, - DOMAIN, - { - DOMAIN: { - CONF_HOST: "fake_host", - CONF_USERNAME: "fake_user", - CONF_PASSWORD: "4321", - CONF_DNSMASQ: 1234, - } - }, - ) - assert not result - - -async def test_interface(hass): - """Test creating an AsusWRT scanner using interface eth1.""" - with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: - AsusWrt().connection.async_connect = AsyncMock() - AsusWrt().connection.async_get_connected_devices = AsyncMock(return_value={}) - result = await async_setup_component( - hass, - DOMAIN, - { - DOMAIN: { - CONF_HOST: "fake_host", - CONF_USERNAME: "fake_user", - CONF_PASSWORD: "4321", - CONF_DNSMASQ: "/", - CONF_INTERFACE: "eth1", - } - }, - ) - assert result - assert hass.data[DATA_ASUSWRT] is not None - - -async def test_no_interface(hass): - """Test creating an AsusWRT scanner using no interface.""" - with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: - AsusWrt().connection.async_connect = AsyncMock() - AsusWrt().is_connected = False - result = await async_setup_component( - hass, - DOMAIN, - { - DOMAIN: { - CONF_HOST: "fake_host", - CONF_USERNAME: "fake_user", - CONF_PASSWORD: "4321", - CONF_DNSMASQ: "/", - CONF_INTERFACE: None, - } - }, - ) - assert not result diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py index 69c70c409d5..994111370fd 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -1,71 +1,150 @@ -"""The tests for the AsusWrt sensor platform.""" - +"""Tests for the AsusWrt sensor.""" +from datetime import timedelta from unittest.mock import AsyncMock, patch from aioasuswrt.asuswrt import Device +import pytest -from homeassistant.components import sensor -from homeassistant.components.asuswrt import ( - CONF_DNSMASQ, - CONF_INTERFACE, +from homeassistant.components import device_tracker, sensor +from homeassistant.components.asuswrt.const import DOMAIN +from homeassistant.components.asuswrt.sensor import _SensorTypes +from homeassistant.components.device_tracker.const import CONF_CONSIDER_HOME +from homeassistant.const import ( + CONF_HOST, CONF_MODE, + CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, - CONF_SENSORS, - DOMAIN, + CONF_USERNAME, + STATE_HOME, + STATE_NOT_HOME, ) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow -VALID_CONFIG_ROUTER_SSH = { - DOMAIN: { - CONF_DNSMASQ: "/", - CONF_HOST: "fake_host", - CONF_INTERFACE: "eth0", - CONF_MODE: "router", - CONF_PORT: "22", - CONF_PROTOCOL: "ssh", - CONF_USERNAME: "fake_user", - CONF_PASSWORD: "fake_pass", - CONF_SENSORS: [ - "devices", - "download_speed", - "download", - "upload_speed", - "upload", - ], - } +from tests.common import MockConfigEntry, async_fire_time_changed + +HOST = "myrouter.asuswrt.com" +IP_ADDRESS = "192.168.1.1" + +CONFIG_DATA = { + CONF_HOST: HOST, + CONF_PORT: 22, + CONF_PROTOCOL: "ssh", + CONF_USERNAME: "user", + CONF_PASSWORD: "pwd", + CONF_MODE: "router", } MOCK_DEVICES = { "a1:b1:c1:d1:e1:f1": Device("a1:b1:c1:d1:e1:f1", "192.168.1.2", "Test"), "a2:b2:c2:d2:e2:f2": Device("a2:b2:c2:d2:e2:f2", "192.168.1.3", "TestTwo"), - "a3:b3:c3:d3:e3:f3": Device("a3:b3:c3:d3:e3:f3", "192.168.1.4", "TestThree"), } MOCK_BYTES_TOTAL = [60000000000, 50000000000] MOCK_CURRENT_TRANSFER_RATES = [20000000, 10000000] -async def test_sensors(hass: HomeAssistant, mock_device_tracker_conf): - """Test creating an AsusWRT sensor.""" - with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: - AsusWrt().connection.async_connect = AsyncMock() - AsusWrt().async_get_connected_devices = AsyncMock(return_value=MOCK_DEVICES) - AsusWrt().async_get_bytes_total = AsyncMock(return_value=MOCK_BYTES_TOTAL) - AsusWrt().async_get_current_transfer_rates = AsyncMock( +@pytest.fixture(name="connect") +def mock_controller_connect(): + """Mock a successful connection.""" + with patch("homeassistant.components.asuswrt.router.AsusWrt") as service_mock: + service_mock.return_value.connection.async_connect = AsyncMock() + service_mock.return_value.is_connected = True + service_mock.return_value.connection.disconnect = AsyncMock() + service_mock.return_value.async_get_connected_devices = AsyncMock( + return_value=MOCK_DEVICES + ) + service_mock.return_value.async_get_bytes_total = AsyncMock( + return_value=MOCK_BYTES_TOTAL + ) + service_mock.return_value.async_get_current_transfer_rates = AsyncMock( return_value=MOCK_CURRENT_TRANSFER_RATES ) + yield service_mock - assert await async_setup_component(hass, DOMAIN, VALID_CONFIG_ROUTER_SSH) - await hass.async_block_till_done() - assert ( - hass.states.get(f"{sensor.DOMAIN}.asuswrt_devices_connected").state == "3" - ) - assert ( - hass.states.get(f"{sensor.DOMAIN}.asuswrt_download_speed").state == "160.0" - ) - assert hass.states.get(f"{sensor.DOMAIN}.asuswrt_download").state == "60.0" - assert hass.states.get(f"{sensor.DOMAIN}.asuswrt_upload_speed").state == "80.0" - assert hass.states.get(f"{sensor.DOMAIN}.asuswrt_upload").state == "50.0" +async def test_sensors(hass, connect): + """Test creating an AsusWRT sensor.""" + entity_reg = await hass.helpers.entity_registry.async_get_registry() + + # Pre-enable the status sensor + entity_reg.async_get_or_create( + sensor.DOMAIN, + DOMAIN, + f"{DOMAIN} {_SensorTypes(_SensorTypes.DEVICES).sensor_name}", + suggested_object_id="asuswrt_connected_devices", + disabled_by=None, + ) + entity_reg.async_get_or_create( + sensor.DOMAIN, + DOMAIN, + f"{DOMAIN} {_SensorTypes(_SensorTypes.DOWNLOAD_SPEED).sensor_name}", + suggested_object_id="asuswrt_download_speed", + disabled_by=None, + ) + entity_reg.async_get_or_create( + sensor.DOMAIN, + DOMAIN, + f"{DOMAIN} {_SensorTypes(_SensorTypes.DOWNLOAD).sensor_name}", + suggested_object_id="asuswrt_download", + disabled_by=None, + ) + entity_reg.async_get_or_create( + sensor.DOMAIN, + DOMAIN, + f"{DOMAIN} {_SensorTypes(_SensorTypes.UPLOAD_SPEED).sensor_name}", + suggested_object_id="asuswrt_upload_speed", + disabled_by=None, + ) + entity_reg.async_get_or_create( + sensor.DOMAIN, + DOMAIN, + f"{DOMAIN} {_SensorTypes(_SensorTypes.UPLOAD).sensor_name}", + suggested_object_id="asuswrt_upload", + disabled_by=None, + ) + + # init config entry + config_entry = MockConfigEntry( + domain=DOMAIN, + data=CONFIG_DATA, + options={CONF_CONSIDER_HOME: 60}, + ) + config_entry.add_to_hass(hass) + + # initial devices setup + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + assert hass.states.get(f"{device_tracker.DOMAIN}.test").state == STATE_HOME + assert hass.states.get(f"{device_tracker.DOMAIN}.testtwo").state == STATE_HOME + assert hass.states.get(f"{sensor.DOMAIN}.asuswrt_connected_devices").state == "2" + assert hass.states.get(f"{sensor.DOMAIN}.asuswrt_download_speed").state == "160.0" + assert hass.states.get(f"{sensor.DOMAIN}.asuswrt_download").state == "60.0" + assert hass.states.get(f"{sensor.DOMAIN}.asuswrt_upload_speed").state == "80.0" + assert hass.states.get(f"{sensor.DOMAIN}.asuswrt_upload").state == "50.0" + + # add one device and remove another + MOCK_DEVICES.pop("a1:b1:c1:d1:e1:f1") + MOCK_DEVICES["a3:b3:c3:d3:e3:f3"] = Device( + "a3:b3:c3:d3:e3:f3", "192.168.1.4", "TestThree" + ) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + # consider home option set, all devices still home + assert hass.states.get(f"{device_tracker.DOMAIN}.test").state == STATE_HOME + assert hass.states.get(f"{device_tracker.DOMAIN}.testtwo").state == STATE_HOME + assert hass.states.get(f"{device_tracker.DOMAIN}.testthree").state == STATE_HOME + assert hass.states.get(f"{sensor.DOMAIN}.asuswrt_connected_devices").state == "2" + + hass.config_entries.async_update_entry( + config_entry, options={CONF_CONSIDER_HOME: 0} + ) + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + # consider home option not set, device "test" not home + assert hass.states.get(f"{device_tracker.DOMAIN}.test").state == STATE_NOT_HOME From 2f40f44670b603caa63abe07e27126ca18a5aa3b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Feb 2021 09:30:55 -1000 Subject: [PATCH 405/796] Update HAP-python to 3.3.0 for homekit (#46497) Changes: https://github.com/ikalchev/HAP-python/compare/v3.2.0...v3.3.0 --- 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 23b43958848..acc61408a48 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.2.0", + "HAP-python==3.3.0", "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1", diff --git a/requirements_all.txt b/requirements_all.txt index dbad80603d4..9ee4663bd17 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -14,7 +14,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.1.1 # homeassistant.components.homekit -HAP-python==3.2.0 +HAP-python==3.3.0 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3fa94bd3117..737b3e4a989 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.2.0 +HAP-python==3.3.0 # homeassistant.components.flick_electric PyFlick==0.0.2 From eecf07d7dfe8bc07ae9cdad1715b9b80d2678932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sat, 13 Feb 2021 21:53:28 +0100 Subject: [PATCH 406/796] Add AEMET OpenData integration (#45074) Co-authored-by: Martin Hjelmare --- .coveragerc | 2 + CODEOWNERS | 1 + homeassistant/components/aemet/__init__.py | 61 + .../components/aemet/abstract_aemet_sensor.py | 57 + homeassistant/components/aemet/config_flow.py | 58 + homeassistant/components/aemet/const.py | 326 ++++ homeassistant/components/aemet/manifest.json | 8 + homeassistant/components/aemet/sensor.py | 114 ++ homeassistant/components/aemet/strings.json | 22 + .../components/aemet/translations/en.json | 22 + homeassistant/components/aemet/weather.py | 113 ++ .../aemet/weather_update_coordinator.py | 637 ++++++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/aemet/__init__.py | 1 + tests/components/aemet/test_config_flow.py | 100 ++ tests/components/aemet/test_init.py | 44 + tests/components/aemet/test_sensor.py | 137 ++ tests/components/aemet/test_weather.py | 61 + tests/components/aemet/util.py | 93 ++ tests/fixtures/aemet/station-3195-data.json | 369 +++++ tests/fixtures/aemet/station-3195.json | 6 + tests/fixtures/aemet/station-list-data.json | 42 + tests/fixtures/aemet/station-list.json | 6 + .../aemet/town-28065-forecast-daily-data.json | 625 ++++++++ .../aemet/town-28065-forecast-daily.json | 6 + .../town-28065-forecast-hourly-data.json | 1416 +++++++++++++++++ .../aemet/town-28065-forecast-hourly.json | 6 + tests/fixtures/aemet/town-id28065.json | 15 + tests/fixtures/aemet/town-list.json | 43 + 31 files changed, 4398 insertions(+) create mode 100644 homeassistant/components/aemet/__init__.py create mode 100644 homeassistant/components/aemet/abstract_aemet_sensor.py create mode 100644 homeassistant/components/aemet/config_flow.py create mode 100644 homeassistant/components/aemet/const.py create mode 100644 homeassistant/components/aemet/manifest.json create mode 100644 homeassistant/components/aemet/sensor.py create mode 100644 homeassistant/components/aemet/strings.json create mode 100644 homeassistant/components/aemet/translations/en.json create mode 100644 homeassistant/components/aemet/weather.py create mode 100644 homeassistant/components/aemet/weather_update_coordinator.py create mode 100644 tests/components/aemet/__init__.py create mode 100644 tests/components/aemet/test_config_flow.py create mode 100644 tests/components/aemet/test_init.py create mode 100644 tests/components/aemet/test_sensor.py create mode 100644 tests/components/aemet/test_weather.py create mode 100644 tests/components/aemet/util.py create mode 100644 tests/fixtures/aemet/station-3195-data.json create mode 100644 tests/fixtures/aemet/station-3195.json create mode 100644 tests/fixtures/aemet/station-list-data.json create mode 100644 tests/fixtures/aemet/station-list.json create mode 100644 tests/fixtures/aemet/town-28065-forecast-daily-data.json create mode 100644 tests/fixtures/aemet/town-28065-forecast-daily.json create mode 100644 tests/fixtures/aemet/town-28065-forecast-hourly-data.json create mode 100644 tests/fixtures/aemet/town-28065-forecast-hourly.json create mode 100644 tests/fixtures/aemet/town-id28065.json create mode 100644 tests/fixtures/aemet/town-list.json diff --git a/.coveragerc b/.coveragerc index 3bf3fa10947..8c7d4b3393d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -23,6 +23,8 @@ omit = homeassistant/components/adguard/sensor.py homeassistant/components/adguard/switch.py homeassistant/components/ads/* + homeassistant/components/aemet/abstract_aemet_sensor.py + homeassistant/components/aemet/weather_update_coordinator.py homeassistant/components/aftership/sensor.py homeassistant/components/agent_dvr/__init__.py homeassistant/components/agent_dvr/alarm_control_panel.py diff --git a/CODEOWNERS b/CODEOWNERS index 3d7ff5f79a5..6e3bc1feb87 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -24,6 +24,7 @@ homeassistant/components/accuweather/* @bieniu homeassistant/components/acmeda/* @atmurray homeassistant/components/adguard/* @frenck homeassistant/components/advantage_air/* @Bre77 +homeassistant/components/aemet/* @noltari homeassistant/components/agent_dvr/* @ispysoftware homeassistant/components/airly/* @bieniu homeassistant/components/airnow/* @asymworks diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py new file mode 100644 index 00000000000..58b1a3b10f0 --- /dev/null +++ b/homeassistant/components/aemet/__init__.py @@ -0,0 +1,61 @@ +"""The AEMET OpenData component.""" +import asyncio +import logging + +from aemet_opendata.interface import AEMET + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant + +from .const import COMPONENTS, DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR +from .weather_update_coordinator import WeatherUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: dict) -> bool: + """Set up the AEMET OpenData component.""" + hass.data.setdefault(DOMAIN, {}) + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Set up AEMET OpenData as config entry.""" + name = config_entry.data[CONF_NAME] + api_key = config_entry.data[CONF_API_KEY] + latitude = config_entry.data[CONF_LATITUDE] + longitude = config_entry.data[CONF_LONGITUDE] + + aemet = AEMET(api_key) + weather_coordinator = WeatherUpdateCoordinator(hass, aemet, latitude, longitude) + + await weather_coordinator.async_refresh() + + hass.data[DOMAIN][config_entry.entry_id] = { + ENTRY_NAME: name, + ENTRY_WEATHER_COORDINATOR: weather_coordinator, + } + + for component in COMPONENTS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in COMPONENTS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/aemet/abstract_aemet_sensor.py b/homeassistant/components/aemet/abstract_aemet_sensor.py new file mode 100644 index 00000000000..6b7c3c69fee --- /dev/null +++ b/homeassistant/components/aemet/abstract_aemet_sensor.py @@ -0,0 +1,57 @@ +"""Abstraction form AEMET OpenData sensors.""" +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ATTRIBUTION, SENSOR_DEVICE_CLASS, SENSOR_NAME, SENSOR_UNIT +from .weather_update_coordinator import WeatherUpdateCoordinator + + +class AbstractAemetSensor(CoordinatorEntity): + """Abstract class for an AEMET OpenData sensor.""" + + def __init__( + self, + name, + unique_id, + sensor_type, + sensor_configuration, + coordinator: WeatherUpdateCoordinator, + ): + """Initialize the sensor.""" + super().__init__(coordinator) + self._name = name + self._unique_id = unique_id + self._sensor_type = sensor_type + self._sensor_name = sensor_configuration[SENSOR_NAME] + self._unit_of_measurement = sensor_configuration.get(SENSOR_UNIT) + self._device_class = sensor_configuration.get(SENSOR_DEVICE_CLASS) + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self._name} {self._sensor_name}" + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return self._unique_id + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def device_class(self): + """Return the device_class.""" + return self._device_class + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/aemet/config_flow.py b/homeassistant/components/aemet/config_flow.py new file mode 100644 index 00000000000..27f389660a8 --- /dev/null +++ b/homeassistant/components/aemet/config_flow.py @@ -0,0 +1,58 @@ +"""Config flow for AEMET OpenData.""" +from aemet_opendata import AEMET +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +import homeassistant.helpers.config_validation as cv + +from .const import DEFAULT_NAME +from .const import DOMAIN # pylint:disable=unused-import + + +class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for AEMET OpenData.""" + + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + latitude = user_input[CONF_LATITUDE] + longitude = user_input[CONF_LONGITUDE] + + await self.async_set_unique_id(f"{latitude}-{longitude}") + self._abort_if_unique_id_configured() + + api_online = await _is_aemet_api_online(self.hass, user_input[CONF_API_KEY]) + if not api_online: + errors["base"] = "invalid_api_key" + + if not errors: + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) + + schema = vol.Schema( + { + vol.Required(CONF_API_KEY): str, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, + vol.Optional( + CONF_LATITUDE, default=self.hass.config.latitude + ): cv.latitude, + vol.Optional( + CONF_LONGITUDE, default=self.hass.config.longitude + ): cv.longitude, + } + ) + + return self.async_show_form(step_id="user", data_schema=schema, errors=errors) + + +async def _is_aemet_api_online(hass, api_key): + aemet = AEMET(api_key) + return await hass.async_add_executor_job( + aemet.get_conventional_observation_stations, False + ) diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py new file mode 100644 index 00000000000..13b9d944bf0 --- /dev/null +++ b/homeassistant/components/aemet/const.py @@ -0,0 +1,326 @@ +"""Constant values for the AEMET OpenData component.""" + +from homeassistant.components.weather import ( + ATTR_CONDITION_CLEAR_NIGHT, + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_FOG, + ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SUNNY, + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, +) +from homeassistant.const import ( + DEGREE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, + PERCENTAGE, + PRECIPITATION_MILLIMETERS_PER_HOUR, + PRESSURE_HPA, + SPEED_KILOMETERS_PER_HOUR, + TEMP_CELSIUS, +) + +ATTRIBUTION = "Powered by AEMET OpenData" +COMPONENTS = ["sensor", "weather"] +DEFAULT_NAME = "AEMET" +DOMAIN = "aemet" +ENTRY_NAME = "name" +ENTRY_WEATHER_COORDINATOR = "weather_coordinator" +UPDATE_LISTENER = "update_listener" +SENSOR_NAME = "sensor_name" +SENSOR_UNIT = "sensor_unit" +SENSOR_DEVICE_CLASS = "sensor_device_class" + +ATTR_API_CONDITION = "condition" +ATTR_API_FORECAST_DAILY = "forecast-daily" +ATTR_API_FORECAST_HOURLY = "forecast-hourly" +ATTR_API_HUMIDITY = "humidity" +ATTR_API_PRESSURE = "pressure" +ATTR_API_RAIN = "rain" +ATTR_API_RAIN_PROB = "rain-probability" +ATTR_API_SNOW = "snow" +ATTR_API_SNOW_PROB = "snow-probability" +ATTR_API_STATION_ID = "station-id" +ATTR_API_STATION_NAME = "station-name" +ATTR_API_STATION_TIMESTAMP = "station-timestamp" +ATTR_API_STORM_PROB = "storm-probability" +ATTR_API_TEMPERATURE = "temperature" +ATTR_API_TEMPERATURE_FEELING = "temperature-feeling" +ATTR_API_TOWN_ID = "town-id" +ATTR_API_TOWN_NAME = "town-name" +ATTR_API_TOWN_TIMESTAMP = "town-timestamp" +ATTR_API_WIND_BEARING = "wind-bearing" +ATTR_API_WIND_MAX_SPEED = "wind-max-speed" +ATTR_API_WIND_SPEED = "wind-speed" + +CONDITIONS_MAP = { + ATTR_CONDITION_CLEAR_NIGHT: { + "11n", # Despejado (de noche) + }, + ATTR_CONDITION_CLOUDY: { + "14", # Nuboso + "14n", # Nuboso (de noche) + "15", # Muy nuboso + "15n", # Muy nuboso (de noche) + "16", # Cubierto + "16n", # Cubierto (de noche) + "17", # Nubes altas + "17n", # Nubes altas (de noche) + }, + ATTR_CONDITION_FOG: { + "81", # Niebla + "81n", # Niebla (de noche) + "82", # Bruma - Neblina + "82n", # Bruma - Neblina (de noche) + }, + ATTR_CONDITION_LIGHTNING: { + "51", # Intervalos nubosos con tormenta + "51n", # Intervalos nubosos con tormenta (de noche) + "52", # Nuboso con tormenta + "52n", # Nuboso con tormenta (de noche) + "53", # Muy nuboso con tormenta + "53n", # Muy nuboso con tormenta (de noche) + "54", # Cubierto con tormenta + "54n", # Cubierto con tormenta (de noche) + }, + ATTR_CONDITION_LIGHTNING_RAINY: { + "61", # Intervalos nubosos con tormenta y lluvia escasa + "61n", # Intervalos nubosos con tormenta y lluvia escasa (de noche) + "62", # Nuboso con tormenta y lluvia escasa + "62n", # Nuboso con tormenta y lluvia escasa (de noche) + "63", # Muy nuboso con tormenta y lluvia escasa + "63n", # Muy nuboso con tormenta y lluvia escasa (de noche) + "64", # Cubierto con tormenta y lluvia escasa + "64n", # Cubierto con tormenta y lluvia escasa (de noche) + }, + ATTR_CONDITION_PARTLYCLOUDY: { + "12", # Poco nuboso + "12n", # Poco nuboso (de noche) + "13", # Intervalos nubosos + "13n", # Intervalos nubosos (de noche) + }, + ATTR_CONDITION_POURING: { + "27", # Chubascos + "27n", # Chubascos (de noche) + }, + ATTR_CONDITION_RAINY: { + "23", # Intervalos nubosos con lluvia + "23n", # Intervalos nubosos con lluvia (de noche) + "24", # Nuboso con lluvia + "24n", # Nuboso con lluvia (de noche) + "25", # Muy nuboso con lluvia + "25n", # Muy nuboso con lluvia (de noche) + "26", # Cubierto con lluvia + "26n", # Cubierto con lluvia (de noche) + "43", # Intervalos nubosos con lluvia escasa + "43n", # Intervalos nubosos con lluvia escasa (de noche) + "44", # Nuboso con lluvia escasa + "44n", # Nuboso con lluvia escasa (de noche) + "45", # Muy nuboso con lluvia escasa + "45n", # Muy nuboso con lluvia escasa (de noche) + "46", # Cubierto con lluvia escasa + "46n", # Cubierto con lluvia escasa (de noche) + }, + ATTR_CONDITION_SNOWY: { + "33", # Intervalos nubosos con nieve + "33n", # Intervalos nubosos con nieve (de noche) + "34", # Nuboso con nieve + "34n", # Nuboso con nieve (de noche) + "35", # Muy nuboso con nieve + "35n", # Muy nuboso con nieve (de noche) + "36", # Cubierto con nieve + "36n", # Cubierto con nieve (de noche) + "71", # Intervalos nubosos con nieve escasa + "71n", # Intervalos nubosos con nieve escasa (de noche) + "72", # Nuboso con nieve escasa + "72n", # Nuboso con nieve escasa (de noche) + "73", # Muy nuboso con nieve escasa + "73n", # Muy nuboso con nieve escasa (de noche) + "74", # Cubierto con nieve escasa + "74n", # Cubierto con nieve escasa (de noche) + }, + ATTR_CONDITION_SUNNY: { + "11", # Despejado + }, +} + +FORECAST_MONITORED_CONDITIONS = [ + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, +] +MONITORED_CONDITIONS = [ + ATTR_API_CONDITION, + ATTR_API_HUMIDITY, + ATTR_API_PRESSURE, + ATTR_API_RAIN, + ATTR_API_RAIN_PROB, + ATTR_API_SNOW, + ATTR_API_SNOW_PROB, + ATTR_API_STATION_ID, + ATTR_API_STATION_NAME, + ATTR_API_STATION_TIMESTAMP, + ATTR_API_STORM_PROB, + ATTR_API_TEMPERATURE, + ATTR_API_TEMPERATURE_FEELING, + ATTR_API_TOWN_ID, + ATTR_API_TOWN_NAME, + ATTR_API_TOWN_TIMESTAMP, + ATTR_API_WIND_BEARING, + ATTR_API_WIND_MAX_SPEED, + ATTR_API_WIND_SPEED, +] + +FORECAST_MODE_DAILY = "daily" +FORECAST_MODE_HOURLY = "hourly" +FORECAST_MODES = [ + FORECAST_MODE_DAILY, + FORECAST_MODE_HOURLY, +] +FORECAST_MODE_ATTR_API = { + FORECAST_MODE_DAILY: ATTR_API_FORECAST_DAILY, + FORECAST_MODE_HOURLY: ATTR_API_FORECAST_HOURLY, +} + +FORECAST_SENSOR_TYPES = { + ATTR_FORECAST_CONDITION: { + SENSOR_NAME: "Condition", + }, + ATTR_FORECAST_PRECIPITATION: { + SENSOR_NAME: "Precipitation", + SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR, + }, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: { + SENSOR_NAME: "Precipitation probability", + SENSOR_UNIT: PERCENTAGE, + }, + ATTR_FORECAST_TEMP: { + SENSOR_NAME: "Temperature", + SENSOR_UNIT: TEMP_CELSIUS, + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + }, + ATTR_FORECAST_TEMP_LOW: { + SENSOR_NAME: "Temperature Low", + SENSOR_UNIT: TEMP_CELSIUS, + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + }, + ATTR_FORECAST_TIME: { + SENSOR_NAME: "Time", + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + }, + ATTR_FORECAST_WIND_BEARING: { + SENSOR_NAME: "Wind bearing", + SENSOR_UNIT: DEGREE, + }, + ATTR_FORECAST_WIND_SPEED: { + SENSOR_NAME: "Wind speed", + SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR, + }, +} +WEATHER_SENSOR_TYPES = { + ATTR_API_CONDITION: { + SENSOR_NAME: "Condition", + }, + ATTR_API_HUMIDITY: { + SENSOR_NAME: "Humidity", + SENSOR_UNIT: PERCENTAGE, + SENSOR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + }, + ATTR_API_PRESSURE: { + SENSOR_NAME: "Pressure", + SENSOR_UNIT: PRESSURE_HPA, + SENSOR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + }, + ATTR_API_RAIN: { + SENSOR_NAME: "Rain", + SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR, + }, + ATTR_API_RAIN_PROB: { + SENSOR_NAME: "Rain probability", + SENSOR_UNIT: PERCENTAGE, + }, + ATTR_API_SNOW: { + SENSOR_NAME: "Snow", + SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR, + }, + ATTR_API_SNOW_PROB: { + SENSOR_NAME: "Snow probability", + SENSOR_UNIT: PERCENTAGE, + }, + ATTR_API_STATION_ID: { + SENSOR_NAME: "Station ID", + }, + ATTR_API_STATION_NAME: { + SENSOR_NAME: "Station name", + }, + ATTR_API_STATION_TIMESTAMP: { + SENSOR_NAME: "Station timestamp", + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + }, + ATTR_API_STORM_PROB: { + SENSOR_NAME: "Storm probability", + SENSOR_UNIT: PERCENTAGE, + }, + ATTR_API_TEMPERATURE: { + SENSOR_NAME: "Temperature", + SENSOR_UNIT: TEMP_CELSIUS, + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + }, + ATTR_API_TEMPERATURE_FEELING: { + SENSOR_NAME: "Temperature feeling", + SENSOR_UNIT: TEMP_CELSIUS, + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + }, + ATTR_API_TOWN_ID: { + SENSOR_NAME: "Town ID", + }, + ATTR_API_TOWN_NAME: { + SENSOR_NAME: "Town name", + }, + ATTR_API_TOWN_TIMESTAMP: { + SENSOR_NAME: "Town timestamp", + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + }, + ATTR_API_WIND_BEARING: { + SENSOR_NAME: "Wind bearing", + SENSOR_UNIT: DEGREE, + }, + ATTR_API_WIND_MAX_SPEED: { + SENSOR_NAME: "Wind max speed", + SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR, + }, + ATTR_API_WIND_SPEED: { + SENSOR_NAME: "Wind speed", + SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR, + }, +} + +WIND_BEARING_MAP = { + "C": None, + "N": 0.0, + "NE": 45.0, + "E": 90.0, + "SE": 135.0, + "S": 180.0, + "SO": 225.0, + "O": 270.0, + "NO": 315.0, +} diff --git a/homeassistant/components/aemet/manifest.json b/homeassistant/components/aemet/manifest.json new file mode 100644 index 00000000000..eb5dc295f29 --- /dev/null +++ b/homeassistant/components/aemet/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "aemet", + "name": "AEMET OpenData", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/aemet", + "requirements": ["AEMET-OpenData==0.1.8"], + "codeowners": ["@noltari"] +} diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py new file mode 100644 index 00000000000..b57de1ce890 --- /dev/null +++ b/homeassistant/components/aemet/sensor.py @@ -0,0 +1,114 @@ +"""Support for the AEMET OpenData service.""" +from .abstract_aemet_sensor import AbstractAemetSensor +from .const import ( + DOMAIN, + ENTRY_NAME, + ENTRY_WEATHER_COORDINATOR, + FORECAST_MODE_ATTR_API, + FORECAST_MODE_DAILY, + FORECAST_MODES, + FORECAST_MONITORED_CONDITIONS, + FORECAST_SENSOR_TYPES, + MONITORED_CONDITIONS, + WEATHER_SENSOR_TYPES, +) +from .weather_update_coordinator import WeatherUpdateCoordinator + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up AEMET OpenData sensor entities based on a config entry.""" + domain_data = hass.data[DOMAIN][config_entry.entry_id] + name = domain_data[ENTRY_NAME] + weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR] + + weather_sensor_types = WEATHER_SENSOR_TYPES + forecast_sensor_types = FORECAST_SENSOR_TYPES + + entities = [] + for sensor_type in MONITORED_CONDITIONS: + unique_id = f"{config_entry.unique_id}-{sensor_type}" + entities.append( + AemetSensor( + name, + unique_id, + sensor_type, + weather_sensor_types[sensor_type], + weather_coordinator, + ) + ) + + for mode in FORECAST_MODES: + name = f"{domain_data[ENTRY_NAME]} {mode}" + + for sensor_type in FORECAST_MONITORED_CONDITIONS: + unique_id = f"{config_entry.unique_id}-forecast-{mode}-{sensor_type}" + entities.append( + AemetForecastSensor( + f"{name} Forecast", + unique_id, + sensor_type, + forecast_sensor_types[sensor_type], + weather_coordinator, + mode, + ) + ) + + async_add_entities(entities) + + +class AemetSensor(AbstractAemetSensor): + """Implementation of an AEMET OpenData sensor.""" + + def __init__( + self, + name, + unique_id, + sensor_type, + sensor_configuration, + weather_coordinator: WeatherUpdateCoordinator, + ): + """Initialize the sensor.""" + super().__init__( + name, unique_id, sensor_type, sensor_configuration, weather_coordinator + ) + self._weather_coordinator = weather_coordinator + + @property + def state(self): + """Return the state of the device.""" + return self._weather_coordinator.data.get(self._sensor_type) + + +class AemetForecastSensor(AbstractAemetSensor): + """Implementation of an AEMET OpenData forecast sensor.""" + + def __init__( + self, + name, + unique_id, + sensor_type, + sensor_configuration, + weather_coordinator: WeatherUpdateCoordinator, + forecast_mode, + ): + """Initialize the sensor.""" + super().__init__( + name, unique_id, sensor_type, sensor_configuration, weather_coordinator + ) + self._weather_coordinator = weather_coordinator + self._forecast_mode = forecast_mode + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self._forecast_mode == FORECAST_MODE_DAILY + + @property + def state(self): + """Return the state of the device.""" + forecasts = self._weather_coordinator.data.get( + FORECAST_MODE_ATTR_API[self._forecast_mode] + ) + if forecasts: + return forecasts[0].get(self._sensor_type) + return None diff --git a/homeassistant/components/aemet/strings.json b/homeassistant/components/aemet/strings.json new file mode 100644 index 00000000000..a25a503bade --- /dev/null +++ b/homeassistant/components/aemet/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + }, + "error": { + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" + }, + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]", + "name": "Name of the integration" + }, + "description": "Set up AEMET OpenData integration. To generate API key go to https://opendata.aemet.es/centrodedescargas/altaUsuario", + "title": "AEMET OpenData" + } + } + } +} diff --git a/homeassistant/components/aemet/translations/en.json b/homeassistant/components/aemet/translations/en.json new file mode 100644 index 00000000000..60e7f5f2ec2 --- /dev/null +++ b/homeassistant/components/aemet/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Location is already configured" + }, + "error": { + "invalid_api_key": "Invalid API key" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name of the integration" + }, + "description": "Set up AEMET OpenData integration. To generate API key go to https://opendata.aemet.es/centrodedescargas/altaUsuario", + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py new file mode 100644 index 00000000000..e54a297cc09 --- /dev/null +++ b/homeassistant/components/aemet/weather.py @@ -0,0 +1,113 @@ +"""Support for the AEMET OpenData service.""" +from homeassistant.components.weather import WeatherEntity +from homeassistant.const import TEMP_CELSIUS +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ( + ATTR_API_CONDITION, + ATTR_API_HUMIDITY, + ATTR_API_PRESSURE, + ATTR_API_TEMPERATURE, + ATTR_API_WIND_BEARING, + ATTR_API_WIND_SPEED, + ATTRIBUTION, + DOMAIN, + ENTRY_NAME, + ENTRY_WEATHER_COORDINATOR, + FORECAST_MODE_ATTR_API, + FORECAST_MODE_DAILY, + FORECAST_MODES, +) +from .weather_update_coordinator import WeatherUpdateCoordinator + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up AEMET OpenData weather entity based on a config entry.""" + domain_data = hass.data[DOMAIN][config_entry.entry_id] + weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR] + + entities = [] + for mode in FORECAST_MODES: + name = f"{domain_data[ENTRY_NAME]} {mode}" + unique_id = f"{config_entry.unique_id} {mode}" + entities.append(AemetWeather(name, unique_id, weather_coordinator, mode)) + + if entities: + async_add_entities(entities, False) + + +class AemetWeather(CoordinatorEntity, WeatherEntity): + """Implementation of an AEMET OpenData sensor.""" + + def __init__( + self, + name, + unique_id, + coordinator: WeatherUpdateCoordinator, + forecast_mode, + ): + """Initialize the sensor.""" + super().__init__(coordinator) + self._name = name + self._unique_id = unique_id + self._forecast_mode = forecast_mode + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def condition(self): + """Return the current condition.""" + return self.coordinator.data[ATTR_API_CONDITION] + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self._forecast_mode == FORECAST_MODE_DAILY + + @property + def forecast(self): + """Return the forecast array.""" + return self.coordinator.data[FORECAST_MODE_ATTR_API[self._forecast_mode]] + + @property + def humidity(self): + """Return the humidity.""" + return self.coordinator.data[ATTR_API_HUMIDITY] + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def pressure(self): + """Return the pressure.""" + return self.coordinator.data[ATTR_API_PRESSURE] + + @property + def temperature(self): + """Return the temperature.""" + return self.coordinator.data[ATTR_API_TEMPERATURE] + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return self._unique_id + + @property + def wind_bearing(self): + """Return the temperature.""" + return self.coordinator.data[ATTR_API_WIND_BEARING] + + @property + def wind_speed(self): + """Return the temperature.""" + return self.coordinator.data[ATTR_API_WIND_SPEED] diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py new file mode 100644 index 00000000000..6a06b1dd391 --- /dev/null +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -0,0 +1,637 @@ +"""Weather data coordinator for the AEMET OpenData service.""" +from dataclasses import dataclass, field +from datetime import timedelta +import logging + +from aemet_opendata.const import ( + AEMET_ATTR_DATE, + AEMET_ATTR_DAY, + AEMET_ATTR_DIRECTION, + AEMET_ATTR_ELABORATED, + AEMET_ATTR_FORECAST, + AEMET_ATTR_HUMIDITY, + AEMET_ATTR_ID, + AEMET_ATTR_IDEMA, + AEMET_ATTR_MAX, + AEMET_ATTR_MIN, + AEMET_ATTR_NAME, + AEMET_ATTR_PRECIPITATION, + AEMET_ATTR_PRECIPITATION_PROBABILITY, + AEMET_ATTR_SKY_STATE, + AEMET_ATTR_SNOW, + AEMET_ATTR_SNOW_PROBABILITY, + AEMET_ATTR_SPEED, + AEMET_ATTR_STATION_DATE, + AEMET_ATTR_STATION_HUMIDITY, + AEMET_ATTR_STATION_LOCATION, + AEMET_ATTR_STATION_PRESSURE_SEA, + AEMET_ATTR_STATION_TEMPERATURE, + AEMET_ATTR_STORM_PROBABILITY, + AEMET_ATTR_TEMPERATURE, + AEMET_ATTR_TEMPERATURE_FEELING, + AEMET_ATTR_WIND, + AEMET_ATTR_WIND_GUST, + ATTR_DATA, +) +from aemet_opendata.helpers import ( + get_forecast_day_value, + get_forecast_hour_value, + get_forecast_interval_value, +) +import async_timeout + +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, +) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt as dt_util + +from .const import ( + ATTR_API_CONDITION, + ATTR_API_FORECAST_DAILY, + ATTR_API_FORECAST_HOURLY, + ATTR_API_HUMIDITY, + ATTR_API_PRESSURE, + ATTR_API_RAIN, + ATTR_API_RAIN_PROB, + ATTR_API_SNOW, + ATTR_API_SNOW_PROB, + ATTR_API_STATION_ID, + ATTR_API_STATION_NAME, + ATTR_API_STATION_TIMESTAMP, + ATTR_API_STORM_PROB, + ATTR_API_TEMPERATURE, + ATTR_API_TEMPERATURE_FEELING, + ATTR_API_TOWN_ID, + ATTR_API_TOWN_NAME, + ATTR_API_TOWN_TIMESTAMP, + ATTR_API_WIND_BEARING, + ATTR_API_WIND_MAX_SPEED, + ATTR_API_WIND_SPEED, + CONDITIONS_MAP, + DOMAIN, + WIND_BEARING_MAP, +) + +_LOGGER = logging.getLogger(__name__) + +WEATHER_UPDATE_INTERVAL = timedelta(minutes=10) + + +def format_condition(condition: str) -> str: + """Return condition from dict CONDITIONS_MAP.""" + for key, value in CONDITIONS_MAP.items(): + if condition in value: + return key + _LOGGER.error('condition "%s" not found in CONDITIONS_MAP', condition) + return condition + + +def format_float(value) -> float: + """Try converting string to float.""" + try: + return float(value) + except ValueError: + return None + + +def format_int(value) -> int: + """Try converting string to int.""" + try: + return int(value) + except ValueError: + return None + + +class TownNotFound(UpdateFailed): + """Raised when town is not found.""" + + +class WeatherUpdateCoordinator(DataUpdateCoordinator): + """Weather data update coordinator.""" + + def __init__(self, hass, aemet, latitude, longitude): + """Initialize coordinator.""" + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=WEATHER_UPDATE_INTERVAL + ) + + self._aemet = aemet + self._station = None + self._town = None + self._latitude = latitude + self._longitude = longitude + self._data = { + "daily": None, + "hourly": None, + "station": None, + } + + async def _async_update_data(self): + data = {} + with async_timeout.timeout(120): + weather_response = await self._get_aemet_weather() + data = self._convert_weather_response(weather_response) + return data + + async def _get_aemet_weather(self): + """Poll weather data from AEMET OpenData.""" + weather = await self.hass.async_add_executor_job(self._get_weather_and_forecast) + return weather + + def _get_weather_station(self): + if not self._station: + self._station = ( + self._aemet.get_conventional_observation_station_by_coordinates( + self._latitude, self._longitude + ) + ) + if self._station: + _LOGGER.debug( + "station found for coordinates [%s, %s]: %s", + self._latitude, + self._longitude, + self._station, + ) + if not self._station: + _LOGGER.debug( + "station not found for coordinates [%s, %s]", + self._latitude, + self._longitude, + ) + return self._station + + def _get_weather_town(self): + if not self._town: + self._town = self._aemet.get_town_by_coordinates( + self._latitude, self._longitude + ) + if self._town: + _LOGGER.debug( + "town found for coordinates [%s, %s]: %s", + self._latitude, + self._longitude, + self._town, + ) + if not self._town: + _LOGGER.error( + "town not found for coordinates [%s, %s]", + self._latitude, + self._longitude, + ) + raise TownNotFound + return self._town + + def _get_weather_and_forecast(self): + """Get weather and forecast data from AEMET OpenData.""" + + self._get_weather_town() + + daily = self._aemet.get_specific_forecast_town_daily(self._town[AEMET_ATTR_ID]) + if not daily: + _LOGGER.error( + 'error fetching daily data for town "%s"', self._town[AEMET_ATTR_ID] + ) + + hourly = self._aemet.get_specific_forecast_town_hourly( + self._town[AEMET_ATTR_ID] + ) + if not hourly: + _LOGGER.error( + 'error fetching hourly data for town "%s"', self._town[AEMET_ATTR_ID] + ) + + station = None + if self._get_weather_station(): + station = self._aemet.get_conventional_observation_station_data( + self._station[AEMET_ATTR_IDEMA] + ) + if not station: + _LOGGER.error( + 'error fetching data for station "%s"', + self._station[AEMET_ATTR_IDEMA], + ) + + if daily: + self._data["daily"] = daily + if hourly: + self._data["hourly"] = hourly + if station: + self._data["station"] = station + + return AemetWeather( + self._data["daily"], + self._data["hourly"], + self._data["station"], + ) + + def _convert_weather_response(self, weather_response): + """Format the weather response correctly.""" + if not weather_response or not weather_response.hourly: + return None + + elaborated = dt_util.parse_datetime( + weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_ELABORATED] + ) + now = dt_util.now() + hour = now.hour + + # Get current day + day = None + for cur_day in weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_FORECAST][ + AEMET_ATTR_DAY + ]: + cur_day_date = dt_util.parse_datetime(cur_day[AEMET_ATTR_DATE]) + if now.date() == cur_day_date.date(): + day = cur_day + break + + # Get station data + station_data = None + if weather_response.station: + station_data = weather_response.station[ATTR_DATA][-1] + + condition = None + humidity = None + pressure = None + rain = None + rain_prob = None + snow = None + snow_prob = None + station_id = None + station_name = None + station_timestamp = None + storm_prob = None + temperature = None + temperature_feeling = None + town_id = None + town_name = None + town_timestamp = dt_util.as_utc(elaborated) + wind_bearing = None + wind_max_speed = None + wind_speed = None + + # Get weather values + if day: + condition = self._get_condition(day, hour) + humidity = self._get_humidity(day, hour) + rain = self._get_rain(day, hour) + rain_prob = self._get_rain_prob(day, hour) + snow = self._get_snow(day, hour) + snow_prob = self._get_snow_prob(day, hour) + station_id = self._get_station_id() + station_name = self._get_station_name() + storm_prob = self._get_storm_prob(day, hour) + temperature = self._get_temperature(day, hour) + temperature_feeling = self._get_temperature_feeling(day, hour) + town_id = self._get_town_id() + town_name = self._get_town_name() + wind_bearing = self._get_wind_bearing(day, hour) + wind_max_speed = self._get_wind_max_speed(day, hour) + wind_speed = self._get_wind_speed(day, hour) + + # Overwrite weather values with closest station data (if present) + if station_data: + if AEMET_ATTR_STATION_DATE in station_data: + station_dt = dt_util.parse_datetime( + station_data[AEMET_ATTR_STATION_DATE] + "Z" + ) + station_timestamp = dt_util.as_utc(station_dt).isoformat() + if AEMET_ATTR_STATION_HUMIDITY in station_data: + humidity = format_float(station_data[AEMET_ATTR_STATION_HUMIDITY]) + if AEMET_ATTR_STATION_PRESSURE_SEA in station_data: + pressure = format_float(station_data[AEMET_ATTR_STATION_PRESSURE_SEA]) + if AEMET_ATTR_STATION_TEMPERATURE in station_data: + temperature = format_float(station_data[AEMET_ATTR_STATION_TEMPERATURE]) + + # Get forecast from weather data + forecast_daily = self._get_daily_forecast_from_weather_response( + weather_response, now + ) + forecast_hourly = self._get_hourly_forecast_from_weather_response( + weather_response, now + ) + + return { + ATTR_API_CONDITION: condition, + ATTR_API_FORECAST_DAILY: forecast_daily, + ATTR_API_FORECAST_HOURLY: forecast_hourly, + ATTR_API_HUMIDITY: humidity, + ATTR_API_TEMPERATURE: temperature, + ATTR_API_TEMPERATURE_FEELING: temperature_feeling, + ATTR_API_PRESSURE: pressure, + ATTR_API_RAIN: rain, + ATTR_API_RAIN_PROB: rain_prob, + ATTR_API_SNOW: snow, + ATTR_API_SNOW_PROB: snow_prob, + ATTR_API_STATION_ID: station_id, + ATTR_API_STATION_NAME: station_name, + ATTR_API_STATION_TIMESTAMP: station_timestamp, + ATTR_API_STORM_PROB: storm_prob, + ATTR_API_TOWN_ID: town_id, + ATTR_API_TOWN_NAME: town_name, + ATTR_API_TOWN_TIMESTAMP: town_timestamp, + ATTR_API_WIND_BEARING: wind_bearing, + ATTR_API_WIND_MAX_SPEED: wind_max_speed, + ATTR_API_WIND_SPEED: wind_speed, + } + + def _get_daily_forecast_from_weather_response(self, weather_response, now): + if weather_response.daily: + parse = False + forecast = [] + for day in weather_response.daily[ATTR_DATA][0][AEMET_ATTR_FORECAST][ + AEMET_ATTR_DAY + ]: + day_date = dt_util.parse_datetime(day[AEMET_ATTR_DATE]) + if now.date() == day_date.date(): + parse = True + if parse: + cur_forecast = self._convert_forecast_day(day_date, day) + if cur_forecast: + forecast.append(cur_forecast) + return forecast + return None + + def _get_hourly_forecast_from_weather_response(self, weather_response, now): + if weather_response.hourly: + parse = False + hour = now.hour + forecast = [] + for day in weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_FORECAST][ + AEMET_ATTR_DAY + ]: + day_date = dt_util.parse_datetime(day[AEMET_ATTR_DATE]) + hour_start = 0 + if now.date() == day_date.date(): + parse = True + hour_start = now.hour + if parse: + for hour in range(hour_start, 24): + cur_forecast = self._convert_forecast_hour(day_date, day, hour) + if cur_forecast: + forecast.append(cur_forecast) + return forecast + return None + + def _convert_forecast_day(self, date, day): + condition = self._get_condition_day(day) + if not condition: + return None + + return { + ATTR_FORECAST_CONDITION: condition, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day( + day + ), + ATTR_FORECAST_TEMP: self._get_temperature_day(day), + ATTR_FORECAST_TEMP_LOW: self._get_temperature_low_day(day), + ATTR_FORECAST_TIME: dt_util.as_utc(date), + ATTR_FORECAST_WIND_SPEED: self._get_wind_speed_day(day), + ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day), + } + + def _convert_forecast_hour(self, date, day, hour): + condition = self._get_condition(day, hour) + if not condition: + return None + + forecast_dt = date.replace(hour=hour, minute=0, second=0) + + return { + ATTR_FORECAST_CONDITION: condition, + ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour), + ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob( + day, hour + ), + ATTR_FORECAST_TEMP: self._get_temperature(day, hour), + ATTR_FORECAST_TIME: dt_util.as_utc(forecast_dt), + ATTR_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour), + ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour), + } + + def _calc_precipitation(self, day, hour): + """Calculate the precipitation.""" + rain_value = self._get_rain(day, hour) + if not rain_value: + rain_value = 0 + + snow_value = self._get_snow(day, hour) + if not snow_value: + snow_value = 0 + + if round(rain_value + snow_value, 1) == 0: + return None + return round(rain_value + snow_value, 1) + + def _calc_precipitation_prob(self, day, hour): + """Calculate the precipitation probability (hour).""" + rain_value = self._get_rain_prob(day, hour) + if not rain_value: + rain_value = 0 + + snow_value = self._get_snow_prob(day, hour) + if not snow_value: + snow_value = 0 + + if rain_value == 0 and snow_value == 0: + return None + return max(rain_value, snow_value) + + @staticmethod + def _get_condition(day_data, hour): + """Get weather condition (hour) from weather data.""" + val = get_forecast_hour_value(day_data[AEMET_ATTR_SKY_STATE], hour) + if val: + return format_condition(val) + return None + + @staticmethod + def _get_condition_day(day_data): + """Get weather condition (day) from weather data.""" + val = get_forecast_day_value(day_data[AEMET_ATTR_SKY_STATE]) + if val: + return format_condition(val) + return None + + @staticmethod + def _get_humidity(day_data, hour): + """Get humidity from weather data.""" + val = get_forecast_hour_value(day_data[AEMET_ATTR_HUMIDITY], hour) + if val: + return format_int(val) + return None + + @staticmethod + def _get_precipitation_prob_day(day_data): + """Get humidity from weather data.""" + val = get_forecast_day_value(day_data[AEMET_ATTR_PRECIPITATION_PROBABILITY]) + if val: + return format_int(val) + return None + + @staticmethod + def _get_rain(day_data, hour): + """Get rain from weather data.""" + val = get_forecast_hour_value(day_data[AEMET_ATTR_PRECIPITATION], hour) + if val: + return format_float(val) + return None + + @staticmethod + def _get_rain_prob(day_data, hour): + """Get rain probability from weather data.""" + val = get_forecast_interval_value( + day_data[AEMET_ATTR_PRECIPITATION_PROBABILITY], hour + ) + if val: + return format_int(val) + return None + + @staticmethod + def _get_snow(day_data, hour): + """Get snow from weather data.""" + val = get_forecast_hour_value(day_data[AEMET_ATTR_SNOW], hour) + if val: + return format_float(val) + return None + + @staticmethod + def _get_snow_prob(day_data, hour): + """Get snow probability from weather data.""" + val = get_forecast_interval_value(day_data[AEMET_ATTR_SNOW_PROBABILITY], hour) + if val: + return format_int(val) + return None + + def _get_station_id(self): + """Get station ID from weather data.""" + if self._station: + return self._station[AEMET_ATTR_IDEMA] + return None + + def _get_station_name(self): + """Get station name from weather data.""" + if self._station: + return self._station[AEMET_ATTR_STATION_LOCATION] + return None + + @staticmethod + def _get_storm_prob(day_data, hour): + """Get storm probability from weather data.""" + val = get_forecast_interval_value(day_data[AEMET_ATTR_STORM_PROBABILITY], hour) + if val: + return format_int(val) + return None + + @staticmethod + def _get_temperature(day_data, hour): + """Get temperature (hour) from weather data.""" + val = get_forecast_hour_value(day_data[AEMET_ATTR_TEMPERATURE], hour) + if val: + return format_int(val) + return None + + @staticmethod + def _get_temperature_day(day_data): + """Get temperature (day) from weather data.""" + val = get_forecast_day_value( + day_data[AEMET_ATTR_TEMPERATURE], key=AEMET_ATTR_MAX + ) + if val: + return format_int(val) + return None + + @staticmethod + def _get_temperature_low_day(day_data): + """Get temperature (day) from weather data.""" + val = get_forecast_day_value( + day_data[AEMET_ATTR_TEMPERATURE], key=AEMET_ATTR_MIN + ) + if val: + return format_int(val) + return None + + @staticmethod + def _get_temperature_feeling(day_data, hour): + """Get temperature from weather data.""" + val = get_forecast_hour_value(day_data[AEMET_ATTR_TEMPERATURE_FEELING], hour) + if val: + return format_int(val) + return None + + def _get_town_id(self): + """Get town ID from weather data.""" + if self._town: + return self._town[AEMET_ATTR_ID] + return None + + def _get_town_name(self): + """Get town name from weather data.""" + if self._town: + return self._town[AEMET_ATTR_NAME] + return None + + @staticmethod + def _get_wind_bearing(day_data, hour): + """Get wind bearing (hour) from weather data.""" + val = get_forecast_hour_value( + day_data[AEMET_ATTR_WIND_GUST], hour, key=AEMET_ATTR_DIRECTION + )[0] + if val in WIND_BEARING_MAP: + return WIND_BEARING_MAP[val] + _LOGGER.error("%s not found in Wind Bearing map", val) + return None + + @staticmethod + def _get_wind_bearing_day(day_data): + """Get wind bearing (day) from weather data.""" + val = get_forecast_day_value( + day_data[AEMET_ATTR_WIND], key=AEMET_ATTR_DIRECTION + ) + if val in WIND_BEARING_MAP: + return WIND_BEARING_MAP[val] + _LOGGER.error("%s not found in Wind Bearing map", val) + return None + + @staticmethod + def _get_wind_max_speed(day_data, hour): + """Get wind max speed from weather data.""" + val = get_forecast_hour_value(day_data[AEMET_ATTR_WIND_GUST], hour) + if val: + return format_int(val) + return None + + @staticmethod + def _get_wind_speed(day_data, hour): + """Get wind speed (hour) from weather data.""" + val = get_forecast_hour_value( + day_data[AEMET_ATTR_WIND_GUST], hour, key=AEMET_ATTR_SPEED + )[0] + if val: + return format_int(val) + return None + + @staticmethod + def _get_wind_speed_day(day_data): + """Get wind speed (day) from weather data.""" + val = get_forecast_day_value(day_data[AEMET_ATTR_WIND], key=AEMET_ATTR_SPEED) + if val: + return format_int(val) + return None + + +@dataclass +class AemetWeather: + """Class to harmonize weather data model.""" + + daily: dict = field(default_factory=dict) + hourly: dict = field(default_factory=dict) + station: dict = field(default_factory=dict) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index f5f550b3073..41d85c4b20b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -11,6 +11,7 @@ FLOWS = [ "acmeda", "adguard", "advantage_air", + "aemet", "agent_dvr", "airly", "airnow", diff --git a/requirements_all.txt b/requirements_all.txt index 9ee4663bd17..d8ec67e1d30 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,6 +1,9 @@ # Home Assistant Core, full dependency set -r requirements.txt +# homeassistant.components.aemet +AEMET-OpenData==0.1.8 + # homeassistant.components.dht # Adafruit-DHT==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 737b3e4a989..3a0ce6dd428 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -3,6 +3,9 @@ -r requirements_test.txt +# homeassistant.components.aemet +AEMET-OpenData==0.1.8 + # homeassistant.components.homekit HAP-python==3.3.0 diff --git a/tests/components/aemet/__init__.py b/tests/components/aemet/__init__.py new file mode 100644 index 00000000000..a92ff2764b1 --- /dev/null +++ b/tests/components/aemet/__init__.py @@ -0,0 +1 @@ +"""Tests for the AEMET OpenData integration.""" diff --git a/tests/components/aemet/test_config_flow.py b/tests/components/aemet/test_config_flow.py new file mode 100644 index 00000000000..3c93a9d6321 --- /dev/null +++ b/tests/components/aemet/test_config_flow.py @@ -0,0 +1,100 @@ +"""Define tests for the AEMET OpenData config flow.""" + +from unittest.mock import MagicMock, patch + +import requests_mock + +from homeassistant import data_entry_flow +from homeassistant.components.aemet.const import DOMAIN +from homeassistant.config_entries import ENTRY_STATE_LOADED, SOURCE_USER +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +import homeassistant.util.dt as dt_util + +from .util import aemet_requests_mock + +from tests.common import MockConfigEntry + +CONFIG = { + CONF_NAME: "aemet", + CONF_API_KEY: "foo", + CONF_LATITUDE: 40.30403754, + CONF_LONGITUDE: -3.72935236, +} + + +async def test_form(hass): + """Test that the form is served with valid input.""" + + with patch( + "homeassistant.components.aemet.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.aemet.async_setup_entry", + return_value=True, + ) as mock_setup_entry, requests_mock.mock() as _m: + aemet_requests_mock(_m) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG + ) + + await hass.async_block_till_done() + + conf_entries = hass.config_entries.async_entries(DOMAIN) + entry = conf_entries[0] + assert entry.state == ENTRY_STATE_LOADED + + 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 len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_duplicated_id(hass): + """Test that the options form.""" + + now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") + with patch("homeassistant.util.dt.now", return_value=now), patch( + "homeassistant.util.dt.utcnow", return_value=now + ), requests_mock.mock() as _m: + aemet_requests_mock(_m) + + entry = MockConfigEntry( + domain=DOMAIN, unique_id="40.30403754--3.72935236", data=CONFIG + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_form_api_offline(hass): + """Test setting up with api call error.""" + mocked_aemet = MagicMock() + + mocked_aemet.get_conventional_observation_stations.return_value = None + + with patch( + "homeassistant.components.aemet.config_flow.AEMET", + return_value=mocked_aemet, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) + + assert result["errors"] == {"base": "invalid_api_key"} diff --git a/tests/components/aemet/test_init.py b/tests/components/aemet/test_init.py new file mode 100644 index 00000000000..f1c6c48f3f3 --- /dev/null +++ b/tests/components/aemet/test_init.py @@ -0,0 +1,44 @@ +"""Define tests for the AEMET OpenData init.""" + +from unittest.mock import patch + +import requests_mock + +from homeassistant.components.aemet.const import DOMAIN +from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +import homeassistant.util.dt as dt_util + +from .util import aemet_requests_mock + +from tests.common import MockConfigEntry + +CONFIG = { + CONF_NAME: "aemet", + CONF_API_KEY: "foo", + CONF_LATITUDE: 40.30403754, + CONF_LONGITUDE: -3.72935236, +} + + +async def test_unload_entry(hass): + """Test that the options form.""" + + now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") + with patch("homeassistant.util.dt.now", return_value=now), patch( + "homeassistant.util.dt.utcnow", return_value=now + ), requests_mock.mock() as _m: + aemet_requests_mock(_m) + + config_entry = MockConfigEntry( + domain=DOMAIN, unique_id="aemet_unique_id", data=CONFIG + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ENTRY_STATE_LOADED + + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ENTRY_STATE_NOT_LOADED diff --git a/tests/components/aemet/test_sensor.py b/tests/components/aemet/test_sensor.py new file mode 100644 index 00000000000..05f2d8d0b50 --- /dev/null +++ b/tests/components/aemet/test_sensor.py @@ -0,0 +1,137 @@ +"""The sensor tests for the AEMET OpenData platform.""" + +from unittest.mock import patch + +from homeassistant.components.weather import ( + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_SNOWY, +) +from homeassistant.const import STATE_UNKNOWN +import homeassistant.util.dt as dt_util + +from .util import async_init_integration + + +async def test_aemet_forecast_create_sensors(hass): + """Test creation of forecast sensors.""" + + now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") + with patch("homeassistant.util.dt.now", return_value=now), patch( + "homeassistant.util.dt.utcnow", return_value=now + ): + await async_init_integration(hass) + + state = hass.states.get("sensor.aemet_daily_forecast_condition") + assert state.state == ATTR_CONDITION_PARTLYCLOUDY + + state = hass.states.get("sensor.aemet_daily_forecast_precipitation") + assert state.state == STATE_UNKNOWN + + state = hass.states.get("sensor.aemet_daily_forecast_precipitation_probability") + assert state.state == "30" + + state = hass.states.get("sensor.aemet_daily_forecast_temperature") + assert state.state == "4" + + state = hass.states.get("sensor.aemet_daily_forecast_temperature_low") + assert state.state == "-4" + + state = hass.states.get("sensor.aemet_daily_forecast_time") + assert state.state == "2021-01-10 00:00:00+00:00" + + state = hass.states.get("sensor.aemet_daily_forecast_wind_bearing") + assert state.state == "45.0" + + state = hass.states.get("sensor.aemet_daily_forecast_wind_speed") + assert state.state == "20" + + state = hass.states.get("sensor.aemet_hourly_forecast_condition") + assert state is None + + state = hass.states.get("sensor.aemet_hourly_forecast_precipitation") + assert state is None + + state = hass.states.get("sensor.aemet_hourly_forecast_precipitation_probability") + assert state is None + + state = hass.states.get("sensor.aemet_hourly_forecast_temperature") + assert state is None + + state = hass.states.get("sensor.aemet_hourly_forecast_temperature_low") + assert state is None + + state = hass.states.get("sensor.aemet_hourly_forecast_time") + assert state is None + + state = hass.states.get("sensor.aemet_hourly_forecast_wind_bearing") + assert state is None + + state = hass.states.get("sensor.aemet_hourly_forecast_wind_speed") + assert state is None + + +async def test_aemet_weather_create_sensors(hass): + """Test creation of weather sensors.""" + + now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") + with patch("homeassistant.util.dt.now", return_value=now), patch( + "homeassistant.util.dt.utcnow", return_value=now + ): + await async_init_integration(hass) + + state = hass.states.get("sensor.aemet_condition") + assert state.state == ATTR_CONDITION_SNOWY + + state = hass.states.get("sensor.aemet_humidity") + assert state.state == "99.0" + + state = hass.states.get("sensor.aemet_pressure") + assert state.state == "1004.4" + + state = hass.states.get("sensor.aemet_rain") + assert state.state == "1.8" + + state = hass.states.get("sensor.aemet_rain_probability") + assert state.state == "100" + + state = hass.states.get("sensor.aemet_snow") + assert state.state == "1.8" + + state = hass.states.get("sensor.aemet_snow_probability") + assert state.state == "100" + + state = hass.states.get("sensor.aemet_station_id") + assert state.state == "3195" + + state = hass.states.get("sensor.aemet_station_name") + assert state.state == "MADRID RETIRO" + + state = hass.states.get("sensor.aemet_station_timestamp") + assert state.state == "2021-01-09T12:00:00+00:00" + + state = hass.states.get("sensor.aemet_storm_probability") + assert state.state == "0" + + state = hass.states.get("sensor.aemet_temperature") + assert state.state == "-0.7" + + state = hass.states.get("sensor.aemet_temperature_feeling") + assert state.state == "-4" + + state = hass.states.get("sensor.aemet_town_id") + assert state.state == "id28065" + + state = hass.states.get("sensor.aemet_town_name") + assert state.state == "Getafe" + + state = hass.states.get("sensor.aemet_town_timestamp") + assert state.state == "2021-01-09 11:47:45+00:00" + + state = hass.states.get("sensor.aemet_wind_bearing") + assert state.state == "90.0" + + state = hass.states.get("sensor.aemet_wind_max_speed") + assert state.state == "24" + + state = hass.states.get("sensor.aemet_wind_speed") + assert state.state == "15" diff --git a/tests/components/aemet/test_weather.py b/tests/components/aemet/test_weather.py new file mode 100644 index 00000000000..eef6107d543 --- /dev/null +++ b/tests/components/aemet/test_weather.py @@ -0,0 +1,61 @@ +"""The sensor tests for the AEMET OpenData platform.""" + +from unittest.mock import patch + +from homeassistant.components.aemet.const import ATTRIBUTION +from homeassistant.components.weather import ( + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_SNOWY, + ATTR_FORECAST, + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, + ATTR_WEATHER_HUMIDITY, + ATTR_WEATHER_PRESSURE, + ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_WIND_BEARING, + ATTR_WEATHER_WIND_SPEED, +) +from homeassistant.const import ATTR_ATTRIBUTION +import homeassistant.util.dt as dt_util + +from .util import async_init_integration + + +async def test_aemet_weather(hass): + """Test states of the weather.""" + + now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") + with patch("homeassistant.util.dt.now", return_value=now), patch( + "homeassistant.util.dt.utcnow", return_value=now + ): + await async_init_integration(hass) + + state = hass.states.get("weather.aemet_daily") + assert state + assert state.state == ATTR_CONDITION_SNOWY + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_WEATHER_HUMIDITY) == 99.0 + assert state.attributes.get(ATTR_WEATHER_PRESSURE) == 1004.4 + assert state.attributes.get(ATTR_WEATHER_TEMPERATURE) == -0.7 + assert state.attributes.get(ATTR_WEATHER_WIND_BEARING) == 90.0 + assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 15 + forecast = state.attributes.get(ATTR_FORECAST)[0] + assert forecast.get(ATTR_FORECAST_CONDITION) == ATTR_CONDITION_PARTLYCLOUDY + assert forecast.get(ATTR_FORECAST_PRECIPITATION) is None + assert forecast.get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 30 + assert forecast.get(ATTR_FORECAST_TEMP) == 4 + assert forecast.get(ATTR_FORECAST_TEMP_LOW) == -4 + assert forecast.get(ATTR_FORECAST_TIME) == dt_util.parse_datetime( + "2021-01-10 00:00:00+00:00" + ) + assert forecast.get(ATTR_FORECAST_WIND_BEARING) == 45.0 + assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 20 + + state = hass.states.get("weather.aemet_hourly") + assert state is None diff --git a/tests/components/aemet/util.py b/tests/components/aemet/util.py new file mode 100644 index 00000000000..991e7459bf6 --- /dev/null +++ b/tests/components/aemet/util.py @@ -0,0 +1,93 @@ +"""Tests for the AEMET OpenData integration.""" + +import requests_mock + +from homeassistant.components.aemet import DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture + + +def aemet_requests_mock(mock): + """Mock requests performed to AEMET OpenData API.""" + + station_3195_fixture = "aemet/station-3195.json" + station_3195_data_fixture = "aemet/station-3195-data.json" + station_list_fixture = "aemet/station-list.json" + station_list_data_fixture = "aemet/station-list-data.json" + + town_28065_forecast_daily_fixture = "aemet/town-28065-forecast-daily.json" + town_28065_forecast_daily_data_fixture = "aemet/town-28065-forecast-daily-data.json" + town_28065_forecast_hourly_fixture = "aemet/town-28065-forecast-hourly.json" + town_28065_forecast_hourly_data_fixture = ( + "aemet/town-28065-forecast-hourly-data.json" + ) + town_id28065_fixture = "aemet/town-id28065.json" + town_list_fixture = "aemet/town-list.json" + + mock.get( + "https://opendata.aemet.es/opendata/api/observacion/convencional/datos/estacion/3195", + text=load_fixture(station_3195_fixture), + ) + mock.get( + "https://opendata.aemet.es/opendata/sh/208c3ca3", + text=load_fixture(station_3195_data_fixture), + ) + mock.get( + "https://opendata.aemet.es/opendata/api/observacion/convencional/todas", + text=load_fixture(station_list_fixture), + ) + mock.get( + "https://opendata.aemet.es/opendata/sh/2c55192f", + text=load_fixture(station_list_data_fixture), + ) + mock.get( + "https://opendata.aemet.es/opendata/api/prediccion/especifica/municipio/diaria/28065", + text=load_fixture(town_28065_forecast_daily_fixture), + ) + mock.get( + "https://opendata.aemet.es/opendata/sh/64e29abb", + text=load_fixture(town_28065_forecast_daily_data_fixture), + ) + mock.get( + "https://opendata.aemet.es/opendata/api/prediccion/especifica/municipio/horaria/28065", + text=load_fixture(town_28065_forecast_hourly_fixture), + ) + mock.get( + "https://opendata.aemet.es/opendata/sh/18ca1886", + text=load_fixture(town_28065_forecast_hourly_data_fixture), + ) + mock.get( + "https://opendata.aemet.es/opendata/api/maestro/municipio/id28065", + text=load_fixture(town_id28065_fixture), + ) + mock.get( + "https://opendata.aemet.es/opendata/api/maestro/municipios", + text=load_fixture(town_list_fixture), + ) + + +async def async_init_integration( + hass: HomeAssistant, + skip_setup: bool = False, +): + """Set up the AEMET OpenData integration in Home Assistant.""" + + with requests_mock.mock() as _m: + aemet_requests_mock(_m) + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "mock", + CONF_LATITUDE: "40.30403754", + CONF_LONGITUDE: "-3.72935236", + CONF_NAME: "AEMET", + }, + ) + entry.add_to_hass(hass) + + if not skip_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/fixtures/aemet/station-3195-data.json b/tests/fixtures/aemet/station-3195-data.json new file mode 100644 index 00000000000..1784a5fb3a4 --- /dev/null +++ b/tests/fixtures/aemet/station-3195-data.json @@ -0,0 +1,369 @@ +[ { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-08T14:00:00", + "prec" : 1.2, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 929.9, + "hr" : 97.0, + "pres_nmar" : 1009.9, + "tamin" : -0.1, + "ta" : 0.1, + "tamax" : 0.2, + "tpr" : -0.3, + "rviento" : 132.0 +}, { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-08T15:00:00", + "prec" : 1.5, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 929.0, + "hr" : 98.0, + "pres_nmar" : 1008.9, + "tamin" : 0.1, + "ta" : 0.2, + "tamax" : 0.3, + "tpr" : 0.0, + "rviento" : 154.0 +}, { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-08T16:00:00", + "prec" : 0.7, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 928.8, + "hr" : 98.0, + "pres_nmar" : 1008.6, + "tamin" : 0.2, + "ta" : 0.3, + "tamax" : 0.3, + "tpr" : 0.0, + "rviento" : 177.0 +}, { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-08T17:00:00", + "prec" : 1.7, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 928.6, + "hr" : 99.0, + "pres_nmar" : 1008.5, + "tamin" : 0.1, + "ta" : 0.1, + "tamax" : 0.3, + "tpr" : 0.0, + "rviento" : 174.0 +}, { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-08T18:00:00", + "prec" : 1.9, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 928.2, + "hr" : 99.0, + "pres_nmar" : 1008.1, + "tamin" : -0.1, + "ta" : -0.1, + "tamax" : 0.1, + "tpr" : -0.3, + "rviento" : 163.0 +}, { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-08T19:00:00", + "prec" : 3.0, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 928.4, + "hr" : 99.0, + "pres_nmar" : 1008.4, + "tamin" : -0.3, + "ta" : -0.3, + "tamax" : 0.0, + "tpr" : -0.5, + "rviento" : 79.0 +}, { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-08T20:00:00", + "prec" : 3.5, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 928.4, + "hr" : 99.0, + "pres_nmar" : 1008.5, + "tamin" : -0.6, + "ta" : -0.6, + "tamax" : -0.3, + "tpr" : -0.7, + "rviento" : 0.0 +}, { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-08T21:00:00", + "prec" : 2.6, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 928.1, + "hr" : 99.0, + "pres_nmar" : 1008.2, + "tamin" : -0.7, + "ta" : -0.7, + "tamax" : -0.5, + "tpr" : -0.7, + "rviento" : 0.0 +}, { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-08T22:00:00", + "prec" : 3.0, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 927.6, + "hr" : 99.0, + "pres_nmar" : 1007.7, + "tamin" : -0.8, + "ta" : -0.8, + "tamax" : -0.7, + "tpr" : -1.0, + "rviento" : 0.0 +}, { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-08T23:00:00", + "prec" : 2.9, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 926.9, + "hr" : 99.0, + "pres_nmar" : 1007.0, + "tamin" : -0.9, + "ta" : -0.9, + "tamax" : -0.7, + "tpr" : -1.0, + "rviento" : 0.0 +}, { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-09T00:00:00", + "prec" : 1.4, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 926.5, + "hr" : 99.0, + "pres_nmar" : 1006.6, + "tamin" : -1.0, + "ta" : -1.0, + "tamax" : -0.8, + "tpr" : -1.2, + "rviento" : 0.0 +}, { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-09T01:00:00", + "prec" : 2.0, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 925.9, + "hr" : 99.0, + "pres_nmar" : 1006.0, + "tamin" : -1.3, + "ta" : -1.3, + "tamax" : -1.0, + "tpr" : -1.4, + "rviento" : 0.0 +}, { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-09T02:00:00", + "prec" : 1.5, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 925.7, + "hr" : 99.0, + "pres_nmar" : 1005.8, + "tamin" : -1.5, + "ta" : -1.4, + "tamax" : -1.3, + "tpr" : -1.4, + "rviento" : 0.0 +}, { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-09T03:00:00", + "prec" : 1.2, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 925.6, + "hr" : 99.0, + "pres_nmar" : 1005.7, + "tamin" : -1.5, + "ta" : -1.4, + "tamax" : -1.4, + "tpr" : -1.4, + "rviento" : 0.0 +}, { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-09T04:00:00", + "prec" : 1.1, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 924.9, + "hr" : 99.0, + "pres_nmar" : 1005.0, + "tamin" : -1.5, + "ta" : -1.5, + "tamax" : -1.4, + "tpr" : -1.7, + "rviento" : 0.0 +}, { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-09T05:00:00", + "prec" : 0.7, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 924.6, + "hr" : 99.0, + "pres_nmar" : 1004.7, + "tamin" : -1.5, + "ta" : -1.5, + "tamax" : -1.4, + "tpr" : -1.7, + "rviento" : 0.0 +}, { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-09T06:00:00", + "prec" : 0.2, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 924.4, + "hr" : 99.0, + "pres_nmar" : 1004.5, + "tamin" : -1.6, + "ta" : -1.6, + "tamax" : -1.5, + "tpr" : -1.7, + "rviento" : 0.0 +}, { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-09T07:00:00", + "prec" : 0.0, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 924.4, + "hr" : 99.0, + "pres_nmar" : 1004.5, + "tamin" : -1.6, + "ta" : -1.6, + "tamax" : -1.6, + "tpr" : -1.7, + "rviento" : 0.0 +}, { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-09T08:00:00", + "prec" : 0.1, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 924.8, + "hr" : 99.0, + "pres_nmar" : 1004.9, + "tamin" : -1.6, + "ta" : -1.6, + "tamax" : -1.5, + "tpr" : -1.7, + "rviento" : 0.0 +}, { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-09T09:00:00", + "prec" : 0.0, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 925.0, + "hr" : 99.0, + "pres_nmar" : 1005.0, + "tamin" : -1.6, + "ta" : -1.3, + "tamax" : -1.3, + "tpr" : -1.4, + "rviento" : 0.0 +}, { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-09T10:00:00", + "prec" : 0.0, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 925.3, + "hr" : 99.0, + "pres_nmar" : 1005.3, + "tamin" : -1.3, + "ta" : -1.2, + "tamax" : -1.1, + "tpr" : -1.4, + "rviento" : 0.0 +}, { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-09T11:00:00", + "prec" : 4.4, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 925.4, + "hr" : 99.0, + "pres_nmar" : 1005.4, + "tamin" : -1.2, + "ta" : -1.0, + "tamax" : -1.0, + "tpr" : -1.2, + "rviento" : 0.0 +}, { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-09T12:00:00", + "prec" : 7.0, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 924.6, + "hr" : 99.0, + "pres_nmar" : 1004.4, + "tamin" : -1.0, + "ta" : -0.7, + "tamax" : -0.6, + "tpr" : -0.7, + "rviento" : 0.0 +} ] diff --git a/tests/fixtures/aemet/station-3195.json b/tests/fixtures/aemet/station-3195.json new file mode 100644 index 00000000000..f97df3bea63 --- /dev/null +++ b/tests/fixtures/aemet/station-3195.json @@ -0,0 +1,6 @@ +{ + "descripcion" : "exito", + "estado" : 200, + "datos" : "https://opendata.aemet.es/opendata/sh/208c3ca3", + "metadatos" : "https://opendata.aemet.es/opendata/sh/55c2971b" +} diff --git a/tests/fixtures/aemet/station-list-data.json b/tests/fixtures/aemet/station-list-data.json new file mode 100644 index 00000000000..8b35bff6e4a --- /dev/null +++ b/tests/fixtures/aemet/station-list-data.json @@ -0,0 +1,42 @@ +[ { + "idema" : "3194U", + "lon" : -3.724167, + "fint" : "2021-01-08T14:00:00", + "prec" : 1.3, + "alt" : 664.0, + "lat" : 40.45167, + "ubi" : "MADRID C. UNIVERSITARIA", + "hr" : 98.0, + "tamin" : 0.6, + "ta" : 0.9, + "tamax" : 1.0, + "tpr" : 0.6 +}, { + "idema" : "3194Y", + "lon" : -3.813369, + "fint" : "2021-01-08T14:00:00", + "prec" : 0.2, + "alt" : 665.0, + "lat" : 40.448437, + "ubi" : "POZUELO DE ALARCON (AUTOM�TICA)", + "hr" : 93.0, + "tamin" : 0.5, + "ta" : 0.6, + "tamax" : 0.6 +}, { + "idema" : "3195", + "lon" : -3.678095, + "fint" : "2021-01-08T14:00:00", + "prec" : 1.2, + "alt" : 667.0, + "lat" : 40.411804, + "ubi" : "MADRID RETIRO", + "pres" : 929.9, + "hr" : 97.0, + "pres_nmar" : 1009.9, + "tamin" : -0.1, + "ta" : 0.1, + "tamax" : 0.2, + "tpr" : -0.3, + "rviento" : 132.0 +} ] diff --git a/tests/fixtures/aemet/station-list.json b/tests/fixtures/aemet/station-list.json new file mode 100644 index 00000000000..6e0dbc97d6d --- /dev/null +++ b/tests/fixtures/aemet/station-list.json @@ -0,0 +1,6 @@ +{ + "descripcion" : "exito", + "estado" : 200, + "datos" : "https://opendata.aemet.es/opendata/sh/2c55192f", + "metadatos" : "https://opendata.aemet.es/opendata/sh/55c2971b" +} diff --git a/tests/fixtures/aemet/town-28065-forecast-daily-data.json b/tests/fixtures/aemet/town-28065-forecast-daily-data.json new file mode 100644 index 00000000000..77877c72f3a --- /dev/null +++ b/tests/fixtures/aemet/town-28065-forecast-daily-data.json @@ -0,0 +1,625 @@ +[ { + "origen" : { + "productor" : "Agencia Estatal de Meteorolog�a - AEMET. Gobierno de Espa�a", + "web" : "http://www.aemet.es", + "enlace" : "http://www.aemet.es/es/eltiempo/prediccion/municipios/getafe-id28065", + "language" : "es", + "copyright" : "� AEMET. Autorizado el uso de la informaci�n y su reproducci�n citando a AEMET como autora de la misma.", + "notaLegal" : "http://www.aemet.es/es/nota_legal" + }, + "elaborado" : "2021-01-09T11:54:00", + "nombre" : "Getafe", + "provincia" : "Madrid", + "prediccion" : { + "dia" : [ { + "probPrecipitacion" : [ { + "value" : 0, + "periodo" : "00-24" + }, { + "value" : 0, + "periodo" : "00-12" + }, { + "value" : 100, + "periodo" : "12-24" + }, { + "value" : 0, + "periodo" : "00-06" + }, { + "value" : 100, + "periodo" : "06-12" + }, { + "value" : 100, + "periodo" : "12-18" + }, { + "value" : 100, + "periodo" : "18-24" + } ], + "cotaNieveProv" : [ { + "value" : "", + "periodo" : "00-24" + }, { + "value" : "", + "periodo" : "00-12" + }, { + "value" : "500", + "periodo" : "12-24" + }, { + "value" : "", + "periodo" : "00-06" + }, { + "value" : "400", + "periodo" : "06-12" + }, { + "value" : "500", + "periodo" : "12-18" + }, { + "value" : "600", + "periodo" : "18-24" + } ], + "estadoCielo" : [ { + "value" : "", + "periodo" : "00-24", + "descripcion" : "" + }, { + "value" : "", + "periodo" : "00-12", + "descripcion" : "" + }, { + "value" : "36", + "periodo" : "12-24", + "descripcion" : "Cubierto con nieve" + }, { + "value" : "", + "periodo" : "00-06", + "descripcion" : "" + }, { + "value" : "36", + "periodo" : "06-12", + "descripcion" : "Cubierto con nieve" + }, { + "value" : "36", + "periodo" : "12-18", + "descripcion" : "Cubierto con nieve" + }, { + "value" : "34n", + "periodo" : "18-24", + "descripcion" : "Nuboso con nieve" + } ], + "viento" : [ { + "direccion" : "", + "velocidad" : 0, + "periodo" : "00-24" + }, { + "direccion" : "", + "velocidad" : 0, + "periodo" : "00-12" + }, { + "direccion" : "E", + "velocidad" : 15, + "periodo" : "12-24" + }, { + "direccion" : "NE", + "velocidad" : 30, + "periodo" : "00-06" + }, { + "direccion" : "E", + "velocidad" : 15, + "periodo" : "06-12" + }, { + "direccion" : "E", + "velocidad" : 5, + "periodo" : "12-18" + }, { + "direccion" : "NE", + "velocidad" : 5, + "periodo" : "18-24" + } ], + "rachaMax" : [ { + "value" : "", + "periodo" : "00-24" + }, { + "value" : "", + "periodo" : "00-12" + }, { + "value" : "", + "periodo" : "12-24" + }, { + "value" : "40", + "periodo" : "00-06" + }, { + "value" : "", + "periodo" : "06-12" + }, { + "value" : "", + "periodo" : "12-18" + }, { + "value" : "", + "periodo" : "18-24" + } ], + "temperatura" : { + "maxima" : 2, + "minima" : -1, + "dato" : [ { + "value" : -1, + "hora" : 6 + }, { + "value" : 0, + "hora" : 12 + }, { + "value" : 1, + "hora" : 18 + }, { + "value" : 1, + "hora" : 24 + } ] + }, + "sensTermica" : { + "maxima" : 1, + "minima" : -9, + "dato" : [ { + "value" : -1, + "hora" : 6 + }, { + "value" : -4, + "hora" : 12 + }, { + "value" : 1, + "hora" : 18 + }, { + "value" : 1, + "hora" : 24 + } ] + }, + "humedadRelativa" : { + "maxima" : 100, + "minima" : 75, + "dato" : [ { + "value" : 100, + "hora" : 6 + }, { + "value" : 100, + "hora" : 12 + }, { + "value" : 95, + "hora" : 18 + }, { + "value" : 75, + "hora" : 24 + } ] + }, + "uvMax" : 1, + "fecha" : "2021-01-09T00:00:00" + }, { + "probPrecipitacion" : [ { + "value" : 30, + "periodo" : "00-24" + }, { + "value" : 25, + "periodo" : "00-12" + }, { + "value" : 5, + "periodo" : "12-24" + }, { + "value" : 5, + "periodo" : "00-06" + }, { + "value" : 15, + "periodo" : "06-12" + }, { + "value" : 5, + "periodo" : "12-18" + }, { + "value" : 0, + "periodo" : "18-24" + } ], + "cotaNieveProv" : [ { + "value" : "600", + "periodo" : "00-24" + }, { + "value" : "600", + "periodo" : "00-12" + }, { + "value" : "", + "periodo" : "12-24" + }, { + "value" : "", + "periodo" : "00-06" + }, { + "value" : "600", + "periodo" : "06-12" + }, { + "value" : "", + "periodo" : "12-18" + }, { + "value" : "", + "periodo" : "18-24" + } ], + "estadoCielo" : [ { + "value" : "13", + "periodo" : "00-24", + "descripcion" : "Intervalos nubosos" + }, { + "value" : "15", + "periodo" : "00-12", + "descripcion" : "Muy nuboso" + }, { + "value" : "12", + "periodo" : "12-24", + "descripcion" : "Poco nuboso" + }, { + "value" : "14n", + "periodo" : "00-06", + "descripcion" : "Nuboso" + }, { + "value" : "15", + "periodo" : "06-12", + "descripcion" : "Muy nuboso" + }, { + "value" : "12", + "periodo" : "12-18", + "descripcion" : "Poco nuboso" + }, { + "value" : "12n", + "periodo" : "18-24", + "descripcion" : "Poco nuboso" + } ], + "viento" : [ { + "direccion" : "NE", + "velocidad" : 20, + "periodo" : "00-24" + }, { + "direccion" : "NE", + "velocidad" : 20, + "periodo" : "00-12" + }, { + "direccion" : "NE", + "velocidad" : 20, + "periodo" : "12-24" + }, { + "direccion" : "N", + "velocidad" : 10, + "periodo" : "00-06" + }, { + "direccion" : "NE", + "velocidad" : 20, + "periodo" : "06-12" + }, { + "direccion" : "NE", + "velocidad" : 15, + "periodo" : "12-18" + }, { + "direccion" : "NE", + "velocidad" : 20, + "periodo" : "18-24" + } ], + "rachaMax" : [ { + "value" : "30", + "periodo" : "00-24" + }, { + "value" : "30", + "periodo" : "00-12" + }, { + "value" : "30", + "periodo" : "12-24" + }, { + "value" : "", + "periodo" : "00-06" + }, { + "value" : "30", + "periodo" : "06-12" + }, { + "value" : "", + "periodo" : "12-18" + }, { + "value" : "", + "periodo" : "18-24" + } ], + "temperatura" : { + "maxima" : 4, + "minima" : -4, + "dato" : [ { + "value" : -1, + "hora" : 6 + }, { + "value" : 3, + "hora" : 12 + }, { + "value" : 1, + "hora" : 18 + }, { + "value" : -1, + "hora" : 24 + } ] + }, + "sensTermica" : { + "maxima" : 1, + "minima" : -7, + "dato" : [ { + "value" : -4, + "hora" : 6 + }, { + "value" : -2, + "hora" : 12 + }, { + "value" : -4, + "hora" : 18 + }, { + "value" : -6, + "hora" : 24 + } ] + }, + "humedadRelativa" : { + "maxima" : 100, + "minima" : 70, + "dato" : [ { + "value" : 90, + "hora" : 6 + }, { + "value" : 75, + "hora" : 12 + }, { + "value" : 80, + "hora" : 18 + }, { + "value" : 80, + "hora" : 24 + } ] + }, + "uvMax" : 1, + "fecha" : "2021-01-10T00:00:00" + }, { + "probPrecipitacion" : [ { + "value" : 0, + "periodo" : "00-24" + }, { + "value" : 0, + "periodo" : "00-12" + }, { + "value" : 0, + "periodo" : "12-24" + } ], + "cotaNieveProv" : [ { + "value" : "", + "periodo" : "00-24" + }, { + "value" : "", + "periodo" : "00-12" + }, { + "value" : "", + "periodo" : "12-24" + } ], + "estadoCielo" : [ { + "value" : "12", + "periodo" : "00-24", + "descripcion" : "Poco nuboso" + }, { + "value" : "12", + "periodo" : "00-12", + "descripcion" : "Poco nuboso" + }, { + "value" : "12", + "periodo" : "12-24", + "descripcion" : "Poco nuboso" + } ], + "viento" : [ { + "direccion" : "N", + "velocidad" : 5, + "periodo" : "00-24" + }, { + "direccion" : "NE", + "velocidad" : 20, + "periodo" : "00-12" + }, { + "direccion" : "NO", + "velocidad" : 10, + "periodo" : "12-24" + } ], + "rachaMax" : [ { + "value" : "", + "periodo" : "00-24" + }, { + "value" : "", + "periodo" : "00-12" + }, { + "value" : "", + "periodo" : "12-24" + } ], + "temperatura" : { + "maxima" : 3, + "minima" : -7, + "dato" : [ ] + }, + "sensTermica" : { + "maxima" : 3, + "minima" : -8, + "dato" : [ ] + }, + "humedadRelativa" : { + "maxima" : 85, + "minima" : 60, + "dato" : [ ] + }, + "uvMax" : 1, + "fecha" : "2021-01-11T00:00:00" + }, { + "probPrecipitacion" : [ { + "value" : 0, + "periodo" : "00-24" + }, { + "value" : 0, + "periodo" : "00-12" + }, { + "value" : 0, + "periodo" : "12-24" + } ], + "cotaNieveProv" : [ { + "value" : "", + "periodo" : "00-24" + }, { + "value" : "", + "periodo" : "00-12" + }, { + "value" : "", + "periodo" : "12-24" + } ], + "estadoCielo" : [ { + "value" : "12", + "periodo" : "00-24", + "descripcion" : "Poco nuboso" + }, { + "value" : "12", + "periodo" : "00-12", + "descripcion" : "Poco nuboso" + }, { + "value" : "12", + "periodo" : "12-24", + "descripcion" : "Poco nuboso" + } ], + "viento" : [ { + "direccion" : "C", + "velocidad" : 0, + "periodo" : "00-24" + }, { + "direccion" : "E", + "velocidad" : 5, + "periodo" : "00-12" + }, { + "direccion" : "C", + "velocidad" : 0, + "periodo" : "12-24" + } ], + "rachaMax" : [ { + "value" : "", + "periodo" : "00-24" + }, { + "value" : "", + "periodo" : "00-12" + }, { + "value" : "", + "periodo" : "12-24" + } ], + "temperatura" : { + "maxima" : -1, + "minima" : -13, + "dato" : [ ] + }, + "sensTermica" : { + "maxima" : -1, + "minima" : -13, + "dato" : [ ] + }, + "humedadRelativa" : { + "maxima" : 100, + "minima" : 65, + "dato" : [ ] + }, + "uvMax" : 2, + "fecha" : "2021-01-12T00:00:00" + }, { + "probPrecipitacion" : [ { + "value" : 0 + } ], + "cotaNieveProv" : [ { + "value" : "" + } ], + "estadoCielo" : [ { + "value" : "11", + "descripcion" : "Despejado" + } ], + "viento" : [ { + "direccion" : "C", + "velocidad" : 0 + } ], + "rachaMax" : [ { + "value" : "" + } ], + "temperatura" : { + "maxima" : 6, + "minima" : -11, + "dato" : [ ] + }, + "sensTermica" : { + "maxima" : 6, + "minima" : -11, + "dato" : [ ] + }, + "humedadRelativa" : { + "maxima" : 100, + "minima" : 65, + "dato" : [ ] + }, + "uvMax" : 2, + "fecha" : "2021-01-13T00:00:00" + }, { + "probPrecipitacion" : [ { + "value" : 0 + } ], + "cotaNieveProv" : [ { + "value" : "" + } ], + "estadoCielo" : [ { + "value" : "12", + "descripcion" : "Poco nuboso" + } ], + "viento" : [ { + "direccion" : "C", + "velocidad" : 0 + } ], + "rachaMax" : [ { + "value" : "" + } ], + "temperatura" : { + "maxima" : 6, + "minima" : -7, + "dato" : [ ] + }, + "sensTermica" : { + "maxima" : 6, + "minima" : -7, + "dato" : [ ] + }, + "humedadRelativa" : { + "maxima" : 100, + "minima" : 80, + "dato" : [ ] + }, + "fecha" : "2021-01-14T00:00:00" + }, { + "probPrecipitacion" : [ { + "value" : 0 + } ], + "cotaNieveProv" : [ { + "value" : "" + } ], + "estadoCielo" : [ { + "value" : "14", + "descripcion" : "Nuboso" + } ], + "viento" : [ { + "direccion" : "C", + "velocidad" : 0 + } ], + "rachaMax" : [ { + "value" : "" + } ], + "temperatura" : { + "maxima" : 5, + "minima" : -4, + "dato" : [ ] + }, + "sensTermica" : { + "maxima" : 5, + "minima" : -4, + "dato" : [ ] + }, + "humedadRelativa" : { + "maxima" : 100, + "minima" : 55, + "dato" : [ ] + }, + "fecha" : "2021-01-15T00:00:00" + } ] + }, + "id" : 28065, + "version" : 1.0 +} ] diff --git a/tests/fixtures/aemet/town-28065-forecast-daily.json b/tests/fixtures/aemet/town-28065-forecast-daily.json new file mode 100644 index 00000000000..35935658c50 --- /dev/null +++ b/tests/fixtures/aemet/town-28065-forecast-daily.json @@ -0,0 +1,6 @@ +{ + "descripcion" : "exito", + "estado" : 200, + "datos" : "https://opendata.aemet.es/opendata/sh/64e29abb", + "metadatos" : "https://opendata.aemet.es/opendata/sh/dfd88b22" +} diff --git a/tests/fixtures/aemet/town-28065-forecast-hourly-data.json b/tests/fixtures/aemet/town-28065-forecast-hourly-data.json new file mode 100644 index 00000000000..2bd3a22235a --- /dev/null +++ b/tests/fixtures/aemet/town-28065-forecast-hourly-data.json @@ -0,0 +1,1416 @@ +[ { + "origen" : { + "productor" : "Agencia Estatal de Meteorolog�a - AEMET. Gobierno de Espa�a", + "web" : "http://www.aemet.es", + "enlace" : "http://www.aemet.es/es/eltiempo/prediccion/municipios/horas/getafe-id28065", + "language" : "es", + "copyright" : "� AEMET. Autorizado el uso de la informaci�n y su reproducci�n citando a AEMET como autora de la misma.", + "notaLegal" : "http://www.aemet.es/es/nota_legal" + }, + "elaborado" : "2021-01-09T11:47:45", + "nombre" : "Getafe", + "provincia" : "Madrid", + "prediccion" : { + "dia" : [ { + "estadoCielo" : [ { + "value" : "36n", + "periodo" : "07", + "descripcion" : "Cubierto con nieve" + }, { + "value" : "36n", + "periodo" : "08", + "descripcion" : "Cubierto con nieve" + }, { + "value" : "36", + "periodo" : "09", + "descripcion" : "Cubierto con nieve" + }, { + "value" : "36", + "periodo" : "10", + "descripcion" : "Cubierto con nieve" + }, { + "value" : "36", + "periodo" : "11", + "descripcion" : "Cubierto con nieve" + }, { + "value" : "36", + "periodo" : "12", + "descripcion" : "Cubierto con nieve" + }, { + "value" : "36", + "periodo" : "13", + "descripcion" : "Cubierto con nieve" + }, { + "value" : "46", + "periodo" : "14", + "descripcion" : "Cubierto con lluvia escasa" + }, { + "value" : "46", + "periodo" : "15", + "descripcion" : "Cubierto con lluvia escasa" + }, { + "value" : "36", + "periodo" : "16", + "descripcion" : "Cubierto con nieve" + }, { + "value" : "36", + "periodo" : "17", + "descripcion" : "Cubierto con nieve" + }, { + "value" : "74n", + "periodo" : "18", + "descripcion" : "Cubierto con nieve escasa" + }, { + "value" : "46n", + "periodo" : "19", + "descripcion" : "Cubierto con lluvia escasa" + }, { + "value" : "46n", + "periodo" : "20", + "descripcion" : "Cubierto con lluvia escasa" + }, { + "value" : "16n", + "periodo" : "21", + "descripcion" : "Cubierto" + }, { + "value" : "16n", + "periodo" : "22", + "descripcion" : "Cubierto" + }, { + "value" : "12n", + "periodo" : "23", + "descripcion" : "Poco nuboso" + } ], + "precipitacion" : [ { + "value" : "1.4", + "periodo" : "07" + }, { + "value" : "2.1", + "periodo" : "08" + }, { + "value" : "1.9", + "periodo" : "09" + }, { + "value" : "2", + "periodo" : "10" + }, { + "value" : "1.9", + "periodo" : "11" + }, { + "value" : "1.8", + "periodo" : "12" + }, { + "value" : "1.5", + "periodo" : "13" + }, { + "value" : "0.5", + "periodo" : "14" + }, { + "value" : "0.6", + "periodo" : "15" + }, { + "value" : "0.8", + "periodo" : "16" + }, { + "value" : "0.6", + "periodo" : "17" + }, { + "value" : "0.2", + "periodo" : "18" + }, { + "value" : "0.2", + "periodo" : "19" + }, { + "value" : "0.1", + "periodo" : "20" + }, { + "value" : "0", + "periodo" : "21" + }, { + "value" : "0", + "periodo" : "22" + }, { + "value" : "0", + "periodo" : "23" + } ], + "probPrecipitacion" : [ { + "value" : "", + "periodo" : "0107" + }, { + "value" : "100", + "periodo" : "0713" + }, { + "value" : "100", + "periodo" : "1319" + }, { + "value" : "100", + "periodo" : "1901" + } ], + "probTormenta" : [ { + "value" : "", + "periodo" : "0107" + }, { + "value" : "0", + "periodo" : "0713" + }, { + "value" : "0", + "periodo" : "1319" + }, { + "value" : "0", + "periodo" : "1901" + } ], + "nieve" : [ { + "value" : "1.4", + "periodo" : "07" + }, { + "value" : "2.1", + "periodo" : "08" + }, { + "value" : "1.9", + "periodo" : "09" + }, { + "value" : "2", + "periodo" : "10" + }, { + "value" : "1.9", + "periodo" : "11" + }, { + "value" : "1.8", + "periodo" : "12" + }, { + "value" : "1.2", + "periodo" : "13" + }, { + "value" : "0.1", + "periodo" : "14" + }, { + "value" : "0.2", + "periodo" : "15" + }, { + "value" : "0.6", + "periodo" : "16" + }, { + "value" : "0.6", + "periodo" : "17" + }, { + "value" : "0.2", + "periodo" : "18" + }, { + "value" : "0.1", + "periodo" : "19" + }, { + "value" : "0", + "periodo" : "20" + }, { + "value" : "0", + "periodo" : "21" + }, { + "value" : "0", + "periodo" : "22" + }, { + "value" : "0", + "periodo" : "23" + } ], + "probNieve" : [ { + "value" : "", + "periodo" : "0107" + }, { + "value" : "100", + "periodo" : "0713" + }, { + "value" : "100", + "periodo" : "1319" + }, { + "value" : "80", + "periodo" : "1901" + } ], + "temperatura" : [ { + "value" : "-1", + "periodo" : "07" + }, { + "value" : "-1", + "periodo" : "08" + }, { + "value" : "-1", + "periodo" : "09" + }, { + "value" : "-1", + "periodo" : "10" + }, { + "value" : "-1", + "periodo" : "11" + }, { + "value" : "-0", + "periodo" : "12" + }, { + "value" : "-0", + "periodo" : "13" + }, { + "value" : "0", + "periodo" : "14" + }, { + "value" : "1", + "periodo" : "15" + }, { + "value" : "1", + "periodo" : "16" + }, { + "value" : "1", + "periodo" : "17" + }, { + "value" : "1", + "periodo" : "18" + }, { + "value" : "1", + "periodo" : "19" + }, { + "value" : "1", + "periodo" : "20" + }, { + "value" : "1", + "periodo" : "21" + }, { + "value" : "1", + "periodo" : "22" + }, { + "value" : "1", + "periodo" : "23" + } ], + "sensTermica" : [ { + "value" : "-8", + "periodo" : "07" + }, { + "value" : "-7", + "periodo" : "08" + }, { + "value" : "-7", + "periodo" : "09" + }, { + "value" : "-6", + "periodo" : "10" + }, { + "value" : "-6", + "periodo" : "11" + }, { + "value" : "-4", + "periodo" : "12" + }, { + "value" : "-4", + "periodo" : "13" + }, { + "value" : "-4", + "periodo" : "14" + }, { + "value" : "-2", + "periodo" : "15" + }, { + "value" : "-2", + "periodo" : "16" + }, { + "value" : "-2", + "periodo" : "17" + }, { + "value" : "1", + "periodo" : "18" + }, { + "value" : "-2", + "periodo" : "19" + }, { + "value" : "1", + "periodo" : "20" + }, { + "value" : "1", + "periodo" : "21" + }, { + "value" : "1", + "periodo" : "22" + }, { + "value" : "-2", + "periodo" : "23" + } ], + "humedadRelativa" : [ { + "value" : "96", + "periodo" : "07" + }, { + "value" : "96", + "periodo" : "08" + }, { + "value" : "99", + "periodo" : "09" + }, { + "value" : "100", + "periodo" : "10" + }, { + "value" : "100", + "periodo" : "11" + }, { + "value" : "100", + "periodo" : "12" + }, { + "value" : "100", + "periodo" : "13" + }, { + "value" : "100", + "periodo" : "14" + }, { + "value" : "100", + "periodo" : "15" + }, { + "value" : "97", + "periodo" : "16" + }, { + "value" : "94", + "periodo" : "17" + }, { + "value" : "93", + "periodo" : "18" + }, { + "value" : "93", + "periodo" : "19" + }, { + "value" : "92", + "periodo" : "20" + }, { + "value" : "89", + "periodo" : "21" + }, { + "value" : "89", + "periodo" : "22" + }, { + "value" : "85", + "periodo" : "23" + } ], + "vientoAndRachaMax" : [ { + "direccion" : [ "NE" ], + "velocidad" : [ "28" ], + "periodo" : "07" + }, { + "value" : "41", + "periodo" : "07" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "27" ], + "periodo" : "08" + }, { + "value" : "41", + "periodo" : "08" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "25" ], + "periodo" : "09" + }, { + "value" : "39", + "periodo" : "09" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "20" ], + "periodo" : "10" + }, { + "value" : "36", + "periodo" : "10" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "17" ], + "periodo" : "11" + }, { + "value" : "29", + "periodo" : "11" + }, { + "direccion" : [ "E" ], + "velocidad" : [ "15" ], + "periodo" : "12" + }, { + "value" : "24", + "periodo" : "12" + }, { + "direccion" : [ "SE" ], + "velocidad" : [ "15" ], + "periodo" : "13" + }, { + "value" : "22", + "periodo" : "13" + }, { + "direccion" : [ "SE" ], + "velocidad" : [ "14" ], + "periodo" : "14" + }, { + "value" : "24", + "periodo" : "14" + }, { + "direccion" : [ "SE" ], + "velocidad" : [ "10" ], + "periodo" : "15" + }, { + "value" : "20", + "periodo" : "15" + }, { + "direccion" : [ "SE" ], + "velocidad" : [ "8" ], + "periodo" : "16" + }, { + "value" : "14", + "periodo" : "16" + }, { + "direccion" : [ "SE" ], + "velocidad" : [ "9" ], + "periodo" : "17" + }, { + "value" : "13", + "periodo" : "17" + }, { + "direccion" : [ "E" ], + "velocidad" : [ "7" ], + "periodo" : "18" + }, { + "value" : "13", + "periodo" : "18" + }, { + "direccion" : [ "SE" ], + "velocidad" : [ "8" ], + "periodo" : "19" + }, { + "value" : "12", + "periodo" : "19" + }, { + "direccion" : [ "SE" ], + "velocidad" : [ "6" ], + "periodo" : "20" + }, { + "value" : "12", + "periodo" : "20" + }, { + "direccion" : [ "E" ], + "velocidad" : [ "6" ], + "periodo" : "21" + }, { + "value" : "8", + "periodo" : "21" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "6" ], + "periodo" : "22" + }, { + "value" : "9", + "periodo" : "22" + }, { + "direccion" : [ "E" ], + "velocidad" : [ "8" ], + "periodo" : "23" + }, { + "value" : "11", + "periodo" : "23" + } ], + "fecha" : "2021-01-09T00:00:00", + "orto" : "08:37", + "ocaso" : "18:07" + }, { + "estadoCielo" : [ { + "value" : "12n", + "periodo" : "00", + "descripcion" : "Poco nuboso" + }, { + "value" : "81n", + "periodo" : "01", + "descripcion" : "Niebla" + }, { + "value" : "81n", + "periodo" : "02", + "descripcion" : "Niebla" + }, { + "value" : "81n", + "periodo" : "03", + "descripcion" : "Niebla" + }, { + "value" : "17n", + "periodo" : "04", + "descripcion" : "Nubes altas" + }, { + "value" : "16n", + "periodo" : "05", + "descripcion" : "Cubierto" + }, { + "value" : "16n", + "periodo" : "06", + "descripcion" : "Cubierto" + }, { + "value" : "16n", + "periodo" : "07", + "descripcion" : "Cubierto" + }, { + "value" : "16n", + "periodo" : "08", + "descripcion" : "Cubierto" + }, { + "value" : "14", + "periodo" : "09", + "descripcion" : "Nuboso" + }, { + "value" : "12", + "periodo" : "10", + "descripcion" : "Poco nuboso" + }, { + "value" : "12", + "periodo" : "11", + "descripcion" : "Poco nuboso" + }, { + "value" : "17", + "periodo" : "12", + "descripcion" : "Nubes altas" + }, { + "value" : "17", + "periodo" : "13", + "descripcion" : "Nubes altas" + }, { + "value" : "17", + "periodo" : "14", + "descripcion" : "Nubes altas" + }, { + "value" : "17", + "periodo" : "15", + "descripcion" : "Nubes altas" + }, { + "value" : "17", + "periodo" : "16", + "descripcion" : "Nubes altas" + }, { + "value" : "17", + "periodo" : "17", + "descripcion" : "Nubes altas" + }, { + "value" : "12n", + "periodo" : "18", + "descripcion" : "Poco nuboso" + }, { + "value" : "12n", + "periodo" : "19", + "descripcion" : "Poco nuboso" + }, { + "value" : "14n", + "periodo" : "20", + "descripcion" : "Nuboso" + }, { + "value" : "16n", + "periodo" : "21", + "descripcion" : "Cubierto" + }, { + "value" : "16n", + "periodo" : "22", + "descripcion" : "Cubierto" + }, { + "value" : "15n", + "periodo" : "23", + "descripcion" : "Muy nuboso" + } ], + "precipitacion" : [ { + "value" : "0", + "periodo" : "00" + }, { + "value" : "0", + "periodo" : "01" + }, { + "value" : "0", + "periodo" : "02" + }, { + "value" : "0", + "periodo" : "03" + }, { + "value" : "0", + "periodo" : "04" + }, { + "value" : "0", + "periodo" : "05" + }, { + "value" : "0", + "periodo" : "06" + }, { + "value" : "0", + "periodo" : "07" + }, { + "value" : "0", + "periodo" : "08" + }, { + "value" : "Ip", + "periodo" : "09" + }, { + "value" : "0", + "periodo" : "10" + }, { + "value" : "0", + "periodo" : "11" + }, { + "value" : "0", + "periodo" : "12" + }, { + "value" : "0", + "periodo" : "13" + }, { + "value" : "0", + "periodo" : "14" + }, { + "value" : "0", + "periodo" : "15" + }, { + "value" : "0", + "periodo" : "16" + }, { + "value" : "0", + "periodo" : "17" + }, { + "value" : "0", + "periodo" : "18" + }, { + "value" : "0", + "periodo" : "19" + }, { + "value" : "0", + "periodo" : "20" + }, { + "value" : "0", + "periodo" : "21" + }, { + "value" : "0", + "periodo" : "22" + }, { + "value" : "0", + "periodo" : "23" + } ], + "probPrecipitacion" : [ { + "value" : "10", + "periodo" : "0107" + }, { + "value" : "15", + "periodo" : "0713" + }, { + "value" : "5", + "periodo" : "1319" + }, { + "value" : "0", + "periodo" : "1901" + } ], + "probTormenta" : [ { + "value" : "0", + "periodo" : "0107" + }, { + "value" : "0", + "periodo" : "0713" + }, { + "value" : "0", + "periodo" : "1319" + }, { + "value" : "0", + "periodo" : "1901" + } ], + "nieve" : [ { + "value" : "0", + "periodo" : "00" + }, { + "value" : "0", + "periodo" : "01" + }, { + "value" : "0", + "periodo" : "02" + }, { + "value" : "0", + "periodo" : "03" + }, { + "value" : "0", + "periodo" : "04" + }, { + "value" : "0", + "periodo" : "05" + }, { + "value" : "0", + "periodo" : "06" + }, { + "value" : "0", + "periodo" : "07" + }, { + "value" : "0", + "periodo" : "08" + }, { + "value" : "Ip", + "periodo" : "09" + }, { + "value" : "0", + "periodo" : "10" + }, { + "value" : "0", + "periodo" : "11" + }, { + "value" : "0", + "periodo" : "12" + }, { + "value" : "0", + "periodo" : "13" + }, { + "value" : "0", + "periodo" : "14" + }, { + "value" : "0", + "periodo" : "15" + }, { + "value" : "0", + "periodo" : "16" + }, { + "value" : "0", + "periodo" : "17" + }, { + "value" : "0", + "periodo" : "18" + }, { + "value" : "0", + "periodo" : "19" + }, { + "value" : "0", + "periodo" : "20" + }, { + "value" : "0", + "periodo" : "21" + }, { + "value" : "0", + "periodo" : "22" + }, { + "value" : "0", + "periodo" : "23" + } ], + "probNieve" : [ { + "value" : "10", + "periodo" : "0107" + }, { + "value" : "10", + "periodo" : "0713" + }, { + "value" : "0", + "periodo" : "1319" + }, { + "value" : "0", + "periodo" : "1901" + } ], + "temperatura" : [ { + "value" : "1", + "periodo" : "00" + }, { + "value" : "0", + "periodo" : "01" + }, { + "value" : "-0", + "periodo" : "02" + }, { + "value" : "-0", + "periodo" : "03" + }, { + "value" : "-1", + "periodo" : "04" + }, { + "value" : "-1", + "periodo" : "05" + }, { + "value" : "-1", + "periodo" : "06" + }, { + "value" : "-2", + "periodo" : "07" + }, { + "value" : "-1", + "periodo" : "08" + }, { + "value" : "-1", + "periodo" : "09" + }, { + "value" : "0", + "periodo" : "10" + }, { + "value" : "2", + "periodo" : "11" + }, { + "value" : "3", + "periodo" : "12" + }, { + "value" : "3", + "periodo" : "13" + }, { + "value" : "3", + "periodo" : "14" + }, { + "value" : "4", + "periodo" : "15" + }, { + "value" : "3", + "periodo" : "16" + }, { + "value" : "2", + "periodo" : "17" + }, { + "value" : "1", + "periodo" : "18" + }, { + "value" : "1", + "periodo" : "19" + }, { + "value" : "1", + "periodo" : "20" + }, { + "value" : "1", + "periodo" : "21" + }, { + "value" : "0", + "periodo" : "22" + }, { + "value" : "-0", + "periodo" : "23" + } ], + "sensTermica" : [ { + "value" : "1", + "periodo" : "00" + }, { + "value" : "0", + "periodo" : "01" + }, { + "value" : "-0", + "periodo" : "02" + }, { + "value" : "-0", + "periodo" : "03" + }, { + "value" : "-4", + "periodo" : "04" + }, { + "value" : "-1", + "periodo" : "05" + }, { + "value" : "-4", + "periodo" : "06" + }, { + "value" : "-6", + "periodo" : "07" + }, { + "value" : "-6", + "periodo" : "08" + }, { + "value" : "-7", + "periodo" : "09" + }, { + "value" : "-5", + "periodo" : "10" + }, { + "value" : "-3", + "periodo" : "11" + }, { + "value" : "-2", + "periodo" : "12" + }, { + "value" : "-1", + "periodo" : "13" + }, { + "value" : "-1", + "periodo" : "14" + }, { + "value" : "0", + "periodo" : "15" + }, { + "value" : "-1", + "periodo" : "16" + }, { + "value" : "-2", + "periodo" : "17" + }, { + "value" : "-4", + "periodo" : "18" + }, { + "value" : "-4", + "periodo" : "19" + }, { + "value" : "-3", + "periodo" : "20" + }, { + "value" : "-4", + "periodo" : "21" + }, { + "value" : "-5", + "periodo" : "22" + }, { + "value" : "-5", + "periodo" : "23" + } ], + "humedadRelativa" : [ { + "value" : "74", + "periodo" : "00" + }, { + "value" : "71", + "periodo" : "01" + }, { + "value" : "80", + "periodo" : "02" + }, { + "value" : "84", + "periodo" : "03" + }, { + "value" : "81", + "periodo" : "04" + }, { + "value" : "78", + "periodo" : "05" + }, { + "value" : "90", + "periodo" : "06" + }, { + "value" : "100", + "periodo" : "07" + }, { + "value" : "100", + "periodo" : "08" + }, { + "value" : "93", + "periodo" : "09" + }, { + "value" : "84", + "periodo" : "10" + }, { + "value" : "78", + "periodo" : "11" + }, { + "value" : "73", + "periodo" : "12" + }, { + "value" : "74", + "periodo" : "13" + }, { + "value" : "74", + "periodo" : "14" + }, { + "value" : "73", + "periodo" : "15" + }, { + "value" : "78", + "periodo" : "16" + }, { + "value" : "79", + "periodo" : "17" + }, { + "value" : "79", + "periodo" : "18" + }, { + "value" : "77", + "periodo" : "19" + }, { + "value" : "75", + "periodo" : "20" + }, { + "value" : "77", + "periodo" : "21" + }, { + "value" : "80", + "periodo" : "22" + }, { + "value" : "80", + "periodo" : "23" + } ], + "vientoAndRachaMax" : [ { + "direccion" : [ "NE" ], + "velocidad" : [ "6" ], + "periodo" : "00" + }, { + "value" : "12", + "periodo" : "00" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "5" ], + "periodo" : "01" + }, { + "value" : "10", + "periodo" : "01" + }, { + "direccion" : [ "N" ], + "velocidad" : [ "6" ], + "periodo" : "02" + }, { + "value" : "11", + "periodo" : "02" + }, { + "direccion" : [ "N" ], + "velocidad" : [ "6" ], + "periodo" : "03" + }, { + "value" : "9", + "periodo" : "03" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "8" ], + "periodo" : "04" + }, { + "value" : "12", + "periodo" : "04" + }, { + "direccion" : [ "N" ], + "velocidad" : [ "5" ], + "periodo" : "05" + }, { + "value" : "11", + "periodo" : "05" + }, { + "direccion" : [ "N" ], + "velocidad" : [ "9" ], + "periodo" : "06" + }, { + "value" : "13", + "periodo" : "06" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "13" ], + "periodo" : "07" + }, { + "value" : "18", + "periodo" : "07" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "17" ], + "periodo" : "08" + }, { + "value" : "25", + "periodo" : "08" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "21" ], + "periodo" : "09" + }, { + "value" : "31", + "periodo" : "09" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "21" ], + "periodo" : "10" + }, { + "value" : "32", + "periodo" : "10" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "21" ], + "periodo" : "11" + }, { + "value" : "30", + "periodo" : "11" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "22" ], + "periodo" : "12" + }, { + "value" : "32", + "periodo" : "12" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "20" ], + "periodo" : "13" + }, { + "value" : "32", + "periodo" : "13" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "19" ], + "periodo" : "14" + }, { + "value" : "30", + "periodo" : "14" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "17" ], + "periodo" : "15" + }, { + "value" : "28", + "periodo" : "15" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "16" ], + "periodo" : "16" + }, { + "value" : "25", + "periodo" : "16" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "16" ], + "periodo" : "17" + }, { + "value" : "24", + "periodo" : "17" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "17" ], + "periodo" : "18" + }, { + "value" : "24", + "periodo" : "18" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "17" ], + "periodo" : "19" + }, { + "value" : "25", + "periodo" : "19" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "16" ], + "periodo" : "20" + }, { + "value" : "25", + "periodo" : "20" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "17" ], + "periodo" : "21" + }, { + "value" : "24", + "periodo" : "21" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "19" ], + "periodo" : "22" + }, { + "value" : "27", + "periodo" : "22" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "21" ], + "periodo" : "23" + }, { + "value" : "30", + "periodo" : "23" + } ], + "fecha" : "2021-01-10T00:00:00", + "orto" : "08:36", + "ocaso" : "18:08" + }, { + "estadoCielo" : [ { + "value" : "14n", + "periodo" : "00", + "descripcion" : "Nuboso" + }, { + "value" : "12n", + "periodo" : "01", + "descripcion" : "Poco nuboso" + }, { + "value" : "11n", + "periodo" : "02", + "descripcion" : "Despejado" + }, { + "value" : "11n", + "periodo" : "03", + "descripcion" : "Despejado" + }, { + "value" : "11n", + "periodo" : "04", + "descripcion" : "Despejado" + }, { + "value" : "11n", + "periodo" : "05", + "descripcion" : "Despejado" + }, { + "value" : "11n", + "periodo" : "06", + "descripcion" : "Despejado" + } ], + "precipitacion" : [ { + "value" : "0", + "periodo" : "00" + }, { + "value" : "0", + "periodo" : "01" + }, { + "value" : "0", + "periodo" : "02" + }, { + "value" : "0", + "periodo" : "03" + }, { + "value" : "0", + "periodo" : "04" + }, { + "value" : "0", + "periodo" : "05" + }, { + "value" : "0", + "periodo" : "06" + } ], + "probPrecipitacion" : [ { + "value" : "0", + "periodo" : "0107" + }, { + "value" : "", + "periodo" : "0713" + }, { + "value" : "", + "periodo" : "1319" + }, { + "value" : "", + "periodo" : "1901" + } ], + "probTormenta" : [ { + "value" : "0", + "periodo" : "0107" + }, { + "value" : "", + "periodo" : "0713" + }, { + "value" : "", + "periodo" : "1319" + }, { + "value" : "", + "periodo" : "1901" + } ], + "nieve" : [ { + "value" : "0", + "periodo" : "00" + }, { + "value" : "0", + "periodo" : "01" + }, { + "value" : "0", + "periodo" : "02" + }, { + "value" : "0", + "periodo" : "03" + }, { + "value" : "0", + "periodo" : "04" + }, { + "value" : "0", + "periodo" : "05" + }, { + "value" : "0", + "periodo" : "06" + } ], + "probNieve" : [ { + "value" : "0", + "periodo" : "0107" + }, { + "value" : "", + "periodo" : "0713" + }, { + "value" : "", + "periodo" : "1319" + }, { + "value" : "", + "periodo" : "1901" + } ], + "temperatura" : [ { + "value" : "-1", + "periodo" : "00" + }, { + "value" : "-1", + "periodo" : "01" + }, { + "value" : "-2", + "periodo" : "02" + }, { + "value" : "-2", + "periodo" : "03" + }, { + "value" : "-3", + "periodo" : "04" + }, { + "value" : "-4", + "periodo" : "05" + }, { + "value" : "-4", + "periodo" : "06" + } ], + "sensTermica" : [ { + "value" : "-6", + "periodo" : "00" + }, { + "value" : "-6", + "periodo" : "01" + }, { + "value" : "-6", + "periodo" : "02" + }, { + "value" : "-6", + "periodo" : "03" + }, { + "value" : "-7", + "periodo" : "04" + }, { + "value" : "-8", + "periodo" : "05" + }, { + "value" : "-8", + "periodo" : "06" + } ], + "humedadRelativa" : [ { + "value" : "81", + "periodo" : "00" + }, { + "value" : "79", + "periodo" : "01" + }, { + "value" : "77", + "periodo" : "02" + }, { + "value" : "76", + "periodo" : "03" + }, { + "value" : "76", + "periodo" : "04" + }, { + "value" : "76", + "periodo" : "05" + }, { + "value" : "78", + "periodo" : "06" + } ], + "vientoAndRachaMax" : [ { + "direccion" : [ "NE" ], + "velocidad" : [ "19" ], + "periodo" : "00" + }, { + "value" : "30", + "periodo" : "00" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "16" ], + "periodo" : "01" + }, { + "value" : "27", + "periodo" : "01" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "12" ], + "periodo" : "02" + }, { + "value" : "22", + "periodo" : "02" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "10" ], + "periodo" : "03" + }, { + "value" : "17", + "periodo" : "03" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "11" ], + "periodo" : "04" + }, { + "value" : "15", + "periodo" : "04" + }, { + "direccion" : [ "NE" ], + "velocidad" : [ "10" ], + "periodo" : "05" + }, { + "value" : "15", + "periodo" : "05" + }, { + "direccion" : [ "N" ], + "velocidad" : [ "10" ], + "periodo" : "06" + }, { + "value" : "15", + "periodo" : "06" + } ], + "fecha" : "2021-01-11T00:00:00", + "orto" : "08:36", + "ocaso" : "18:09" + } ] + }, + "id" : "28065", + "version" : "1.0" +} ] diff --git a/tests/fixtures/aemet/town-28065-forecast-hourly.json b/tests/fixtures/aemet/town-28065-forecast-hourly.json new file mode 100644 index 00000000000..2fbcaaeb33e --- /dev/null +++ b/tests/fixtures/aemet/town-28065-forecast-hourly.json @@ -0,0 +1,6 @@ +{ + "descripcion" : "exito", + "estado" : 200, + "datos" : "https://opendata.aemet.es/opendata/sh/18ca1886", + "metadatos" : "https://opendata.aemet.es/opendata/sh/93a7c63d" +} diff --git a/tests/fixtures/aemet/town-id28065.json b/tests/fixtures/aemet/town-id28065.json new file mode 100644 index 00000000000..342b163062c --- /dev/null +++ b/tests/fixtures/aemet/town-id28065.json @@ -0,0 +1,15 @@ +[ { + "latitud" : "40�18'14.535144\"", + "id_old" : "28325", + "url" : "getafe-id28065", + "latitud_dec" : "40.30403754", + "altitud" : "622", + "capital" : "Getafe", + "num_hab" : "173057", + "zona_comarcal" : "722802", + "destacada" : "1", + "nombre" : "Getafe", + "longitud_dec" : "-3.72935236", + "id" : "id28065", + "longitud" : "-3�43'45.668496\"" +} ] diff --git a/tests/fixtures/aemet/town-list.json b/tests/fixtures/aemet/town-list.json new file mode 100644 index 00000000000..d5ed23ef935 --- /dev/null +++ b/tests/fixtures/aemet/town-list.json @@ -0,0 +1,43 @@ +[ { + "latitud" : "40�18'14.535144\"", + "id_old" : "28325", + "url" : "getafe-id28065", + "latitud_dec" : "40.30403754", + "altitud" : "622", + "capital" : "Getafe", + "num_hab" : "173057", + "zona_comarcal" : "722802", + "destacada" : "1", + "nombre" : "Getafe", + "longitud_dec" : "-3.72935236", + "id" : "id28065", + "longitud" : "-3�43'45.668496\"" +}, { + "latitud" : "40�19'54.277752\"", + "id_old" : "28370", + "url" : "leganes-id28074", + "latitud_dec" : "40.33174382", + "altitud" : "667", + "capital" : "Legan�s", + "num_hab" : "186696", + "zona_comarcal" : "722802", + "destacada" : "1", + "nombre" : "Legan�s", + "longitud_dec" : "-3.76655557", + "id" : "id28074", + "longitud" : "-3�45'59.600052\"" +}, { + "latitud" : "40�24'30.282876\"", + "id_old" : "28001", + "url" : "madrid-id28079", + "latitud_dec" : "40.40841191", + "altitud" : "657", + "capital" : "Madrid", + "num_hab" : "3165235", + "zona_comarcal" : "722802", + "destacada" : "1", + "nombre" : "Madrid", + "longitud_dec" : "-3.68760088", + "id" : "id28079", + "longitud" : "-3�41'15.363168\"" +} ] From 7148071be89b0ecbb5cfb33140b3f4702db7035f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 13 Feb 2021 23:50:25 +0100 Subject: [PATCH 407/796] Improve Elgato code quality (#46505) --- .../components/elgato/config_flow.py | 128 +++++----- homeassistant/components/elgato/light.py | 24 +- tests/components/elgato/__init__.py | 11 +- tests/components/elgato/test_config_flow.py | 219 +++++++----------- tests/components/elgato/test_init.py | 2 +- tests/components/elgato/test_light.py | 3 +- 6 files changed, 153 insertions(+), 234 deletions(-) diff --git a/homeassistant/components/elgato/config_flow.py b/homeassistant/components/elgato/config_flow.py index 60cc08dc9b3..e9138afd86c 100644 --- a/homeassistant/components/elgato/config_flow.py +++ b/homeassistant/components/elgato/config_flow.py @@ -1,13 +1,15 @@ """Config flow to configure the Elgato Key Light integration.""" -from typing import Any, Dict, Optional +from __future__ import annotations -from elgato import Elgato, ElgatoError, Info +from typing import Any, Dict + +from elgato import Elgato, ElgatoError import voluptuous as vol from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import ConfigType from .const import CONF_SERIAL_NUMBER, DOMAIN # pylint: disable=unused-import @@ -18,91 +20,54 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL + host: str + port: int + serial_number: str + async def async_step_user( - self, user_input: Optional[ConfigType] = None + self, user_input: Dict[str, Any] | None = None ) -> Dict[str, Any]: """Handle a flow initiated by the user.""" if user_input is None: - return self._show_setup_form() + return self._async_show_setup_form() + + self.host = user_input[CONF_HOST] + self.port = user_input[CONF_PORT] try: - info = await self._get_elgato_info( - user_input[CONF_HOST], user_input[CONF_PORT] - ) + await self._get_elgato_serial_number(raise_on_progress=False) except ElgatoError: - return self._show_setup_form({"base": "cannot_connect"}) + return self._async_show_setup_form({"base": "cannot_connect"}) - # Check if already configured - await self.async_set_unique_id(info.serial_number) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=info.serial_number, - data={ - CONF_HOST: user_input[CONF_HOST], - CONF_PORT: user_input[CONF_PORT], - CONF_SERIAL_NUMBER: info.serial_number, - }, - ) + return self._async_create_entry() async def async_step_zeroconf( - self, user_input: Optional[ConfigType] = None + self, discovery_info: Dict[str, Any] ) -> Dict[str, Any]: """Handle zeroconf discovery.""" - if user_input is None: - return self.async_abort(reason="cannot_connect") + self.host = discovery_info[CONF_HOST] + self.port = discovery_info[CONF_PORT] try: - info = await self._get_elgato_info( - user_input[CONF_HOST], user_input[CONF_PORT] - ) + await self._get_elgato_serial_number() except ElgatoError: return self.async_abort(reason="cannot_connect") - # Check if already configured - await self.async_set_unique_id(info.serial_number) - self._abort_if_unique_id_configured(updates={CONF_HOST: user_input[CONF_HOST]}) - - self.context.update( - { - CONF_HOST: user_input[CONF_HOST], - CONF_PORT: user_input[CONF_PORT], - CONF_SERIAL_NUMBER: info.serial_number, - "title_placeholders": {"serial_number": info.serial_number}, - } + return self.async_show_form( + step_id="zeroconf_confirm", + description_placeholders={"serial_number": self.serial_number}, ) - # Prepare configuration flow - return self._show_confirm_dialog() - async def async_step_zeroconf_confirm( - self, user_input: ConfigType = None + self, _: Dict[str, Any] | None = None ) -> Dict[str, Any]: """Handle a flow initiated by zeroconf.""" - if user_input is None: - return self._show_confirm_dialog() + return self._async_create_entry() - try: - info = await self._get_elgato_info( - self.context.get(CONF_HOST), self.context.get(CONF_PORT) - ) - except ElgatoError: - return self.async_abort(reason="cannot_connect") - - # Check if already configured - await self.async_set_unique_id(info.serial_number) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=self.context.get(CONF_SERIAL_NUMBER), - data={ - CONF_HOST: self.context.get(CONF_HOST), - CONF_PORT: self.context.get(CONF_PORT), - CONF_SERIAL_NUMBER: self.context.get(CONF_SERIAL_NUMBER), - }, - ) - - def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: + @callback + def _async_show_setup_form( + self, errors: Dict[str, str] | None = None + ) -> Dict[str, Any]: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -115,20 +80,33 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - def _show_confirm_dialog(self) -> Dict[str, Any]: - """Show the confirm dialog to the user.""" - serial_number = self.context.get(CONF_SERIAL_NUMBER) - return self.async_show_form( - step_id="zeroconf_confirm", - description_placeholders={"serial_number": serial_number}, + @callback + def _async_create_entry(self) -> Dict[str, Any]: + return self.async_create_entry( + title=self.serial_number, + data={ + CONF_HOST: self.host, + CONF_PORT: self.port, + CONF_SERIAL_NUMBER: self.serial_number, + }, ) - async def _get_elgato_info(self, host: str, port: int) -> Info: + async def _get_elgato_serial_number(self, raise_on_progress: bool = True) -> None: """Get device information from an Elgato Key Light device.""" session = async_get_clientsession(self.hass) elgato = Elgato( - host, - port=port, + host=self.host, + port=self.port, session=session, ) - return await elgato.info() + info = await elgato.info() + + # Check if already configured + await self.async_set_unique_id( + info.serial_number, raise_on_progress=raise_on_progress + ) + self._abort_if_unique_id_configured( + updates={CONF_HOST: self.host, CONF_PORT: self.port} + ) + + self.serial_number = info.serial_number diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index 313b5600248..eea80e60b15 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -1,7 +1,9 @@ """Support for LED lights.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable, Dict, List from elgato import Elgato, ElgatoError, Info, State @@ -42,7 +44,7 @@ async def async_setup_entry( """Set up Elgato Key Light based on a config entry.""" elgato: Elgato = hass.data[DOMAIN][entry.entry_id][DATA_ELGATO_CLIENT] info = await elgato.info() - async_add_entities([ElgatoLight(entry.entry_id, elgato, info)], True) + async_add_entities([ElgatoLight(elgato, info)], True) class ElgatoLight(LightEntity): @@ -50,15 +52,14 @@ class ElgatoLight(LightEntity): def __init__( self, - entry_id: str, elgato: Elgato, info: Info, ): """Initialize Elgato Key Light.""" - self._brightness: Optional[int] = None + self._brightness: int | None = None self._info: Info = info - self._state: Optional[bool] = None - self._temperature: Optional[int] = None + self._state: bool | None = None + self._temperature: int | None = None self._available = True self.elgato = elgato @@ -81,22 +82,22 @@ class ElgatoLight(LightEntity): return self._info.serial_number @property - def brightness(self) -> Optional[int]: + def brightness(self) -> int | None: """Return the brightness of this light between 1..255.""" return self._brightness @property - def color_temp(self): + def color_temp(self) -> int | None: """Return the CT color value in mireds.""" return self._temperature @property - def min_mireds(self): + def min_mireds(self) -> int: """Return the coldest color_temp that this light supports.""" return 143 @property - def max_mireds(self): + def max_mireds(self) -> int: """Return the warmest color_temp that this light supports.""" return 344 @@ -116,9 +117,8 @@ class ElgatoLight(LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" - data = {} + data: Dict[str, bool | int] = {ATTR_ON: True} - data[ATTR_ON] = True if ATTR_ON in kwargs: data[ATTR_ON] = kwargs[ATTR_ON] diff --git a/tests/components/elgato/__init__.py b/tests/components/elgato/__init__.py index 3b1942aee14..ea63bc0c4d0 100644 --- a/tests/components/elgato/__init__.py +++ b/tests/components/elgato/__init__.py @@ -14,27 +14,26 @@ async def init_integration( skip_setup: bool = False, ) -> MockConfigEntry: """Set up the Elgato Key Light integration in Home Assistant.""" - aioclient_mock.get( - "http://1.2.3.4:9123/elgato/accessory-info", + "http://127.0.0.1:9123/elgato/accessory-info", text=load_fixture("elgato/info.json"), headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.put( - "http://1.2.3.4:9123/elgato/lights", + "http://127.0.0.1:9123/elgato/lights", text=load_fixture("elgato/state.json"), headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( - "http://1.2.3.4:9123/elgato/lights", + "http://127.0.0.1:9123/elgato/lights", text=load_fixture("elgato/state.json"), headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( - "http://5.6.7.8:9123/elgato/accessory-info", + "http://127.0.0.2:9123/elgato/accessory-info", text=load_fixture("elgato/info.json"), headers={"Content-Type": CONTENT_TYPE_JSON}, ) @@ -43,7 +42,7 @@ async def init_integration( domain=DOMAIN, unique_id="CN11A1A00001", data={ - CONF_HOST: "1.2.3.4", + CONF_HOST: "127.0.0.1", CONF_PORT: 9123, CONF_SERIAL_NUMBER: "CN11A1A00001", }, diff --git a/tests/components/elgato/test_config_flow.py b/tests/components/elgato/test_config_flow.py index c1dfa697041..0f3ff032722 100644 --- a/tests/components/elgato/test_config_flow.py +++ b/tests/components/elgato/test_config_flow.py @@ -2,10 +2,9 @@ import aiohttp from homeassistant import data_entry_flow -from homeassistant.components.elgato import config_flow -from homeassistant.components.elgato.const import CONF_SERIAL_NUMBER +from homeassistant.components.elgato.const import CONF_SERIAL_NUMBER, DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF -from homeassistant.const import CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SOURCE, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant from . import init_integration @@ -14,62 +13,97 @@ from tests.common import load_fixture from tests.test_util.aiohttp import AiohttpClientMocker -async def test_show_user_form(hass: HomeAssistant) -> None: - """Test that the user set up form is served.""" +async def test_full_user_flow_implementation( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the full manual user flow from start to finish.""" + aioclient_mock.get( + "http://127.0.0.1:9123/elgato/accessory-info", + text=load_fixture("elgato/info.json"), + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + # Start a discovered configuration flow, to guarantee a user flow doesn't abort + await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_ZEROCONF}, + data={ + "host": "127.0.0.1", + "hostname": "example.local.", + "port": 9123, + "properties": {}, + }, + ) + result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": SOURCE_USER}, + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, ) assert result["step_id"] == "user" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: "127.0.0.1", CONF_PORT: 9123} + ) -async def test_show_zeroconf_confirm_form(hass: HomeAssistant) -> None: - """Test that the zeroconf confirmation form is served.""" - flow = config_flow.ElgatoFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_ZEROCONF, CONF_SERIAL_NUMBER: "12345"} - result = await flow.async_step_zeroconf_confirm() + assert result["data"][CONF_HOST] == "127.0.0.1" + assert result["data"][CONF_PORT] == 9123 + assert result["data"][CONF_SERIAL_NUMBER] == "CN11A1A00001" + assert result["title"] == "CN11A1A00001" + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["description_placeholders"] == {CONF_SERIAL_NUMBER: "12345"} - assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + entries = hass.config_entries.async_entries(DOMAIN) + assert entries[0].unique_id == "CN11A1A00001" -async def test_show_zerconf_form( +async def test_full_zeroconf_flow_implementation( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: - """Test that the zeroconf confirmation form is served.""" + """Test the zeroconf flow from start to finish.""" aioclient_mock.get( - "http://1.2.3.4:9123/elgato/accessory-info", + "http://127.0.0.1:9123/elgato/accessory-info", text=load_fixture("elgato/info.json"), headers={"Content-Type": CONTENT_TYPE_JSON}, ) - flow = config_flow.ElgatoFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_ZEROCONF} - result = await flow.async_step_zeroconf({"host": "1.2.3.4", "port": 9123}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_ZEROCONF}, + data={ + "host": "127.0.0.1", + "hostname": "example.local.", + "port": 9123, + "properties": {}, + }, + ) - assert flow.context[CONF_HOST] == "1.2.3.4" - assert flow.context[CONF_PORT] == 9123 - assert flow.context[CONF_SERIAL_NUMBER] == "CN11A1A00001" assert result["description_placeholders"] == {CONF_SERIAL_NUMBER: "CN11A1A00001"} assert result["step_id"] == "zeroconf_confirm" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["data"][CONF_HOST] == "127.0.0.1" + assert result["data"][CONF_PORT] == 9123 + assert result["data"][CONF_SERIAL_NUMBER] == "CN11A1A00001" + assert result["title"] == "CN11A1A00001" + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + async def test_connection_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we show user form on Elgato Key Light connection error.""" - aioclient_mock.get("http://1.2.3.4/elgato/accessory-info", exc=aiohttp.ClientError) + aioclient_mock.get( + "http://127.0.0.1/elgato/accessory-info", exc=aiohttp.ClientError + ) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_HOST: "1.2.3.4", CONF_PORT: 9123}, + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 9123}, ) assert result["errors"] == {"base": "cannot_connect"} @@ -81,51 +115,20 @@ async def test_zeroconf_connection_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort zeroconf flow on Elgato Key Light connection error.""" - aioclient_mock.get("http://1.2.3.4/elgato/accessory-info", exc=aiohttp.ClientError) + aioclient_mock.get( + "http://127.0.0.1/elgato/accessory-info", exc=aiohttp.ClientError + ) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, + DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={"host": "1.2.3.4", "port": 9123}, + data={"host": "127.0.0.1", "port": 9123}, ) assert result["reason"] == "cannot_connect" assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT -async def test_zeroconf_confirm_connection_error( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test we abort zeroconf flow on Elgato Key Light connection error.""" - aioclient_mock.get("http://1.2.3.4/elgato/accessory-info", exc=aiohttp.ClientError) - - flow = config_flow.ElgatoFlowHandler() - flow.hass = hass - flow.context = { - "source": SOURCE_ZEROCONF, - CONF_HOST: "1.2.3.4", - CONF_PORT: 9123, - } - result = await flow.async_step_zeroconf_confirm( - user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 9123} - ) - - assert result["reason"] == "cannot_connect" - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - - -async def test_zeroconf_no_data( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test we abort if zeroconf provides no data.""" - flow = config_flow.ElgatoFlowHandler() - flow.hass = hass - result = await flow.async_step_zeroconf() - - assert result["reason"] == "cannot_connect" - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - - async def test_user_device_exists_abort( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: @@ -133,9 +136,9 @@ async def test_user_device_exists_abort( await init_integration(hass, aioclient_mock) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_HOST: "1.2.3.4", CONF_PORT: 9123}, + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 9123}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -148,84 +151,22 @@ async def test_zeroconf_device_exists_abort( await init_integration(hass, aioclient_mock) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": SOURCE_ZEROCONF}, - data={"host": "1.2.3.4", "port": 9123}, + DOMAIN, + context={CONF_SOURCE: SOURCE_ZEROCONF}, + data={"host": "127.0.0.1", "port": 9123}, ) assert result["reason"] == "already_configured" assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": SOURCE_ZEROCONF, CONF_HOST: "1.2.3.4", "port": 9123}, - data={"host": "5.6.7.8", "port": 9123}, + DOMAIN, + context={CONF_SOURCE: SOURCE_ZEROCONF}, + data={"host": "127.0.0.2", "port": 9123}, ) assert result["reason"] == "already_configured" assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - entries = hass.config_entries.async_entries(config_flow.DOMAIN) - assert entries[0].data[CONF_HOST] == "5.6.7.8" - - -async def test_full_user_flow_implementation( - hass: HomeAssistant, aioclient_mock -) -> None: - """Test the full manual user flow from start to finish.""" - aioclient_mock.get( - "http://1.2.3.4:9123/elgato/accessory-info", - text=load_fixture("elgato/info.json"), - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": SOURCE_USER}, - ) - - assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 9123} - ) - - assert result["data"][CONF_HOST] == "1.2.3.4" - assert result["data"][CONF_PORT] == 9123 - assert result["data"][CONF_SERIAL_NUMBER] == "CN11A1A00001" - assert result["title"] == "CN11A1A00001" - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - - entries = hass.config_entries.async_entries(config_flow.DOMAIN) - assert entries[0].unique_id == "CN11A1A00001" - - -async def test_full_zeroconf_flow_implementation( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test the full manual user flow from start to finish.""" - aioclient_mock.get( - "http://1.2.3.4:9123/elgato/accessory-info", - text=load_fixture("elgato/info.json"), - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - - flow = config_flow.ElgatoFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_ZEROCONF} - result = await flow.async_step_zeroconf({"host": "1.2.3.4", "port": 9123}) - - assert flow.context[CONF_HOST] == "1.2.3.4" - assert flow.context[CONF_PORT] == 9123 - assert flow.context[CONF_SERIAL_NUMBER] == "CN11A1A00001" - assert result["description_placeholders"] == {CONF_SERIAL_NUMBER: "CN11A1A00001"} - assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - result = await flow.async_step_zeroconf_confirm(user_input={CONF_HOST: "1.2.3.4"}) - assert result["data"][CONF_HOST] == "1.2.3.4" - assert result["data"][CONF_PORT] == 9123 - assert result["data"][CONF_SERIAL_NUMBER] == "CN11A1A00001" - assert result["title"] == "CN11A1A00001" - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + entries = hass.config_entries.async_entries(DOMAIN) + assert entries[0].data[CONF_HOST] == "127.0.0.2" diff --git a/tests/components/elgato/test_init.py b/tests/components/elgato/test_init.py index 2f0e39e05a8..069e533c423 100644 --- a/tests/components/elgato/test_init.py +++ b/tests/components/elgato/test_init.py @@ -14,7 +14,7 @@ async def test_config_entry_not_ready( ) -> None: """Test the Elgato Key Light configuration entry not ready.""" aioclient_mock.get( - "http://1.2.3.4:9123/elgato/accessory-info", exc=aiohttp.ClientError + "http://127.0.0.1:9123/elgato/accessory-info", exc=aiohttp.ClientError ) entry = await init_integration(hass, aioclient_mock) diff --git a/tests/components/elgato/test_light.py b/tests/components/elgato/test_light.py index 838608c0aac..aed569c18fe 100644 --- a/tests/components/elgato/test_light.py +++ b/tests/components/elgato/test_light.py @@ -1,7 +1,8 @@ """Tests for the Elgato Key Light light platform.""" from unittest.mock import patch -from homeassistant.components.elgato.light import ElgatoError +from elgato import ElgatoError + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, From 84488b9c281226017e0704a62695190e2c104790 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sat, 13 Feb 2021 18:21:15 -0500 Subject: [PATCH 408/796] Use core constants for sma (#46501) --- homeassistant/components/sma/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 119d9a366d6..94bab40a3b7 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -11,6 +11,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_PATH, CONF_SCAN_INTERVAL, + CONF_SENSORS, CONF_SSL, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, @@ -27,7 +28,6 @@ CONF_CUSTOM = "custom" CONF_FACTOR = "factor" CONF_GROUP = "group" CONF_KEY = "key" -CONF_SENSORS = "sensors" CONF_UNIT = "unit" GROUPS = ["user", "installer"] @@ -86,7 +86,6 @@ PLATFORM_SCHEMA = vol.All( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up SMA WebConnect sensor.""" - # Check config again during load - dependency available config = _check_sensor_schema(config) From 5db4d78dc75f93c48647cb13a7aef3811261e1e7 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sat, 13 Feb 2021 18:21:42 -0500 Subject: [PATCH 409/796] Use core constants for rpi_rf (#46500) --- homeassistant/components/rpi_rf/switch.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rpi_rf/switch.py b/homeassistant/components/rpi_rf/switch.py index 78c2153a7b3..4ac7283b194 100644 --- a/homeassistant/components/rpi_rf/switch.py +++ b/homeassistant/components/rpi_rf/switch.py @@ -6,7 +6,12 @@ from threading import RLock import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import CONF_NAME, CONF_SWITCHES, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + CONF_NAME, + CONF_PROTOCOL, + CONF_SWITCHES, + EVENT_HOMEASSISTANT_STOP, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -14,7 +19,6 @@ _LOGGER = logging.getLogger(__name__) CONF_CODE_OFF = "code_off" CONF_CODE_ON = "code_on" CONF_GPIO = "gpio" -CONF_PROTOCOL = "protocol" CONF_PULSELENGTH = "pulselength" CONF_SIGNAL_REPETITIONS = "signal_repetitions" From dfe173d6196c0a13195cb5f0bcf33c0e84714459 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sat, 13 Feb 2021 18:22:28 -0500 Subject: [PATCH 410/796] Use core constants for rmvtransport (#46502) --- homeassistant/components/rmvtransport/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index eb053de8950..da42c0cc927 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -8,7 +8,7 @@ from RMVtransport.rmvtransport import RMVtransportApiConnectionError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, TIME_MINUTES +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_TIMEOUT, TIME_MINUTES from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -25,7 +25,6 @@ CONF_LINES = "lines" CONF_PRODUCTS = "products" CONF_TIME_OFFSET = "time_offset" CONF_MAX_JOURNEYS = "max_journeys" -CONF_TIMEOUT = "timeout" DEFAULT_NAME = "RMV Journey" From 17a4678906269b07627d4b79a76234c4a9a980d9 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 14 Feb 2021 00:06:28 +0000 Subject: [PATCH 411/796] [ci skip] Translation update --- .../components/philips_js/translations/cs.json | 15 +++++++++++++++ .../components/philips_js/translations/et.json | 10 +++++++++- .../components/powerwall/translations/et.json | 7 +++++-- .../components/powerwall/translations/pl.json | 2 +- .../components/roku/translations/et.json | 1 + .../components/shelly/translations/pl.json | 6 +++--- .../components/tesla/translations/et.json | 4 ++++ 7 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/philips_js/translations/cs.json diff --git a/homeassistant/components/philips_js/translations/cs.json b/homeassistant/components/philips_js/translations/cs.json new file mode 100644 index 00000000000..8a5866b5959 --- /dev/null +++ b/homeassistant/components/philips_js/translations/cs.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "api_version": "Verze API", + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/et.json b/homeassistant/components/philips_js/translations/et.json index ef5e3e0ffce..c77ef726411 100644 --- a/homeassistant/components/philips_js/translations/et.json +++ b/homeassistant/components/philips_js/translations/et.json @@ -1,9 +1,17 @@ { "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, "step": { "user": { "data": { - "api_version": "API versioon" + "api_version": "API versioon", + "host": "Host" } } } diff --git a/homeassistant/components/powerwall/translations/et.json b/homeassistant/components/powerwall/translations/et.json index eaa70dc0a22..4a937029296 100644 --- a/homeassistant/components/powerwall/translations/et.json +++ b/homeassistant/components/powerwall/translations/et.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchenduse loomine nurjus. Proovi uuesti", + "invalid_auth": "Vigane autentimine", "unknown": "Ootamatu t\u00f5rge", "wrong_version": "Teie Powerwall kasutab tarkvaraversiooni, mida ei toetata. Kaaluge tarkvara uuendamist v\u00f5i probleemist teavitamist, et see saaks lahendatud." }, @@ -12,7 +14,8 @@ "step": { "user": { "data": { - "ip_address": "IP aadress" + "ip_address": "IP aadress", + "password": "Salas\u00f5na" }, "description": "Parool on tavaliselt Backup Gateway seerianumbri viimased 5 t\u00e4hem\u00e4rki ja selle leiad Tesla rakendusest v\u00f5i Backup Gateway 2 luugilt leitud parooli viimased 5 m\u00e4rki.", "title": "Powerwalliga \u00fchendamine" diff --git a/homeassistant/components/powerwall/translations/pl.json b/homeassistant/components/powerwall/translations/pl.json index 059aab2c014..272f28df3b9 100644 --- a/homeassistant/components/powerwall/translations/pl.json +++ b/homeassistant/components/powerwall/translations/pl.json @@ -17,7 +17,7 @@ "ip_address": "Adres IP", "password": "Has\u0142o" }, - "description": "Has\u0142o to zazwyczaj 5 ostatnich znak\u00f3w numeru seryjnego Backup Gateway i mo\u017cna je znale\u017a\u0107 w aplikacji Telsa; lub ostatnie 5 znak\u00f3w has\u0142a na wewn\u0119trznej stronie drzwiczek Backup Gateway 2.", + "description": "Has\u0142o to zazwyczaj 5 ostatnich znak\u00f3w numeru seryjnego Backup Gateway i mo\u017cna je znale\u017a\u0107 w aplikacji Tesla; lub ostatnie 5 znak\u00f3w has\u0142a na wewn\u0119trznej stronie drzwiczek Backup Gateway 2.", "title": "Po\u0142\u0105czenie z Powerwall" } } diff --git a/homeassistant/components/roku/translations/et.json b/homeassistant/components/roku/translations/et.json index 6727f539f57..17bce39f5df 100644 --- a/homeassistant/components/roku/translations/et.json +++ b/homeassistant/components/roku/translations/et.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", "unknown": "Tundmatu viga" }, "error": { diff --git a/homeassistant/components/shelly/translations/pl.json b/homeassistant/components/shelly/translations/pl.json index a6ca567d91a..b0c4dd11b1b 100644 --- a/homeassistant/components/shelly/translations/pl.json +++ b/homeassistant/components/shelly/translations/pl.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "confirm_discovery": { - "description": "Czy chcesz skonfigurowa\u0107 {model} ({host})?\n\nUrz\u0105dzenia zasilane bateryjnie, z ustawionym has\u0142em, nale\u017cy wybudzi\u0107 przed konfiguracj\u0105.\nUrz\u0105dzenia zasilane bateryjnie, bez ustawionego has\u0142a, zostan\u0105 dodane gdy urz\u0105dzenie si\u0119 wybudzi. Mo\u017cesz r\u0119cznie wybudzi\u0107 urz\u0105dzenie jego przyciskiem lub poczeka\u0107 na aktualizacj\u0119 danych z urz\u0105dzenia." + "description": "Czy chcesz skonfigurowa\u0107 {model} ({host})?\n\nUrz\u0105dzenia zasilane bateryjnie, z ustawionym has\u0142em, nale\u017cy wybudzi\u0107 przed konfiguracj\u0105.\nUrz\u0105dzenia zasilane bateryjnie, bez ustawionego has\u0142a, zostan\u0105 dodane, gdy si\u0119 wybudz\u0105. Mo\u017cesz r\u0119cznie wybudzi\u0107 urz\u0105dzenie przyciskiem na obudowie lub poczeka\u0107 na aktualizacj\u0119 danych z urz\u0105dzenia." }, "credentials": { "data": { @@ -24,13 +24,13 @@ "data": { "host": "Nazwa hosta lub adres IP" }, - "description": "Przed skonfigurowaniem urz\u0105dzenia zasilane bateryjnie nale\u017cy, wybudzi\u0107 naciskaj\u0105c przycisk na urz\u0105dzeniu." + "description": "Przed skonfigurowaniem urz\u0105dzenia zasilane bateryjnie nale\u017cy, wybudzi\u0107 naciskaj\u0105c przycisk na obudowie." } } }, "device_automation": { "trigger_subtype": { - "button": "Przycisk", + "button": "przycisk", "button1": "pierwszy", "button2": "drugi", "button3": "trzeci" diff --git a/homeassistant/components/tesla/translations/et.json b/homeassistant/components/tesla/translations/et.json index ae427f5d1e7..c7ceae36990 100644 --- a/homeassistant/components/tesla/translations/et.json +++ b/homeassistant/components/tesla/translations/et.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, "error": { "already_configured": "Konto on juba h\u00e4\u00e4lestatud", "cannot_connect": "\u00dchendamine nurjus", From 1845f69729f362fac26b7529e5de386f78b7ba99 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Feb 2021 16:23:19 -1000 Subject: [PATCH 412/796] Update tuya for new fan entity model (#45870) --- homeassistant/components/tuya/fan.py | 41 ++++++++++++---------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index a66e1ff92a4..b13b7c3602c 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -10,6 +10,10 @@ from homeassistant.components.fan import ( ) from homeassistant.const import CONF_PLATFORM, STATE_OFF from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.percentage import ( + ordered_list_item_to_percentage, + percentage_to_ordered_list_item, +) from . import TuyaDevice from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW @@ -61,27 +65,21 @@ class TuyaFanDevice(TuyaDevice, FanEntity): """Init Tuya fan device.""" super().__init__(tuya, platform) self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) - self.speeds = [STATE_OFF] + self.speeds = [] async def async_added_to_hass(self): """Create fan list when add to hass.""" await super().async_added_to_hass() self.speeds.extend(self._tuya.speed_list()) - def set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - if speed == STATE_OFF: + def set_percentage(self, percentage: int) -> None: + """Set the speed percentage of the fan.""" + if percentage == 0: self.turn_off() else: - self._tuya.set_speed(speed) + tuya_speed = percentage_to_ordered_list_item(self.speeds, percentage) + self._tuya.set_speed(tuya_speed) - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # def turn_on( self, speed: str = None, @@ -90,8 +88,8 @@ class TuyaFanDevice(TuyaDevice, FanEntity): **kwargs, ) -> None: """Turn on the fan.""" - if speed is not None: - self.set_speed(speed) + if percentage is not None: + self.set_percentage(percentage) else: self._tuya.turn_on() @@ -118,16 +116,13 @@ class TuyaFanDevice(TuyaDevice, FanEntity): return self._tuya.state() @property - def speed(self) -> str: + def percentage(self) -> str: """Return the current speed.""" - if self.is_on: - return self._tuya.speed() - return STATE_OFF - - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return self.speeds + if not self.is_on: + return 0 + if self.speeds is None: + return None + return ordered_list_item_to_percentage(self.speeds, self._tuya.speed()) @property def supported_features(self) -> int: From 7a401d3d5d9aa7bf76048187e1aedb3a4235a718 Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Sat, 13 Feb 2021 22:35:57 -0500 Subject: [PATCH 413/796] Fix missing condition in nws (#46513) --- homeassistant/components/nws/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nws/const.py b/homeassistant/components/nws/const.py index 574ad6925ac..f055bab0203 100644 --- a/homeassistant/components/nws/const.py +++ b/homeassistant/components/nws/const.py @@ -35,7 +35,7 @@ CONDITION_CLASSES = { "Hot", "Cold", ], - ATTR_CONDITION_SNOWY: ["Snow", "Sleet", "Blizzard"], + ATTR_CONDITION_SNOWY: ["Snow", "Sleet", "Snow/sleet", "Blizzard"], ATTR_CONDITION_SNOWY_RAINY: [ "Rain/snow", "Rain/sleet", From c76758f77537f29dfd2241572d877d81b9229f5e Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sun, 14 Feb 2021 04:21:02 -0500 Subject: [PATCH 414/796] Use core constants for temper (#46508) --- homeassistant/components/temper/sensor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py index fd26b1702dc..c47aa1878fc 100644 --- a/homeassistant/components/temper/sensor.py +++ b/homeassistant/components/temper/sensor.py @@ -5,13 +5,17 @@ from temperusb.temper import TemperHandler import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME, TEMP_FAHRENHEIT +from homeassistant.const import ( + CONF_NAME, + CONF_OFFSET, + DEVICE_DEFAULT_NAME, + TEMP_FAHRENHEIT, +) from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) CONF_SCALE = "scale" -CONF_OFFSET = "offset" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -26,7 +30,6 @@ TEMPER_SENSORS = [] def get_temper_devices(): """Scan the Temper devices from temperusb.""" - return TemperHandler().get_devices() From 854504cccc8de1aeb524d5536e1ba7627f4fda8a Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sun, 14 Feb 2021 04:21:53 -0500 Subject: [PATCH 415/796] Use core constants for switcher_kis (#46507) --- homeassistant/components/switcher_kis/__init__.py | 4 +--- homeassistant/components/switcher_kis/switch.py | 6 ------ 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index 244ed708cc7..d081b3331c7 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -8,7 +8,7 @@ from aioswitcher.bridge import SwitcherV2Bridge import voluptuous as vol from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_DEVICE_ID, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform @@ -20,7 +20,6 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "switcher_kis" -CONF_DEVICE_ID = "device_id" CONF_DEVICE_PASSWORD = "device_password" CONF_PHONE_ID = "phone_id" @@ -48,7 +47,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: """Set up the switcher component.""" - phone_id = config[DOMAIN][CONF_PHONE_ID] device_id = config[DOMAIN][CONF_DEVICE_ID] device_password = config[DOMAIN][CONF_DEVICE_PASSWORD] diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 5e75a0e6090..6b4b5026c2f 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -62,7 +62,6 @@ async def async_setup_platform( async def async_set_auto_off_service(entity, service_call: ServiceCallType) -> None: """Use for handling setting device auto-off service calls.""" - async with SwitcherV2Api( hass.loop, device_data.ip_addr, @@ -76,7 +75,6 @@ async def async_setup_platform( entity, service_call: ServiceCallType ) -> None: """Use for handling turning device on with a timer service calls.""" - async with SwitcherV2Api( hass.loop, device_data.ip_addr, @@ -133,7 +131,6 @@ class SwitcherControl(SwitchEntity): @property def is_on(self) -> bool: """Return True if entity is on.""" - return self._state == SWITCHER_STATE_ON @property @@ -144,7 +141,6 @@ class SwitcherControl(SwitchEntity): @property def device_state_attributes(self) -> Dict: """Return the optional state attributes.""" - attribs = {} for prop, attr in DEVICE_PROPERTIES_TO_HA_ATTRIBUTES.items(): @@ -157,7 +153,6 @@ class SwitcherControl(SwitchEntity): @property def available(self) -> bool: """Return True if entity is available.""" - return self._state in [SWITCHER_STATE_ON, SWITCHER_STATE_OFF] async def async_added_to_hass(self) -> None: @@ -188,7 +183,6 @@ class SwitcherControl(SwitchEntity): async def _control_device(self, send_on: bool) -> None: """Turn the entity on or off.""" - response: SwitcherV2ControlResponseMSG = None async with SwitcherV2Api( self.hass.loop, From 294d3c6529363b7548a81687cc4f607d7a88c5bd Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sun, 14 Feb 2021 05:28:55 -0500 Subject: [PATCH 416/796] Use core constants for thethingsnetwork (#46520) --- homeassistant/components/thethingsnetwork/sensor.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/thethingsnetwork/sensor.py b/homeassistant/components/thethingsnetwork/sensor.py index eab843069f4..2e7b7f9499b 100644 --- a/homeassistant/components/thethingsnetwork/sensor.py +++ b/homeassistant/components/thethingsnetwork/sensor.py @@ -8,7 +8,14 @@ import async_timeout import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONTENT_TYPE_JSON, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED +from homeassistant.const import ( + ATTR_DEVICE_ID, + ATTR_TIME, + CONF_DEVICE_ID, + CONTENT_TYPE_JSON, + HTTP_NOT_FOUND, + HTTP_UNAUTHORIZED, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -17,12 +24,9 @@ from . import DATA_TTN, TTN_ACCESS_KEY, TTN_APP_ID, TTN_DATA_STORAGE_URL _LOGGER = logging.getLogger(__name__) -ATTR_DEVICE_ID = "device_id" ATTR_RAW = "raw" -ATTR_TIME = "time" DEFAULT_TIMEOUT = 10 -CONF_DEVICE_ID = "device_id" CONF_VALUES = "values" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( From 3ea02e646d94d61e391c8294d23c9544891cdb6b Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sun, 14 Feb 2021 05:30:40 -0500 Subject: [PATCH 417/796] Use core constants for trend (#46521) --- homeassistant/components/trend/binary_sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py index 4b4bd48bfe3..b7079a3311a 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -15,6 +15,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, + CONF_ATTRIBUTE, CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FRIENDLY_NAME, @@ -40,7 +41,6 @@ ATTR_INVERT = "invert" ATTR_SAMPLE_DURATION = "sample_duration" ATTR_SAMPLE_COUNT = "sample_count" -CONF_ATTRIBUTE = "attribute" CONF_INVERT = "invert" CONF_MAX_SAMPLES = "max_samples" CONF_MIN_GRADIENT = "min_gradient" @@ -66,7 +66,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the trend sensors.""" - setup_reload_service(hass, DOMAIN, PLATFORMS) sensors = [] From 811e1cc3e67a8276ab09f9dddf434fe316a3e1b5 Mon Sep 17 00:00:00 2001 From: Khole Date: Sun, 14 Feb 2021 10:39:31 +0000 Subject: [PATCH 418/796] Add hive hub 360 sensors (#46320) --- homeassistant/components/hive/binary_sensor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index 30e5ae049f0..41f1dacc8f3 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -5,6 +5,8 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_MOTION, DEVICE_CLASS_OPENING, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_SOUND, BinarySensorEntity, ) @@ -14,6 +16,9 @@ DEVICETYPE = { "contactsensor": DEVICE_CLASS_OPENING, "motionsensor": DEVICE_CLASS_MOTION, "Connectivity": DEVICE_CLASS_CONNECTIVITY, + "SMOKE_CO": DEVICE_CLASS_SMOKE, + "DOG_BARK": DEVICE_CLASS_SOUND, + "GLASS_BREAK": DEVICE_CLASS_SOUND, } PARALLEL_UPDATES = 0 SCAN_INTERVAL = timedelta(seconds=15) From 2c3a2bd35e4590888f031275d1f7b5e1c1b56eac Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sun, 14 Feb 2021 06:16:30 -0500 Subject: [PATCH 419/796] Clean up template (#46509) --- homeassistant/components/template/__init__.py | 2 -- homeassistant/components/template/alarm_control_panel.py | 2 -- homeassistant/components/template/binary_sensor.py | 2 -- homeassistant/components/template/cover.py | 5 +---- homeassistant/components/template/fan.py | 1 - homeassistant/components/template/light.py | 4 ---- homeassistant/components/template/lock.py | 2 -- homeassistant/components/template/sensor.py | 3 --- homeassistant/components/template/switch.py | 2 -- homeassistant/components/template/template_entity.py | 1 - homeassistant/components/template/vacuum.py | 2 -- 11 files changed, 1 insertion(+), 25 deletions(-) diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index 079569a9324..cc8862afcf4 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -7,13 +7,11 @@ from .const import DOMAIN, EVENT_TEMPLATE_RELOADED, PLATFORMS async def async_setup_reload_service(hass): """Create the reload service for the template domain.""" - if hass.services.has_service(DOMAIN, SERVICE_RELOAD): return async def _reload_config(call): """Reload the template platform config.""" - await async_reload_integration_platforms(hass, DOMAIN, PLATFORMS) hass.bus.async_fire(EVENT_TEMPLATE_RELOADED, context=call.context) diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index ccefab767be..f56c5b27572 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -81,7 +81,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def _async_create_entities(hass, config): """Create Template Alarm Control Panels.""" - alarm_control_panels = [] for device, device_config in config[CONF_ALARM_CONTROL_PANELS].items(): @@ -114,7 +113,6 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Template Alarm Control Panels.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index f996b91a61e..b810c7faee1 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -97,7 +97,6 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template binary sensors.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) @@ -141,7 +140,6 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity): async def async_added_to_hass(self): """Register callbacks.""" - self.add_template_attribute("_state", self._template, None, self._update_state) if self._delay_on_raw is not None: diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 93ffd2fd988..278cd1c80bb 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -20,6 +20,7 @@ from homeassistant.components.cover import ( CoverEntity, ) from homeassistant.const import ( + CONF_COVERS, CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_ENTITY_PICTURE_TEMPLATE, @@ -53,8 +54,6 @@ _VALID_STATES = [ "false", ] -CONF_COVERS = "covers" - CONF_POSITION_TEMPLATE = "position_template" CONF_TILT_TEMPLATE = "tilt_template" OPEN_ACTION = "open_cover" @@ -161,7 +160,6 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Template cover.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) @@ -228,7 +226,6 @@ class CoverTemplate(TemplateEntity, CoverEntity): async def async_added_to_hass(self): """Register callbacks.""" - if self._template: self.add_template_attribute( "_position", self._template, None, self._update_state diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 5d01790f21a..ac77b3dc333 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -159,7 +159,6 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template fans.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 42493136b48..0edaacbb5ca 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -137,7 +137,6 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template lights.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) @@ -259,7 +258,6 @@ class LightTemplate(TemplateEntity, LightEntity): async def async_added_to_hass(self): """Register callbacks.""" - if self._template: self.add_template_attribute( "_state", self._template, None, self._update_state @@ -404,7 +402,6 @@ class LightTemplate(TemplateEntity, LightEntity): @callback def _update_state(self, result): """Update the state from the template.""" - if isinstance(result, TemplateError): # This behavior is legacy self._state = False @@ -431,7 +428,6 @@ class LightTemplate(TemplateEntity, LightEntity): @callback def _update_temperature(self, render): """Update the temperature from the template.""" - try: if render in ("None", ""): self._temperature = None diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index 2cd8bc00266..692f06e28fe 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -60,7 +60,6 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template lock.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) @@ -129,7 +128,6 @@ class TemplateLock(TemplateEntity, LockEntity): async def async_added_to_hass(self): """Register callbacks.""" - self.add_template_attribute( "_state", self._state_template, None, self._update_state ) diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index aea14884812..c67e3a275a3 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -59,7 +59,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def _async_create_entities(hass, config): """Create the template sensors.""" - sensors = [] for device, device_config in config[CONF_SENSORS].items(): @@ -96,7 +95,6 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template sensors.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) @@ -140,7 +138,6 @@ class SensorTemplate(TemplateEntity, Entity): async def async_added_to_hass(self): """Register callbacks.""" - self.add_template_attribute("_state", self._template, None, self._update_state) if self._friendly_name_template is not None: self.add_template_attribute("_name", self._friendly_name_template) diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index 511338b5aa1..412c4507d1f 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -90,7 +90,6 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template switches.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) @@ -147,7 +146,6 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): async def async_added_to_hass(self): """Register callbacks.""" - if self._template is None: # restore state after startup diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 49b0edfab02..f350eb87d61 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -223,7 +223,6 @@ class TemplateEntity(Entity): updates: List[TrackTemplateResult], ) -> None: """Call back the results to the attributes.""" - if event: self.async_set_context(event.context) diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 5bf8148b96e..171aeb7af92 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -147,7 +147,6 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template vacuums.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) @@ -337,7 +336,6 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): async def async_added_to_hass(self): """Register callbacks.""" - if self._template is not None: self.add_template_attribute( "_state", self._template, None, self._update_state From accba85e35d0e7e19f5bcb6855f429c6ef52b197 Mon Sep 17 00:00:00 2001 From: Andrey Kupreychik Date: Sun, 14 Feb 2021 19:09:19 +0700 Subject: [PATCH 420/796] Add keenetic_ndms2 config flow (#38353) --- .coveragerc | 4 + .../components/keenetic_ndms2/__init__.py | 91 ++++++ .../keenetic_ndms2/binary_sensor.py | 72 +++++ .../components/keenetic_ndms2/config_flow.py | 159 +++++++++ .../components/keenetic_ndms2/const.py | 21 ++ .../keenetic_ndms2/device_tracker.py | 301 +++++++++++++----- .../components/keenetic_ndms2/manifest.json | 5 +- .../components/keenetic_ndms2/router.py | 187 +++++++++++ .../components/keenetic_ndms2/strings.json | 36 +++ .../keenetic_ndms2/translations/en.json | 36 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + tests/components/keenetic_ndms2/__init__.py | 27 ++ .../keenetic_ndms2/test_config_flow.py | 169 ++++++++++ 15 files changed, 1036 insertions(+), 78 deletions(-) create mode 100644 homeassistant/components/keenetic_ndms2/binary_sensor.py create mode 100644 homeassistant/components/keenetic_ndms2/config_flow.py create mode 100644 homeassistant/components/keenetic_ndms2/const.py create mode 100644 homeassistant/components/keenetic_ndms2/router.py create mode 100644 homeassistant/components/keenetic_ndms2/strings.json create mode 100644 homeassistant/components/keenetic_ndms2/translations/en.json create mode 100644 tests/components/keenetic_ndms2/__init__.py create mode 100644 tests/components/keenetic_ndms2/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 8c7d4b3393d..17dd078d15b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -466,7 +466,11 @@ omit = homeassistant/components/kaiterra/* homeassistant/components/kankun/switch.py homeassistant/components/keba/* + homeassistant/components/keenetic_ndms2/__init__.py + homeassistant/components/keenetic_ndms2/binary_sensor.py + homeassistant/components/keenetic_ndms2/const.py homeassistant/components/keenetic_ndms2/device_tracker.py + homeassistant/components/keenetic_ndms2/router.py homeassistant/components/kef/* homeassistant/components/keyboard/* homeassistant/components/keyboard_remote/* diff --git a/homeassistant/components/keenetic_ndms2/__init__.py b/homeassistant/components/keenetic_ndms2/__init__.py index cb0a718d716..42d747b5238 100644 --- a/homeassistant/components/keenetic_ndms2/__init__.py +++ b/homeassistant/components/keenetic_ndms2/__init__.py @@ -1 +1,92 @@ """The keenetic_ndms2 component.""" + +from homeassistant.components import binary_sensor, device_tracker +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL +from homeassistant.core import Config, HomeAssistant + +from .const import ( + CONF_CONSIDER_HOME, + CONF_INCLUDE_ARP, + CONF_INCLUDE_ASSOCIATED, + CONF_INTERFACES, + CONF_TRY_HOTSPOT, + DEFAULT_CONSIDER_HOME, + DEFAULT_INTERFACE, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + ROUTER, + UNDO_UPDATE_LISTENER, +) +from .router import KeeneticRouter + +PLATFORMS = [device_tracker.DOMAIN, binary_sensor.DOMAIN] + + +async def async_setup(hass: HomeAssistant, _config: Config) -> bool: + """Set up configured entries.""" + hass.data.setdefault(DOMAIN, {}) + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up the component.""" + + async_add_defaults(hass, config_entry) + + router = KeeneticRouter(hass, config_entry) + await router.async_setup() + + undo_listener = config_entry.add_update_listener(update_listener) + + hass.data[DOMAIN][config_entry.entry_id] = { + ROUTER: router, + UNDO_UPDATE_LISTENER: undo_listener, + } + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() + + for component in PLATFORMS: + await hass.config_entries.async_forward_entry_unload(config_entry, component) + + router: KeeneticRouter = hass.data[DOMAIN][config_entry.entry_id][ROUTER] + + await router.async_teardown() + + hass.data[DOMAIN].pop(config_entry.entry_id) + + return True + + +async def update_listener(hass, config_entry): + """Handle options update.""" + await hass.config_entries.async_reload(config_entry.entry_id) + + +def async_add_defaults(hass: HomeAssistant, config_entry: ConfigEntry): + """Populate default options.""" + host: str = config_entry.data[CONF_HOST] + imported_options: dict = hass.data[DOMAIN].get(f"imported_options_{host}", {}) + options = { + CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, + CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME, + CONF_INTERFACES: [DEFAULT_INTERFACE], + CONF_TRY_HOTSPOT: True, + CONF_INCLUDE_ARP: True, + CONF_INCLUDE_ASSOCIATED: True, + **imported_options, + **config_entry.options, + } + + if options.keys() - config_entry.options.keys(): + hass.config_entries.async_update_entry(config_entry, options=options) diff --git a/homeassistant/components/keenetic_ndms2/binary_sensor.py b/homeassistant/components/keenetic_ndms2/binary_sensor.py new file mode 100644 index 00000000000..5da52eff00d --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/binary_sensor.py @@ -0,0 +1,72 @@ +"""The Keenetic Client class.""" +import logging + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import KeeneticRouter +from .const import DOMAIN, ROUTER + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities +): + """Set up device tracker for Keenetic NDMS2 component.""" + router: KeeneticRouter = hass.data[DOMAIN][config_entry.entry_id][ROUTER] + + async_add_entities([RouterOnlineBinarySensor(router)]) + + +class RouterOnlineBinarySensor(BinarySensorEntity): + """Representation router connection status.""" + + def __init__(self, router: KeeneticRouter): + """Initialize the APCUPSd binary device.""" + self._router = router + + @property + def name(self): + """Return the name of the online status sensor.""" + return f"{self._router.name} Online" + + @property + def unique_id(self) -> str: + """Return a unique identifier for this device.""" + return f"online_{self._router.config_entry.entry_id}" + + @property + def is_on(self): + """Return true if the UPS is online, else false.""" + return self._router.available + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_CONNECTIVITY + + @property + def should_poll(self) -> bool: + """Return False since entity pushes its state to HA.""" + return False + + @property + def device_info(self): + """Return a client description for device registry.""" + return self._router.device_info + + async def async_added_to_hass(self): + """Client entity created.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self._router.signal_update, + self.async_write_ha_state, + ) + ) diff --git a/homeassistant/components/keenetic_ndms2/config_flow.py b/homeassistant/components/keenetic_ndms2/config_flow.py new file mode 100644 index 00000000000..9338cb05935 --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/config_flow.py @@ -0,0 +1,159 @@ +"""Config flow for Keenetic NDMS2.""" +from typing import List + +from ndms2_client import Client, ConnectionException, InterfaceInfo, TelnetConnection +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv + +from .const import ( + CONF_CONSIDER_HOME, + CONF_INCLUDE_ARP, + CONF_INCLUDE_ASSOCIATED, + CONF_INTERFACES, + CONF_TRY_HOTSPOT, + DEFAULT_CONSIDER_HOME, + DEFAULT_INTERFACE, + DEFAULT_SCAN_INTERVAL, + DEFAULT_TELNET_PORT, + DOMAIN, + ROUTER, +) +from .router import KeeneticRouter + + +class KeeneticFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return KeeneticOptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + if user_input is not None: + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_HOST] == user_input[CONF_HOST]: + return self.async_abort(reason="already_configured") + + _client = Client( + TelnetConnection( + user_input[CONF_HOST], + user_input[CONF_PORT], + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + timeout=10, + ) + ) + + try: + router_info = await self.hass.async_add_executor_job( + _client.get_router_info + ) + except ConnectionException: + errors["base"] = "cannot_connect" + else: + return self.async_create_entry(title=router_info.name, data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_PORT, default=DEFAULT_TELNET_PORT): int, + } + ), + errors=errors, + ) + + async def async_step_import(self, user_input=None): + """Import a config entry.""" + return await self.async_step_user(user_input) + + +class KeeneticOptionsFlowHandler(config_entries.OptionsFlow): + """Handle options.""" + + def __init__(self, config_entry: ConfigEntry): + """Initialize options flow.""" + self.config_entry = config_entry + self._interface_options = {} + + async def async_step_init(self, _user_input=None): + """Manage the options.""" + router: KeeneticRouter = self.hass.data[DOMAIN][self.config_entry.entry_id][ + ROUTER + ] + + interfaces: List[InterfaceInfo] = await self.hass.async_add_executor_job( + router.client.get_interfaces + ) + + self._interface_options = { + interface.name: (interface.description or interface.name) + for interface in interfaces + if interface.type.lower() == "bridge" + } + return await self.async_step_user() + + async def async_step_user(self, user_input=None): + """Manage the device tracker options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = vol.Schema( + { + vol.Required( + CONF_SCAN_INTERVAL, + default=self.config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ), + ): int, + vol.Required( + CONF_CONSIDER_HOME, + default=self.config_entry.options.get( + CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME + ), + ): int, + vol.Required( + CONF_INTERFACES, + default=self.config_entry.options.get( + CONF_INTERFACES, [DEFAULT_INTERFACE] + ), + ): cv.multi_select(self._interface_options), + vol.Optional( + CONF_TRY_HOTSPOT, + default=self.config_entry.options.get(CONF_TRY_HOTSPOT, True), + ): bool, + vol.Optional( + CONF_INCLUDE_ARP, + default=self.config_entry.options.get(CONF_INCLUDE_ARP, True), + ): bool, + vol.Optional( + CONF_INCLUDE_ASSOCIATED, + default=self.config_entry.options.get( + CONF_INCLUDE_ASSOCIATED, True + ), + ): bool, + } + ) + + return self.async_show_form(step_id="user", data_schema=options) diff --git a/homeassistant/components/keenetic_ndms2/const.py b/homeassistant/components/keenetic_ndms2/const.py new file mode 100644 index 00000000000..1818cfab6a6 --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/const.py @@ -0,0 +1,21 @@ +"""Constants used in the Keenetic NDMS2 components.""" + +from homeassistant.components.device_tracker.const import ( + DEFAULT_CONSIDER_HOME as _DEFAULT_CONSIDER_HOME, +) + +DOMAIN = "keenetic_ndms2" +ROUTER = "router" +UNDO_UPDATE_LISTENER = "undo_update_listener" +DEFAULT_TELNET_PORT = 23 +DEFAULT_SCAN_INTERVAL = 120 +DEFAULT_CONSIDER_HOME = _DEFAULT_CONSIDER_HOME.seconds +DEFAULT_INTERFACE = "Home" + +CONF_CONSIDER_HOME = "consider_home" +CONF_INTERFACES = "interfaces" +CONF_TRY_HOTSPOT = "try_hotspot" +CONF_INCLUDE_ARP = "include_arp" +CONF_INCLUDE_ASSOCIATED = "include_associated" + +CONF_LEGACY_INTERFACE = "interface" diff --git a/homeassistant/components/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py index d98806dfc05..9df222a326c 100644 --- a/homeassistant/components/keenetic_ndms2/device_tracker.py +++ b/homeassistant/components/keenetic_ndms2/device_tracker.py @@ -1,102 +1,253 @@ -"""Support for Zyxel Keenetic NDMS2 based routers.""" +"""Support for Keenetic routers as device tracker.""" +from datetime import timedelta import logging +from typing import List, Optional, Set -from ndms2_client import Client, ConnectionException, TelnetConnection +from ndms2_client import Device import voluptuous as vol from homeassistant.components.device_tracker import ( - DOMAIN, - PLATFORM_SCHEMA, - DeviceScanner, + DOMAIN as DEVICE_TRACKER_DOMAIN, + PLATFORM_SCHEMA as DEVICE_TRACKER_SCHEMA, + SOURCE_TYPE_ROUTER, ) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_connect +import homeassistant.util.dt as dt_util + +from .const import ( + CONF_CONSIDER_HOME, + CONF_INTERFACES, + CONF_LEGACY_INTERFACE, + DEFAULT_CONSIDER_HOME, + DEFAULT_INTERFACE, + DEFAULT_SCAN_INTERVAL, + DEFAULT_TELNET_PORT, + DOMAIN, + ROUTER, +) +from .router import KeeneticRouter _LOGGER = logging.getLogger(__name__) -# Interface name to track devices for. Most likely one will not need to -# change it from default 'Home'. This is needed not to track Guest WI-FI- -# clients and router itself -CONF_INTERFACE = "interface" - -DEFAULT_INTERFACE = "Home" -DEFAULT_PORT = 23 - - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Required(CONF_PORT, default=DEFAULT_TELNET_PORT): cv.port, vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_INTERFACE, default=DEFAULT_INTERFACE): cv.string, + vol.Required(CONF_LEGACY_INTERFACE, default=DEFAULT_INTERFACE): cv.string, } ) -def get_scanner(_hass, config): - """Validate the configuration and return a Keenetic NDMS2 scanner.""" - scanner = KeeneticNDMS2DeviceScanner(config[DOMAIN]) +async def async_get_scanner(hass: HomeAssistant, config): + """Import legacy configuration from YAML.""" - return scanner if scanner.success_init else None + scanner_config = config[DEVICE_TRACKER_DOMAIN] + scan_interval: Optional[timedelta] = scanner_config.get(CONF_SCAN_INTERVAL) + consider_home: Optional[timedelta] = scanner_config.get(CONF_CONSIDER_HOME) + + host: str = scanner_config[CONF_HOST] + hass.data[DOMAIN][f"imported_options_{host}"] = { + CONF_INTERFACES: [scanner_config[CONF_LEGACY_INTERFACE]], + CONF_SCAN_INTERVAL: int(scan_interval.total_seconds()) + if scan_interval + else DEFAULT_SCAN_INTERVAL, + CONF_CONSIDER_HOME: int(consider_home.total_seconds()) + if consider_home + else DEFAULT_CONSIDER_HOME, + } + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_HOST: scanner_config[CONF_HOST], + CONF_PORT: scanner_config[CONF_PORT], + CONF_USERNAME: scanner_config[CONF_USERNAME], + CONF_PASSWORD: scanner_config[CONF_PASSWORD], + }, + ) + ) + + _LOGGER.warning( + "Your Keenetic NDMS2 configuration has been imported into the UI, " + "please remove it from configuration.yaml. " + "Loading Keenetic NDMS2 via scanner setup is now deprecated" + ) + + return None -class KeeneticNDMS2DeviceScanner(DeviceScanner): - """This class scans for devices using keenetic NDMS2 web interface.""" +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities +): + """Set up device tracker for Keenetic NDMS2 component.""" + router: KeeneticRouter = hass.data[DOMAIN][config_entry.entry_id][ROUTER] - def __init__(self, config): - """Initialize the scanner.""" + tracked = set() - self.last_results = [] + @callback + def update_from_router(): + """Update the status of devices.""" + update_items(router, async_add_entities, tracked) - self._interface = config[CONF_INTERFACE] + update_from_router() - self._client = Client( - TelnetConnection( - config.get(CONF_HOST), - config.get(CONF_PORT), - config.get(CONF_USERNAME), - config.get(CONF_PASSWORD), + registry = await entity_registry.async_get_registry(hass) + # Restore devices that are not a part of active clients list. + restored = [] + for entity_entry in registry.entities.values(): + if ( + entity_entry.config_entry_id == config_entry.entry_id + and entity_entry.domain == DEVICE_TRACKER_DOMAIN + ): + mac = entity_entry.unique_id.partition("_")[0] + if mac not in tracked: + tracked.add(mac) + restored.append( + KeeneticTracker( + Device( + mac=mac, + # restore the original name as set by the router before + name=entity_entry.original_name, + ip=None, + interface=None, + ), + router, + ) + ) + + if restored: + async_add_entities(restored) + + async_dispatcher_connect(hass, router.signal_update, update_from_router) + + +@callback +def update_items(router: KeeneticRouter, async_add_entities, tracked: Set[str]): + """Update tracked device state from the hub.""" + new_tracked: List[KeeneticTracker] = [] + for mac, device in router.last_devices.items(): + if mac not in tracked: + tracked.add(mac) + new_tracked.append(KeeneticTracker(device, router)) + + if new_tracked: + async_add_entities(new_tracked) + + +class KeeneticTracker(ScannerEntity): + """Representation of network device.""" + + def __init__(self, device: Device, router: KeeneticRouter): + """Initialize the tracked device.""" + self._device = device + self._router = router + self._last_seen = ( + dt_util.utcnow() if device.mac in router.last_devices else None + ) + + @property + def should_poll(self) -> bool: + """Return False since entity pushes its state to HA.""" + return False + + @property + def is_connected(self): + """Return true if the device is connected to the network.""" + return ( + self._last_seen + and (dt_util.utcnow() - self._last_seen) + < self._router.consider_home_interval + ) + + @property + def source_type(self): + """Return the source type of the client.""" + return SOURCE_TYPE_ROUTER + + @property + def name(self) -> str: + """Return the name of the device.""" + return self._device.name or self._device.mac + + @property + def unique_id(self) -> str: + """Return a unique identifier for this device.""" + return f"{self._device.mac}_{self._router.config_entry.entry_id}" + + @property + def ip_address(self) -> str: + """Return the primary ip address of the device.""" + return self._device.ip if self.is_connected else None + + @property + def mac_address(self) -> str: + """Return the mac address of the device.""" + return self._device.mac + + @property + def available(self) -> bool: + """Return if controller is available.""" + return self._router.available + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + if self.is_connected: + return { + "interface": self._device.interface, + } + return None + + @property + def device_info(self): + """Return a client description for device registry.""" + info = { + "connections": {(CONNECTION_NETWORK_MAC, self._device.mac)}, + "identifiers": {(DOMAIN, self._device.mac)}, + } + + if self._device.name: + info["name"] = self._device.name + + return info + + async def async_added_to_hass(self): + """Client entity created.""" + _LOGGER.debug("New network device tracker %s (%s)", self.name, self.unique_id) + + @callback + def update_device(): + _LOGGER.debug( + "Updating Keenetic tracked device %s (%s)", + self.entity_id, + self.unique_id, + ) + new_device = self._router.last_devices.get(self._device.mac) + if new_device: + self._device = new_device + self._last_seen = dt_util.utcnow() + + self.async_write_ha_state() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, self._router.signal_update, update_device ) ) - - self.success_init = self._update_info() - _LOGGER.info("Scanner initialized") - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - - return [device.mac for device in self.last_results] - - def get_device_name(self, device): - """Return the name of the given device or None if we don't know.""" - name = next( - (result.name for result in self.last_results if result.mac == device), None - ) - return name - - def get_extra_attributes(self, device): - """Return the IP of the given device.""" - attributes = next( - ({"ip": result.ip} for result in self.last_results if result.mac == device), - {}, - ) - return attributes - - def _update_info(self): - """Get ARP from keenetic router.""" - _LOGGER.debug("Fetching devices from router...") - - try: - self.last_results = [ - dev - for dev in self._client.get_devices() - if dev.interface == self._interface - ] - _LOGGER.debug("Successfully fetched data from router") - return True - - except ConnectionException: - _LOGGER.error("Error fetching data from router") - return False diff --git a/homeassistant/components/keenetic_ndms2/manifest.json b/homeassistant/components/keenetic_ndms2/manifest.json index 9d4c9f35716..da8321a8bdc 100644 --- a/homeassistant/components/keenetic_ndms2/manifest.json +++ b/homeassistant/components/keenetic_ndms2/manifest.json @@ -1,7 +1,8 @@ { "domain": "keenetic_ndms2", - "name": "Keenetic NDMS2 Routers", + "name": "Keenetic NDMS2 Router", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/keenetic_ndms2", - "requirements": ["ndms2_client==0.0.11"], + "requirements": ["ndms2_client==0.1.1"], "codeowners": ["@foxel"] } diff --git a/homeassistant/components/keenetic_ndms2/router.py b/homeassistant/components/keenetic_ndms2/router.py new file mode 100644 index 00000000000..340b25ff725 --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/router.py @@ -0,0 +1,187 @@ +"""The Keenetic Client class.""" +from datetime import timedelta +import logging +from typing import Callable, Dict, Optional + +from ndms2_client import Client, ConnectionException, Device, TelnetConnection +from ndms2_client.client import RouterInfo + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_call_later +import homeassistant.util.dt as dt_util + +from .const import ( + CONF_CONSIDER_HOME, + CONF_INCLUDE_ARP, + CONF_INCLUDE_ASSOCIATED, + CONF_INTERFACES, + CONF_TRY_HOTSPOT, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +class KeeneticRouter: + """Keenetic client Object.""" + + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry): + """Initialize the Client.""" + self.hass = hass + self.config_entry = config_entry + self._last_devices: Dict[str, Device] = {} + self._router_info: Optional[RouterInfo] = None + self._connection: Optional[TelnetConnection] = None + self._client: Optional[Client] = None + self._cancel_periodic_update: Optional[Callable] = None + self._available = False + self._progress = None + + @property + def client(self): + """Read-only accessor for the client connection.""" + return self._client + + @property + def last_devices(self): + """Read-only accessor for last_devices.""" + return self._last_devices + + @property + def host(self): + """Return the host of this hub.""" + return self.config_entry.data[CONF_HOST] + + @property + def device_info(self): + """Return the host of this hub.""" + return { + "identifiers": {(DOMAIN, f"router-{self.config_entry.entry_id}")}, + "manufacturer": self.manufacturer, + "model": self.model, + "name": self.name, + "sw_version": self.firmware, + } + + @property + def name(self): + """Return the name of the hub.""" + return self._router_info.name if self._router_info else self.host + + @property + def model(self): + """Return the model of the hub.""" + return self._router_info.model if self._router_info else None + + @property + def firmware(self): + """Return the firmware of the hub.""" + return self._router_info.fw_version if self._router_info else None + + @property + def manufacturer(self): + """Return the firmware of the hub.""" + return self._router_info.manufacturer if self._router_info else None + + @property + def available(self): + """Return if the hub is connected.""" + return self._available + + @property + def consider_home_interval(self): + """Config entry option defining number of seconds from last seen to away.""" + return timedelta(seconds=self.config_entry.options[CONF_CONSIDER_HOME]) + + @property + def signal_update(self): + """Event specific per router entry to signal updates.""" + return f"keenetic-update-{self.config_entry.entry_id}" + + async def request_update(self): + """Request an update.""" + if self._progress is not None: + await self._progress + return + + self._progress = self.hass.async_create_task(self.async_update()) + await self._progress + + self._progress = None + + async def async_update(self): + """Update devices information.""" + await self.hass.async_add_executor_job(self._update_devices) + async_dispatcher_send(self.hass, self.signal_update) + + async def async_setup(self): + """Set up the connection.""" + self._connection = TelnetConnection( + self.config_entry.data[CONF_HOST], + self.config_entry.data[CONF_PORT], + self.config_entry.data[CONF_USERNAME], + self.config_entry.data[CONF_PASSWORD], + ) + self._client = Client(self._connection) + + try: + await self.hass.async_add_executor_job(self._update_router_info) + except ConnectionException as error: + raise ConfigEntryNotReady from error + + async def async_update_data(_now): + await self.request_update() + self._cancel_periodic_update = async_call_later( + self.hass, + self.config_entry.options[CONF_SCAN_INTERVAL], + async_update_data, + ) + + await async_update_data(dt_util.utcnow()) + + async def async_teardown(self): + """Teardown up the connection.""" + if self._cancel_periodic_update: + self._cancel_periodic_update() + self._connection.disconnect() + + def _update_router_info(self): + try: + self._router_info = self._client.get_router_info() + self._available = True + except Exception: + self._available = False + raise + + def _update_devices(self): + """Get ARP from keenetic router.""" + _LOGGER.debug("Fetching devices from router...") + + try: + _response = self._client.get_devices( + try_hotspot=self.config_entry.options[CONF_TRY_HOTSPOT], + include_arp=self.config_entry.options[CONF_INCLUDE_ARP], + include_associated=self.config_entry.options[CONF_INCLUDE_ASSOCIATED], + ) + self._last_devices = { + dev.mac: dev + for dev in _response + if dev.interface in self.config_entry.options[CONF_INTERFACES] + } + _LOGGER.debug("Successfully fetched data from router: %s", str(_response)) + self._router_info = self._client.get_router_info() + self._available = True + + except ConnectionException: + _LOGGER.error("Error fetching data from router") + self._available = False diff --git a/homeassistant/components/keenetic_ndms2/strings.json b/homeassistant/components/keenetic_ndms2/strings.json new file mode 100644 index 00000000000..15629ba0f2f --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/strings.json @@ -0,0 +1,36 @@ +{ + "config": { + "step": { + "user": { + "title": "Set up Keenetic NDMS2 Router", + "data": { + "name": "Name", + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "port": "[%key:common::config_flow::data::port%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "Scan interval", + "consider_home": "Consider home interval", + "interfaces": "Choose interfaces to scan", + "try_hotspot": "Use 'ip hotspot' data (most accurate)", + "include_arp": "Use ARP data (ignored if hotspot data used)", + "include_associated": "Use WiFi AP associations data (ignored if hotspot data used)" + } + } + } + } +} diff --git a/homeassistant/components/keenetic_ndms2/translations/en.json b/homeassistant/components/keenetic_ndms2/translations/en.json new file mode 100644 index 00000000000..1849d68c651 --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/en.json @@ -0,0 +1,36 @@ +{ + "config": { + "step": { + "user": { + "title": "Set up Keenetic NDMS2 Router", + "data": { + "name": "Name", + "host": "Host", + "username": "Username", + "password": "Password", + "port": "Port" + } + } + }, + "error": { + "cannot_connect": "Connection Unsuccessful" + }, + "abort": { + "already_configured": "This router is already configured" + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "Scan interval", + "consider_home": "Consider home interval", + "interfaces": "Choose interfaces to scan", + "try_hotspot": "Use 'ip hotspot' data (most accurate)", + "include_arp": "Use ARP data (if hotspot disabled/unavailable)", + "include_associated": "Use WiFi AP associations data (if hotspot disabled/unavailable)" + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 41d85c4b20b..6ff72cf5572 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -113,6 +113,7 @@ FLOWS = [ "isy994", "izone", "juicenet", + "keenetic_ndms2", "kodi", "konnected", "kulersky", diff --git a/requirements_all.txt b/requirements_all.txt index d8ec67e1d30..de1a0b221c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -973,7 +973,7 @@ n26==0.2.7 nad_receiver==0.0.12 # homeassistant.components.keenetic_ndms2 -ndms2_client==0.0.11 +ndms2_client==0.1.1 # homeassistant.components.ness_alarm nessclient==0.9.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a0ce6dd428..93c66806ade 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -502,6 +502,9 @@ motionblinds==0.4.8 # homeassistant.components.tts mutagen==1.45.1 +# homeassistant.components.keenetic_ndms2 +ndms2_client==0.1.1 + # homeassistant.components.ness_alarm nessclient==0.9.15 diff --git a/tests/components/keenetic_ndms2/__init__.py b/tests/components/keenetic_ndms2/__init__.py new file mode 100644 index 00000000000..1fce0dbe2a6 --- /dev/null +++ b/tests/components/keenetic_ndms2/__init__.py @@ -0,0 +1,27 @@ +"""Tests for the Keenetic NDMS2 component.""" +from homeassistant.components.keenetic_ndms2 import const +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) + +MOCK_NAME = "Keenetic Ultra 2030" + +MOCK_DATA = { + CONF_HOST: "0.0.0.0", + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + CONF_PORT: 23, +} + +MOCK_OPTIONS = { + CONF_SCAN_INTERVAL: 15, + const.CONF_CONSIDER_HOME: 150, + const.CONF_TRY_HOTSPOT: False, + const.CONF_INCLUDE_ARP: True, + const.CONF_INCLUDE_ASSOCIATED: True, + const.CONF_INTERFACES: ["Home", "VPS0"], +} diff --git a/tests/components/keenetic_ndms2/test_config_flow.py b/tests/components/keenetic_ndms2/test_config_flow.py new file mode 100644 index 00000000000..aa5369fdc0a --- /dev/null +++ b/tests/components/keenetic_ndms2/test_config_flow.py @@ -0,0 +1,169 @@ +"""Test Keenetic NDMS2 setup process.""" + +from unittest.mock import Mock, patch + +from ndms2_client import ConnectionException +from ndms2_client.client import InterfaceInfo, RouterInfo +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components import keenetic_ndms2 as keenetic +from homeassistant.components.keenetic_ndms2 import const +from homeassistant.helpers.typing import HomeAssistantType + +from . import MOCK_DATA, MOCK_NAME, MOCK_OPTIONS + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="connect") +def mock_keenetic_connect(): + """Mock connection routine.""" + with patch("ndms2_client.client.Client.get_router_info") as mock_get_router_info: + mock_get_router_info.return_value = RouterInfo( + name=MOCK_NAME, + fw_version="3.0.4", + fw_channel="stable", + model="mock", + hw_version="0000", + manufacturer="pytest", + vendor="foxel", + region="RU", + ) + yield + + +@pytest.fixture(name="connect_error") +def mock_keenetic_connect_failed(): + """Mock connection routine.""" + with patch( + "ndms2_client.client.Client.get_router_info", + side_effect=ConnectionException("Mocked failure"), + ): + yield + + +async def test_flow_works(hass: HomeAssistantType, connect): + """Test config flow.""" + + result = await hass.config_entries.flow.async_init( + keenetic.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.keenetic_ndms2.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.keenetic_ndms2.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_DATA, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == MOCK_NAME + assert result2["data"] == MOCK_DATA + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_works(hass: HomeAssistantType, connect): + """Test config flow.""" + + with patch( + "homeassistant.components.keenetic_ndms2.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.keenetic_ndms2.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + keenetic.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_DATA, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_NAME + assert result["data"] == MOCK_DATA + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_options(hass): + """Test updating options.""" + entry = MockConfigEntry(domain=keenetic.DOMAIN, data=MOCK_DATA) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.keenetic_ndms2.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.keenetic_ndms2.async_setup_entry", return_value=True + ) as mock_setup_entry: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + # fake router + hass.data.setdefault(keenetic.DOMAIN, {}) + hass.data[keenetic.DOMAIN][entry.entry_id] = { + keenetic.ROUTER: Mock( + client=Mock( + get_interfaces=Mock( + return_value=[ + InterfaceInfo.from_dict({"id": name, "type": "bridge"}) + for name in MOCK_OPTIONS[const.CONF_INTERFACES] + ] + ) + ) + ) + } + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input=MOCK_OPTIONS, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["data"] == MOCK_OPTIONS + + +async def test_host_already_configured(hass, connect): + """Test host already configured.""" + + entry = MockConfigEntry( + domain=keenetic.DOMAIN, data=MOCK_DATA, options=MOCK_OPTIONS + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + keenetic.DOMAIN, context={"source": "user"} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_DATA + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" + + +async def test_connection_error(hass, connect_error): + """Test error when connection is unsuccessful.""" + + result = await hass.config_entries.flow.async_init( + keenetic.DOMAIN, context={"source": "user"} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_DATA + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} From dfa973f9ef6d03651cd7084f63e2c5017d006436 Mon Sep 17 00:00:00 2001 From: kpine Date: Sun, 14 Feb 2021 04:24:29 -0800 Subject: [PATCH 421/796] Add barrier covers to zwave_js integration (#46379) --- homeassistant/components/zwave_js/cover.py | 79 +- .../components/zwave_js/discovery.py | 47 +- homeassistant/components/zwave_js/switch.py | 67 +- tests/components/zwave_js/conftest.py | 14 + tests/components/zwave_js/test_cover.py | 215 +++- tests/components/zwave_js/test_switch.py | 141 +++ .../fixtures/zwave_js/cover_zw062_state.json | 936 ++++++++++++++++++ 7 files changed, 1483 insertions(+), 16 deletions(-) create mode 100644 tests/fixtures/zwave_js/cover_zw062_state.json diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index 38c891f7376..ff77bdb408d 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -3,9 +3,11 @@ import logging from typing import Any, Callable, List, Optional from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.model.value import Value as ZwaveValue from homeassistant.components.cover import ( ATTR_POSITION, + DEVICE_CLASS_GARAGE, DOMAIN as COVER_DOMAIN, SUPPORT_CLOSE, SUPPORT_OPEN, @@ -20,7 +22,15 @@ from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity LOGGER = logging.getLogger(__name__) -SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE + +BARRIER_TARGET_CLOSE = 0 +BARRIER_TARGET_OPEN = 255 + +BARRIER_STATE_CLOSED = 0 +BARRIER_STATE_CLOSING = 252 +BARRIER_STATE_STOPPED = 253 +BARRIER_STATE_OPENING = 254 +BARRIER_STATE_OPEN = 255 async def async_setup_entry( @@ -33,7 +43,10 @@ async def async_setup_entry( def async_add_cover(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave cover.""" entities: List[ZWaveBaseEntity] = [] - entities.append(ZWaveCover(config_entry, client, info)) + if info.platform_hint == "motorized_barrier": + entities.append(ZwaveMotorizedBarrier(config_entry, client, info)) + else: + entities.append(ZWaveCover(config_entry, client, info)) async_add_entities(entities) hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( @@ -99,3 +112,65 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity): target_value = self.get_zwave_value("Close") or self.get_zwave_value("Down") if target_value: await self.info.node.async_set_value(target_value, False) + + +class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity): + """Representation of a Z-Wave motorized barrier device.""" + + def __init__( + self, + config_entry: ConfigEntry, + client: ZwaveClient, + info: ZwaveDiscoveryInfo, + ) -> None: + """Initialize a ZwaveMotorizedBarrier entity.""" + super().__init__(config_entry, client, info) + self._target_state: ZwaveValue = self.get_zwave_value( + "targetState", add_to_watched_value_ids=False + ) + + @property + def supported_features(self) -> Optional[int]: + """Flag supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE + + @property + def device_class(self) -> Optional[str]: + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_GARAGE + + @property + def is_opening(self) -> Optional[bool]: + """Return if the cover is opening or not.""" + if self.info.primary_value.value is None: + return None + return bool(self.info.primary_value.value == BARRIER_STATE_OPENING) + + @property + def is_closing(self) -> Optional[bool]: + """Return if the cover is closing or not.""" + if self.info.primary_value.value is None: + return None + return bool(self.info.primary_value.value == BARRIER_STATE_CLOSING) + + @property + def is_closed(self) -> Optional[bool]: + """Return if the cover is closed or not.""" + if self.info.primary_value.value is None: + return None + # If a barrier is in the stopped state, the only way to proceed is by + # issuing an open cover command. Return None in this case which + # produces an unknown state and allows it to be resolved with an open + # command. + if self.info.primary_value.value == BARRIER_STATE_STOPPED: + return None + + return bool(self.info.primary_value.value == BARRIER_STATE_CLOSED) + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the garage door.""" + await self.info.node.async_set_value(self._target_state, BARRIER_TARGET_OPEN) + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close the garage door.""" + await self.info.node.async_set_value(self._target_state, BARRIER_TARGET_CLOSE) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 62d45a5ae5e..1aa70ea2fa0 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -1,7 +1,7 @@ """Map Z-Wave nodes and values to Home Assistant entities.""" from dataclasses import dataclass -from typing import Generator, Optional, Set, Union +from typing import Generator, List, Optional, Set, Union from zwave_js_server.const import CommandClass from zwave_js_server.model.node import Node as ZwaveNode @@ -78,7 +78,7 @@ class ZWaveDiscoverySchema: # [optional] the node's specific device class must match ANY of these values device_class_specific: Optional[Set[str]] = None # [optional] additional values that ALL need to be present on the node for this scheme to pass - required_values: Optional[Set[ZWaveValueDiscoverySchema]] = None + required_values: Optional[List[ZWaveValueDiscoverySchema]] = None # [optional] bool to specify if this primary value may be discovered by multiple platforms allow_multi: bool = False @@ -345,10 +345,22 @@ DISCOVERY_SCHEMAS = [ command_class={CommandClass.SWITCH_BINARY}, property={"currentValue"} ), ), + # binary switch + # barrier operator signaling states + ZWaveDiscoverySchema( + platform="switch", + hint="barrier_event_signaling_state", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.BARRIER_OPERATOR}, + property={"signalingState"}, + type={"number"}, + ), + ), # cover + # window coverings ZWaveDiscoverySchema( platform="cover", - hint="cover", + hint="window_cover", device_class_generic={"Multilevel Switch"}, device_class_specific={ "Motor Control Class A", @@ -362,6 +374,24 @@ DISCOVERY_SCHEMAS = [ type={"number"}, ), ), + # cover + # motorized barriers + ZWaveDiscoverySchema( + platform="cover", + hint="motorized_barrier", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.BARRIER_OPERATOR}, + property={"currentState"}, + type={"number"}, + ), + required_values=[ + ZWaveValueDiscoverySchema( + command_class={CommandClass.BARRIER_OPERATOR}, + property={"targetState"}, + type={"number"}, + ), + ], + ), # fan ZWaveDiscoverySchema( platform="fan", @@ -430,13 +460,10 @@ def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None continue # check additional required values if schema.required_values is not None: - required_values_present = True - for val_scheme in schema.required_values: - for val in node.values.values(): - if not check_value(val, val_scheme): - required_values_present = False - break - if not required_values_present: + if not all( + any(check_value(val, val_scheme) for val in node.values.values()) + for val_scheme in schema.required_values + ): continue # all checks passed, this value belongs to an entity yield ZwaveDiscoveryInfo( diff --git a/homeassistant/components/zwave_js/switch.py b/homeassistant/components/zwave_js/switch.py index 8feba5911f8..a325e9821f7 100644 --- a/homeassistant/components/zwave_js/switch.py +++ b/homeassistant/components/zwave_js/switch.py @@ -17,6 +17,10 @@ from .entity import ZWaveBaseEntity LOGGER = logging.getLogger(__name__) +BARRIER_EVENT_SIGNALING_OFF = 0 +BARRIER_EVENT_SIGNALING_ON = 255 + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ) -> None: @@ -27,7 +31,12 @@ async def async_setup_entry( def async_add_switch(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave Switch.""" entities: List[ZWaveBaseEntity] = [] - entities.append(ZWaveSwitch(config_entry, client, info)) + if info.platform_hint == "barrier_event_signaling_state": + entities.append( + ZWaveBarrierEventSignalingSwitch(config_entry, client, info) + ) + else: + entities.append(ZWaveSwitch(config_entry, client, info)) async_add_entities(entities) @@ -62,3 +71,59 @@ class ZWaveSwitch(ZWaveBaseEntity, SwitchEntity): target_value = self.get_zwave_value("targetValue") if target_value is not None: await self.info.node.async_set_value(target_value, False) + + +class ZWaveBarrierEventSignalingSwitch(ZWaveBaseEntity, SwitchEntity): + """This switch is used to turn on or off a barrier device's event signaling subsystem.""" + + def __init__( + self, + config_entry: ConfigEntry, + client: ZwaveClient, + info: ZwaveDiscoveryInfo, + ) -> None: + """Initialize a ZWaveBarrierEventSignalingSwitch entity.""" + super().__init__(config_entry, client, info) + self._name = self.generate_name(include_value_name=True) + self._state: Optional[bool] = None + + self._update_state() + + @callback + def on_value_update(self) -> None: + """Call when a watched value is added or updated.""" + self._update_state() + + @property + def name(self) -> str: + """Return default name from device name and value name combination.""" + return self._name + + @property + def is_on(self) -> Optional[bool]: # type: ignore + """Return a boolean for the state of the switch.""" + return self._state + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + await self.info.node.async_set_value( + self.info.primary_value, BARRIER_EVENT_SIGNALING_ON + ) + # this value is not refreshed, so assume success + self._state = True + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + await self.info.node.async_set_value( + self.info.primary_value, BARRIER_EVENT_SIGNALING_OFF + ) + # this value is not refreshed, so assume success + self._state = False + self.async_write_ha_state() + + @callback + def _update_state(self) -> None: + self._state = None + if self.info.primary_value.value is not None: + self._state = self.info.primary_value.value == BARRIER_EVENT_SIGNALING_ON diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 9cb950ba6e7..31b7d795a60 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -158,6 +158,12 @@ def in_wall_smart_fan_control_state_fixture(): return json.loads(load_fixture("zwave_js/in_wall_smart_fan_control_state.json")) +@pytest.fixture(name="gdc_zw062_state", scope="session") +def motorized_barrier_cover_state_fixture(): + """Load the motorized barrier cover node state fixture data.""" + return json.loads(load_fixture("zwave_js/cover_zw062_state.json")) + + @pytest.fixture(name="client") def mock_client_fixture(controller_state, version_state): """Mock a client.""" @@ -345,3 +351,11 @@ def multiple_devices_fixture( node = Node(client, lock_schlage_be469_state) client.driver.controller.nodes[node.node_id] = node return client.driver.controller.nodes + + +@pytest.fixture(name="gdc_zw062") +def motorized_barrier_cover_fixture(client, gdc_zw062_state): + """Mock a motorized barrier node.""" + node = Node(client, gdc_zw062_state) + client.driver.controller.nodes[node.node_id] = node + return node diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index 5e50ffb226e..2378453e31a 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -1,13 +1,28 @@ """Test the Z-Wave JS cover platform.""" from zwave_js_server.event import Event -from homeassistant.components.cover import ATTR_CURRENT_POSITION +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + DEVICE_CLASS_GARAGE, + DOMAIN, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, + STATE_UNKNOWN, +) WINDOW_COVER_ENTITY = "cover.zws_12" +GDC_COVER_ENTITY = "cover.aeon_labs_garage_door_controller_gen5" -async def test_cover(hass, client, chain_actuator_zws12, integration): - """Test the light entity.""" +async def test_window_cover(hass, client, chain_actuator_zws12, integration): + """Test the cover entity.""" node = chain_actuator_zws12 state = hass.states.get(WINDOW_COVER_ENTITY) @@ -282,3 +297,197 @@ async def test_cover(hass, client, chain_actuator_zws12, integration): state = hass.states.get(WINDOW_COVER_ENTITY) assert state.state == "closed" + + +async def test_motor_barrier_cover(hass, client, gdc_zw062, integration): + """Test the cover entity.""" + node = gdc_zw062 + + state = hass.states.get(GDC_COVER_ENTITY) + assert state + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_GARAGE + + assert state.state == STATE_CLOSED + + # Test open + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, {"entity_id": GDC_COVER_ENTITY}, blocking=True + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 12 + assert args["value"] == 255 + assert args["valueId"] == { + "ccVersion": 0, + "commandClass": 102, + "commandClassName": "Barrier Operator", + "endpoint": 0, + "metadata": { + "label": "Target Barrier State", + "max": 255, + "min": 0, + "readable": True, + "states": {"0": "Closed", "255": "Open"}, + "type": "number", + "writeable": True, + }, + "property": "targetState", + "propertyName": "targetState", + } + + # state doesn't change until currentState value update is received + state = hass.states.get(GDC_COVER_ENTITY) + assert state.state == STATE_CLOSED + + client.async_send_command.reset_mock() + + # Test close + await hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER, {"entity_id": GDC_COVER_ENTITY}, blocking=True + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 12 + assert args["value"] == 0 + assert args["valueId"] == { + "ccVersion": 0, + "commandClass": 102, + "commandClassName": "Barrier Operator", + "endpoint": 0, + "metadata": { + "label": "Target Barrier State", + "max": 255, + "min": 0, + "readable": True, + "states": {"0": "Closed", "255": "Open"}, + "type": "number", + "writeable": True, + }, + "property": "targetState", + "propertyName": "targetState", + } + + # state doesn't change until currentState value update is received + state = hass.states.get(GDC_COVER_ENTITY) + assert state.state == STATE_CLOSED + + client.async_send_command.reset_mock() + + # Barrier sends an opening state + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 12, + "args": { + "commandClassName": "Barrier Operator", + "commandClass": 102, + "endpoint": 0, + "property": "currentState", + "newValue": 254, + "prevValue": 0, + "propertyName": "currentState", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(GDC_COVER_ENTITY) + assert state.state == STATE_OPENING + + # Barrier sends an opened state + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 12, + "args": { + "commandClassName": "Barrier Operator", + "commandClass": 102, + "endpoint": 0, + "property": "currentState", + "newValue": 255, + "prevValue": 254, + "propertyName": "currentState", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(GDC_COVER_ENTITY) + assert state.state == STATE_OPEN + + # Barrier sends a closing state + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 12, + "args": { + "commandClassName": "Barrier Operator", + "commandClass": 102, + "endpoint": 0, + "property": "currentState", + "newValue": 252, + "prevValue": 255, + "propertyName": "currentState", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(GDC_COVER_ENTITY) + assert state.state == STATE_CLOSING + + # Barrier sends a closed state + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 12, + "args": { + "commandClassName": "Barrier Operator", + "commandClass": 102, + "endpoint": 0, + "property": "currentState", + "newValue": 0, + "prevValue": 252, + "propertyName": "currentState", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(GDC_COVER_ENTITY) + assert state.state == STATE_CLOSED + + # Barrier sends a stopped state + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 12, + "args": { + "commandClassName": "Barrier Operator", + "commandClass": 102, + "endpoint": 0, + "property": "currentState", + "newValue": 253, + "prevValue": 252, + "propertyName": "currentState", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(GDC_COVER_ENTITY) + assert state.state == STATE_UNKNOWN diff --git a/tests/components/zwave_js/test_switch.py b/tests/components/zwave_js/test_switch.py index a1d177cc5d8..ea6e27d9b72 100644 --- a/tests/components/zwave_js/test_switch.py +++ b/tests/components/zwave_js/test_switch.py @@ -2,6 +2,9 @@ from zwave_js_server.event import Event +from homeassistant.components.switch import DOMAIN, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.const import STATE_OFF, STATE_ON + from .common import SWITCH_ENTITY @@ -83,3 +86,141 @@ async def test_switch(hass, hank_binary_switch, integration, client): "value": False, } assert args["value"] is False + + +async def test_barrier_signaling_switch(hass, gdc_zw062, integration, client): + """Test barrier signaling state switch.""" + node = gdc_zw062 + entity = "switch.aeon_labs_garage_door_controller_gen5_signaling_state_visual" + + state = hass.states.get(entity) + assert state + assert state.state == "on" + + # Test turning off + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {"entity_id": entity}, blocking=True + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 12 + assert args["value"] == 0 + assert args["valueId"] == { + "ccVersion": 0, + "commandClass": 102, + "commandClassName": "Barrier Operator", + "endpoint": 0, + "metadata": { + "label": "Signaling State (Visual)", + "max": 255, + "min": 0, + "readable": True, + "states": {"0": "Off", "255": "On"}, + "type": "number", + "writeable": True, + }, + "property": "signalingState", + "propertyKey": 2, + "propertyKeyName": "2", + "propertyName": "signalingState", + "value": 255, + } + + # state change is optimistic and writes state + await hass.async_block_till_done() + + state = hass.states.get(entity) + assert state.state == STATE_OFF + + client.async_send_command.reset_mock() + + # Test turning on + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {"entity_id": entity}, blocking=True + ) + + # Note: the valueId's value is still 255 because we never + # received an updated value + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 12 + assert args["value"] == 255 + assert args["valueId"] == { + "ccVersion": 0, + "commandClass": 102, + "commandClassName": "Barrier Operator", + "endpoint": 0, + "metadata": { + "label": "Signaling State (Visual)", + "max": 255, + "min": 0, + "readable": True, + "states": {"0": "Off", "255": "On"}, + "type": "number", + "writeable": True, + }, + "property": "signalingState", + "propertyKey": 2, + "propertyKeyName": "2", + "propertyName": "signalingState", + "value": 255, + } + + # state change is optimistic and writes state + await hass.async_block_till_done() + + state = hass.states.get(entity) + assert state.state == STATE_ON + + # Received a refresh off + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 12, + "args": { + "commandClassName": "Barrier Operator", + "commandClass": 102, + "endpoint": 0, + "property": "signalingState", + "propertyKey": 2, + "newValue": 0, + "prevValue": 0, + "propertyName": "signalingState", + "propertyKeyName": "2", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(entity) + assert state.state == STATE_OFF + + # Received a refresh off + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 12, + "args": { + "commandClassName": "Barrier Operator", + "commandClass": 102, + "endpoint": 0, + "property": "signalingState", + "propertyKey": 2, + "newValue": 255, + "prevValue": 255, + "propertyName": "signalingState", + "propertyKeyName": "2", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(entity) + assert state.state == STATE_ON diff --git a/tests/fixtures/zwave_js/cover_zw062_state.json b/tests/fixtures/zwave_js/cover_zw062_state.json new file mode 100644 index 00000000000..107225e0dcc --- /dev/null +++ b/tests/fixtures/zwave_js/cover_zw062_state.json @@ -0,0 +1,936 @@ +{ + "nodeId": 12, + "index": 0, + "installerIcon": 7680, + "userIcon": 7680, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Routing Slave", + "generic": "Entry Control", + "specific": "Secure Barrier Add-on", + "mandatorySupportedCCs": [ + "Application Status", + "Association", + "Association Group Information", + "Barrier Operator", + "Battery", + "Device Reset Locally", + "Manufacturer Specific", + "Notification", + "Powerlevel", + "Security", + "Security 2", + "Supervision", + "Transport Service", + "Version", + "Z-Wave Plus Info" + ], + "mandatoryControlCCs": [] + }, + "isListening": true, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": true, + "version": 4, + "isBeaming": true, + "manufacturerId": 134, + "productId": 62, + "productType": 259, + "firmwareVersion": "1.12", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 5, + "deviceConfig": { + "manufacturerId": 134, + "manufacturer": "AEON Labs", + "label": "ZW062", + "description": "Aeon Labs Garage Door Controller Gen5", + "devices": [ + { + "productType": "0x0003", + "productId": "0x003e" + }, + { + "productType": "0x0103", + "productId": "0x003e" + }, + { + "productType": "0x0203", + "productId": "0x003e" + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "associations": {}, + "paramInformation": { + "_map": {} + } + }, + "label": "ZW062", + "neighbors": [ + 1, + 8, + 11, + 15, + 19, + 21, + 22, + 24, + 25, + 26, + 27, + 29 + ], + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 12, + "index": 0, + "installerIcon": 7680, + "userIcon": 7680 + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value" + } + }, + { + "endpoint": 0, + "commandClass": 102, + "commandClassName": "Barrier Operator", + "property": "currentState", + "propertyName": "currentState", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Current Barrier State", + "states": { + "0": "Closed", + "252": "Closing", + "253": "Stopped", + "254": "Opening", + "255": "Open" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 102, + "commandClassName": "Barrier Operator", + "property": "position", + "propertyName": "position", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 100, + "label": "Barrier Position", + "unit": "%" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 102, + "commandClassName": "Barrier Operator", + "property": "signalingState", + "propertyKey": 1, + "propertyName": "signalingState", + "propertyKeyName": "1", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Signaling State (Audible)", + "states": { + "0": "Off", + "255": "On" + } + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 102, + "commandClassName": "Barrier Operator", + "property": "signalingState", + "propertyKey": 2, + "propertyName": "signalingState", + "propertyKeyName": "2", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Signaling State (Visual)", + "states": { + "0": "Off", + "255": "On" + } + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 102, + "commandClassName": "Barrier Operator", + "property": "targetState", + "propertyName": "targetState", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Target Barrier State", + "states": { + "0": "Closed", + "255": "Open" + } + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 32, + "propertyName": "Startup ringtone", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 100, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Startup ringtone", + "description": "Configure the default startup ringtone", + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 35, + "propertyName": "Calibration timout", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 1, + "max": 255, + "default": 60, + "format": 0, + "allowManualEntry": true, + "label": "Calibration timout", + "description": "Set the timeout of all calibration steps for the Sensor.", + "isFromConfig": true + }, + "value": 13 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 36, + "propertyName": "Number of alarm musics", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "valueSize": 1, + "min": 1, + "max": 100, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Number of alarm musics", + "description": "Get the number of alarm musics", + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 39, + "propertyName": "Unknown state alarm mode", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 0, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Unknown state alarm mode", + "description": "Configuration alarm mode when the garage door is in \"unknown\" state", + "isFromConfig": true + }, + "value": 100927488 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 40, + "propertyName": "Closed alarm mode", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 0, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Closed alarm mode", + "description": "Configure the alarm mode when the garage door is in closed position.", + "isFromConfig": true + }, + "value": 33883392 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 41, + "propertyName": "Tamper switch configuration", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": true, + "label": "Tamper switch configuration", + "description": "Configuration report for the tamper switch State", + "isFromConfig": true + }, + "value": 15 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 42, + "propertyName": "Battery state", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "valueSize": 1, + "min": 0, + "max": 16, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Battery state", + "description": "Configuration report for the battery state of Sensor", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 45, + "propertyName": "Temperature", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "valueSize": 2, + "min": 0, + "max": 500, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Temperature", + "description": "Get the environment temperature", + "isFromConfig": true + }, + "value": 550 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 47, + "propertyName": "Button definition", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Mode 0", + "1": "Mode 1" + }, + "label": "Button definition", + "description": "Define the function of Button- or Button+.", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 80, + "propertyName": "Door state change report type", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 2, + "default": 2, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "Send hail CC", + "2": "Send barrier operator report CC" + }, + "label": "Door state change report type", + "description": "Configure the door state change report type", + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 241, + "propertyName": "Pair the Sensor", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1431655681, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Stop sensor pairing", + "1431655681": "Start sensor pairing" + }, + "label": "Pair the Sensor", + "description": "Pair the Sensor with Garage Door Controller", + "isFromConfig": true + }, + "value": 33554943 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 252, + "propertyName": "Lock Configuration", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Configuration enabled", + "1": "Configuration disabled (locked)" + }, + "label": "Lock Configuration", + "description": "Enable/disable configuration", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 37, + "propertyKey": 255, + "propertyName": "Disable opening alarm", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disable alarm prompt", + "1": "Enable alarm prompt" + }, + "label": "Disable opening alarm", + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 37, + "propertyKey": 65280, + "propertyName": "Opening alarm volume", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 1, + "max": 10, + "default": 8, + "format": 0, + "allowManualEntry": true, + "label": "Opening alarm volume", + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 37, + "propertyKey": 16711680, + "propertyName": "Opening alarm choice", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 1, + "max": 4, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Opening alarm choice", + "description": "Alarm mode when the garage door is opening", + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 37, + "propertyKey": 251658240, + "propertyName": "Opening alarm LED mode", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 1, + "max": 10, + "default": 10, + "format": 0, + "allowManualEntry": true, + "label": "Opening alarm LED mode", + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 255, + "propertyName": "Disable closing alarm", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disable alarm prompt", + "1": "Enable alarm prompt" + }, + "label": "Disable closing alarm", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 65280, + "propertyName": "Closing alarm volume", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 1, + "max": 10, + "default": 8, + "format": 0, + "allowManualEntry": true, + "label": "Closing alarm volume", + "isFromConfig": true + }, + "value": 8 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 16711680, + "propertyName": "Closing alarm choice", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 1, + "max": 4, + "default": 2, + "format": 0, + "allowManualEntry": true, + "label": "Closing alarm choice", + "description": "Alarm mode when the garage door is closing", + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 251658240, + "propertyName": "Closing alarm LED mode", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 1, + "max": 10, + "default": 6, + "format": 0, + "allowManualEntry": true, + "label": "Closing alarm LED mode", + "isFromConfig": true + }, + "value": 8 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 34, + "propertyName": "Sensor Calibration", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Calibration not active", + "1": "Begin calibration" + }, + "label": "Sensor Calibration", + "description": "Perform Sensor Calibration", + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 43, + "propertyName": "Play or Pause ringtone", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 255, + "default": 1, + "format": 1, + "allowManualEntry": true, + "label": "Play or Pause ringtone", + "description": "Start playing or Stop playing the ringtone", + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 44, + "propertyName": "Ringtone test volume", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 10, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Ringtone test volume", + "description": "Set volume for test of ringtone", + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Alarm Type" + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Alarm Level" + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 134 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 259 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 62 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "3.99" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "1.12" + ] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + } + ] +} From 10e88cd23d39cbd46454f293763b1b53392ef8e5 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 14 Feb 2021 04:36:05 -0800 Subject: [PATCH 422/796] Improve nest defense against broken event loop on shutdown (#46494) --- homeassistant/components/nest/__init__.py | 2 +- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 85e591707ad..b0abd24012a 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -211,7 +211,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): if DATA_SDM not in entry.data: # Legacy API return True - + _LOGGER.debug("Stopping nest subscriber") subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] subscriber.stop_async() unload_ok = all( diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index f9bc135693d..c68dbe6ee2f 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.2.9"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.2.10"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [{"macaddress":"18B430*"}] diff --git a/requirements_all.txt b/requirements_all.txt index de1a0b221c2..110f573bc90 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.9 +google-nest-sdm==0.2.10 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 93c66806ade..3b8e9858cad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -369,7 +369,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.2.9 +google-nest-sdm==0.2.10 # homeassistant.components.gree greeclimate==0.10.3 From a5a45f29e2953bc61a63f72bda1fd8a643efd7b4 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sun, 14 Feb 2021 07:46:58 -0500 Subject: [PATCH 423/796] Cleanup unused loggers (#46510) --- homeassistant/components/agent_dvr/config_flow.py | 3 --- homeassistant/components/airvisual/sensor.py | 4 ---- homeassistant/components/asuswrt/__init__.py | 3 --- homeassistant/components/asuswrt/device_tracker.py | 3 --- homeassistant/components/aurora/binary_sensor.py | 4 ---- homeassistant/components/aurora/sensor.py | 4 ---- homeassistant/components/blueprint/websocket_api.py | 3 --- homeassistant/components/bmw_connected_drive/config_flow.py | 5 ----- homeassistant/components/dyson/air_quality.py | 4 ---- homeassistant/components/dyson/sensor.py | 4 ---- homeassistant/components/econet/binary_sensor.py | 4 ---- homeassistant/components/econet/sensor.py | 5 ----- homeassistant/components/fireservicerota/binary_sensor.py | 4 ---- homeassistant/components/gree/switch.py | 3 --- homeassistant/components/hangouts/const.py | 5 ----- homeassistant/components/homekit/config_flow.py | 3 --- homeassistant/components/homematicip_cloud/__init__.py | 4 ---- homeassistant/components/ipma/const.py | 4 ---- homeassistant/components/izone/discovery.py | 5 ----- homeassistant/components/lutron_caseta/device_trigger.py | 4 ---- homeassistant/components/met/const.py | 4 ---- homeassistant/components/modbus/__init__.py | 4 ---- homeassistant/components/motion_blinds/config_flow.py | 5 ----- homeassistant/components/motion_blinds/sensor.py | 4 ---- homeassistant/components/mqtt/camera.py | 3 --- homeassistant/components/mqtt/config_flow.py | 3 --- homeassistant/components/mqtt/debug_info.py | 3 --- homeassistant/components/mqtt/device_automation.py | 3 --- .../components/mqtt/device_tracker/schema_discovery.py | 3 --- homeassistant/components/mqtt/fan.py | 3 --- homeassistant/components/mqtt/light/__init__.py | 3 --- homeassistant/components/mqtt/lock.py | 3 --- homeassistant/components/mqtt/scene.py | 3 --- homeassistant/components/mqtt/sensor.py | 3 --- homeassistant/components/mqtt/subscription.py | 3 --- homeassistant/components/mqtt/switch.py | 3 --- homeassistant/components/mqtt/vacuum/__init__.py | 3 --- homeassistant/components/mqtt/vacuum/schema_legacy.py | 3 --- homeassistant/components/mqtt/vacuum/schema_state.py | 3 --- homeassistant/components/neato/api.py | 3 --- homeassistant/components/neato/config_flow.py | 2 -- homeassistant/components/nest/device_trigger.py | 3 --- homeassistant/components/nws/weather.py | 3 --- homeassistant/components/ondilo_ico/config_flow.py | 2 -- homeassistant/components/ozw/fan.py | 3 --- homeassistant/components/plaato/sensor.py | 4 ---- homeassistant/components/proxmoxve/binary_sensor.py | 4 ---- homeassistant/components/rachio/webhooks.py | 6 ------ .../components/totalconnect/alarm_control_panel.py | 2 -- homeassistant/components/totalconnect/binary_sensor.py | 4 ---- homeassistant/components/tuya/climate.py | 3 --- homeassistant/components/zha/binary_sensor.py | 3 --- homeassistant/components/zwave_js/api.py | 3 --- homeassistant/components/zwave_js/climate.py | 3 --- homeassistant/components/zwave_js/fan.py | 3 --- tests/components/feedreader/test_init.py | 3 --- tests/components/harmony/conftest.py | 3 --- tests/components/harmony/test_activity_changes.py | 5 ----- tests/components/hyperion/__init__.py | 3 --- tests/components/hyperion/test_config_flow.py | 4 ---- tests/components/hyperion/test_light.py | 3 --- tests/components/hyperion/test_switch.py | 2 -- tests/components/litejet/test_init.py | 3 --- tests/components/litejet/test_scene.py | 3 --- tests/scripts/test_check_config.py | 3 --- 65 files changed, 223 deletions(-) diff --git a/homeassistant/components/agent_dvr/config_flow.py b/homeassistant/components/agent_dvr/config_flow.py index 9448b8d3123..15ef58ced7e 100644 --- a/homeassistant/components/agent_dvr/config_flow.py +++ b/homeassistant/components/agent_dvr/config_flow.py @@ -1,6 +1,4 @@ """Config flow to configure Agent devices.""" -import logging - from agent import AgentConnectionError, AgentError from agent.a import Agent import voluptuous as vol @@ -13,7 +11,6 @@ from .const import DOMAIN, SERVER_URL # pylint:disable=unused-import from .helpers import generate_url DEFAULT_PORT = 8090 -_LOGGER = logging.getLogger(__name__) class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 680059af411..3c1aef128ab 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -1,6 +1,4 @@ """Support for AirVisual air quality sensors.""" -from logging import getLogger - from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, @@ -31,8 +29,6 @@ from .const import ( INTEGRATION_TYPE_GEOGRAPHY_NAME, ) -_LOGGER = getLogger(__name__) - ATTR_CITY = "city" ATTR_COUNTRY = "country" ATTR_POLLUTANT_SYMBOL = "pollutant_symbol" diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index d2eb47fa2d2..28e8fe76684 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -1,6 +1,5 @@ """Support for ASUSWRT devices.""" import asyncio -import logging import voluptuous as vol @@ -41,8 +40,6 @@ PLATFORMS = ["device_tracker", "sensor"] CONF_PUB_KEY = "pub_key" SECRET_GROUP = "Password or SSH Key" -_LOGGER = logging.getLogger(__name__) - CONFIG_SCHEMA = vol.Schema( vol.All( cv.deprecated(DOMAIN), diff --git a/homeassistant/components/asuswrt/device_tracker.py b/homeassistant/components/asuswrt/device_tracker.py index 85553674dba..385b25755b0 100644 --- a/homeassistant/components/asuswrt/device_tracker.py +++ b/homeassistant/components/asuswrt/device_tracker.py @@ -1,5 +1,4 @@ """Support for ASUSWRT routers.""" -import logging from typing import Dict from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER @@ -15,8 +14,6 @@ from .router import AsusWrtRouter DEFAULT_DEVICE_NAME = "Unknown device" -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities diff --git a/homeassistant/components/aurora/binary_sensor.py b/homeassistant/components/aurora/binary_sensor.py index 3ea1faa7949..a6d5a1817b2 100644 --- a/homeassistant/components/aurora/binary_sensor.py +++ b/homeassistant/components/aurora/binary_sensor.py @@ -1,13 +1,9 @@ """Support for Aurora Forecast binary sensor.""" -import logging - from homeassistant.components.binary_sensor import BinarySensorEntity from . import AuroraEntity from .const import COORDINATOR, DOMAIN -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, entry, async_add_entries): """Set up the binary_sensor platform.""" diff --git a/homeassistant/components/aurora/sensor.py b/homeassistant/components/aurora/sensor.py index 51ccb3dc4dd..731c6d08afd 100644 --- a/homeassistant/components/aurora/sensor.py +++ b/homeassistant/components/aurora/sensor.py @@ -1,13 +1,9 @@ """Support for Aurora Forecast sensor.""" -import logging - from homeassistant.const import PERCENTAGE from . import AuroraEntity from .const import COORDINATOR, DOMAIN -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, entry, async_add_entries): """Set up the sensor platform.""" diff --git a/homeassistant/components/blueprint/websocket_api.py b/homeassistant/components/blueprint/websocket_api.py index 6968d4530cd..05ae2816696 100644 --- a/homeassistant/components/blueprint/websocket_api.py +++ b/homeassistant/components/blueprint/websocket_api.py @@ -1,5 +1,4 @@ """Websocket API for blueprint.""" -import logging from typing import Dict, Optional import async_timeout @@ -15,8 +14,6 @@ from . import importer, models from .const import DOMAIN from .errors import FileAlreadyExists -_LOGGER = logging.getLogger(__package__) - @callback def async_setup(hass: HomeAssistant): diff --git a/homeassistant/components/bmw_connected_drive/config_flow.py b/homeassistant/components/bmw_connected_drive/config_flow.py index 8315a1322e2..fbfa20aff1a 100644 --- a/homeassistant/components/bmw_connected_drive/config_flow.py +++ b/homeassistant/components/bmw_connected_drive/config_flow.py @@ -1,6 +1,4 @@ """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 @@ -12,9 +10,6 @@ from homeassistant.core import callback from . import DOMAIN # pylint: disable=unused-import from .const import CONF_ALLOWED_REGIONS, CONF_READ_ONLY, CONF_USE_LOCATION -_LOGGER = logging.getLogger(__name__) - - DATA_SCHEMA = vol.Schema( { vol.Required(CONF_USERNAME): str, diff --git a/homeassistant/components/dyson/air_quality.py b/homeassistant/components/dyson/air_quality.py index d23b2b1ef88..3bf4f2bb34c 100644 --- a/homeassistant/components/dyson/air_quality.py +++ b/homeassistant/components/dyson/air_quality.py @@ -1,6 +1,4 @@ """Support for Dyson Pure Cool Air Quality Sensors.""" -import logging - from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_state_v2 import DysonEnvironmentalSensorV2State @@ -10,8 +8,6 @@ from . import DYSON_DEVICES, DysonEntity ATTRIBUTION = "Dyson purifier air quality sensor" -_LOGGER = logging.getLogger(__name__) - DYSON_AIQ_DEVICES = "dyson_aiq_devices" ATTR_VOC = "volatile_organic_compounds" diff --git a/homeassistant/components/dyson/sensor.py b/homeassistant/components/dyson/sensor.py index f1198188b5c..80a64e787f0 100644 --- a/homeassistant/components/dyson/sensor.py +++ b/homeassistant/components/dyson/sensor.py @@ -1,6 +1,4 @@ """Support for Dyson Pure Cool Link Sensors.""" -import logging - from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_cool_link import DysonPureCoolLink @@ -58,8 +56,6 @@ SENSOR_NAMES = { DYSON_SENSOR_DEVICES = "dyson_sensor_devices" -_LOGGER = logging.getLogger(__name__) - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dyson Sensors.""" diff --git a/homeassistant/components/econet/binary_sensor.py b/homeassistant/components/econet/binary_sensor.py index ec8131c5105..b87e6bb0cd0 100644 --- a/homeassistant/components/econet/binary_sensor.py +++ b/homeassistant/components/econet/binary_sensor.py @@ -1,6 +1,4 @@ """Support for Rheem EcoNet water heaters.""" -import logging - from pyeconet.equipment import EquipmentType from homeassistant.components.binary_sensor import ( @@ -12,8 +10,6 @@ from homeassistant.components.binary_sensor import ( from . import EcoNetEntity from .const import DOMAIN, EQUIPMENT -_LOGGER = logging.getLogger(__name__) - SENSOR_NAME_RUNNING = "running" SENSOR_NAME_SHUTOFF_VALVE = "shutoff_valve" SENSOR_NAME_VACATION = "vacation" diff --git a/homeassistant/components/econet/sensor.py b/homeassistant/components/econet/sensor.py index 6ae14d18aa1..e0ef7dc6ce9 100644 --- a/homeassistant/components/econet/sensor.py +++ b/homeassistant/components/econet/sensor.py @@ -1,6 +1,4 @@ """Support for Rheem EcoNet water heaters.""" -import logging - from pyeconet.equipment import EquipmentType from homeassistant.const import ( @@ -25,9 +23,6 @@ WIFI_SIGNAL = "wifi_signal" RUNNING_STATE = "running_state" -_LOGGER = logging.getLogger(__name__) - - async def async_setup_entry(hass, entry, async_add_entities): """Set up EcoNet sensor based on a config entry.""" equipment = hass.data[DOMAIN][EQUIPMENT][entry.entry_id] diff --git a/homeassistant/components/fireservicerota/binary_sensor.py b/homeassistant/components/fireservicerota/binary_sensor.py index fc06e605cbd..3f04adc2f72 100644 --- a/homeassistant/components/fireservicerota/binary_sensor.py +++ b/homeassistant/components/fireservicerota/binary_sensor.py @@ -1,6 +1,4 @@ """Binary Sensor platform for FireServiceRota integration.""" -import logging - from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -11,8 +9,6 @@ from homeassistant.helpers.update_coordinator import ( from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN as FIRESERVICEROTA_DOMAIN -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities diff --git a/homeassistant/components/gree/switch.py b/homeassistant/components/gree/switch.py index f4e9792a589..fa1f1550e83 100644 --- a/homeassistant/components/gree/switch.py +++ b/homeassistant/components/gree/switch.py @@ -1,5 +1,4 @@ """Support for interface with a Gree climate systems.""" -import logging from typing import Optional from homeassistant.components.switch import DEVICE_CLASS_SWITCH, SwitchEntity @@ -8,8 +7,6 @@ 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.""" diff --git a/homeassistant/components/hangouts/const.py b/homeassistant/components/hangouts/const.py index 0508bf48703..3a78e9bbe80 100644 --- a/homeassistant/components/hangouts/const.py +++ b/homeassistant/components/hangouts/const.py @@ -1,14 +1,9 @@ """Constants for Google Hangouts Component.""" -import logging - import voluptuous as vol from homeassistant.components.notify import ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET import homeassistant.helpers.config_validation as cv -_LOGGER = logging.getLogger(".") - - DOMAIN = "hangouts" CONF_2FA = "2fa" diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index f16d14e9330..d6278c0ca94 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -1,5 +1,4 @@ """Config flow for HomeKit integration.""" -import logging import random import string @@ -101,8 +100,6 @@ _EMPTY_ENTITY_FILTER = { CONF_EXCLUDE_ENTITIES: [], } -_LOGGER = logging.getLogger(__name__) - class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for HomeKit.""" diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index 7ea6a4fe0b4..ca1af8266c6 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -1,6 +1,4 @@ """Support for HomematicIP Cloud devices.""" -import logging - import voluptuous as vol from homeassistant import config_entries @@ -23,8 +21,6 @@ from .generic_entity import HomematicipGenericEntity # noqa: F401 from .hap import HomematicipAuth, HomematicipHAP # noqa: F401 from .services import async_setup_services, async_unload_services -_LOGGER = logging.getLogger(__name__) - CONFIG_SCHEMA = vol.Schema( { vol.Optional(DOMAIN, default=[]): vol.All( diff --git a/homeassistant/components/ipma/const.py b/homeassistant/components/ipma/const.py index 04064db2b88..47434d7f76b 100644 --- a/homeassistant/components/ipma/const.py +++ b/homeassistant/components/ipma/const.py @@ -1,6 +1,4 @@ """Constants for IPMA component.""" -import logging - from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN DOMAIN = "ipma" @@ -8,5 +6,3 @@ DOMAIN = "ipma" HOME_LOCATION_NAME = "Home" ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.ipma_{HOME_LOCATION_NAME}" - -_LOGGER = logging.getLogger(".") diff --git a/homeassistant/components/izone/discovery.py b/homeassistant/components/izone/discovery.py index 7690600786e..2a4ad516af1 100644 --- a/homeassistant/components/izone/discovery.py +++ b/homeassistant/components/izone/discovery.py @@ -1,7 +1,4 @@ """Internal discovery service for iZone AC.""" - -import logging - import pizone from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -18,8 +15,6 @@ from .const import ( DISPATCH_ZONE_UPDATE, ) -_LOGGER = logging.getLogger(__name__) - class DiscoveryService(pizone.Listener): """Discovery data and interfacing with pizone library.""" diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index 80d147191e6..86ee5e46b51 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -1,5 +1,4 @@ """Provides device triggers for lutron caseta.""" -import logging from typing import List import voluptuous as vol @@ -32,9 +31,6 @@ from .const import ( LUTRON_CASETA_BUTTON_EVENT, ) -_LOGGER = logging.getLogger(__name__) - - SUPPORTED_INPUTS_EVENTS_TYPES = [ACTION_PRESS, ACTION_RELEASE] LUTRON_BUTTON_TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( diff --git a/homeassistant/components/met/const.py b/homeassistant/components/met/const.py index 8c507eb0b8d..b78c412393d 100644 --- a/homeassistant/components/met/const.py +++ b/homeassistant/components/met/const.py @@ -1,6 +1,4 @@ """Constants for Met component.""" -import logging - from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -191,5 +189,3 @@ ATTR_MAP = { ATTR_WEATHER_WIND_BEARING: "wind_bearing", ATTR_WEATHER_WIND_SPEED: "wind_speed", } - -_LOGGER = logging.getLogger(".") diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index fe6811a35d9..428ddfadb14 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -1,6 +1,4 @@ """Support for Modbus.""" -import logging - import voluptuous as vol from homeassistant.components.cover import ( @@ -69,8 +67,6 @@ from .const import ( ) from .modbus import modbus_setup -_LOGGER = logging.getLogger(__name__) - BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string}) CLIMATE_SCHEMA = vol.Schema( diff --git a/homeassistant/components/motion_blinds/config_flow.py b/homeassistant/components/motion_blinds/config_flow.py index cb85b45e0e0..d2bf216e26a 100644 --- a/homeassistant/components/motion_blinds/config_flow.py +++ b/homeassistant/components/motion_blinds/config_flow.py @@ -1,6 +1,4 @@ """Config flow to configure Motion Blinds using their WLAN API.""" -import logging - from motionblinds import MotionDiscovery import voluptuous as vol @@ -11,9 +9,6 @@ from homeassistant.const import CONF_API_KEY, CONF_HOST from .const import DEFAULT_GATEWAY_NAME, DOMAIN from .gateway import ConnectMotionGateway -_LOGGER = logging.getLogger(__name__) - - CONFIG_SCHEMA = vol.Schema( { vol.Optional(CONF_HOST): str, diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py index dd637696e77..f8a673b3079 100644 --- a/homeassistant/components/motion_blinds/sensor.py +++ b/homeassistant/components/motion_blinds/sensor.py @@ -1,6 +1,4 @@ """Support for Motion Blinds sensors.""" -import logging - from motionblinds import BlindType from homeassistant.const import ( @@ -14,8 +12,6 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, KEY_COORDINATOR, KEY_GATEWAY -_LOGGER = logging.getLogger(__name__) - ATTR_BATTERY_VOLTAGE = "battery_voltage" TYPE_BLIND = "blind" TYPE_GATEWAY = "gateway" diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 684e41214fe..cc58741a923 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -1,6 +1,5 @@ """Camera that loads a picture from an MQTT topic.""" import functools -import logging import voluptuous as vol @@ -23,8 +22,6 @@ from .mixins import ( async_setup_entry_helper, ) -_LOGGER = logging.getLogger(__name__) - CONF_TOPIC = "topic" DEFAULT_NAME = "MQTT Camera" diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index c71541b58b0..e19aaecc3db 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -1,6 +1,5 @@ """Config flow for MQTT.""" from collections import OrderedDict -import logging import queue import voluptuous as vol @@ -31,8 +30,6 @@ from .const import ( ) from .util import MQTT_WILL_BIRTH_SCHEMA -_LOGGER = logging.getLogger(__name__) - @config_entries.HANDLERS.register("mqtt") class FlowHandler(config_entries.ConfigFlow): diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py index a3c56652253..52aeb20e3aa 100644 --- a/homeassistant/components/mqtt/debug_info.py +++ b/homeassistant/components/mqtt/debug_info.py @@ -1,7 +1,6 @@ """Helper to handle a set of topics to subscribe to.""" from collections import deque from functools import wraps -import logging from typing import Any from homeassistant.helpers.typing import HomeAssistantType @@ -9,8 +8,6 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import ATTR_DISCOVERY_PAYLOAD, ATTR_DISCOVERY_TOPIC from .models import MessageCallbackType -_LOGGER = logging.getLogger(__name__) - DATA_MQTT_DEBUG_INFO = "mqtt_debug_info" STORED_MESSAGES = 10 diff --git a/homeassistant/components/mqtt/device_automation.py b/homeassistant/components/mqtt/device_automation.py index d3e1f33421d..50d6a6e4d19 100644 --- a/homeassistant/components/mqtt/device_automation.py +++ b/homeassistant/components/mqtt/device_automation.py @@ -1,6 +1,5 @@ """Provides device automations for MQTT.""" import functools -import logging import voluptuous as vol @@ -10,8 +9,6 @@ from . import device_trigger from .. import mqtt from .mixins import async_setup_entry_helper -_LOGGER = logging.getLogger(__name__) - AUTOMATION_TYPE_TRIGGER = "trigger" AUTOMATION_TYPES = [AUTOMATION_TYPE_TRIGGER] AUTOMATION_TYPES_SCHEMA = vol.In(AUTOMATION_TYPES) diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index 296f66889b3..bd5d9a1e60e 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -1,6 +1,5 @@ """Support for tracking MQTT enabled devices identified through discovery.""" import functools -import logging import voluptuous as vol @@ -34,8 +33,6 @@ from ..mixins import ( async_setup_entry_helper, ) -_LOGGER = logging.getLogger(__name__) - CONF_PAYLOAD_HOME = "payload_home" CONF_PAYLOAD_NOT_HOME = "payload_not_home" CONF_SOURCE_TYPE = "source_type" diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index ef3ecffc0e0..24d652062ae 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -1,6 +1,5 @@ """Support for MQTT fans.""" import functools -import logging import voluptuous as vol @@ -48,8 +47,6 @@ from .mixins import ( async_setup_entry_helper, ) -_LOGGER = logging.getLogger(__name__) - CONF_STATE_VALUE_TEMPLATE = "state_value_template" CONF_SPEED_STATE_TOPIC = "speed_state_topic" CONF_SPEED_COMMAND_TOPIC = "speed_command_topic" diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 72c438df812..412302273ac 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -1,6 +1,5 @@ """Support for MQTT lights.""" import functools -import logging import voluptuous as vol @@ -15,8 +14,6 @@ from .schema_basic import PLATFORM_SCHEMA_BASIC, async_setup_entity_basic from .schema_json import PLATFORM_SCHEMA_JSON, async_setup_entity_json from .schema_template import PLATFORM_SCHEMA_TEMPLATE, async_setup_entity_template -_LOGGER = logging.getLogger(__name__) - def validate_mqtt_light(value): """Validate MQTT light schema.""" diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 408bc07b9d9..e66b93f51c0 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -1,6 +1,5 @@ """Support for MQTT locks.""" import functools -import logging import voluptuous as vol @@ -37,8 +36,6 @@ from .mixins import ( async_setup_entry_helper, ) -_LOGGER = logging.getLogger(__name__) - CONF_PAYLOAD_LOCK = "payload_lock" CONF_PAYLOAD_UNLOCK = "payload_unlock" diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 061c7df3fdc..c6d9140af61 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -1,6 +1,5 @@ """Support for MQTT scenes.""" import functools -import logging import voluptuous as vol @@ -20,8 +19,6 @@ from .mixins import ( async_setup_entry_helper, ) -_LOGGER = logging.getLogger(__name__) - DEFAULT_NAME = "MQTT Scene" DEFAULT_RETAIN = False diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 78a05179210..d017cb2ce85 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -1,7 +1,6 @@ """Support for MQTT sensors.""" from datetime import timedelta import functools -import logging from typing import Optional import voluptuous as vol @@ -38,8 +37,6 @@ from .mixins import ( async_setup_entry_helper, ) -_LOGGER = logging.getLogger(__name__) - CONF_EXPIRE_AFTER = "expire_after" DEFAULT_NAME = "MQTT Sensor" diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py index c61c30c922e..5c2efabc266 100644 --- a/homeassistant/components/mqtt/subscription.py +++ b/homeassistant/components/mqtt/subscription.py @@ -1,5 +1,4 @@ """Helper to handle a set of topics to subscribe to.""" -import logging from typing import Any, Callable, Dict, Optional import attr @@ -12,8 +11,6 @@ from .. import mqtt from .const import DEFAULT_QOS from .models import MessageCallbackType -_LOGGER = logging.getLogger(__name__) - @attr.s(slots=True) class EntitySubscription: diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 73c7d55f6b0..939d6bb98b1 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -1,6 +1,5 @@ """Support for MQTT switches.""" import functools -import logging import voluptuous as vol @@ -42,8 +41,6 @@ from .mixins import ( async_setup_entry_helper, ) -_LOGGER = logging.getLogger(__name__) - DEFAULT_NAME = "MQTT Switch" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OFF = "OFF" diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 4801c1ea994..85fd1247381 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -1,6 +1,5 @@ """Support for MQTT vacuums.""" import functools -import logging import voluptuous as vol @@ -13,8 +12,6 @@ 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 -_LOGGER = logging.getLogger(__name__) - def validate_mqtt_vacuum(value): """Validate MQTT vacuum schema.""" diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 577e6a70ccd..ca91b8948d4 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -1,6 +1,5 @@ """Support for Legacy MQTT vacuum.""" import json -import logging import voluptuous as vol @@ -39,8 +38,6 @@ from ..mixins import ( ) from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services -_LOGGER = logging.getLogger(__name__) - SERVICE_TO_STRING = { SUPPORT_TURN_ON: "turn_on", SUPPORT_TURN_OFF: "turn_off", diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index c754ba1604a..3e43736ab2e 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -1,6 +1,5 @@ """Support for a State MQTT vacuum.""" import json -import logging import voluptuous as vol @@ -43,8 +42,6 @@ from ..mixins import ( ) from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services -_LOGGER = logging.getLogger(__name__) - SERVICE_TO_STRING = { SUPPORT_START: "start", SUPPORT_PAUSE: "pause", diff --git a/homeassistant/components/neato/api.py b/homeassistant/components/neato/api.py index 931d7cdb712..31988fc175e 100644 --- a/homeassistant/components/neato/api.py +++ b/homeassistant/components/neato/api.py @@ -1,14 +1,11 @@ """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.""" diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py index 449de72b158..1f2f575ae50 100644 --- a/homeassistant/components/neato/config_flow.py +++ b/homeassistant/components/neato/config_flow.py @@ -11,8 +11,6 @@ from homeassistant.helpers import config_entry_oauth2_flow # pylint: disable=unused-import from .const import NEATO_DOMAIN -_LOGGER = logging.getLogger(__name__) - class OAuth2FlowHandler( config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=NEATO_DOMAIN diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py index e5bd7ea1ca8..ee16ca1166f 100644 --- a/homeassistant/components/nest/device_trigger.py +++ b/homeassistant/components/nest/device_trigger.py @@ -1,5 +1,4 @@ """Provides device automations for Nest.""" -import logging from typing import List import voluptuous as vol @@ -17,8 +16,6 @@ from homeassistant.helpers.typing import ConfigType from .const import DATA_SUBSCRIBER, DOMAIN from .events import DEVICE_TRAIT_TRIGGER_MAP, NEST_EVENT -_LOGGER = logging.getLogger(__name__) - DEVICE = "device" TRIGGER_TYPES = set(DEVICE_TRAIT_TRIGGER_MAP.values()) diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 34c2909188f..9f4e69bdb8c 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -1,6 +1,5 @@ """Support for NWS weather service.""" from datetime import timedelta -import logging from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, @@ -47,8 +46,6 @@ from .const import ( NWS_DATA, ) -_LOGGER = logging.getLogger(__name__) - PARALLEL_UPDATES = 0 OBSERVATION_VALID_TIME = timedelta(minutes=20) diff --git a/homeassistant/components/ondilo_ico/config_flow.py b/homeassistant/components/ondilo_ico/config_flow.py index c6a164e913b..74c668a3d2c 100644 --- a/homeassistant/components/ondilo_ico/config_flow.py +++ b/homeassistant/components/ondilo_ico/config_flow.py @@ -7,8 +7,6 @@ from homeassistant.helpers import config_entry_oauth2_flow from .const import DOMAIN from .oauth_impl import OndiloOauth2Implementation -_LOGGER = logging.getLogger(__name__) - class OAuth2FlowHandler( config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN diff --git a/homeassistant/components/ozw/fan.py b/homeassistant/components/ozw/fan.py index b4054207d0f..be0bd372b65 100644 --- a/homeassistant/components/ozw/fan.py +++ b/homeassistant/components/ozw/fan.py @@ -1,5 +1,4 @@ """Support for Z-Wave fans.""" -import logging import math from homeassistant.components.fan import ( @@ -17,8 +16,6 @@ from homeassistant.util.percentage import ( from .const import DATA_UNSUBSCRIBE, DOMAIN from .entity import ZWaveDeviceEntity -_LOGGER = logging.getLogger(__name__) - SUPPORTED_FEATURES = SUPPORT_SET_SPEED SPEED_RANGE = (1, 99) # off is not included diff --git a/homeassistant/components/plaato/sensor.py b/homeassistant/components/plaato/sensor.py index 1dd347305e1..ae767add18b 100644 --- a/homeassistant/components/plaato/sensor.py +++ b/homeassistant/components/plaato/sensor.py @@ -1,6 +1,4 @@ """Support for Plaato Airlock sensors.""" - -import logging from typing import Optional from pyplaato.models.device import PlaatoDevice @@ -25,8 +23,6 @@ from .const import ( ) from .entity import PlaatoEntity -_LOGGER = logging.getLogger(__name__) - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Plaato sensor.""" diff --git a/homeassistant/components/proxmoxve/binary_sensor.py b/homeassistant/components/proxmoxve/binary_sensor.py index 014766b532e..1151c2ec332 100644 --- a/homeassistant/components/proxmoxve/binary_sensor.py +++ b/homeassistant/components/proxmoxve/binary_sensor.py @@ -1,13 +1,9 @@ """Binary sensor to read Proxmox VE data.""" -import logging - from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import COORDINATOR, DOMAIN, ProxmoxEntity -_LOGGER = logging.getLogger(__name__) - async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up binary sensors.""" diff --git a/homeassistant/components/rachio/webhooks.py b/homeassistant/components/rachio/webhooks.py index c175117efcb..94c79a1504f 100644 --- a/homeassistant/components/rachio/webhooks.py +++ b/homeassistant/components/rachio/webhooks.py @@ -1,7 +1,4 @@ """Webhooks used by rachio.""" - -import logging - from aiohttp import web from homeassistant.const import URL_API @@ -80,9 +77,6 @@ SIGNAL_MAP = { } -_LOGGER = logging.getLogger(__name__) - - @callback def async_register_webhook(hass, webhook_id, entry_id): """Register a webhook.""" diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index d7c17a1ccff..affff382365 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -21,8 +21,6 @@ from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, entry, async_add_entities) -> None: """Set up TotalConnect alarm panels based on a config entry.""" diff --git a/homeassistant/components/totalconnect/binary_sensor.py b/homeassistant/components/totalconnect/binary_sensor.py index e296b12fa59..6bee603d1b1 100644 --- a/homeassistant/components/totalconnect/binary_sensor.py +++ b/homeassistant/components/totalconnect/binary_sensor.py @@ -1,6 +1,4 @@ """Interfaces with TotalConnect sensors.""" -import logging - from homeassistant.components.binary_sensor import ( DEVICE_CLASS_DOOR, DEVICE_CLASS_GAS, @@ -10,8 +8,6 @@ from homeassistant.components.binary_sensor import ( from .const import DOMAIN -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, entry, async_add_entities) -> None: """Set up TotalConnect device sensors based on a config entry.""" diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index da851d4a776..0502273818c 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -1,6 +1,5 @@ """Support for the Tuya climate devices.""" from datetime import timedelta -import logging from homeassistant.components.climate import ( DOMAIN as SENSOR_DOMAIN, @@ -56,8 +55,6 @@ TUYA_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_TUYA.items()} FAN_MODES = {FAN_LOW, FAN_MEDIUM, FAN_HIGH} -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up tuya sensors dynamically through tuya discovery.""" diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 48f35e035f0..a0d8abc1233 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -1,6 +1,5 @@ """Binary sensors on Zigbee Home Automation networks.""" import functools -import logging from homeassistant.components.binary_sensor import ( DEVICE_CLASS_GAS, @@ -32,8 +31,6 @@ from .core.const import ( from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity -_LOGGER = logging.getLogger(__name__) - # Zigbee Cluster Library Zone Type to Home Assistant device class CLASS_MAPPING = { 0x000D: DEVICE_CLASS_MOTION, diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 03a917217a9..91c316d307e 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -1,6 +1,5 @@ """Websocket API for Z-Wave JS.""" import json -import logging from aiohttp import hdrs, web, web_exceptions import voluptuous as vol @@ -17,8 +16,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DATA_CLIENT, DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY -_LOGGER = logging.getLogger(__name__) - ID = "id" ENTRY_ID = "entry_id" NODE_ID = "node_id" diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index a0b0648932c..689187f0b34 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -1,5 +1,4 @@ """Representation of Z-Wave thermostats.""" -import logging from typing import Any, Callable, Dict, List, Optional from zwave_js_server.client import Client as ZwaveClient @@ -47,8 +46,6 @@ from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity -_LOGGER = logging.getLogger(__name__) - # Map Z-Wave HVAC Mode to Home Assistant value # Note: We treat "auto" as "heat_cool" as most Z-Wave devices # report auto_changeover as auto without schedule support. diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index 58b85eabef1..e957d774e56 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -1,5 +1,4 @@ """Support for Z-Wave fans.""" -import logging import math from typing import Any, Callable, List, Optional @@ -22,8 +21,6 @@ from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity -_LOGGER = logging.getLogger(__name__) - SUPPORTED_FEATURES = SUPPORT_SET_SPEED SPEED_RANGE = (1, 99) # off is not included diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index 307ba577de3..a433bbe9935 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -1,6 +1,5 @@ """The tests for the feedreader component.""" from datetime import timedelta -from logging import getLogger from os import remove from os.path import exists import time @@ -24,8 +23,6 @@ from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, load_fixture -_LOGGER = getLogger(__name__) - URL = "http://some.rss.local/rss_feed.xml" VALID_CONFIG_1 = {feedreader.DOMAIN: {CONF_URLS: [URL]}} VALID_CONFIG_2 = {feedreader.DOMAIN: {CONF_URLS: [URL], CONF_SCAN_INTERVAL: 60}} diff --git a/tests/components/harmony/conftest.py b/tests/components/harmony/conftest.py index 6ca483b2588..29e897916b9 100644 --- a/tests/components/harmony/conftest.py +++ b/tests/components/harmony/conftest.py @@ -1,5 +1,4 @@ """Fixtures for harmony tests.""" -import logging from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from aioharmony.const import ClientCallbackType @@ -9,8 +8,6 @@ from homeassistant.components.harmony.const import ACTIVITY_POWER_OFF from .const import NILE_TV_ACTIVITY_ID, PLAY_MUSIC_ACTIVITY_ID, WATCH_TV_ACTIVITY_ID -_LOGGER = logging.getLogger(__name__) - ACTIVITIES_TO_IDS = { ACTIVITY_POWER_OFF: -1, "Watch TV": WATCH_TV_ACTIVITY_ID, diff --git a/tests/components/harmony/test_activity_changes.py b/tests/components/harmony/test_activity_changes.py index ff76c3ce998..dbbc6beef5b 100644 --- a/tests/components/harmony/test_activity_changes.py +++ b/tests/components/harmony/test_activity_changes.py @@ -1,7 +1,4 @@ """Test the Logitech Harmony Hub activity switches.""" - -import logging - from homeassistant.components.harmony.const import DOMAIN from homeassistant.components.remote import ATTR_ACTIVITY, DOMAIN as REMOTE_DOMAIN from homeassistant.components.switch import ( @@ -22,8 +19,6 @@ from .const import ENTITY_PLAY_MUSIC, ENTITY_REMOTE, ENTITY_WATCH_TV, HUB_NAME from tests.common import MockConfigEntry -_LOGGER = logging.getLogger(__name__) - async def test_switch_toggles(mock_hc, hass, mock_write_config): """Ensure calls to the switch modify the harmony state.""" diff --git a/tests/components/hyperion/__init__.py b/tests/components/hyperion/__init__.py index e50de207d00..954d9abc129 100644 --- a/tests/components/hyperion/__init__.py +++ b/tests/components/hyperion/__init__.py @@ -1,7 +1,6 @@ """Tests for the Hyperion component.""" from __future__ import annotations -import logging from types import TracebackType from typing import Any, Dict, Optional, Type from unittest.mock import AsyncMock, Mock, patch @@ -63,8 +62,6 @@ TEST_AUTH_NOT_REQUIRED_RESP = { "info": {"required": False}, } -_LOGGER = logging.getLogger(__name__) - class AsyncContextManagerMock(Mock): """An async context manager mock for Hyperion.""" diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py index ef7046660d6..beb642792c9 100644 --- a/tests/components/hyperion/test_config_flow.py +++ b/tests/components/hyperion/test_config_flow.py @@ -1,6 +1,4 @@ """Tests for the Hyperion config flow.""" - -import logging from typing import Any, Dict, Optional from unittest.mock import AsyncMock, patch @@ -41,8 +39,6 @@ from . import ( from tests.common import MockConfigEntry -_LOGGER = logging.getLogger(__name__) - TEST_IP_ADDRESS = "192.168.0.1" TEST_HOST_PORT: Dict[str, Any] = { CONF_HOST: TEST_HOST, diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index c3226cdd389..fe4279ed731 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -1,5 +1,4 @@ """Tests for the Hyperion integration.""" -import logging from typing import Optional from unittest.mock import AsyncMock, Mock, call, patch @@ -52,8 +51,6 @@ from . import ( setup_test_config_entry, ) -_LOGGER = logging.getLogger(__name__) - COLOR_BLACK = color_util.COLORS["black"] diff --git a/tests/components/hyperion/test_switch.py b/tests/components/hyperion/test_switch.py index dcfba9662bf..34030787e20 100644 --- a/tests/components/hyperion/test_switch.py +++ b/tests/components/hyperion/test_switch.py @@ -1,5 +1,4 @@ """Tests for the Hyperion integration.""" -import logging from unittest.mock import AsyncMock, call, patch from hyperion.const import ( @@ -35,7 +34,6 @@ TEST_COMPONENTS = [ {"enabled": True, "name": "LEDDEVICE"}, ] -_LOGGER = logging.getLogger(__name__) TEST_SWITCH_COMPONENT_BASE_ENTITY_ID = "switch.test_instance_1_component" TEST_SWITCH_COMPONENT_ALL_ENTITY_ID = f"{TEST_SWITCH_COMPONENT_BASE_ENTITY_ID}_all" diff --git a/tests/components/litejet/test_init.py b/tests/components/litejet/test_init.py index 3861e7a058e..4aee0086cbd 100644 --- a/tests/components/litejet/test_init.py +++ b/tests/components/litejet/test_init.py @@ -1,13 +1,10 @@ """The tests for the litejet component.""" -import logging import unittest from homeassistant.components import litejet from tests.common import get_test_home_assistant -_LOGGER = logging.getLogger(__name__) - class TestLiteJet(unittest.TestCase): """Test the litejet component.""" diff --git a/tests/components/litejet/test_scene.py b/tests/components/litejet/test_scene.py index c2297af6d3f..fe9298cf187 100644 --- a/tests/components/litejet/test_scene.py +++ b/tests/components/litejet/test_scene.py @@ -1,5 +1,4 @@ """The tests for the litejet component.""" -import logging import unittest from unittest import mock @@ -9,8 +8,6 @@ from homeassistant.components import litejet from tests.common import get_test_home_assistant from tests.components.scene import common -_LOGGER = logging.getLogger(__name__) - ENTITY_SCENE = "scene.mock_scene_1" ENTITY_SCENE_NUMBER = 1 ENTITY_OTHER_SCENE = "scene.mock_scene_2" diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 6eaaee87af0..ea6048dfc9e 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -1,5 +1,4 @@ """Test check_config script.""" -import logging from unittest.mock import patch import pytest @@ -9,8 +8,6 @@ import homeassistant.scripts.check_config as check_config from tests.common import get_test_config_dir, patch_yaml_files -_LOGGER = logging.getLogger(__name__) - BASE_CONFIG = ( "homeassistant:\n" " name: Home\n" From 27d16af36b642f739ba5efef68d65a24bc5dc247 Mon Sep 17 00:00:00 2001 From: Tom Parker-Shemilt Date: Sun, 14 Feb 2021 13:23:31 +0000 Subject: [PATCH 424/796] Don't allow recursive secrets loading (#41812) Co-authored-by: Martin Hjelmare --- homeassistant/util/yaml/loader.py | 5 +++++ tests/util/yaml/test_init.py | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 746806f527d..294cd0ac570 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -275,6 +275,11 @@ def _load_secret_yaml(secret_path: str) -> JSON_TYPE: def secret_yaml(loader: SafeLineLoader, node: yaml.nodes.Node) -> JSON_TYPE: """Load secrets and embed it into the configuration YAML.""" + if os.path.basename(loader.name) == SECRET_YAML: + _LOGGER.error("secrets.yaml: attempt to load secret from within secrets file") + raise HomeAssistantError( + "secrets.yaml: attempt to load secret from within secrets file" + ) secret_path = os.path.dirname(loader.name) while True: secrets = _load_secret_yaml(secret_path) diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index 34097287bc3..e28a12acf71 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -463,6 +463,17 @@ def test_duplicate_key(caplog): assert "contains duplicate key" in caplog.text +def test_no_recursive_secrets(caplog): + """Test that loading of secrets from the secrets file fails correctly.""" + files = {YAML_CONFIG_FILE: "key: !secret a", yaml.SECRET_YAML: "a: 1\nb: !secret a"} + with patch_yaml_files(files), pytest.raises(HomeAssistantError) as e: + load_yaml_config_file(YAML_CONFIG_FILE) + assert e.value.args == ( + "secrets.yaml: attempt to load secret from within secrets file", + ) + assert "attempt to load secret from within secrets file" in caplog.text + + def test_input_class(): """Test input class.""" input = yaml_loader.Input("hello") From 97776088618e279df65c768690718ee547ba091f Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Sun, 14 Feb 2021 16:24:00 +0100 Subject: [PATCH 425/796] Add myself to RFLink codeowners (#46511) --- CODEOWNERS | 1 + homeassistant/components/rflink/manifest.json | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 6e3bc1feb87..c6794a605d9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -378,6 +378,7 @@ homeassistant/components/random/* @fabaff homeassistant/components/recollect_waste/* @bachya homeassistant/components/rejseplanen/* @DarkFox homeassistant/components/repetier/* @MTrab +homeassistant/components/rflink/* @javicalle homeassistant/components/rfxtrx/* @danielhiversen @elupus @RobBie1221 homeassistant/components/ring/* @balloob homeassistant/components/risco/* @OnFreund diff --git a/homeassistant/components/rflink/manifest.json b/homeassistant/components/rflink/manifest.json index f3854a139f2..ebd1fb5afdc 100644 --- a/homeassistant/components/rflink/manifest.json +++ b/homeassistant/components/rflink/manifest.json @@ -3,5 +3,7 @@ "name": "RFLink", "documentation": "https://www.home-assistant.io/integrations/rflink", "requirements": ["rflink==0.0.58"], - "codeowners": [] + "codeowners": [ + "@javicalle" + ] } From 855bd653b4de85a4fd995c724af88999be203fb3 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 14 Feb 2021 17:40:30 +0100 Subject: [PATCH 426/796] Update modbus test harness (#44892) * Update test harness to allow discovery_info testing. This is a needed update, before the whole config is converted to the new structure, because it allows test of both the old and new configuration style. The following changes have been made: - Combine test functions into a single base_test. - Prepare for new mock by combining the 2 common test functions into one. - Change mock to be only modbusHub - Do not replace whole modbus class, but only the class that connects to pymodbus (modbusHub). This allows test of modbus configurations and not only component configurations, and is needed when testing discovery activated platform. - Add test of climate. Warning this is merely test of discovery, the real cover tests still needs to be added. - Add test of cover. Warning this is merely test of discovery, the real cover tests still needs to be added. * Update comment for old config * Do not use hass.data, but instead patch pymodbus library. * Add test of configuration (old/new way as available). * Move assert to test function. Make assert more understandable by removing it from the helper. add base_config_test (check just calls base_test) to make it clear if test is wanted or just controlling the config. * use setup_component to load Modbus since it is an integration. * Optimized flow in test helper. --- tests/components/modbus/conftest.py | 190 +++++++++++------- .../modbus/test_modbus_binary_sensor.py | 87 ++++---- .../components/modbus/test_modbus_climate.py | 75 +++++++ tests/components/modbus/test_modbus_cover.py | 136 +++++++++++++ tests/components/modbus/test_modbus_sensor.py | 65 ++++-- tests/components/modbus/test_modbus_switch.py | 141 +++++++------ 6 files changed, 487 insertions(+), 207 deletions(-) create mode 100644 tests/components/modbus/test_modbus_climate.py create mode 100644 tests/components/modbus/test_modbus_cover.py diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index a3e6078ea09..403607b110f 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -1,31 +1,25 @@ """The tests for the Modbus sensor component.""" +from datetime import timedelta +import logging from unittest import mock -from unittest.mock import patch import pytest -from homeassistant.components.modbus.const import ( - CALL_TYPE_COIL, - CALL_TYPE_DISCRETE, - CALL_TYPE_REGISTER_INPUT, - DEFAULT_HUB, - MODBUS_DOMAIN as DOMAIN, +from homeassistant.components.modbus.const import DEFAULT_HUB, MODBUS_DOMAIN as DOMAIN +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PLATFORM, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_TYPE, ) -from homeassistant.const import CONF_PLATFORM, CONF_SCAN_INTERVAL from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed - -@pytest.fixture() -def mock_hub(hass): - """Mock hub.""" - with patch("homeassistant.components.modbus.setup", return_value=True): - hub = mock.MagicMock() - hub.name = "hub" - hass.data[DOMAIN] = {DEFAULT_HUB: hub} - yield hub +_LOGGER = logging.getLogger(__name__) class ReadResult: @@ -37,63 +31,119 @@ class ReadResult: self.bits = register_words -async def setup_base_test( - sensor_name, +async def base_test( hass, - use_mock_hub, - data_array, + config_device, + device_name, entity_domain, - scan_interval, -): - """Run setup device for given config.""" - # Full sensor configuration - config = { - entity_domain: { - CONF_PLATFORM: "modbus", - CONF_SCAN_INTERVAL: scan_interval, - **data_array, - } - } - - # Initialize sensor - now = dt_util.utcnow() - with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): - assert await async_setup_component(hass, entity_domain, config) - await hass.async_block_till_done() - - entity_id = f"{entity_domain}.{sensor_name}" - device = hass.states.get(entity_id) - if device is None: - pytest.fail("CONFIG failed, see output") - return entity_id, now, device - - -async def run_base_read_test( - entity_id, - hass, - use_mock_hub, - register_type, + array_name_discovery, + array_name_old_config, register_words, expected, - now, + method_discovery=False, + check_config_only=False, + config_modbus=None, + scan_interval=None, ): - """Run test for given config.""" - # Setup inputs for the sensor - read_result = ReadResult(register_words) - if register_type == CALL_TYPE_COIL: - use_mock_hub.read_coils.return_value = read_result - elif register_type == CALL_TYPE_DISCRETE: - use_mock_hub.read_discrete_inputs.return_value = read_result - elif register_type == CALL_TYPE_REGISTER_INPUT: - use_mock_hub.read_input_registers.return_value = read_result - else: # CALL_TYPE_REGISTER_HOLDING - use_mock_hub.read_holding_registers.return_value = read_result + """Run test on device for given config.""" - # Trigger update call with time_changed event - with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): - async_fire_time_changed(hass, now) - await hass.async_block_till_done() + if config_modbus is None: + config_modbus = { + DOMAIN: { + CONF_NAME: DEFAULT_HUB, + CONF_TYPE: "tcp", + CONF_HOST: "modbusTest", + CONF_PORT: 5001, + }, + } - # Check state - state = hass.states.get(entity_id).state - assert state == expected + mock_sync = mock.MagicMock() + with mock.patch( + "homeassistant.components.modbus.modbus.ModbusTcpClient", return_value=mock_sync + ), mock.patch( + "homeassistant.components.modbus.modbus.ModbusSerialClient", + return_value=mock_sync, + ), mock.patch( + "homeassistant.components.modbus.modbus.ModbusUdpClient", return_value=mock_sync + ): + + # Setup inputs for the sensor + read_result = ReadResult(register_words) + mock_sync.read_coils.return_value = read_result + mock_sync.read_discrete_inputs.return_value = read_result + mock_sync.read_input_registers.return_value = read_result + mock_sync.read_holding_registers.return_value = read_result + + # mock timer and add old/new config + now = dt_util.utcnow() + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + if method_discovery and config_device is not None: + # setup modbus which in turn does setup for the devices + config_modbus[DOMAIN].update( + {array_name_discovery: [{**config_device}]} + ) + config_device = None + assert await async_setup_component(hass, DOMAIN, config_modbus) + await hass.async_block_till_done() + + # setup platform old style + if config_device is not None: + config_device = { + entity_domain: { + CONF_PLATFORM: DOMAIN, + array_name_old_config: [ + { + **config_device, + } + ], + } + } + if scan_interval is not None: + config_device[entity_domain][CONF_SCAN_INTERVAL] = scan_interval + assert await async_setup_component(hass, entity_domain, config_device) + await hass.async_block_till_done() + + assert DOMAIN in hass.data + if config_device is not None: + entity_id = f"{entity_domain}.{device_name}" + device = hass.states.get(entity_id) + if device is None: + pytest.fail("CONFIG failed, see output") + if check_config_only: + return + + # Trigger update call with time_changed event + now = now + timedelta(seconds=scan_interval + 60) + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # Check state + entity_id = f"{entity_domain}.{device_name}" + return hass.states.get(entity_id).state + + +async def base_config_test( + hass, + config_device, + device_name, + entity_domain, + array_name_discovery, + array_name_old_config, + method_discovery=False, + config_modbus=None, +): + """Check config of device for given config.""" + + await base_test( + hass, + config_device, + device_name, + entity_domain, + array_name_discovery, + array_name_old_config, + None, + None, + method_discovery=method_discovery, + check_config_only=True, + ) diff --git a/tests/components/modbus/test_modbus_binary_sensor.py b/tests/components/modbus/test_modbus_binary_sensor.py index 3bc7223c865..4cd586f390f 100644 --- a/tests/components/modbus/test_modbus_binary_sensor.py +++ b/tests/components/modbus/test_modbus_binary_sensor.py @@ -1,6 +1,4 @@ """The tests for the Modbus sensor component.""" -from datetime import timedelta - import pytest from homeassistant.components.binary_sensor import DOMAIN as SENSOR_DOMAIN @@ -10,83 +8,76 @@ from homeassistant.components.modbus.const import ( CONF_INPUT_TYPE, CONF_INPUTS, ) -from homeassistant.const import CONF_ADDRESS, CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_SLAVE, STATE_OFF, STATE_ON -from .conftest import run_base_read_test, setup_base_test +from .conftest import base_config_test, base_test +@pytest.mark.parametrize("do_options", [False, True]) +async def test_config_binary_sensor(hass, do_options): + """Run test for binary sensor.""" + sensor_name = "test_sensor" + config_sensor = { + CONF_NAME: sensor_name, + CONF_ADDRESS: 51, + } + if do_options: + config_sensor.update( + { + CONF_SLAVE: 10, + CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, + } + ) + await base_config_test( + hass, + config_sensor, + sensor_name, + SENSOR_DOMAIN, + None, + CONF_INPUTS, + method_discovery=False, + ) + + +@pytest.mark.parametrize("do_type", [CALL_TYPE_COIL, CALL_TYPE_DISCRETE]) @pytest.mark.parametrize( - "cfg,regs,expected", + "regs,expected", [ ( - { - CONF_INPUT_TYPE: CALL_TYPE_COIL, - }, [0xFF], STATE_ON, ), ( - { - CONF_INPUT_TYPE: CALL_TYPE_COIL, - }, [0x01], STATE_ON, ), ( - { - CONF_INPUT_TYPE: CALL_TYPE_COIL, - }, [0x00], STATE_OFF, ), ( - { - CONF_INPUT_TYPE: CALL_TYPE_COIL, - }, [0x80], STATE_OFF, ), ( - { - CONF_INPUT_TYPE: CALL_TYPE_COIL, - }, [0xFE], STATE_OFF, ), - ( - { - CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, - }, - [0xFF], - STATE_ON, - ), - ( - { - CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, - }, - [0x00], - STATE_OFF, - ), ], ) -async def test_coil_true(hass, mock_hub, cfg, regs, expected): +async def test_all_binary_sensor(hass, do_type, regs, expected): """Run test for given config.""" sensor_name = "modbus_test_binary_sensor" - scan_interval = 5 - entity_id, now, device = await setup_base_test( + state = await base_test( + hass, + {CONF_NAME: sensor_name, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: do_type}, sensor_name, - hass, - mock_hub, - {CONF_INPUTS: [dict(**{CONF_NAME: sensor_name, CONF_ADDRESS: 1234}, **cfg)]}, SENSOR_DOMAIN, - scan_interval, - ) - await run_base_read_test( - entity_id, - hass, - mock_hub, - cfg.get(CONF_INPUT_TYPE), + None, + CONF_INPUTS, regs, expected, - now + timedelta(seconds=scan_interval + 1), + method_discovery=False, + scan_interval=5, ) + assert state == expected diff --git a/tests/components/modbus/test_modbus_climate.py b/tests/components/modbus/test_modbus_climate.py new file mode 100644 index 00000000000..bbdaed63995 --- /dev/null +++ b/tests/components/modbus/test_modbus_climate.py @@ -0,0 +1,75 @@ +"""The tests for the Modbus climate component.""" +import pytest + +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.components.modbus.const import ( + CONF_CLIMATES, + CONF_CURRENT_TEMP, + CONF_DATA_COUNT, + CONF_TARGET_TEMP, +) +from homeassistant.const import CONF_NAME, CONF_SCAN_INTERVAL, CONF_SLAVE + +from .conftest import base_config_test, base_test + + +@pytest.mark.parametrize("do_options", [False, True]) +async def test_config_climate(hass, do_options): + """Run test for climate.""" + device_name = "test_climate" + device_config = { + CONF_NAME: device_name, + CONF_TARGET_TEMP: 117, + CONF_CURRENT_TEMP: 117, + CONF_SLAVE: 10, + } + if do_options: + device_config.update( + { + CONF_SCAN_INTERVAL: 20, + CONF_DATA_COUNT: 2, + } + ) + await base_config_test( + hass, + device_config, + device_name, + CLIMATE_DOMAIN, + CONF_CLIMATES, + None, + method_discovery=True, + ) + + +@pytest.mark.parametrize( + "regs,expected", + [ + ( + [0x00], + "auto", + ), + ], +) +async def test_temperature_climate(hass, regs, expected): + """Run test for given config.""" + climate_name = "modbus_test_climate" + return + state = await base_test( + hass, + { + CONF_NAME: climate_name, + CONF_SLAVE: 1, + CONF_TARGET_TEMP: 117, + CONF_CURRENT_TEMP: 117, + CONF_DATA_COUNT: 2, + }, + climate_name, + CLIMATE_DOMAIN, + CONF_CLIMATES, + None, + regs, + expected, + method_discovery=True, + scan_interval=5, + ) + assert state == expected diff --git a/tests/components/modbus/test_modbus_cover.py b/tests/components/modbus/test_modbus_cover.py new file mode 100644 index 00000000000..ff765314745 --- /dev/null +++ b/tests/components/modbus/test_modbus_cover.py @@ -0,0 +1,136 @@ +"""The tests for the Modbus cover component.""" +import pytest + +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.components.modbus.const import CALL_TYPE_COIL, CONF_REGISTER +from homeassistant.const import ( + CONF_COVERS, + CONF_NAME, + CONF_SCAN_INTERVAL, + CONF_SLAVE, + STATE_OPEN, + STATE_OPENING, +) + +from .conftest import base_config_test, base_test + + +@pytest.mark.parametrize("do_options", [False, True]) +@pytest.mark.parametrize("read_type", [CALL_TYPE_COIL, CONF_REGISTER]) +async def test_config_cover(hass, do_options, read_type): + """Run test for cover.""" + device_name = "test_cover" + device_config = { + CONF_NAME: device_name, + read_type: 1234, + } + if do_options: + device_config.update( + { + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 20, + } + ) + await base_config_test( + hass, + device_config, + device_name, + COVER_DOMAIN, + CONF_COVERS, + None, + method_discovery=True, + ) + + +@pytest.mark.parametrize( + "regs,expected", + [ + ( + [0x00], + STATE_OPENING, + ), + ( + [0x80], + STATE_OPENING, + ), + ( + [0xFE], + STATE_OPENING, + ), + ( + [0xFF], + STATE_OPENING, + ), + ( + [0x01], + STATE_OPENING, + ), + ], +) +async def test_coil_cover(hass, regs, expected): + """Run test for given config.""" + cover_name = "modbus_test_cover" + state = await base_test( + hass, + { + CONF_NAME: cover_name, + CALL_TYPE_COIL: 1234, + CONF_SLAVE: 1, + }, + cover_name, + COVER_DOMAIN, + CONF_COVERS, + None, + regs, + expected, + method_discovery=True, + scan_interval=5, + ) + assert state == expected + + +@pytest.mark.parametrize( + "regs,expected", + [ + ( + [0x00], + STATE_OPEN, + ), + ( + [0x80], + STATE_OPEN, + ), + ( + [0xFE], + STATE_OPEN, + ), + ( + [0xFF], + STATE_OPEN, + ), + ( + [0x01], + STATE_OPEN, + ), + ], +) +async def test_register_COVER(hass, regs, expected): + """Run test for given config.""" + cover_name = "modbus_test_cover" + state = await base_test( + hass, + { + CONF_NAME: cover_name, + CONF_REGISTER: 1234, + CONF_SLAVE: 1, + }, + cover_name, + COVER_DOMAIN, + CONF_COVERS, + None, + regs, + expected, + method_discovery=True, + scan_interval=5, + ) + assert state == expected diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py index 3f7c0fc60df..71a5213db9e 100644 --- a/tests/components/modbus/test_modbus_sensor.py +++ b/tests/components/modbus/test_modbus_sensor.py @@ -1,6 +1,4 @@ """The tests for the Modbus sensor component.""" -from datetime import timedelta - import pytest from homeassistant.components.modbus.const import ( @@ -20,9 +18,41 @@ from homeassistant.components.modbus.const import ( DATA_TYPE_UINT, ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import CONF_NAME, CONF_OFFSET +from homeassistant.const import CONF_NAME, CONF_OFFSET, CONF_SLAVE -from .conftest import run_base_read_test, setup_base_test +from .conftest import base_config_test, base_test + + +@pytest.mark.parametrize("do_options", [False, True]) +async def test_config_sensor(hass, do_options): + """Run test for sensor.""" + sensor_name = "test_sensor" + config_sensor = { + CONF_NAME: sensor_name, + CONF_REGISTER: 51, + } + if do_options: + config_sensor.update( + { + CONF_SLAVE: 10, + CONF_COUNT: 1, + CONF_DATA_TYPE: "int", + CONF_PRECISION: 0, + CONF_SCALE: 1, + CONF_REVERSE_ORDER: False, + CONF_OFFSET: 0, + CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, + } + ) + await base_config_test( + hass, + config_sensor, + sensor_name, + SENSOR_DOMAIN, + None, + CONF_REGISTERS, + method_discovery=False, + ) @pytest.mark.parametrize( @@ -235,28 +265,19 @@ from .conftest import run_base_read_test, setup_base_test ), ], ) -async def test_all_sensor(hass, mock_hub, cfg, regs, expected): +async def test_all_sensor(hass, cfg, regs, expected): """Run test for sensor.""" sensor_name = "modbus_test_sensor" - scan_interval = 5 - entity_id, now, device = await setup_base_test( + state = await base_test( + hass, + {CONF_NAME: sensor_name, CONF_REGISTER: 1234, **cfg}, sensor_name, - hass, - mock_hub, - { - CONF_REGISTERS: [ - dict(**{CONF_NAME: sensor_name, CONF_REGISTER: 1234}, **cfg) - ] - }, SENSOR_DOMAIN, - scan_interval, - ) - await run_base_read_test( - entity_id, - hass, - mock_hub, - cfg.get(CONF_REGISTER_TYPE), + None, + CONF_REGISTERS, regs, expected, - now + timedelta(seconds=scan_interval + 1), + method_discovery=False, + scan_interval=5, ) + assert state == expected diff --git a/tests/components/modbus/test_modbus_switch.py b/tests/components/modbus/test_modbus_switch.py index da2ff953660..5c4717c9cf8 100644 --- a/tests/components/modbus/test_modbus_switch.py +++ b/tests/components/modbus/test_modbus_switch.py @@ -1,11 +1,8 @@ """The tests for the Modbus switch component.""" -from datetime import timedelta - import pytest from homeassistant.components.modbus.const import ( CALL_TYPE_COIL, - CALL_TYPE_REGISTER_HOLDING, CONF_COILS, CONF_REGISTER, CONF_REGISTERS, @@ -20,7 +17,43 @@ from homeassistant.const import ( STATE_ON, ) -from .conftest import run_base_read_test, setup_base_test +from .conftest import base_config_test, base_test + + +@pytest.mark.parametrize("do_options", [False, True]) +@pytest.mark.parametrize("read_type", [CALL_TYPE_COIL, CONF_REGISTER]) +async def test_config_switch(hass, do_options, read_type): + """Run test for switch.""" + device_name = "test_switch" + + if read_type == CONF_REGISTER: + device_config = { + CONF_NAME: device_name, + CONF_REGISTER: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + } + array_type = CONF_REGISTERS + else: + device_config = { + CONF_NAME: device_name, + read_type: 1234, + CONF_SLAVE: 10, + } + array_type = CONF_COILS + if do_options: + device_config.update({}) + + await base_config_test( + hass, + device_config, + device_name, + SWITCH_DOMAIN, + None, + array_type, + method_discovery=False, + ) @pytest.mark.parametrize( @@ -48,32 +81,26 @@ from .conftest import run_base_read_test, setup_base_test ), ], ) -async def test_coil_switch(hass, mock_hub, regs, expected): +async def test_coil_switch(hass, regs, expected): """Run test for given config.""" switch_name = "modbus_test_switch" - scan_interval = 5 - entity_id, now, device = await setup_base_test( - switch_name, + state = await base_test( hass, - mock_hub, { - CONF_COILS: [ - {CONF_NAME: switch_name, CALL_TYPE_COIL: 1234, CONF_SLAVE: 1}, - ] + CONF_NAME: switch_name, + CALL_TYPE_COIL: 1234, + CONF_SLAVE: 1, }, + switch_name, SWITCH_DOMAIN, - scan_interval, - ) - - await run_base_read_test( - entity_id, - hass, - mock_hub, - CALL_TYPE_COIL, + None, + CONF_COILS, regs, expected, - now + timedelta(seconds=scan_interval + 1), + method_discovery=False, + scan_interval=5, ) + assert state == expected @pytest.mark.parametrize( @@ -101,38 +128,28 @@ async def test_coil_switch(hass, mock_hub, regs, expected): ), ], ) -async def test_register_switch(hass, mock_hub, regs, expected): +async def test_register_switch(hass, regs, expected): """Run test for given config.""" switch_name = "modbus_test_switch" - scan_interval = 5 - entity_id, now, device = await setup_base_test( - switch_name, + state = await base_test( hass, - mock_hub, { - CONF_REGISTERS: [ - { - CONF_NAME: switch_name, - CONF_REGISTER: 1234, - CONF_SLAVE: 1, - CONF_COMMAND_OFF: 0x00, - CONF_COMMAND_ON: 0x01, - }, - ] + CONF_NAME: switch_name, + CONF_REGISTER: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, }, + switch_name, SWITCH_DOMAIN, - scan_interval, - ) - - await run_base_read_test( - entity_id, - hass, - mock_hub, - CALL_TYPE_REGISTER_HOLDING, + None, + CONF_REGISTERS, regs, expected, - now + timedelta(seconds=scan_interval + 1), + method_discovery=False, + scan_interval=5, ) + assert state == expected @pytest.mark.parametrize( @@ -152,35 +169,25 @@ async def test_register_switch(hass, mock_hub, regs, expected): ), ], ) -async def test_register_state_switch(hass, mock_hub, regs, expected): +async def test_register_state_switch(hass, regs, expected): """Run test for given config.""" switch_name = "modbus_test_switch" - scan_interval = 5 - entity_id, now, device = await setup_base_test( - switch_name, + state = await base_test( hass, - mock_hub, { - CONF_REGISTERS: [ - { - CONF_NAME: switch_name, - CONF_REGISTER: 1234, - CONF_SLAVE: 1, - CONF_COMMAND_OFF: 0x04, - CONF_COMMAND_ON: 0x40, - }, - ] + CONF_NAME: switch_name, + CONF_REGISTER: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x04, + CONF_COMMAND_ON: 0x40, }, + switch_name, SWITCH_DOMAIN, - scan_interval, - ) - - await run_base_read_test( - entity_id, - hass, - mock_hub, - CALL_TYPE_REGISTER_HOLDING, + None, + CONF_REGISTERS, regs, expected, - now + timedelta(seconds=scan_interval + 1), + method_discovery=False, + scan_interval=5, ) + assert state == expected From f8f86fbe48cea849f04ac52238e004d72ec7eef6 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 14 Feb 2021 19:54:11 +0100 Subject: [PATCH 427/796] Do not trigger when template is true at startup (#46423) --- homeassistant/components/template/trigger.py | 30 ++++++++- tests/components/template/test_trigger.py | 70 ++++++++++++++++++-- 2 files changed, 94 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index 80ad585486b..9e6ee086c73 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -37,21 +37,49 @@ async def async_attach_trigger( template.attach(hass, time_delta) delay_cancel = None job = HassJob(action) + armed = False + + # Arm at setup if the template is already false. + try: + if not result_as_boolean(value_template.async_render()): + armed = True + except exceptions.TemplateError as ex: + _LOGGER.warning( + "Error initializing 'template' trigger for '%s': %s", + automation_info["name"], + ex, + ) @callback def template_listener(event, updates): """Listen for state changes and calls action.""" - nonlocal delay_cancel + nonlocal delay_cancel, armed result = updates.pop().result + if isinstance(result, exceptions.TemplateError): + _LOGGER.warning( + "Error evaluating 'template' trigger for '%s': %s", + automation_info["name"], + result, + ) + return + if delay_cancel: # pylint: disable=not-callable delay_cancel() delay_cancel = None if not result_as_boolean(result): + armed = True return + # Only fire when previously armed. + if not armed: + return + + # Fire! + armed = False + 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") diff --git a/tests/components/template/test_trigger.py b/tests/components/template/test_trigger.py index de4974cb1b6..3ba79e85bf2 100644 --- a/tests/components/template/test_trigger.py +++ b/tests/components/template/test_trigger.py @@ -43,13 +43,15 @@ async def test_if_fires_on_change_bool(hass, calls): automation.DOMAIN: { "trigger": { "platform": "template", - "value_template": "{{ states.test.entity.state and true }}", + "value_template": '{{ states.test.entity.state == "world" and true }}', }, "action": {"service": "test.automation"}, } }, ) + assert len(calls) == 0 + hass.states.async_set("test.entity", "world") await hass.async_block_till_done() assert len(calls) == 1 @@ -75,13 +77,15 @@ async def test_if_fires_on_change_str(hass, calls): automation.DOMAIN: { "trigger": { "platform": "template", - "value_template": '{{ states.test.entity.state and "true" }}', + "value_template": '{{ states.test.entity.state == "world" and "true" }}', }, "action": {"service": "test.automation"}, } }, ) + assert len(calls) == 0 + hass.states.async_set("test.entity", "world") await hass.async_block_till_done() assert len(calls) == 1 @@ -96,7 +100,7 @@ async def test_if_fires_on_change_str_crazy(hass, calls): automation.DOMAIN: { "trigger": { "platform": "template", - "value_template": '{{ states.test.entity.state and "TrUE" }}', + "value_template": '{{ states.test.entity.state == "world" and "TrUE" }}', }, "action": {"service": "test.automation"}, } @@ -108,6 +112,62 @@ async def test_if_fires_on_change_str_crazy(hass, calls): assert len(calls) == 1 +async def test_if_not_fires_when_true_at_setup(hass, calls): + """Test for not firing during startup.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "template", + "value_template": '{{ states.test.entity.state == "hello" }}', + }, + "action": {"service": "test.automation"}, + } + }, + ) + + assert len(calls) == 0 + + hass.states.async_set("test.entity", "hello", force_update=True) + await hass.async_block_till_done() + assert len(calls) == 0 + + +async def test_if_not_fires_because_fail(hass, calls): + """Test for not firing after TemplateError.""" + hass.states.async_set("test.number", "1") + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "template", + "value_template": "{{ 84 / states.test.number.state|int == 42 }}", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + assert len(calls) == 0 + + hass.states.async_set("test.number", "2") + await hass.async_block_till_done() + assert len(calls) == 1 + + hass.states.async_set("test.number", "0") + await hass.async_block_till_done() + assert len(calls) == 1 + + hass.states.async_set("test.number", "2") + await hass.async_block_till_done() + assert len(calls) == 1 + + async def test_if_not_fires_on_change_bool(hass, calls): """Test for not firing on boolean change.""" assert await async_setup_component( @@ -117,7 +177,7 @@ async def test_if_not_fires_on_change_bool(hass, calls): automation.DOMAIN: { "trigger": { "platform": "template", - "value_template": "{{ states.test.entity.state and false }}", + "value_template": '{{ states.test.entity.state == "world" and false }}', }, "action": {"service": "test.automation"}, } @@ -198,7 +258,7 @@ async def test_if_fires_on_two_change(hass, calls): automation.DOMAIN: { "trigger": { "platform": "template", - "value_template": "{{ states.test.entity.state and true }}", + "value_template": "{{ states.test.entity.state == 'world' }}", }, "action": {"service": "test.automation"}, } From c9df42b69a0e9e07dc9bac70ffb63386619f01a4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Feb 2021 09:42:55 -1000 Subject: [PATCH 428/796] Add support for pre-filtering events to the event bus (#46371) --- homeassistant/components/recorder/__init__.py | 24 +++-- homeassistant/config_entries.py | 18 ++-- homeassistant/core.py | 59 ++++++++---- homeassistant/helpers/device_registry.py | 19 ++-- homeassistant/helpers/entity_registry.py | 14 ++- homeassistant/helpers/event.py | 61 +++++++++++-- homeassistant/scripts/benchmark/__init__.py | 89 ++++++++++++++++--- .../mqtt/test_device_tracker_discovery.py | 1 + tests/components/mqtt/test_discovery.py | 1 + tests/components/unifi/test_device_tracker.py | 1 + tests/test_core.py | 29 ++++++ 11 files changed, 256 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 0f8a5ae7f8f..16232bcaa16 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -252,7 +252,22 @@ class Recorder(threading.Thread): @callback def async_initialize(self): """Initialize the recorder.""" - self.hass.bus.async_listen(MATCH_ALL, self.event_listener) + self.hass.bus.async_listen( + MATCH_ALL, self.event_listener, event_filter=self._async_event_filter + ) + + @callback + def _async_event_filter(self, event): + """Filter events.""" + if event.event_type in self.exclude_t: + return False + + entity_id = event.data.get(ATTR_ENTITY_ID) + if entity_id is not None: + if not self.entity_filter(entity_id): + return False + + return True def do_adhoc_purge(self, **kwargs): """Trigger an adhoc purge retaining keep_days worth of data.""" @@ -378,13 +393,6 @@ class Recorder(threading.Thread): self._timechanges_seen = 0 self._commit_event_session_or_retry() continue - if event.event_type in self.exclude_t: - continue - - entity_id = event.data.get(ATTR_ENTITY_ID) - if entity_id is not None: - if not self.entity_filter(entity_id): - continue try: if event.event_type == EVENT_STATE_CHANGED: diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index bbc1479524a..7225b7c375d 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1139,17 +1139,13 @@ class EntityRegistryDisabledHandler: def async_setup(self) -> None: """Set up the disable handler.""" self.hass.bus.async_listen( - entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, self._handle_entry_updated + entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, + self._handle_entry_updated, + event_filter=_handle_entry_updated_filter, ) async def _handle_entry_updated(self, event: Event) -> None: """Handle entity registry entry update.""" - if ( - event.data["action"] != "update" - or "disabled_by" not in event.data["changes"] - ): - return - if self.registry is None: self.registry = await entity_registry.async_get_registry(self.hass) @@ -1203,6 +1199,14 @@ class EntityRegistryDisabledHandler: ) +@callback +def _handle_entry_updated_filter(event: Event) -> bool: + """Handle entity registry entry update filter.""" + if event.data["action"] != "update" or "disabled_by" not in event.data["changes"]: + return False + return True + + async def support_entry_unload(hass: HomeAssistant, domain: str) -> bool: """Test if a domain supports entry unloading.""" integration = await loader.async_get_integration(hass, domain) diff --git a/homeassistant/core.py b/homeassistant/core.py index fff16cdd31f..b62dd1ee7d5 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -28,6 +28,7 @@ from typing import ( Mapping, Optional, Set, + Tuple, TypeVar, Union, cast, @@ -661,7 +662,7 @@ class EventBus: def __init__(self, hass: HomeAssistant) -> None: """Initialize a new event bus.""" - self._listeners: Dict[str, List[HassJob]] = {} + self._listeners: Dict[str, List[Tuple[HassJob, Optional[Callable]]]] = {} self._hass = hass @callback @@ -717,7 +718,14 @@ class EventBus: if not listeners: return - for job in listeners: + for job, event_filter in listeners: + if event_filter is not None: + try: + if not event_filter(event): + continue + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error in event filter") + continue self._hass.async_add_hass_job(job, event) def listen(self, event_type: str, listener: Callable) -> CALLBACK_TYPE: @@ -737,23 +745,38 @@ class EventBus: return remove_listener @callback - def async_listen(self, event_type: str, listener: Callable) -> CALLBACK_TYPE: + def async_listen( + self, + event_type: str, + listener: Callable, + event_filter: Optional[Callable] = None, + ) -> CALLBACK_TYPE: """Listen for all events or events of a specific type. To listen to all events specify the constant ``MATCH_ALL`` as event_type. + An optional event_filter, which must be a callable decorated with + @callback that returns a boolean value, determines if the + listener callable should run. + This method must be run in the event loop. """ - return self._async_listen_job(event_type, HassJob(listener)) + if event_filter is not None and not is_callback(event_filter): + raise HomeAssistantError(f"Event filter {event_filter} is not a callback") + return self._async_listen_filterable_job( + event_type, (HassJob(listener), event_filter) + ) @callback - def _async_listen_job(self, event_type: str, hassjob: HassJob) -> CALLBACK_TYPE: - self._listeners.setdefault(event_type, []).append(hassjob) + def _async_listen_filterable_job( + self, event_type: str, filterable_job: Tuple[HassJob, Optional[Callable]] + ) -> CALLBACK_TYPE: + self._listeners.setdefault(event_type, []).append(filterable_job) def remove_listener() -> None: """Remove the listener.""" - self._async_remove_listener(event_type, hassjob) + self._async_remove_listener(event_type, filterable_job) return remove_listener @@ -786,12 +809,12 @@ class EventBus: This method must be run in the event loop. """ - job: Optional[HassJob] = None + filterable_job: Optional[Tuple[HassJob, Optional[Callable]]] = None @callback def _onetime_listener(event: Event) -> None: """Remove listener from event bus and then fire listener.""" - nonlocal job + nonlocal filterable_job if hasattr(_onetime_listener, "run"): return # Set variable so that we will never run twice. @@ -800,22 +823,24 @@ class EventBus: # multiple times as well. # This will make sure the second time it does nothing. setattr(_onetime_listener, "run", True) - assert job is not None - self._async_remove_listener(event_type, job) + assert filterable_job is not None + self._async_remove_listener(event_type, filterable_job) self._hass.async_run_job(listener, event) - job = HassJob(_onetime_listener) + filterable_job = (HassJob(_onetime_listener), None) - return self._async_listen_job(event_type, job) + return self._async_listen_filterable_job(event_type, filterable_job) @callback - def _async_remove_listener(self, event_type: str, hassjob: HassJob) -> None: + def _async_remove_listener( + self, event_type: str, filterable_job: Tuple[HassJob, Optional[Callable]] + ) -> None: """Remove a listener of a specific event_type. This method must be run in the event loop. """ try: - self._listeners[event_type].remove(hassjob) + self._listeners[event_type].remove(filterable_job) # delete event_type list if empty if not self._listeners[event_type]: @@ -823,7 +848,9 @@ class EventBus: except (KeyError, ValueError): # KeyError is key event_type listener did not exist # ValueError if listener did not exist within event_type - _LOGGER.exception("Unable to remove unknown job listener %s", hassjob) + _LOGGER.exception( + "Unable to remove unknown job listener %s", filterable_job + ) class State: diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 0d62b2cab47..77dc2cdf609 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -686,25 +686,34 @@ def async_setup_cleanup(hass: HomeAssistantType, dev_reg: DeviceRegistry) -> Non ) async def entity_registry_changed(event: Event) -> None: - """Handle entity updated or removed.""" + """Handle entity updated or removed dispatch.""" + await debounced_cleanup.async_call() + + @callback + def entity_registry_changed_filter(event: Event) -> bool: + """Handle entity updated or removed filter.""" if ( event.data["action"] == "update" and "device_id" not in event.data["changes"] ) or event.data["action"] == "create": - return + return False - await debounced_cleanup.async_call() + return True if hass.is_running: hass.bus.async_listen( - entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, entity_registry_changed + entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, + entity_registry_changed, + event_filter=entity_registry_changed_filter, ) return async def startup_clean(event: Event) -> None: """Clean up on startup.""" hass.bus.async_listen( - entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, entity_registry_changed + entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, + entity_registry_changed, + event_filter=entity_registry_changed_filter, ) await debounced_cleanup.async_call() diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 418c3f90304..0938ea9165f 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -641,12 +641,14 @@ def async_setup_entity_restore( ) -> None: """Set up the entity restore mechanism.""" + @callback + def cleanup_restored_states_filter(event: Event) -> bool: + """Clean up restored states filter.""" + return bool(event.data["action"] == "remove") + @callback def cleanup_restored_states(event: Event) -> None: """Clean up restored states.""" - if event.data["action"] != "remove": - return - state = hass.states.get(event.data["entity_id"]) if state is None or not state.attributes.get(ATTR_RESTORED): @@ -654,7 +656,11 @@ def async_setup_entity_restore( hass.states.async_remove(event.data["entity_id"], context=event.context) - hass.bus.async_listen(EVENT_ENTITY_REGISTRY_UPDATED, cleanup_restored_states) + hass.bus.async_listen( + EVENT_ENTITY_REGISTRY_UPDATED, + cleanup_restored_states, + event_filter=cleanup_restored_states_filter, + ) if hass.is_running: return diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 102a84863bd..f496c7088a4 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -180,7 +180,7 @@ def async_track_state_change( job = HassJob(action) @callback - def state_change_listener(event: Event) -> None: + def state_change_filter(event: Event) -> bool: """Handle specific state changes.""" if from_state is not None: old_state = event.data.get("old_state") @@ -188,15 +188,21 @@ def async_track_state_change( old_state = old_state.state if not match_from_state(old_state): - return + return False + if to_state is not None: new_state = event.data.get("new_state") if new_state is not None: new_state = new_state.state if not match_to_state(new_state): - return + return False + return True + + @callback + def state_change_dispatcher(event: Event) -> None: + """Handle specific state changes.""" hass.async_run_hass_job( job, event.data.get("entity_id"), @@ -204,6 +210,14 @@ def async_track_state_change( event.data.get("new_state"), ) + @callback + def state_change_listener(event: Event) -> None: + """Handle specific state changes.""" + if not state_change_filter(event): + return + + state_change_dispatcher(event) + if entity_ids != MATCH_ALL: # If we have a list of entity ids we use # async_track_state_change_event to route @@ -215,7 +229,9 @@ def async_track_state_change( # entity_id. return async_track_state_change_event(hass, entity_ids, state_change_listener) - return hass.bus.async_listen(EVENT_STATE_CHANGED, state_change_listener) + return hass.bus.async_listen( + EVENT_STATE_CHANGED, state_change_dispatcher, event_filter=state_change_filter + ) track_state_change = threaded_listener_factory(async_track_state_change) @@ -246,6 +262,11 @@ def async_track_state_change_event( if TRACK_STATE_CHANGE_LISTENER not in hass.data: + @callback + def _async_state_change_filter(event: Event) -> bool: + """Filter state changes by entity_id.""" + return event.data.get("entity_id") in entity_callbacks + @callback def _async_state_change_dispatcher(event: Event) -> None: """Dispatch state changes by entity_id.""" @@ -263,7 +284,9 @@ def async_track_state_change_event( ) hass.data[TRACK_STATE_CHANGE_LISTENER] = hass.bus.async_listen( - EVENT_STATE_CHANGED, _async_state_change_dispatcher + EVENT_STATE_CHANGED, + _async_state_change_dispatcher, + event_filter=_async_state_change_filter, ) job = HassJob(action) @@ -329,6 +352,12 @@ def async_track_entity_registry_updated_event( if TRACK_ENTITY_REGISTRY_UPDATED_LISTENER not in hass.data: + @callback + def _async_entity_registry_updated_filter(event: Event) -> bool: + """Filter entity registry updates by entity_id.""" + entity_id = event.data.get("old_entity_id", event.data["entity_id"]) + return entity_id in entity_callbacks + @callback def _async_entity_registry_updated_dispatcher(event: Event) -> None: """Dispatch entity registry updates by entity_id.""" @@ -347,7 +376,9 @@ def async_track_entity_registry_updated_event( ) hass.data[TRACK_ENTITY_REGISTRY_UPDATED_LISTENER] = hass.bus.async_listen( - EVENT_ENTITY_REGISTRY_UPDATED, _async_entity_registry_updated_dispatcher + EVENT_ENTITY_REGISTRY_UPDATED, + _async_entity_registry_updated_dispatcher, + event_filter=_async_entity_registry_updated_filter, ) job = HassJob(action) @@ -404,6 +435,11 @@ def async_track_state_added_domain( if TRACK_STATE_ADDED_DOMAIN_LISTENER not in hass.data: + @callback + def _async_state_change_filter(event: Event) -> bool: + """Filter state changes by entity_id.""" + return event.data.get("old_state") is None + @callback def _async_state_change_dispatcher(event: Event) -> None: """Dispatch state changes by entity_id.""" @@ -413,7 +449,9 @@ def async_track_state_added_domain( _async_dispatch_domain_event(hass, event, domain_callbacks) hass.data[TRACK_STATE_ADDED_DOMAIN_LISTENER] = hass.bus.async_listen( - EVENT_STATE_CHANGED, _async_state_change_dispatcher + EVENT_STATE_CHANGED, + _async_state_change_dispatcher, + event_filter=_async_state_change_filter, ) job = HassJob(action) @@ -450,6 +488,11 @@ def async_track_state_removed_domain( if TRACK_STATE_REMOVED_DOMAIN_LISTENER not in hass.data: + @callback + def _async_state_change_filter(event: Event) -> bool: + """Filter state changes by entity_id.""" + return event.data.get("new_state") is None + @callback def _async_state_change_dispatcher(event: Event) -> None: """Dispatch state changes by entity_id.""" @@ -459,7 +502,9 @@ def async_track_state_removed_domain( _async_dispatch_domain_event(hass, event, domain_callbacks) hass.data[TRACK_STATE_REMOVED_DOMAIN_LISTENER] = hass.bus.async_listen( - EVENT_STATE_CHANGED, _async_state_change_dispatcher + EVENT_STATE_CHANGED, + _async_state_change_dispatcher, + event_filter=_async_state_change_filter, ) job = HassJob(action) diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index 48e6d7d5302..3f590362504 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -62,7 +62,7 @@ async def fire_events(hass): """Fire a million events.""" count = 0 event_name = "benchmark_event" - event = asyncio.Event() + events_to_fire = 10 ** 6 @core.callback def listener(_): @@ -70,17 +70,48 @@ async def fire_events(hass): nonlocal count count += 1 - if count == 10 ** 6: - event.set() - hass.bus.async_listen(event_name, listener) - for _ in range(10 ** 6): + for _ in range(events_to_fire): hass.bus.async_fire(event_name) start = timer() - await event.wait() + await hass.async_block_till_done() + + assert count == events_to_fire + + return timer() - start + + +@benchmark +async def fire_events_with_filter(hass): + """Fire a million events with a filter that rejects them.""" + count = 0 + event_name = "benchmark_event" + events_to_fire = 10 ** 6 + + @core.callback + def event_filter(event): + """Filter event.""" + return False + + @core.callback + def listener(_): + """Handle event.""" + nonlocal count + count += 1 + + hass.bus.async_listen(event_name, listener, event_filter=event_filter) + + for _ in range(events_to_fire): + hass.bus.async_fire(event_name) + + start = timer() + + await hass.async_block_till_done() + + assert count == 0 return timer() - start @@ -154,7 +185,7 @@ async def state_changed_event_helper(hass): """Run a million events through state changed event helper with 1000 entities.""" count = 0 entity_id = "light.kitchen" - event = asyncio.Event() + events_to_fire = 10 ** 6 @core.callback def listener(*args): @@ -162,9 +193,6 @@ async def state_changed_event_helper(hass): nonlocal count count += 1 - if count == 10 ** 6: - event.set() - hass.helpers.event.async_track_state_change_event( [f"{entity_id}{idx}" for idx in range(1000)], listener ) @@ -175,12 +203,49 @@ async def state_changed_event_helper(hass): "new_state": core.State(entity_id, "on"), } - for _ in range(10 ** 6): + for _ in range(events_to_fire): hass.bus.async_fire(EVENT_STATE_CHANGED, event_data) start = timer() - await event.wait() + await hass.async_block_till_done() + + assert count == events_to_fire + + return timer() - start + + +@benchmark +async def state_changed_event_filter_helper(hass): + """Run a million events through state changed event helper with 1000 entities that all get filtered.""" + count = 0 + entity_id = "light.kitchen" + events_to_fire = 10 ** 6 + + @core.callback + def listener(*args): + """Handle event.""" + nonlocal count + count += 1 + + hass.helpers.event.async_track_state_change_event( + [f"{entity_id}{idx}" for idx in range(1000)], listener + ) + + event_data = { + "entity_id": "switch.no_listeners", + "old_state": core.State(entity_id, "off"), + "new_state": core.State(entity_id, "on"), + } + + for _ in range(events_to_fire): + hass.bus.async_fire(EVENT_STATE_CHANGED, event_data) + + start = timer() + + await hass.async_block_till_done() + + assert count == 0 return timer() - start diff --git a/tests/components/mqtt/test_device_tracker_discovery.py b/tests/components/mqtt/test_device_tracker_discovery.py index 2c445ee0fa5..f158a878fcd 100644 --- a/tests/components/mqtt/test_device_tracker_discovery.py +++ b/tests/components/mqtt/test_device_tracker_discovery.py @@ -194,6 +194,7 @@ async def test_cleanup_device_tracker(hass, device_reg, entity_reg, mqtt_mock): device_reg.async_remove_device(device_entry.id) await hass.async_block_till_done() + await hass.async_block_till_done() # Verify device and registry entries are cleared device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index c9b0879d490..fed0dfa54d6 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -411,6 +411,7 @@ async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): device_reg.async_remove_device(device_entry.id) await hass.async_block_till_done() + await hass.async_block_till_done() # Verify device and registry entries are cleared device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 39465a34aef..e8081a831c2 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -353,6 +353,7 @@ async def test_remove_clients(hass, aioclient_mock): } controller.api.session_handler(SIGNAL_DATA) await hass.async_block_till_done() + await hass.async_block_till_done() assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 diff --git a/tests/test_core.py b/tests/test_core.py index dfd5b925e1c..88b4e1d58f6 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -379,6 +379,35 @@ async def test_eventbus_add_remove_listener(hass): unsub() +async def test_eventbus_filtered_listener(hass): + """Test we can prefilter events.""" + calls = [] + + @ha.callback + def listener(event): + """Mock listener.""" + calls.append(event) + + @ha.callback + def filter(event): + """Mock filter.""" + return not event.data["filtered"] + + unsub = hass.bus.async_listen("test", listener, event_filter=filter) + + hass.bus.async_fire("test", {"filtered": True}) + await hass.async_block_till_done() + + assert len(calls) == 0 + + hass.bus.async_fire("test", {"filtered": False}) + await hass.async_block_till_done() + + assert len(calls) == 1 + + unsub() + + async def test_eventbus_unsubscribe_listener(hass): """Test unsubscribe listener from returned function.""" calls = [] From 1444afbe5a0c2d023f4995ae366938e338233ee7 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sun, 14 Feb 2021 15:47:08 -0500 Subject: [PATCH 429/796] Use core constants for vasttrafik (#46539) --- homeassistant/components/vasttrafik/sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/vasttrafik/sensor.py b/homeassistant/components/vasttrafik/sensor.py index 8b1609be6ba..882274f8e84 100644 --- a/homeassistant/components/vasttrafik/sensor.py +++ b/homeassistant/components/vasttrafik/sensor.py @@ -6,7 +6,7 @@ import vasttrafik import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +from homeassistant.const import ATTR_ATTRIBUTION, CONF_DELAY, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -20,7 +20,6 @@ ATTR_LINE = "line" ATTR_TRACK = "track" ATTRIBUTION = "Data provided by Västtrafik" -CONF_DELAY = "delay" CONF_DEPARTURES = "departures" CONF_FROM = "from" CONF_HEADING = "heading" @@ -55,7 +54,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the departure sensor.""" - planner = vasttrafik.JournyPlanner(config.get(CONF_KEY), config.get(CONF_SECRET)) sensors = [] From aa061e5818ff01203eb91cc957b8e2d1d71afe38 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Feb 2021 10:52:18 -1000 Subject: [PATCH 430/796] Fix variable name from script refactoring (#46503) --- homeassistant/helpers/script.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 3cc8348961f..69ba082e573 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -44,6 +44,7 @@ from homeassistant.const import ( CONF_REPEAT, CONF_SCENE, CONF_SEQUENCE, + CONF_SERVICE, CONF_TARGET, CONF_TIMEOUT, CONF_UNTIL, @@ -438,9 +439,9 @@ class _ScriptRun: ) running_script = ( - params["domain"] == "automation" - and params["service_name"] == "trigger" - or params["domain"] in ("python_script", "script") + params[CONF_DOMAIN] == "automation" + and params[CONF_SERVICE] == "trigger" + or params[CONF_DOMAIN] in ("python_script", "script") ) # If this might start a script then disable the call timeout. # Otherwise use the normal service call limit. From f1c792b4c800b38c2cd420b41ce3b82470cdf66a Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sun, 14 Feb 2021 16:21:55 -0500 Subject: [PATCH 431/796] Use core constants for uvc (#46538) --- homeassistant/components/uvc/camera.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index ae10c7db48f..a20b99d673a 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -9,7 +9,7 @@ from uvcclient import camera as uvc_camera, nvr import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera -from homeassistant.const import CONF_PORT, CONF_SSL +from homeassistant.const import CONF_PASSWORD, CONF_PORT, CONF_SSL from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -17,7 +17,6 @@ _LOGGER = logging.getLogger(__name__) CONF_NVR = "nvr" CONF_KEY = "key" -CONF_PASSWORD = "password" DEFAULT_PASSWORD = "ubnt" DEFAULT_PORT = 7080 @@ -197,7 +196,6 @@ class UnifiVideoCamera(Camera): def camera_image(self): """Return the image of this camera.""" - if not self._camera: if not self._login(): return From a5f372018c61e9186e60187df5762ff9f856446b Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sun, 14 Feb 2021 17:01:18 -0500 Subject: [PATCH 432/796] Use core constants for volvooncall (#46543) --- homeassistant/components/volvooncall/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/volvooncall/__init__.py b/homeassistant/components/volvooncall/__init__.py index 7b7dffbef18..792fcc25eff 100644 --- a/homeassistant/components/volvooncall/__init__.py +++ b/homeassistant/components/volvooncall/__init__.py @@ -8,6 +8,7 @@ from volvooncall import Connection from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, + CONF_REGION, CONF_RESOURCES, CONF_SCAN_INTERVAL, CONF_USERNAME, @@ -32,7 +33,6 @@ _LOGGER = logging.getLogger(__name__) MIN_UPDATE_INTERVAL = timedelta(minutes=1) DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1) -CONF_REGION = "region" CONF_SERVICE_URL = "service_url" CONF_SCANDINAVIAN_MILES = "scandinavian_miles" CONF_MUTABLE = "mutable" From 7e88487800bab311b2cd630d6941a45e08066623 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sun, 14 Feb 2021 17:01:53 -0500 Subject: [PATCH 433/796] Use core constants for wemo (#46544) --- homeassistant/components/wemo/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 75ca322b9a3..0075b5dc851 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -12,7 +12,7 @@ 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.const import CONF_DISCOVERY, 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 @@ -57,7 +57,6 @@ def coerce_host_port(value): CONF_STATIC = "static" -CONF_DISCOVERY = "discovery" DEFAULT_DISCOVERY = True From 12abe5707d549cc07934271be29d381c0babdee1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Feb 2021 12:05:31 -1000 Subject: [PATCH 434/796] Fix typing on tuya fan percentage (#46541) --- homeassistant/components/tuya/fan.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index b13b7c3602c..12e963f05d3 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -1,5 +1,6 @@ """Support for Tuya fans.""" from datetime import timedelta +from typing import Optional from homeassistant.components.fan import ( DOMAIN as SENSOR_DOMAIN, @@ -116,7 +117,7 @@ class TuyaFanDevice(TuyaDevice, FanEntity): return self._tuya.state() @property - def percentage(self) -> str: + def percentage(self) -> Optional[int]: """Return the current speed.""" if not self.is_on: return 0 From 0af634a9f8810fd87d7fe91cd778668f9983e17b Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 15 Feb 2021 00:14:36 +0000 Subject: [PATCH 435/796] [ci skip] Translation update --- .../components/aemet/translations/ca.json | 22 +++++++ .../components/aemet/translations/cs.json | 19 ++++++ .../components/aemet/translations/et.json | 22 +++++++ .../components/aemet/translations/pl.json | 22 +++++++ .../components/aemet/translations/ru.json | 22 +++++++ .../aemet/translations/zh-Hant.json | 22 +++++++ .../components/airvisual/translations/cs.json | 12 ++++ .../components/asuswrt/translations/ca.json | 45 ++++++++++++++ .../components/asuswrt/translations/cs.json | 25 ++++++++ .../components/asuswrt/translations/et.json | 45 ++++++++++++++ .../components/asuswrt/translations/pl.json | 45 ++++++++++++++ .../components/asuswrt/translations/ru.json | 45 ++++++++++++++ .../asuswrt/translations/zh-Hant.json | 45 ++++++++++++++ .../components/econet/translations/cs.json | 21 +++++++ .../keenetic_ndms2/translations/ca.json | 36 +++++++++++ .../keenetic_ndms2/translations/en.json | 62 +++++++++---------- .../keenetic_ndms2/translations/et.json | 36 +++++++++++ .../components/mazda/translations/cs.json | 27 ++++++++ .../components/mysensors/translations/cs.json | 14 +++++ .../philips_js/translations/ca.json | 24 +++++++ .../philips_js/translations/cs.json | 4 ++ .../components/plaato/translations/cs.json | 1 + .../components/powerwall/translations/cs.json | 7 ++- .../components/roku/translations/cs.json | 1 + .../components/tesla/translations/cs.json | 4 ++ 25 files changed, 595 insertions(+), 33 deletions(-) create mode 100644 homeassistant/components/aemet/translations/ca.json create mode 100644 homeassistant/components/aemet/translations/cs.json create mode 100644 homeassistant/components/aemet/translations/et.json create mode 100644 homeassistant/components/aemet/translations/pl.json create mode 100644 homeassistant/components/aemet/translations/ru.json create mode 100644 homeassistant/components/aemet/translations/zh-Hant.json create mode 100644 homeassistant/components/asuswrt/translations/ca.json create mode 100644 homeassistant/components/asuswrt/translations/cs.json create mode 100644 homeassistant/components/asuswrt/translations/et.json create mode 100644 homeassistant/components/asuswrt/translations/pl.json create mode 100644 homeassistant/components/asuswrt/translations/ru.json create mode 100644 homeassistant/components/asuswrt/translations/zh-Hant.json create mode 100644 homeassistant/components/econet/translations/cs.json create mode 100644 homeassistant/components/keenetic_ndms2/translations/ca.json create mode 100644 homeassistant/components/keenetic_ndms2/translations/et.json create mode 100644 homeassistant/components/mazda/translations/cs.json create mode 100644 homeassistant/components/mysensors/translations/cs.json create mode 100644 homeassistant/components/philips_js/translations/ca.json diff --git a/homeassistant/components/aemet/translations/ca.json b/homeassistant/components/aemet/translations/ca.json new file mode 100644 index 00000000000..85b22e72d76 --- /dev/null +++ b/homeassistant/components/aemet/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada" + }, + "error": { + "invalid_api_key": "Clau API inv\u00e0lida" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nom de la integraci\u00f3" + }, + "description": "Configura la integraci\u00f3 d'AEMET OpenData. Per generar la clau API, v\u00e9s a https://opendata.aemet.es/centrodedescargas/altaUsuario", + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/cs.json b/homeassistant/components/aemet/translations/cs.json new file mode 100644 index 00000000000..d31920a8745 --- /dev/null +++ b/homeassistant/components/aemet/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno" + }, + "error": { + "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API" + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/et.json b/homeassistant/components/aemet/translations/et.json new file mode 100644 index 00000000000..bc0a26179d5 --- /dev/null +++ b/homeassistant/components/aemet/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Asukoht on juba m\u00e4\u00e4ratud" + }, + "error": { + "invalid_api_key": "Vale API v\u00f5ti" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad", + "name": "Sidumise nimi" + }, + "description": "Seadista AEMET OpenData sidumine. API v\u00f5tme loomiseks mine aadressile https://opendata.aemet.es/centrodedescargas/altaUsuario", + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/pl.json b/homeassistant/components/aemet/translations/pl.json new file mode 100644 index 00000000000..2c5c24fae2a --- /dev/null +++ b/homeassistant/components/aemet/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Lokalizacja jest ju\u017c skonfigurowana" + }, + "error": { + "invalid_api_key": "Nieprawid\u0142owy klucz API" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa integracji" + }, + "description": "Skonfiguruj integracj\u0119 AEMET OpenData. Aby wygenerowa\u0107 klucz API, przejd\u017a do https://opendata.aemet.es/centrodedescargas/altaUsuario", + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/ru.json b/homeassistant/components/aemet/translations/ru.json new file mode 100644 index 00000000000..4da9a032d2b --- /dev/null +++ b/homeassistant/components/aemet/translations/ru.json @@ -0,0 +1,22 @@ +{ + "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_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\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 AEMET OpenData. \u0427\u0442\u043e\u0431\u044b \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 https://opendata.aemet.es/centrodedescargas/altaUsuario.", + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/zh-Hant.json b/homeassistant/components/aemet/translations/zh-Hant.json new file mode 100644 index 00000000000..75b251ae2ff --- /dev/null +++ b/homeassistant/components/aemet/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "invalid_api_key": "API \u5bc6\u9470\u7121\u6548" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "name": "\u6574\u5408\u540d\u7a31" + }, + "description": "\u6b32\u8a2d\u5b9a AEMET OpenData \u6574\u5408\u3002\u8acb\u81f3 https://opendata.aemet.es/centrodedescargas/altaUsuario \u7522\u751f API \u5bc6\u9470", + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/cs.json b/homeassistant/components/airvisual/translations/cs.json index 5c26dcf98ef..0ff97bbacfe 100644 --- a/homeassistant/components/airvisual/translations/cs.json +++ b/homeassistant/components/airvisual/translations/cs.json @@ -17,6 +17,18 @@ "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka" } }, + "geography_by_coords": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka" + } + }, + "geography_by_name": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + }, "node_pro": { "data": { "ip_address": "Hostitel", diff --git a/homeassistant/components/asuswrt/translations/ca.json b/homeassistant/components/asuswrt/translations/ca.json new file mode 100644 index 00000000000..2b15199a092 --- /dev/null +++ b/homeassistant/components/asuswrt/translations/ca.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_host": "Nom de l'amfitri\u00f3 o l'adre\u00e7a IP inv\u00e0lids", + "pwd_and_ssh": "Proporciona, nom\u00e9s, la contrasenya o el fitxer de claus SSH", + "pwd_or_ssh": "Proporciona la contrasenya o el fitxer de claus SSH", + "ssh_not_file": "No s'ha trobat el fitxer de claus SSH", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "mode": "Mode", + "name": "Nom", + "password": "Contrasenya", + "port": "Port", + "protocol": "Protocol de comunicacions a utilitzar", + "ssh_key": "Ruta al fitxer de claus SSH (en lloc de la contrasenya)", + "username": "Nom d'usuari" + }, + "description": "Introdueix el par\u00e0metre necessari per connectar-te al router", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Segons d'espera abans de considerar un dispositiu a fora", + "dnsmasq": "La ubicaci\u00f3 dins el router dels fitxers dnsmasq.leases", + "interface": "La interf\u00edcie de la qual obtenir les estad\u00edstiques (per exemple, eth0, eth1, etc.)", + "require_ip": "Els dispositius han de tenir una IP (per al mode de punt d'acc\u00e9s)", + "track_unknown": "Segueix dispositius desconeguts/sense nom" + }, + "title": "Opcions d'AsusWRT" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/cs.json b/homeassistant/components/asuswrt/translations/cs.json new file mode 100644 index 00000000000..d9766e9a6d0 --- /dev/null +++ b/homeassistant/components/asuswrt/translations/cs.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_host": "Neplatn\u00fd hostitel nebo IP adresa", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "mode": "Re\u017eim", + "name": "Jm\u00e9no", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + }, + "title": "AsusWRT" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/et.json b/homeassistant/components/asuswrt/translations/et.json new file mode 100644 index 00000000000..8cc14b7353b --- /dev/null +++ b/homeassistant/components/asuswrt/translations/et.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_host": "Sobimatu hostinimi v\u00f5i IP-aadress", + "pwd_and_ssh": "Sisesta ainult parooli v\u00f5i SSH v\u00f5tmefail", + "pwd_or_ssh": "Sisesta parool v\u00f5i SSH v\u00f5tmefail", + "ssh_not_file": "SSH v\u00f5tmefaili ei leitud", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host", + "mode": "Re\u017eiim", + "name": "Nimi", + "password": "Salas\u00f5na", + "port": "Port", + "protocol": "Kasutatav sideprotokoll", + "ssh_key": "Rada SSH v\u00f5tmefailini (parooli asemel)", + "username": "Kasutajanimi" + }, + "description": "M\u00e4\u00e4ra ruuteriga \u00fchenduse loomiseks vajalik parameeter", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Mitu sekundit oodata, enne kui lugeda seade eemal olevaks", + "dnsmasq": "Dnsmasq.leases failide asukoht ruuteris", + "interface": "Liides kust soovite statistikat (n\u00e4iteks eth0, eth1 jne.)", + "require_ip": "Seadmetel peab olema IP (p\u00e4\u00e4supunkti re\u017eiimi jaoks)", + "track_unknown": "J\u00e4lgi tundmatuid / nimetamata seadmeid" + }, + "title": "AsusWRT valikud" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/pl.json b/homeassistant/components/asuswrt/translations/pl.json new file mode 100644 index 00000000000..b646c8e4503 --- /dev/null +++ b/homeassistant/components/asuswrt/translations/pl.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_host": "Nieprawid\u0142owa nazwa hosta lub adres IP", + "pwd_and_ssh": "Podaj tylko has\u0142o lub plik z kluczem SSH", + "pwd_or_ssh": "Podaj has\u0142o lub plik z kluczem SSH", + "ssh_not_file": "Nie znaleziono pliku z kluczem SSH", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "mode": "Tryb", + "name": "Nazwa", + "password": "Has\u0142o", + "port": "Port", + "protocol": "Wybierz protok\u00f3\u0142 komunikacyjny", + "ssh_key": "\u015acie\u017cka do pliku z kluczem SSH (zamiast has\u0142a)", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Ustaw wymagany parametr, aby po\u0142\u0105czy\u0107 si\u0119 z routerem", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Czas w sekundach, zanim urz\u0105dzenie zostanie uznane za \"nieobecne\"", + "dnsmasq": "Lokalizacja w routerze plik\u00f3w dnsmasq.leases", + "interface": "Interfejs, z kt\u00f3rego chcesz uzyska\u0107 statystyki (np. Eth0, eth1 itp.)", + "require_ip": "Urz\u0105dzenia musz\u0105 mie\u0107 adres IP (w trybie punktu dost\u0119pu)", + "track_unknown": "\u015aled\u017a nieznane / nienazwane urz\u0105dzenia" + }, + "title": "Opcje AsusWRT" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/ru.json b/homeassistant/components/asuswrt/translations/ru.json new file mode 100644 index 00000000000..236f7642c12 --- /dev/null +++ b/homeassistant/components/asuswrt/translations/ru.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "pwd_and_ssh": "\u041d\u0443\u0436\u043d\u043e \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u0430\u0440\u043e\u043b\u044c \u0438\u043b\u0438 \u0442\u043e\u043b\u044c\u043a\u043e \u0444\u0430\u0439\u043b \u043a\u043b\u044e\u0447\u0430 SSH.", + "pwd_or_ssh": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0438\u043b\u0438 \u0444\u0430\u0439\u043b \u043a\u043b\u044e\u0447\u0430 SSH.", + "ssh_not_file": "\u0424\u0430\u0439\u043b \u043a\u043b\u044e\u0447\u0430 SSH \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "mode": "\u0420\u0435\u0436\u0438\u043c", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0441\u0432\u044f\u0437\u0438", + "ssh_key": "\u041f\u0443\u0442\u044c \u0444\u0430\u0439\u043b\u0443 \u043a\u043b\u044e\u0447\u0435\u0439 SSH (\u0432\u043c\u0435\u0441\u0442\u043e \u043f\u0430\u0440\u043e\u043b\u044f)", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u0440\u043e\u0443\u0442\u0435\u0440\u0443.", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "\u0412\u0440\u0435\u043c\u044f \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445), \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u041d\u0435 \u0434\u043e\u043c\u0430\"", + "dnsmasq": "\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0432 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0435 \u0444\u0430\u0439\u043b\u043e\u0432 dnsmasq.leases", + "interface": "\u0418\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441, \u0441 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0443 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, eth0, eth1 \u0438 \u0442. \u0434.)", + "require_ip": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0441 IP-\u0430\u0434\u0440\u0435\u0441\u043e\u043c (\u0434\u043b\u044f \u0440\u0435\u0436\u0438\u043c\u0430 \u0442\u043e\u0447\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430)", + "track_unknown": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0435/\u0431\u0435\u0437\u044b\u043c\u044f\u043d\u043d\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AsusWRT" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/zh-Hant.json b/homeassistant/components/asuswrt/translations/zh-Hant.json new file mode 100644 index 00000000000..8caddacd23e --- /dev/null +++ b/homeassistant/components/asuswrt/translations/zh-Hant.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740", + "pwd_and_ssh": "\u50c5\u63d0\u4f9b\u5bc6\u78bc\u6216 SSH \u5bc6\u9470\u6a94\u6848", + "pwd_or_ssh": "\u8acb\u8f38\u5165\u5bc6\u78bc\u6216 SSH \u5bc6\u9470\u6a94\u6848", + "ssh_not_file": "\u627e\u4e0d\u5230 SSH \u5bc6\u9470\u6a94\u6848", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "mode": "\u6a21\u5f0f", + "name": "\u540d\u7a31", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "protocol": "\u4f7f\u7528\u901a\u8a0a\u606f\u5354\u5b9a", + "ssh_key": "SSH \u5bc6\u9470\u6a94\u6848\u8def\u5f91\uff08\u975e\u5bc6\u78bc\uff09", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8a2d\u5b9a\u6240\u9700\u53c3\u6578\u4ee5\u9023\u7dda\u81f3\u8def\u7531\u5668", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "\u8996\u70ba\u96e2\u958b\u7684\u7b49\u5019\u79d2\u6578", + "dnsmasq": "dnsmasq.leases \u6a94\u6848\u65bc\u8def\u7531\u5668\u4e2d\u6240\u5728\u4f4d\u7f6e", + "interface": "\u6240\u8981\u9032\u884c\u7d71\u8a08\u7684\u4ecb\u9762\u53e3\uff08\u4f8b\u5982 eth0\u3001eth1 \u7b49\uff09", + "require_ip": "\u88dd\u7f6e\u5fc5\u9808\u5177\u6709 IP\uff08\u7528\u65bc AP \u6a21\u5f0f\uff09", + "track_unknown": "\u8ffd\u8e64\u672a\u77e5 / \u672a\u547d\u540d\u88dd\u7f6e" + }, + "title": "AsusWRT \u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/cs.json b/homeassistant/components/econet/translations/cs.json new file mode 100644 index 00000000000..bd8b0799628 --- /dev/null +++ b/homeassistant/components/econet/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/ca.json b/homeassistant/components/keenetic_ndms2/translations/ca.json new file mode 100644 index 00000000000..f15b11b3eb4 --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/ca.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja ha estat configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "name": "Nom", + "password": "Contrasenya", + "port": "Port", + "username": "Nom d'usuari" + }, + "title": "Configuraci\u00f3 del router Keenetic NDMS2" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "consider_home": "Interval per considerar a casa", + "include_arp": "Utilitza dades d'ARP (s'ignorar\u00e0 si s'utilitzen dades de 'punt d'acc\u00e9s')", + "include_associated": "Utilitza dades d'associacions d'AP WiFi (s'ignorar\u00e0 si s'utilitzen dades de 'punt d'acc\u00e9s')", + "interfaces": "Escull les interf\u00edcies a escanejar", + "scan_interval": "Interval d'escaneig", + "try_hotspot": "Utilitza dades de 'punt d'acc\u00e9s IP' (m\u00e9s precisi\u00f3)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/en.json b/homeassistant/components/keenetic_ndms2/translations/en.json index 1849d68c651..e95f2f740ef 100644 --- a/homeassistant/components/keenetic_ndms2/translations/en.json +++ b/homeassistant/components/keenetic_ndms2/translations/en.json @@ -1,36 +1,36 @@ { - "config": { - "step": { - "user": { - "title": "Set up Keenetic NDMS2 Router", - "data": { - "name": "Name", - "host": "Host", - "username": "Username", - "password": "Password", - "port": "Port" + "config": { + "abort": { + "already_configured": "Account is already configured" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name", + "password": "Password", + "port": "Port", + "username": "Username" + }, + "title": "Set up Keenetic NDMS2 Router" + } } - } }, - "error": { - "cannot_connect": "Connection Unsuccessful" - }, - "abort": { - "already_configured": "This router is already configured" - } - }, - "options": { - "step": { - "user": { - "data": { - "scan_interval": "Scan interval", - "consider_home": "Consider home interval", - "interfaces": "Choose interfaces to scan", - "try_hotspot": "Use 'ip hotspot' data (most accurate)", - "include_arp": "Use ARP data (if hotspot disabled/unavailable)", - "include_associated": "Use WiFi AP associations data (if hotspot disabled/unavailable)" + "options": { + "step": { + "user": { + "data": { + "consider_home": "Consider home interval", + "include_arp": "Use ARP data (ignored if hotspot data used)", + "include_associated": "Use WiFi AP associations data (ignored if hotspot data used)", + "interfaces": "Choose interfaces to scan", + "scan_interval": "Scan interval", + "try_hotspot": "Use 'ip hotspot' data (most accurate)" + } + } } - } } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/et.json b/homeassistant/components/keenetic_ndms2/translations/et.json new file mode 100644 index 00000000000..dc500be7e1a --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/et.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nimi", + "password": "Salas\u00f5na", + "port": "Port", + "username": "Kasutajanimi" + }, + "title": "Seadista Keenetic NDMS2 ruuter" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "consider_home": "M\u00e4\u00e4ra n\u00e4htavuse aeg", + "include_arp": "Kasuta ARP andmeid (ignoreeritakse, kui kasutatakse kuumkoha andmeid)", + "include_associated": "Kasuta WiFi AP seoste andmeid (ignoreeritakse kui kasutatakse kuumkoha andmeid)", + "interfaces": "Sk\u00e4nnitavate liideste valimine", + "scan_interval": "P\u00e4ringute intervall", + "try_hotspot": "Kasuta 'ip hotspot' andmeid (k\u00f5ige t\u00e4psem)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/cs.json b/homeassistant/components/mazda/translations/cs.json new file mode 100644 index 00000000000..8a929fb58d7 --- /dev/null +++ b/homeassistant/components/mazda/translations/cs.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "reauth": { + "data": { + "email": "E-mail", + "password": "Heslo" + } + }, + "user": { + "data": { + "email": "E-mail", + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/cs.json b/homeassistant/components/mysensors/translations/cs.json new file mode 100644 index 00000000000..9d3cfbd2508 --- /dev/null +++ b/homeassistant/components/mysensors/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "error": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + } + }, + "title": "MySensors" +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/ca.json b/homeassistant/components/philips_js/translations/ca.json new file mode 100644 index 00000000000..505a6472ea8 --- /dev/null +++ b/homeassistant/components/philips_js/translations/ca.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "api_version": "Versi\u00f3 de l'API", + "host": "Amfitri\u00f3" + } + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "Es demani que el dispositiu s'engegui" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/cs.json b/homeassistant/components/philips_js/translations/cs.json index 8a5866b5959..a39944a8dba 100644 --- a/homeassistant/components/philips_js/translations/cs.json +++ b/homeassistant/components/philips_js/translations/cs.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { diff --git a/homeassistant/components/plaato/translations/cs.json b/homeassistant/components/plaato/translations/cs.json index 582a3e3a180..4d736d1c695 100644 --- a/homeassistant/components/plaato/translations/cs.json +++ b/homeassistant/components/plaato/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace.", "webhook_not_internet_accessible": "V\u00e1\u0161 Home Assistant mus\u00ed b\u00fdt p\u0159\u00edstupn\u00fd z internetu, aby mohl p\u0159ij\u00edmat zpr\u00e1vy webhook." }, diff --git a/homeassistant/components/powerwall/translations/cs.json b/homeassistant/components/powerwall/translations/cs.json index 698934ad10e..d6e5cd5904b 100644 --- a/homeassistant/components/powerwall/translations/cs.json +++ b/homeassistant/components/powerwall/translations/cs.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba", "wrong_version": "Powerwall pou\u017e\u00edv\u00e1 verzi softwaru, kter\u00e1 nen\u00ed podporov\u00e1na. Zva\u017ete upgrade nebo nahlaste probl\u00e9m, aby mohl b\u00fdt vy\u0159e\u0161en." }, @@ -12,7 +14,8 @@ "step": { "user": { "data": { - "ip_address": "IP adresa" + "ip_address": "IP adresa", + "password": "Heslo" }, "title": "P\u0159ipojen\u00ed k powerwall" } diff --git a/homeassistant/components/roku/translations/cs.json b/homeassistant/components/roku/translations/cs.json index 89ca523af47..6914a519285 100644 --- a/homeassistant/components/roku/translations/cs.json +++ b/homeassistant/components/roku/translations/cs.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "error": { diff --git a/homeassistant/components/tesla/translations/cs.json b/homeassistant/components/tesla/translations/cs.json index c611b85d734..9c117223d40 100644 --- a/homeassistant/components/tesla/translations/cs.json +++ b/homeassistant/components/tesla/translations/cs.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, "error": { "already_configured": "\u00da\u010det je ji\u017e nastaven", "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", From a3b733f1ec99c9802a4564edca7ce85ebf13dcd0 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 14 Feb 2021 19:35:14 -0500 Subject: [PATCH 436/796] Add additional supported feature support to universal media player (#44711) * add additional supported feature support to universal media player * add missing services --- .../components/universal/media_player.py | 74 ++++++++++- .../components/universal/test_media_player.py | 116 +++++++++++++++++- 2 files changed, 188 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 8fff0e80dfb..2a5fcee34dc 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -20,6 +20,7 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_PLAYLIST, ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION_UPDATED_AT, + ATTR_MEDIA_REPEAT, ATTR_MEDIA_SEASON, ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_SERIES_TITLE, @@ -28,13 +29,23 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_TRACK, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, + ATTR_SOUND_MODE, + ATTR_SOUND_MODE_LIST, DOMAIN, SERVICE_CLEAR_PLAYLIST, SERVICE_PLAY_MEDIA, + SERVICE_SELECT_SOUND_MODE, SERVICE_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_REPEAT_SET, + SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, + SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, @@ -55,7 +66,9 @@ from homeassistant.const import ( SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK, SERVICE_MEDIA_STOP, + SERVICE_REPEAT_SET, SERVICE_SHUFFLE_SET, + SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_DOWN, @@ -382,6 +395,16 @@ class UniversalMediaPlayer(MediaPlayerEntity): """Name of the current running app.""" return self._child_attr(ATTR_APP_NAME) + @property + def sound_mode(self): + """Return the current sound mode of the device.""" + return self._override_or_child_attr(ATTR_SOUND_MODE) + + @property + def sound_mode_list(self): + """List of available sound modes.""" + return self._override_or_child_attr(ATTR_SOUND_MODE_LIST) + @property def source(self): """Return the current input source of the device.""" @@ -392,6 +415,11 @@ class UniversalMediaPlayer(MediaPlayerEntity): """List of available input sources.""" return self._override_or_child_attr(ATTR_INPUT_SOURCE_LIST) + @property + def repeat(self): + """Boolean if repeating is enabled.""" + return self._override_or_child_attr(ATTR_MEDIA_REPEAT) + @property def shuffle(self): """Boolean if shuffling is enabled.""" @@ -407,6 +435,22 @@ class UniversalMediaPlayer(MediaPlayerEntity): if SERVICE_TURN_OFF in self._cmds: flags |= SUPPORT_TURN_OFF + if SERVICE_MEDIA_PLAY_PAUSE in self._cmds: + flags |= SUPPORT_PLAY | SUPPORT_PAUSE + else: + if SERVICE_MEDIA_PLAY in self._cmds: + flags |= SUPPORT_PLAY + if SERVICE_MEDIA_PAUSE in self._cmds: + flags |= SUPPORT_PAUSE + + if SERVICE_MEDIA_STOP in self._cmds: + flags |= SUPPORT_STOP + + if SERVICE_MEDIA_NEXT_TRACK in self._cmds: + flags |= SUPPORT_NEXT_TRACK + if SERVICE_MEDIA_PREVIOUS_TRACK in self._cmds: + flags |= SUPPORT_PREVIOUS_TRACK + if any([cmd in self._cmds for cmd in [SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN]]): flags |= SUPPORT_VOLUME_STEP if SERVICE_VOLUME_SET in self._cmds: @@ -415,7 +459,10 @@ class UniversalMediaPlayer(MediaPlayerEntity): if SERVICE_VOLUME_MUTE in self._cmds and ATTR_MEDIA_VOLUME_MUTED in self._attrs: flags |= SUPPORT_VOLUME_MUTE - if SERVICE_SELECT_SOURCE in self._cmds: + if ( + SERVICE_SELECT_SOURCE in self._cmds + and ATTR_INPUT_SOURCE_LIST in self._attrs + ): flags |= SUPPORT_SELECT_SOURCE if SERVICE_CLEAR_PLAYLIST in self._cmds: @@ -424,6 +471,15 @@ class UniversalMediaPlayer(MediaPlayerEntity): if SERVICE_SHUFFLE_SET in self._cmds and ATTR_MEDIA_SHUFFLE in self._attrs: flags |= SUPPORT_SHUFFLE_SET + if SERVICE_REPEAT_SET in self._cmds and ATTR_MEDIA_REPEAT in self._attrs: + flags |= SUPPORT_REPEAT_SET + + if ( + SERVICE_SELECT_SOUND_MODE in self._cmds + and ATTR_SOUND_MODE_LIST in self._attrs + ): + flags |= SUPPORT_SELECT_SOUND_MODE + return flags @property @@ -502,6 +558,13 @@ class UniversalMediaPlayer(MediaPlayerEntity): """Play or pause the media player.""" await self._async_call_service(SERVICE_MEDIA_PLAY_PAUSE) + async def async_select_sound_mode(self, sound_mode): + """Select sound mode.""" + data = {ATTR_SOUND_MODE: sound_mode} + await self._async_call_service( + SERVICE_SELECT_SOUND_MODE, data, allow_override=True + ) + async def async_select_source(self, source): """Set the input source.""" data = {ATTR_INPUT_SOURCE: source} @@ -516,6 +579,15 @@ class UniversalMediaPlayer(MediaPlayerEntity): data = {ATTR_MEDIA_SHUFFLE: shuffle} await self._async_call_service(SERVICE_SHUFFLE_SET, data, allow_override=True) + async def async_set_repeat(self, repeat): + """Set repeat mode.""" + data = {ATTR_MEDIA_REPEAT: repeat} + await self._async_call_service(SERVICE_REPEAT_SET, data, allow_override=True) + + async def async_toggle(self): + """Toggle the power on the media player.""" + await self._async_call_service(SERVICE_TOGGLE) + async def async_update(self): """Update state in HA.""" for child_name in self._children: diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index fd75620f318..75cf029af40 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -51,6 +51,7 @@ class MockMediaPlayer(media_player.MediaPlayerEntity): self._tracks = 12 self._media_image_url = None self._shuffle = False + self._sound_mode = None self.service_calls = { "turn_on": mock_service( @@ -71,6 +72,9 @@ class MockMediaPlayer(media_player.MediaPlayerEntity): "media_pause": mock_service( hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_PAUSE ), + "media_stop": mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_STOP + ), "media_previous_track": mock_service( hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_PREVIOUS_TRACK ), @@ -92,12 +96,21 @@ class MockMediaPlayer(media_player.MediaPlayerEntity): "media_play_pause": mock_service( hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_PLAY_PAUSE ), + "select_sound_mode": mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_SELECT_SOUND_MODE + ), "select_source": mock_service( hass, media_player.DOMAIN, media_player.SERVICE_SELECT_SOURCE ), + "toggle": mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_TOGGLE + ), "clear_playlist": mock_service( hass, media_player.DOMAIN, media_player.SERVICE_CLEAR_PLAYLIST ), + "repeat_set": mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_REPEAT_SET + ), "shuffle_set": mock_service( hass, media_player.DOMAIN, media_player.SERVICE_SHUFFLE_SET ), @@ -162,18 +175,30 @@ class MockMediaPlayer(media_player.MediaPlayerEntity): """Mock pause.""" self._state = STATE_PAUSED + def select_sound_mode(self, sound_mode): + """Set the sound mode.""" + self._sound_mode = sound_mode + def select_source(self, source): """Set the input source.""" self._source = source + def async_toggle(self): + """Toggle the power on the media player.""" + self._state = STATE_OFF if self._state == STATE_ON else STATE_ON + def clear_playlist(self): """Clear players playlist.""" self._tracks = 0 def set_shuffle(self, shuffle): - """Clear players playlist.""" + """Enable/disable shuffle mode.""" self._shuffle = shuffle + def set_repeat(self, repeat): + """Enable/disable repeat mode.""" + self._repeat = repeat + class TestMediaPlayer(unittest.TestCase): """Test the media_player module.""" @@ -205,9 +230,18 @@ class TestMediaPlayer(unittest.TestCase): self.mock_source_id = f"{input_select.DOMAIN}.source" self.hass.states.set(self.mock_source_id, "dvd") + self.mock_sound_mode_list_id = f"{input_select.DOMAIN}.sound_mode_list" + self.hass.states.set(self.mock_sound_mode_list_id, ["music", "movie"]) + + self.mock_sound_mode_id = f"{input_select.DOMAIN}.sound_mode" + self.hass.states.set(self.mock_sound_mode_id, "music") + self.mock_shuffle_switch_id = switch.ENTITY_ID_FORMAT.format("shuffle") self.hass.states.set(self.mock_shuffle_switch_id, STATE_OFF) + self.mock_repeat_switch_id = switch.ENTITY_ID_FORMAT.format("repeat") + self.hass.states.set(self.mock_repeat_switch_id, STATE_OFF) + self.config_children_only = { "name": "test", "platform": "universal", @@ -230,6 +264,9 @@ class TestMediaPlayer(unittest.TestCase): "source_list": self.mock_source_list_id, "state": self.mock_state_switch_id, "shuffle": self.mock_shuffle_switch_id, + "repeat": self.mock_repeat_switch_id, + "sound_mode_list": self.mock_sound_mode_list_id, + "sound_mode": self.mock_sound_mode_id, }, } self.addCleanup(self.tear_down_cleanup) @@ -507,6 +544,17 @@ class TestMediaPlayer(unittest.TestCase): asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert ump.is_volume_muted + def test_sound_mode_list_children_and_attr(self): + """Test sound mode list property w/ children and attrs.""" + config = validate_config(self.config_children_and_attr) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + assert "['music', 'movie']" == ump.sound_mode_list + + self.hass.states.set(self.mock_sound_mode_list_id, ["music", "movie", "game"]) + assert "['music', 'movie', 'game']" == ump.sound_mode_list + def test_source_list_children_and_attr(self): """Test source list property w/ children and attrs.""" config = validate_config(self.config_children_and_attr) @@ -518,6 +566,17 @@ class TestMediaPlayer(unittest.TestCase): self.hass.states.set(self.mock_source_list_id, ["dvd", "htpc", "game"]) assert "['dvd', 'htpc', 'game']" == ump.source_list + def test_sound_mode_children_and_attr(self): + """Test sound modeproperty w/ children and attrs.""" + config = validate_config(self.config_children_and_attr) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + assert "music" == ump.sound_mode + + self.hass.states.set(self.mock_sound_mode_id, "movie") + assert "movie" == ump.sound_mode + def test_source_children_and_attr(self): """Test source property w/ children and attrs.""" config = validate_config(self.config_children_and_attr) @@ -579,8 +638,17 @@ class TestMediaPlayer(unittest.TestCase): "volume_down": excmd, "volume_mute": excmd, "volume_set": excmd, + "select_sound_mode": excmd, "select_source": excmd, + "repeat_set": excmd, "shuffle_set": excmd, + "media_play": excmd, + "media_pause": excmd, + "media_stop": excmd, + "media_next_track": excmd, + "media_previous_track": excmd, + "toggle": excmd, + "clear_playlist": excmd, } config = validate_config(config) @@ -598,13 +666,41 @@ class TestMediaPlayer(unittest.TestCase): | universal.SUPPORT_TURN_OFF | universal.SUPPORT_VOLUME_STEP | universal.SUPPORT_VOLUME_MUTE + | universal.SUPPORT_SELECT_SOUND_MODE | universal.SUPPORT_SELECT_SOURCE + | universal.SUPPORT_REPEAT_SET | universal.SUPPORT_SHUFFLE_SET | universal.SUPPORT_VOLUME_SET + | universal.SUPPORT_PLAY + | universal.SUPPORT_PAUSE + | universal.SUPPORT_STOP + | universal.SUPPORT_NEXT_TRACK + | universal.SUPPORT_PREVIOUS_TRACK + | universal.SUPPORT_CLEAR_PLAYLIST ) assert check_flags == ump.supported_features + def test_supported_features_play_pause(self): + """Test supported media commands with play_pause function.""" + config = copy(self.config_children_and_attr) + excmd = {"service": "media_player.test", "data": {"entity_id": "test"}} + config["commands"] = {"media_play_pause": excmd} + config = validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + + self.mock_mp_1._state = STATE_PLAYING + self.mock_mp_1.schedule_update_ha_state() + self.hass.block_till_done() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + + check_flags = universal.SUPPORT_PLAY | universal.SUPPORT_PAUSE + + assert check_flags == ump.supported_features + def test_service_call_no_active_child(self): """Test a service call to children with no active child.""" config = validate_config(self.config_children_and_attr) @@ -663,6 +759,11 @@ class TestMediaPlayer(unittest.TestCase): ).result() assert 1 == len(self.mock_mp_2.service_calls["media_pause"]) + asyncio.run_coroutine_threadsafe( + ump.async_media_stop(), self.hass.loop + ).result() + assert 1 == len(self.mock_mp_2.service_calls["media_stop"]) + asyncio.run_coroutine_threadsafe( ump.async_media_previous_track(), self.hass.loop ).result() @@ -696,6 +797,11 @@ class TestMediaPlayer(unittest.TestCase): ).result() assert 1 == len(self.mock_mp_2.service_calls["media_play_pause"]) + asyncio.run_coroutine_threadsafe( + ump.async_select_sound_mode("music"), self.hass.loop + ).result() + assert 1 == len(self.mock_mp_2.service_calls["select_sound_mode"]) + asyncio.run_coroutine_threadsafe( ump.async_select_source("dvd"), self.hass.loop ).result() @@ -706,11 +812,19 @@ class TestMediaPlayer(unittest.TestCase): ).result() assert 1 == len(self.mock_mp_2.service_calls["clear_playlist"]) + asyncio.run_coroutine_threadsafe( + ump.async_set_repeat(True), self.hass.loop + ).result() + assert 1 == len(self.mock_mp_2.service_calls["repeat_set"]) + asyncio.run_coroutine_threadsafe( ump.async_set_shuffle(True), self.hass.loop ).result() assert 1 == len(self.mock_mp_2.service_calls["shuffle_set"]) + asyncio.run_coroutine_threadsafe(ump.async_toggle(), self.hass.loop).result() + assert 1 == len(self.mock_mp_2.service_calls["toggle"]) + def test_service_call_to_command(self): """Test service call to command.""" config = copy(self.config_children_only) From 06c8fc6ef1f4895b23b3b83f8ea9d58cbae6b4d7 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sun, 14 Feb 2021 20:14:48 -0500 Subject: [PATCH 437/796] Use core constants for wemo and whois (#46548) --- homeassistant/components/whois/sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 7ec5c3dac5e..0e3c0c6e0da 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -6,14 +6,12 @@ import voluptuous as vol import whois from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, TIME_DAYS +from homeassistant.const import CONF_DOMAIN, CONF_NAME, TIME_DAYS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -CONF_DOMAIN = "domain" - DEFAULT_NAME = "Whois" ATTR_EXPIRES = "expires" From cfdaadf5d962f740e409b93b541c6e01fff331ea Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 14 Feb 2021 22:05:23 -0500 Subject: [PATCH 438/796] Allow users to set device class for universal media player (#46550) --- .../components/universal/media_player.py | 27 +++++++++++++++++-- .../components/universal/test_media_player.py | 22 +++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 2a5fcee34dc..e4891dca68a 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -1,9 +1,14 @@ """Combination of multiple media players for a universal controller.""" from copy import copy +from typing import Optional import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity +from homeassistant.components.media_player import ( + DEVICE_CLASSES_SCHEMA, + PLATFORM_SCHEMA, + MediaPlayerEntity, +) from homeassistant.components.media_player.const import ( ATTR_APP_ID, ATTR_APP_NAME, @@ -56,6 +61,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, ATTR_SUPPORTED_FEATURES, + CONF_DEVICE_CLASS, CONF_NAME, CONF_STATE, CONF_STATE_TEMPLATE, @@ -109,6 +115,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_ATTRS, default={}): vol.Or( cv.ensure_list(ATTRS_SCHEMA), ATTRS_SCHEMA ), + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_STATE_TEMPLATE): cv.template, }, extra=vol.REMOVE_EXTRA, @@ -126,6 +133,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= config.get(CONF_CHILDREN), config.get(CONF_COMMANDS), config.get(CONF_ATTRS), + config.get(CONF_DEVICE_CLASS), config.get(CONF_STATE_TEMPLATE), ) @@ -135,7 +143,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class UniversalMediaPlayer(MediaPlayerEntity): """Representation of an universal media player.""" - def __init__(self, hass, name, children, commands, attributes, state_template=None): + def __init__( + self, + hass, + name, + children, + commands, + attributes, + device_class=None, + state_template=None, + ): """Initialize the Universal media device.""" self.hass = hass self._name = name @@ -150,6 +167,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): self._child_state = None self._state_template_result = None self._state_template = state_template + self._device_class = device_class async def async_added_to_hass(self): """Subscribe to children and template state changes.""" @@ -255,6 +273,11 @@ class UniversalMediaPlayer(MediaPlayerEntity): """No polling needed.""" return False + @property + def device_class(self) -> Optional[str]: + """Return the class of this device.""" + return self._device_class + @property def master_state(self): """Return the master state for entity or None.""" diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 75cf029af40..8d8bc80234e 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -872,6 +872,25 @@ async def test_state_template(hass): assert hass.states.get("media_player.tv").state == STATE_OFF +async def test_device_class(hass): + """Test device_class property.""" + hass.states.async_set("sensor.test_sensor", "on") + + await async_setup_component( + hass, + "media_player", + { + "media_player": { + "platform": "universal", + "name": "tv", + "device_class": "tv", + } + }, + ) + await hass.async_block_till_done() + assert hass.states.get("media_player.tv").attributes["device_class"] == "tv" + + async def test_invalid_state_template(hass): """Test invalid state template sets state to None.""" hass.states.async_set("sensor.test_sensor", "on") @@ -1001,6 +1020,9 @@ async def test_reload(hass): assert hass.states.get("media_player.tv") is None assert hass.states.get("media_player.master_bed_tv").state == "on" assert hass.states.get("media_player.master_bed_tv").attributes["source"] == "act2" + assert ( + "device_class" not in hass.states.get("media_player.master_bed_tv").attributes + ) def _get_fixtures_base_path(): From 3f4828f5e1d1146cd7baeb783ea1ddd5f7e097d6 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 14 Feb 2021 20:32:37 -0800 Subject: [PATCH 439/796] Add additional stream HLS payload tests (#46517) * Add tests for HLS playlist view details * Add tests for hls playlist payloads * Update tests/components/stream/test_hls.py Co-authored-by: uvjustin <46082645+uvjustin@users.noreply.github.com> * Update tests/components/stream/test_hls.py Co-authored-by: uvjustin <46082645+uvjustin@users.noreply.github.com> * Update tests/components/stream/test_hls.py Co-authored-by: uvjustin <46082645+uvjustin@users.noreply.github.com> * Update tests/components/stream/test_hls.py Co-authored-by: uvjustin <46082645+uvjustin@users.noreply.github.com> * Update tests/components/stream/test_hls.py Co-authored-by: uvjustin <46082645+uvjustin@users.noreply.github.com> * Update tests/components/stream/test_hls.py Co-authored-by: uvjustin <46082645+uvjustin@users.noreply.github.com> * Update tests/components/stream/test_hls.py Co-authored-by: uvjustin <46082645+uvjustin@users.noreply.github.com> Co-authored-by: uvjustin <46082645+uvjustin@users.noreply.github.com> --- tests/components/stream/test_hls.py | 158 +++++++++++++++++++++++++--- 1 file changed, 146 insertions(+), 12 deletions(-) diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index b575b3877fa..7811cac2a2a 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -1,11 +1,15 @@ """The tests for hls streams.""" from datetime import timedelta +import io from unittest.mock import patch from urllib.parse import urlparse import av +import pytest from homeassistant.components.stream import create_stream +from homeassistant.components.stream.const import MAX_SEGMENTS, NUM_PLAYLIST_SEGMENTS +from homeassistant.components.stream.core import Segment from homeassistant.const import HTTP_NOT_FOUND from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -13,8 +17,61 @@ import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed from tests.components.stream.common import generate_h264_video +STREAM_SOURCE = "some-stream-source" +SEQUENCE_BYTES = io.BytesIO(b"some-bytes") +DURATION = 10 -async def test_hls_stream(hass, hass_client, stream_worker_sync): + +class HlsClient: + """Test fixture for fetching the hls stream.""" + + def __init__(self, http_client, parsed_url): + """Initialize HlsClient.""" + self.http_client = http_client + self.parsed_url = parsed_url + + async def get(self, path=None): + """Fetch the hls stream for the specified path.""" + url = self.parsed_url.path + if path: + # Strip off the master playlist suffix and replace with path + url = "/".join(self.parsed_url.path.split("/")[:-1]) + path + return await self.http_client.get(url) + + +@pytest.fixture +def hls_stream(hass, hass_client): + """Create test fixture for creating an HLS client for a stream.""" + + async def create_client_for_stream(stream): + http_client = await hass_client() + parsed_url = urlparse(stream.endpoint_url("hls")) + return HlsClient(http_client, parsed_url) + + return create_client_for_stream + + +def playlist_response(sequence, segments): + """Create a an hls playlist response for tests to assert on.""" + response = [ + "#EXTM3U", + "#EXT-X-VERSION:7", + "#EXT-X-TARGETDURATION:10", + '#EXT-X-MAP:URI="init.mp4"', + f"#EXT-X-MEDIA-SEQUENCE:{sequence}", + ] + for segment in segments: + response.extend( + [ + "#EXTINF:10.0000,", + f"./segment/{segment}.m4s", + ] + ) + response.append("") + return "\n".join(response) + + +async def test_hls_stream(hass, hls_stream, stream_worker_sync): """ Test hls stream. @@ -32,27 +89,22 @@ async def test_hls_stream(hass, hass_client, stream_worker_sync): # Request stream stream.add_provider("hls") stream.start() - url = stream.endpoint_url("hls") - http_client = await hass_client() + hls_client = await hls_stream(stream) # Fetch playlist - parsed_url = urlparse(url) - playlist_response = await http_client.get(parsed_url.path) + playlist_response = await hls_client.get() assert playlist_response.status == 200 # Fetch init playlist = await playlist_response.text() - playlist_url = "/".join(parsed_url.path.split("/")[:-1]) - init_url = playlist_url + "/init.mp4" - init_response = await http_client.get(init_url) + init_response = await hls_client.get("/init.mp4") assert init_response.status == 200 # Fetch segment playlist = await playlist_response.text() - playlist_url = "/".join(parsed_url.path.split("/")[:-1]) - segment_url = playlist_url + "/" + playlist.splitlines()[-1] - segment_response = await http_client.get(segment_url) + segment_url = "/" + playlist.splitlines()[-1] + segment_response = await hls_client.get(segment_url) assert segment_response.status == 200 stream_worker_sync.resume() @@ -61,7 +113,7 @@ async def test_hls_stream(hass, hass_client, stream_worker_sync): stream.stop() # Ensure playlist not accessible after stream ends - fail_response = await http_client.get(parsed_url.path) + fail_response = await hls_client.get() assert fail_response.status == HTTP_NOT_FOUND @@ -176,3 +228,85 @@ async def test_stream_keepalive(hass): # Stop stream, if it hasn't quit already stream.stop() + + +async def test_hls_playlist_view_no_output(hass, hass_client, hls_stream): + """Test rendering the hls playlist with no output segments.""" + await async_setup_component(hass, "stream", {"stream": {}}) + + stream = create_stream(hass, STREAM_SOURCE) + stream.add_provider("hls") + + hls_client = await hls_stream(stream) + + # Fetch playlist + resp = await hls_client.get("/playlist.m3u8") + assert resp.status == 404 + + +async def test_hls_playlist_view(hass, hls_stream, stream_worker_sync): + """Test rendering the hls playlist with 1 and 2 output segments.""" + await async_setup_component(hass, "stream", {"stream": {}}) + + stream = create_stream(hass, STREAM_SOURCE) + stream_worker_sync.pause() + hls = stream.add_provider("hls") + + hls.put(Segment(1, SEQUENCE_BYTES, DURATION)) + await hass.async_block_till_done() + + hls_client = await hls_stream(stream) + + resp = await hls_client.get("/playlist.m3u8") + assert resp.status == 200 + assert await resp.text() == playlist_response(sequence=1, segments=[1]) + + hls.put(Segment(2, SEQUENCE_BYTES, DURATION)) + await hass.async_block_till_done() + resp = await hls_client.get("/playlist.m3u8") + assert resp.status == 200 + assert await resp.text() == playlist_response(sequence=1, segments=[1, 2]) + + stream_worker_sync.resume() + stream.stop() + + +async def test_hls_max_segments(hass, hls_stream, stream_worker_sync): + """Test rendering the hls playlist with more segments than the segment deque can hold.""" + await async_setup_component(hass, "stream", {"stream": {}}) + + stream = create_stream(hass, STREAM_SOURCE) + stream_worker_sync.pause() + hls = stream.add_provider("hls") + + hls_client = await hls_stream(stream) + + # Produce enough segments to overfill the output buffer by one + for sequence in range(1, MAX_SEGMENTS + 2): + hls.put(Segment(sequence, SEQUENCE_BYTES, DURATION)) + await hass.async_block_till_done() + + resp = await hls_client.get("/playlist.m3u8") + assert resp.status == 200 + + # Only NUM_PLAYLIST_SEGMENTS are returned in the playlist. + start = MAX_SEGMENTS + 2 - NUM_PLAYLIST_SEGMENTS + assert await resp.text() == playlist_response( + sequence=start, segments=range(start, MAX_SEGMENTS + 2) + ) + + # Fetch the actual segments with a fake byte payload + with patch( + "homeassistant.components.stream.hls.get_m4s", return_value=b"fake-payload" + ): + # The segment that fell off the buffer is not accessible + segment_response = await hls_client.get("/segment/1.m4s") + assert segment_response.status == 404 + + # However all segments in the buffer are accessible, even those that were not in the playlist. + for sequence in range(2, MAX_SEGMENTS + 2): + segment_response = await hls_client.get(f"/segment/{sequence}.m4s") + assert segment_response.status == 200 + + stream_worker_sync.resume() + stream.stop() From 5a907ebafc41db485d03421ef487aa1b52196a43 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 15 Feb 2021 11:51:56 +0100 Subject: [PATCH 440/796] Remove @home-assistant/core from MQTT codeowners (#46562) --- CODEOWNERS | 2 +- homeassistant/components/mqtt/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index c6794a605d9..ba727de5694 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -286,7 +286,7 @@ homeassistant/components/monoprice/* @etsinko @OnFreund homeassistant/components/moon/* @fabaff homeassistant/components/motion_blinds/* @starkillerOG homeassistant/components/mpd/* @fabaff -homeassistant/components/mqtt/* @home-assistant/core @emontnemery +homeassistant/components/mqtt/* @emontnemery homeassistant/components/msteams/* @peroyvind homeassistant/components/my/* @home-assistant/core homeassistant/components/myq/* @bdraco diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json index 4d44090a4e3..9de3b071844 100644 --- a/homeassistant/components/mqtt/manifest.json +++ b/homeassistant/components/mqtt/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/mqtt", "requirements": ["paho-mqtt==1.5.1"], "dependencies": ["http"], - "codeowners": ["@home-assistant/core", "@emontnemery"] + "codeowners": ["@emontnemery"] } From c8e04ee9607da48a4860603907175aa772db4c5d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 15 Feb 2021 12:16:28 +0100 Subject: [PATCH 441/796] Bump hatasmota to 0.2.9 (#46561) --- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_light.py | 30 +++++++++---------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 12604d3ed81..17e72a57ce6 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.2.8"], + "requirements": ["hatasmota==0.2.9"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"] diff --git a/requirements_all.txt b/requirements_all.txt index 110f573bc90..342849b291c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ hass-nabucasa==0.41.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.2.8 +hatasmota==0.2.9 # homeassistant.components.jewish_calendar hdate==0.9.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b8e9858cad..7b49c63f3c5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -393,7 +393,7 @@ hangups==0.4.11 hass-nabucasa==0.41.0 # homeassistant.components.tasmota -hatasmota==0.2.8 +hatasmota==0.2.9 # 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 d64e39aacf0..a60f167c38f 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -924,7 +924,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): await common.async_turn_on(hass, "light.test", brightness=255, transition=4) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 1;NoDelay;Speed 8;NoDelay;Dimmer 100", + "NoDelay;Fade2 1;NoDelay;Speed2 8;NoDelay;Dimmer 100", 0, False, ) @@ -934,7 +934,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): 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", + "NoDelay;Fade2 1;NoDelay;Speed2 40;NoDelay;Dimmer 100", 0, False, ) @@ -944,7 +944,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): 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", + "NoDelay;Fade2 1;NoDelay;Speed2 1;NoDelay;Power1 OFF", 0, False, ) @@ -954,7 +954,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): await common.async_turn_on(hass, "light.test", brightness=128, transition=4) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 1;NoDelay;Speed 16;NoDelay;Dimmer 50", + "NoDelay;Fade2 1;NoDelay;Speed2 16;NoDelay;Dimmer 50", 0, False, ) @@ -972,7 +972,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): await common.async_turn_off(hass, "light.test", transition=6) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 1;NoDelay;Speed 24;NoDelay;Power1 OFF", + "NoDelay;Fade2 1;NoDelay;Speed2 24;NoDelay;Power1 OFF", 0, False, ) @@ -990,7 +990,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): await common.async_turn_off(hass, "light.test", transition=0) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 0;NoDelay;Power1 OFF", + "NoDelay;Fade2 0;NoDelay;Power1 OFF", 0, False, ) @@ -1011,7 +1011,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): await common.async_turn_on(hass, "light.test", rgb_color=[255, 0, 0], transition=6) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 1;NoDelay;Speed 24;NoDelay;Power1 ON;NoDelay;Color2 255,0,0", + "NoDelay;Fade2 1;NoDelay;Speed2 24;NoDelay;Power1 ON;NoDelay;Color2 255,0,0", 0, False, ) @@ -1032,7 +1032,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): await common.async_turn_on(hass, "light.test", rgb_color=[255, 0, 0], transition=6) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 1;NoDelay;Speed 12;NoDelay;Power1 ON;NoDelay;Color2 255,0,0", + "NoDelay;Fade2 1;NoDelay;Speed2 12;NoDelay;Power1 ON;NoDelay;Color2 255,0,0", 0, False, ) @@ -1051,7 +1051,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): await common.async_turn_on(hass, "light.test", color_temp=500, transition=6) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 1;NoDelay;Speed 24;NoDelay;Power1 ON;NoDelay;CT 500", + "NoDelay;Fade2 1;NoDelay;Speed2 24;NoDelay;Power1 ON;NoDelay;CT 500", 0, False, ) @@ -1070,7 +1070,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): await common.async_turn_on(hass, "light.test", color_temp=326, transition=6) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 1;NoDelay;Speed 40;NoDelay;Power1 ON;NoDelay;CT 326", + "NoDelay;Fade2 1;NoDelay;Speed2 40;NoDelay;Power1 ON;NoDelay;CT 326", 0, False, ) @@ -1103,7 +1103,7 @@ async def test_transition_fixed(hass, mqtt_mock, setup_tasmota): await common.async_turn_on(hass, "light.test", brightness=255, transition=4) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 1;NoDelay;Speed 8;NoDelay;Dimmer 100", + "NoDelay;Fade2 1;NoDelay;Speed2 8;NoDelay;Dimmer 100", 0, False, ) @@ -1113,7 +1113,7 @@ async def test_transition_fixed(hass, mqtt_mock, setup_tasmota): 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", + "NoDelay;Fade2 1;NoDelay;Speed2 40;NoDelay;Dimmer 100", 0, False, ) @@ -1123,7 +1123,7 @@ async def test_transition_fixed(hass, mqtt_mock, setup_tasmota): await common.async_turn_on(hass, "light.test", brightness=0, transition=4) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 1;NoDelay;Speed 8;NoDelay;Power1 OFF", + "NoDelay;Fade2 1;NoDelay;Speed2 8;NoDelay;Power1 OFF", 0, False, ) @@ -1133,7 +1133,7 @@ async def test_transition_fixed(hass, mqtt_mock, setup_tasmota): await common.async_turn_on(hass, "light.test", brightness=128, transition=4) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 1;NoDelay;Speed 8;NoDelay;Dimmer 50", + "NoDelay;Fade2 1;NoDelay;Speed2 8;NoDelay;Dimmer 50", 0, False, ) @@ -1143,7 +1143,7 @@ async def test_transition_fixed(hass, mqtt_mock, setup_tasmota): await common.async_turn_on(hass, "light.test", brightness=128, transition=0) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 0;NoDelay;Dimmer 50", + "NoDelay;Fade2 0;NoDelay;Dimmer 50", 0, False, ) From bed29fd4b1c77f53029f251430b559b9d3e5600e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Pol=C3=ADvka?= Date: Mon, 15 Feb 2021 12:33:13 +0100 Subject: [PATCH 442/796] Convert better from byte value to percentage in futurenow (#45042) Co-authored-by: Franck Nijhof --- homeassistant/components/futurenow/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/futurenow/light.py b/homeassistant/components/futurenow/light.py index 6b05c96416f..04a731a7272 100644 --- a/homeassistant/components/futurenow/light.py +++ b/homeassistant/components/futurenow/light.py @@ -51,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def to_futurenow_level(level): """Convert the given Home Assistant light level (0-255) to FutureNow (0-100).""" - return int((level * 100) / 255) + return round((level * 100) / 255) def to_hass_level(level): From c5b9ad83c2a8f363a3f912f542ce966034ee1274 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Feb 2021 01:39:51 -1000 Subject: [PATCH 443/796] Log ffmpeg errors for homekit cameras (#46545) --- .../components/homekit/type_cameras.py | 27 +++++++++++++++++-- tests/components/homekit/test_type_cameras.py | 9 +++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index 22bf37aa0c3..0a499bf5d24 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -1,8 +1,9 @@ """Class to hold all camera accessories.""" +import asyncio from datetime import timedelta import logging -from haffmpeg.core import HAFFmpeg +from haffmpeg.core import FFMPEG_STDERR, HAFFmpeg from pyhap.camera import ( VIDEO_CODEC_PARAM_LEVEL_TYPES, VIDEO_CODEC_PARAM_PROFILE_ID_TYPES, @@ -114,6 +115,7 @@ RESOLUTIONS = [ VIDEO_PROFILE_NAMES = ["baseline", "main", "high"] FFMPEG_WATCH_INTERVAL = timedelta(seconds=5) +FFMPEG_LOGGER = "ffmpeg_logger" FFMPEG_WATCHER = "ffmpeg_watcher" FFMPEG_PID = "ffmpeg_pid" SESSION_ID = "session_id" @@ -371,7 +373,12 @@ class Camera(HomeAccessory, PyhapCamera): _LOGGER.debug("FFmpeg output settings: %s", output) stream = HAFFmpeg(self._ffmpeg.binary) opened = await stream.open( - cmd=[], input_source=input_source, output=output, stdout_pipe=False + cmd=[], + input_source=input_source, + output=output, + extra_cmd="-hide_banner -nostats", + stderr_pipe=True, + stdout_pipe=False, ) if not opened: _LOGGER.error("Failed to open ffmpeg stream") @@ -386,9 +393,14 @@ class Camera(HomeAccessory, PyhapCamera): session_info["stream"] = stream session_info[FFMPEG_PID] = stream.process.pid + stderr_reader = await stream.get_reader(source=FFMPEG_STDERR) + async def watch_session(_): await self._async_ffmpeg_watch(session_info["id"]) + session_info[FFMPEG_LOGGER] = asyncio.create_task( + self._async_log_stderr_stream(stderr_reader) + ) session_info[FFMPEG_WATCHER] = async_track_time_interval( self.hass, watch_session, @@ -397,6 +409,16 @@ class Camera(HomeAccessory, PyhapCamera): return await self._async_ffmpeg_watch(session_info["id"]) + async def _async_log_stderr_stream(self, stderr_reader): + """Log output from ffmpeg.""" + _LOGGER.debug("%s: ffmpeg: started", self.display_name) + while True: + line = await stderr_reader.readline() + if line == b"": + return + + _LOGGER.debug("%s: ffmpeg: %s", self.display_name, line.rstrip()) + async def _async_ffmpeg_watch(self, session_id): """Check to make sure ffmpeg is still running and cleanup if not.""" ffmpeg_pid = self.sessions[session_id][FFMPEG_PID] @@ -414,6 +436,7 @@ class Camera(HomeAccessory, PyhapCamera): if FFMPEG_WATCHER not in self.sessions[session_id]: return self.sessions[session_id].pop(FFMPEG_WATCHER)() + self.sessions[session_id].pop(FFMPEG_LOGGER).cancel() async def stop_stream(self, session_info): """Stop the stream for the given ``session_id``.""" diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index f4c7169310f..c9b2ebc422c 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -99,6 +99,7 @@ def _get_exits_after_startup_mock_ffmpeg(): ffmpeg.open = AsyncMock(return_value=True) ffmpeg.close = AsyncMock(return_value=True) ffmpeg.kill = AsyncMock(return_value=True) + ffmpeg.get_reader = AsyncMock() return ffmpeg @@ -108,6 +109,7 @@ def _get_working_mock_ffmpeg(): ffmpeg.open = AsyncMock(return_value=True) ffmpeg.close = AsyncMock(return_value=True) ffmpeg.kill = AsyncMock(return_value=True) + ffmpeg.get_reader = AsyncMock() return ffmpeg @@ -118,6 +120,7 @@ def _get_failing_mock_ffmpeg(): ffmpeg.open = AsyncMock(return_value=False) ffmpeg.close = AsyncMock(side_effect=OSError) ffmpeg.kill = AsyncMock(side_effect=OSError) + ffmpeg.get_reader = AsyncMock() return ffmpeg @@ -189,6 +192,8 @@ async def test_camera_stream_source_configured(hass, run_driver, events): input_source="-i /dev/null", output=expected_output.format(**session_info), stdout_pipe=False, + extra_cmd="-hide_banner -nostats", + stderr_pipe=True, ) await _async_setup_endpoints(hass, acc) @@ -472,6 +477,8 @@ async def test_camera_stream_source_configured_and_copy_codec(hass, run_driver, input_source="-i /dev/null", output=expected_output.format(**session_info), stdout_pipe=False, + extra_cmd="-hide_banner -nostats", + stderr_pipe=True, ) @@ -542,6 +549,8 @@ async def test_camera_streaming_fails_after_starting_ffmpeg(hass, run_driver, ev input_source="-i /dev/null", output=expected_output.format(**session_info), stdout_pipe=False, + extra_cmd="-hide_banner -nostats", + stderr_pipe=True, ) From 9917bb76fbaed50d57910a167f11ffe1b465cee6 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Mon, 15 Feb 2021 23:37:53 +0800 Subject: [PATCH 444/796] Use httpx in generic camera (#46576) * Use httpx in generic camera * Remove commented out code --- homeassistant/components/generic/camera.py | 59 +++++++--------------- tests/components/generic/test_camera.py | 50 ++++++++++-------- 2 files changed, 46 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 2e798b8cc4b..28db66b4f3e 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -2,10 +2,7 @@ import asyncio import logging -import aiohttp -import async_timeout -import requests -from requests.auth import HTTPDigestAuth +import httpx import voluptuous as vol from homeassistant.components.camera import ( @@ -25,7 +22,7 @@ from homeassistant.const import ( ) from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.reload import async_setup_reload_service from . import DOMAIN, PLATFORMS @@ -39,6 +36,7 @@ CONF_STREAM_SOURCE = "stream_source" CONF_FRAMERATE = "framerate" DEFAULT_NAME = "Generic Camera" +GET_IMAGE_TIMEOUT = 10 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -93,9 +91,9 @@ class GenericCamera(Camera): if username and password: if self._authentication == HTTP_DIGEST_AUTHENTICATION: - self._auth = HTTPDigestAuth(username, password) + self._auth = httpx.DigestAuth(username, password) else: - self._auth = aiohttp.BasicAuth(username, password=password) + self._auth = httpx.BasicAuth(username, password=password) else: self._auth = None @@ -129,40 +127,19 @@ class GenericCamera(Camera): if url == self._last_url and self._limit_refetch: return self._last_image - # aiohttp don't support DigestAuth yet - if self._authentication == HTTP_DIGEST_AUTHENTICATION: - - def fetch(): - """Read image from a URL.""" - try: - response = requests.get( - url, timeout=10, auth=self._auth, verify=self.verify_ssl - ) - return response.content - except requests.exceptions.RequestException as error: - _LOGGER.error( - "Error getting new camera image from %s: %s", self._name, error - ) - return self._last_image - - self._last_image = await self.hass.async_add_executor_job(fetch) - # async - else: - try: - websession = async_get_clientsession( - self.hass, verify_ssl=self.verify_ssl - ) - with async_timeout.timeout(10): - response = await websession.get(url, auth=self._auth) - self._last_image = await response.read() - except asyncio.TimeoutError: - _LOGGER.error("Timeout getting camera image from %s", self._name) - return self._last_image - except aiohttp.ClientError as err: - _LOGGER.error( - "Error getting new camera image from %s: %s", self._name, err - ) - return self._last_image + try: + async_client = get_async_client(self.hass, verify_ssl=self.verify_ssl) + response = await async_client.get( + url, auth=self._auth, timeout=GET_IMAGE_TIMEOUT + ) + response.raise_for_status() + self._last_image = response.content + except httpx.TimeoutException: + _LOGGER.error("Timeout getting camera image from %s", self._name) + return self._last_image + except (httpx.RequestError, httpx.HTTPStatusError) as err: + _LOGGER.error("Error getting new camera image from %s: %s", self._name, err) + return self._last_image self._last_url = url return self._last_image diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index 65f5306c4d8..7f5b3bb3b53 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -3,6 +3,8 @@ import asyncio from os import path from unittest.mock import patch +import respx + from homeassistant import config as hass_config from homeassistant.components.generic import DOMAIN from homeassistant.components.websocket_api.const import TYPE_RESULT @@ -14,9 +16,10 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component -async def test_fetching_url(aioclient_mock, hass, hass_client): +@respx.mock +async def test_fetching_url(hass, hass_client): """Test that it fetches the given url.""" - aioclient_mock.get("http://example.com", text="hello world") + respx.get("http://example.com").respond(text="hello world") await async_setup_component( hass, @@ -38,12 +41,12 @@ async def test_fetching_url(aioclient_mock, hass, hass_client): resp = await client.get("/api/camera_proxy/camera.config_test") assert resp.status == 200 - assert aioclient_mock.call_count == 1 + assert respx.calls.call_count == 1 body = await resp.text() assert body == "hello world" resp = await client.get("/api/camera_proxy/camera.config_test") - assert aioclient_mock.call_count == 2 + assert respx.calls.call_count == 2 async def test_fetching_without_verify_ssl(aioclient_mock, hass, hass_client): @@ -100,12 +103,13 @@ async def test_fetching_url_with_verify_ssl(aioclient_mock, hass, hass_client): assert resp.status == 200 -async def test_limit_refetch(aioclient_mock, hass, hass_client): +@respx.mock +async def test_limit_refetch(hass, hass_client): """Test that it fetches the given url.""" - aioclient_mock.get("http://example.com/5a", text="hello world") - aioclient_mock.get("http://example.com/10a", text="hello world") - aioclient_mock.get("http://example.com/15a", text="hello planet") - aioclient_mock.get("http://example.com/20a", status=HTTP_NOT_FOUND) + respx.get("http://example.com/5a").respond(text="hello world") + respx.get("http://example.com/10a").respond(text="hello world") + respx.get("http://example.com/15a").respond(text="hello planet") + respx.get("http://example.com/20a").respond(status_code=HTTP_NOT_FOUND) await async_setup_component( hass, @@ -129,19 +133,19 @@ async def test_limit_refetch(aioclient_mock, hass, hass_client): with patch("async_timeout.timeout", side_effect=asyncio.TimeoutError()): resp = await client.get("/api/camera_proxy/camera.config_test") - assert aioclient_mock.call_count == 0 + assert respx.calls.call_count == 0 assert resp.status == HTTP_INTERNAL_SERVER_ERROR hass.states.async_set("sensor.temp", "10") resp = await client.get("/api/camera_proxy/camera.config_test") - assert aioclient_mock.call_count == 1 + assert respx.calls.call_count == 1 assert resp.status == 200 body = await resp.text() assert body == "hello world" resp = await client.get("/api/camera_proxy/camera.config_test") - assert aioclient_mock.call_count == 1 + assert respx.calls.call_count == 1 assert resp.status == 200 body = await resp.text() assert body == "hello world" @@ -150,7 +154,7 @@ async def test_limit_refetch(aioclient_mock, hass, hass_client): # Url change = fetch new image resp = await client.get("/api/camera_proxy/camera.config_test") - assert aioclient_mock.call_count == 2 + assert respx.calls.call_count == 2 assert resp.status == 200 body = await resp.text() assert body == "hello planet" @@ -158,7 +162,7 @@ async def test_limit_refetch(aioclient_mock, hass, hass_client): # Cause a template render error hass.states.async_remove("sensor.temp") resp = await client.get("/api/camera_proxy/camera.config_test") - assert aioclient_mock.call_count == 2 + assert respx.calls.call_count == 2 assert resp.status == 200 body = await resp.text() assert body == "hello planet" @@ -285,11 +289,12 @@ async def test_no_stream_source(aioclient_mock, hass, hass_client, hass_ws_clien } -async def test_camera_content_type(aioclient_mock, hass, hass_client): +@respx.mock +async def test_camera_content_type(hass, hass_client): """Test generic camera with custom content_type.""" svg_image = "" urlsvg = "https://upload.wikimedia.org/wikipedia/commons/0/02/SVG_logo.svg" - aioclient_mock.get(urlsvg, text=svg_image) + respx.get(urlsvg).respond(text=svg_image) cam_config_svg = { "name": "config_test_svg", @@ -309,23 +314,24 @@ async def test_camera_content_type(aioclient_mock, hass, hass_client): client = await hass_client() resp_1 = await client.get("/api/camera_proxy/camera.config_test_svg") - assert aioclient_mock.call_count == 1 + assert respx.calls.call_count == 1 assert resp_1.status == 200 assert resp_1.content_type == "image/svg+xml" body = await resp_1.text() assert body == svg_image resp_2 = await client.get("/api/camera_proxy/camera.config_test_jpg") - assert aioclient_mock.call_count == 2 + assert respx.calls.call_count == 2 assert resp_2.status == 200 assert resp_2.content_type == "image/jpeg" body = await resp_2.text() assert body == svg_image -async def test_reloading(aioclient_mock, hass, hass_client): +@respx.mock +async def test_reloading(hass, hass_client): """Test we can cleanly reload.""" - aioclient_mock.get("http://example.com", text="hello world") + respx.get("http://example.com").respond(text="hello world") await async_setup_component( hass, @@ -347,7 +353,7 @@ async def test_reloading(aioclient_mock, hass, hass_client): resp = await client.get("/api/camera_proxy/camera.config_test") assert resp.status == 200 - assert aioclient_mock.call_count == 1 + assert respx.calls.call_count == 1 body = await resp.text() assert body == "hello world" @@ -374,7 +380,7 @@ async def test_reloading(aioclient_mock, hass, hass_client): resp = await client.get("/api/camera_proxy/camera.reload") assert resp.status == 200 - assert aioclient_mock.call_count == 2 + assert respx.calls.call_count == 2 body = await resp.text() assert body == "hello world" From a5d943b5f15c20b4c4f01bfdb412f18c297032be Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 15 Feb 2021 18:33:42 +0200 Subject: [PATCH 445/796] MQTT cover Bugfixes (#46479) * MQTT cover Bugfixes * Remove period --- homeassistant/components/mqtt/cover.py | 44 +++++++------ tests/components/mqtt/test_cover.py | 89 +++++++++++++------------- 2 files changed, 67 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 54ef6cfb539..7b7f983b1e4 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -129,14 +129,14 @@ def validate_options(value): ): _LOGGER.warning( "using 'value_template' for 'position_topic' is deprecated " - "and will be removed from Home Assistant in version 2021.6" + "and will be removed from Home Assistant in version 2021.6, " "please replace it with 'position_template'" ) if CONF_TILT_INVERT_STATE in value: _LOGGER.warning( "'tilt_invert_state' is deprecated " - "and will be removed from Home Assistant in version 2021.6" + "and will be removed from Home Assistant in version 2021.6, " "please invert tilt using 'tilt_min' & 'tilt_max'" ) @@ -172,9 +172,7 @@ PLATFORM_SCHEMA = vol.All( CONF_TILT_CLOSED_POSITION, default=DEFAULT_TILT_CLOSED_POSITION ): int, vol.Optional(CONF_TILT_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional( - CONF_TILT_INVERT_STATE, default=DEFAULT_TILT_INVERT_STATE - ): cv.boolean, + vol.Optional(CONF_TILT_INVERT_STATE): cv.boolean, vol.Optional(CONF_TILT_MAX, default=DEFAULT_TILT_MAX): int, vol.Optional(CONF_TILT_MIN, default=DEFAULT_TILT_MIN): int, vol.Optional( @@ -247,15 +245,22 @@ class MqttCover(MqttEntity, CoverEntity): ) self._tilt_optimistic = config[CONF_TILT_STATE_OPTIMISTIC] - template = self._config.get(CONF_VALUE_TEMPLATE) - if template is not None: - template.hass = self.hass + value_template = self._config.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + value_template.hass = self.hass + set_position_template = self._config.get(CONF_SET_POSITION_TEMPLATE) if set_position_template is not None: set_position_template.hass = self.hass + + get_position_template = self._config.get(CONF_GET_POSITION_TEMPLATE) + if get_position_template is not None: + get_position_template.hass = self.hass + set_tilt_template = self._config.get(CONF_TILT_COMMAND_TEMPLATE) if set_tilt_template is not None: set_tilt_template.hass = self.hass + tilt_status_template = self._config.get(CONF_TILT_STATUS_TEMPLATE) if tilt_status_template is not None: tilt_status_template.hass = self.hass @@ -290,24 +295,21 @@ class MqttCover(MqttEntity, CoverEntity): def state_message_received(msg): """Handle new MQTT state messages.""" payload = msg.payload - template = self._config.get(CONF_VALUE_TEMPLATE) - if template is not None: - payload = template.async_render_with_possible_json_value(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_STATE_STOPPED]: - if ( - self._optimistic - or self._config.get(CONF_GET_POSITION_TOPIC) is None - ): - self._state = ( - STATE_CLOSED if self._state == STATE_CLOSING else STATE_OPEN - ) - else: + if self._config.get(CONF_GET_POSITION_TOPIC) is not None: self._state = ( STATE_CLOSED if self._position == DEFAULT_POSITION_CLOSED else STATE_OPEN ) + else: + self._state = ( + STATE_CLOSED if self._state == STATE_CLOSING else STATE_OPEN + ) elif payload == self._config[CONF_STATE_OPENING]: self._state = STATE_OPENING elif payload == self._config[CONF_STATE_CLOSING]: @@ -616,7 +618,7 @@ class MqttCover(MqttEntity, CoverEntity): max_percent = 100 min_percent = 0 position_percentage = min(max(position_percentage, min_percent), max_percent) - if range_type == TILT_PAYLOAD and self._config[CONF_TILT_INVERT_STATE]: + if range_type == TILT_PAYLOAD and self._config.get(CONF_TILT_INVERT_STATE): return 100 - position_percentage return position_percentage @@ -640,6 +642,6 @@ class MqttCover(MqttEntity, CoverEntity): position = round(current_range * (percentage / 100.0)) position += offset - if range_type == TILT_PAYLOAD and self._config[CONF_TILT_INVERT_STATE]: + if range_type == TILT_PAYLOAD and self._config.get(CONF_TILT_INVERT_STATE): position = max_range - position + offset return position diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 87b016e2d59..44144642f40 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -2032,10 +2032,10 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) -async def test_deprecated_value_template_for_position_topic_warnning( +async def test_deprecated_value_template_for_position_topic_warning( hass, caplog, mqtt_mock ): - """Test warnning when value_template is used for position_topic.""" + """Test warning when value_template is used for position_topic.""" assert await async_setup_component( hass, cover.DOMAIN, @@ -2054,13 +2054,13 @@ async def test_deprecated_value_template_for_position_topic_warnning( assert ( "using 'value_template' for 'position_topic' is deprecated " - "and will be removed from Home Assistant in version 2021.6" + "and will be removed from Home Assistant in version 2021.6, " "please replace it with 'position_template'" ) in caplog.text -async def test_deprecated_tilt_invert_state_warnning(hass, caplog, mqtt_mock): - """Test warnning when tilt_invert_state is used.""" +async def test_deprecated_tilt_invert_state_warning(hass, caplog, mqtt_mock): + """Test warning when tilt_invert_state is used.""" assert await async_setup_component( hass, cover.DOMAIN, @@ -2077,11 +2077,33 @@ async def test_deprecated_tilt_invert_state_warnning(hass, caplog, mqtt_mock): assert ( "'tilt_invert_state' is deprecated " - "and will be removed from Home Assistant in version 2021.6" + "and will be removed from Home Assistant in version 2021.6, " "please invert tilt using 'tilt_min' & 'tilt_max'" ) in caplog.text +async def test_no_deprecated_tilt_invert_state_warning(hass, caplog, mqtt_mock): + """Test warning when tilt_invert_state is used.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + } + }, + ) + await hass.async_block_till_done() + + assert ( + "'tilt_invert_state' is deprecated " + "and will be removed from Home Assistant in version 2021.6, " + "please invert tilt using 'tilt_min' & 'tilt_max'" + ) not in caplog.text + + async def test_no_deprecated_warning_for_position_topic_using_position_template( hass, caplog, mqtt_mock ): @@ -2104,7 +2126,7 @@ async def test_no_deprecated_warning_for_position_topic_using_position_template( assert ( "using 'value_template' for 'position_topic' is deprecated " - "and will be removed from Home Assistant in version 2021.6" + "and will be removed from Home Assistant in version 2021.6, " "please replace it with 'position_template'" ) not in caplog.text @@ -2221,8 +2243,8 @@ async def test_set_state_via_position_using_stopped_state(hass, mqtt_mock): assert state.state == STATE_OPEN -async def test_set_state_via_stopped_state_optimistic(hass, mqtt_mock): - """Test the controlling state via stopped state in optimistic mode.""" +async def test_position_via_position_topic_template(hass, mqtt_mock): + """Test position by updating status via position template.""" assert await async_setup_component( hass, cover.DOMAIN, @@ -2231,51 +2253,28 @@ async def test_set_state_via_stopped_state_optimistic(hass, mqtt_mock): "platform": "mqtt", "name": "test", "state_topic": "state-topic", - "position_topic": "get-position-topic", - "position_open": 100, - "position_closed": 0, - "state_open": "OPEN", - "state_closed": "CLOSE", - "state_stopped": "STOPPED", - "state_opening": "OPENING", - "state_closing": "CLOSING", "command_topic": "command-topic", - "qos": 0, - "optimistic": True, + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": "{{ (value | multiply(0.01)) | int }}", } }, ) await hass.async_block_till_done() - async_fire_mqtt_message(hass, "state-topic", "OPEN") + async_fire_mqtt_message(hass, "get-position-topic", "99") - state = hass.states.get("cover.test") - assert state.state == STATE_OPEN + current_cover_position_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_POSITION + ] + assert current_cover_position_position == 0 - async_fire_mqtt_message(hass, "get-position-topic", "50") + async_fire_mqtt_message(hass, "get-position-topic", "5000") - state = hass.states.get("cover.test") - assert state.state == STATE_OPEN - - async_fire_mqtt_message(hass, "state-topic", "OPENING") - - state = hass.states.get("cover.test") - assert state.state == STATE_OPENING - - async_fire_mqtt_message(hass, "state-topic", "STOPPED") - - state = hass.states.get("cover.test") - assert state.state == STATE_OPEN - - async_fire_mqtt_message(hass, "state-topic", "CLOSING") - - state = hass.states.get("cover.test") - assert state.state == STATE_CLOSING - - async_fire_mqtt_message(hass, "state-topic", "STOPPED") - - state = hass.states.get("cover.test") - assert state.state == STATE_CLOSED + current_cover_position_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_POSITION + ] + assert current_cover_position_position == 50 async def test_set_state_via_stopped_state_no_position_topic(hass, mqtt_mock): From 886067a32713e18fa445342c16175b28e2d38cbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 15 Feb 2021 18:18:45 +0100 Subject: [PATCH 446/796] Add websocket handlers to hassio (#46571) --- homeassistant/components/hassio/__init__.py | 27 +++--- homeassistant/components/hassio/const.py | 45 +++++++--- .../components/hassio/websocket_api.py | 84 +++++++++++++++++++ tests/components/hassio/test_init.py | 68 +++++++++++++++ 4 files changed, 202 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/hassio/websocket_api.py diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index e8b874b2334..1c246ae753b 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -10,7 +10,6 @@ from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components.homeassistant import SERVICE_CHECK_CONFIG import homeassistant.config as conf_util from homeassistant.const import ( - ATTR_NAME, EVENT_CORE_CONFIG_UPDATE, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, @@ -24,15 +23,27 @@ from homeassistant.util.dt import utcnow from .addon_panel import async_setup_addon_panel from .auth import async_setup_auth_view -from .const import ATTR_DISCOVERY +from .const import ( + ATTR_ADDON, + ATTR_ADDONS, + ATTR_DISCOVERY, + ATTR_FOLDERS, + ATTR_HOMEASSISTANT, + ATTR_INPUT, + ATTR_NAME, + ATTR_PASSWORD, + ATTR_SNAPSHOT, + DOMAIN, +) from .discovery import async_setup_discovery_view from .handler import HassIO, HassioAPIError, api_data from .http import HassIOView from .ingress import async_setup_ingress_view +from .websocket_api import async_load_websocket_api _LOGGER = logging.getLogger(__name__) -DOMAIN = "hassio" + STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 @@ -62,13 +73,6 @@ SERVICE_SNAPSHOT_PARTIAL = "snapshot_partial" SERVICE_RESTORE_FULL = "restore_full" SERVICE_RESTORE_PARTIAL = "restore_partial" -ATTR_ADDON = "addon" -ATTR_INPUT = "input" -ATTR_SNAPSHOT = "snapshot" -ATTR_ADDONS = "addons" -ATTR_FOLDERS = "folders" -ATTR_HOMEASSISTANT = "homeassistant" -ATTR_PASSWORD = "password" SCHEMA_NO_DATA = vol.Schema({}) @@ -101,6 +105,7 @@ SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend( } ) + MAP_SERVICE_API = { SERVICE_ADDON_START: ("/addons/{addon}/start", SCHEMA_ADDON, 60, False), SERVICE_ADDON_STOP: ("/addons/{addon}/stop", SCHEMA_ADDON, 60, False), @@ -290,6 +295,8 @@ async def async_setup(hass, config): _LOGGER.error("Missing %s environment variable", env) return False + async_load_websocket_api(hass) + host = os.environ["HASSIO"] websession = hass.helpers.aiohttp_client.async_get_clientsession() hass.data[DOMAIN] = hassio = HassIO(hass.loop, websession, host) diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index ffccb325395..00893f83401 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -1,21 +1,42 @@ """Hass.io const variables.""" -ATTR_ADDONS = "addons" -ATTR_DISCOVERY = "discovery" +DOMAIN = "hassio" + ATTR_ADDON = "addon" -ATTR_NAME = "name" -ATTR_SERVICE = "service" -ATTR_CONFIG = "config" -ATTR_UUID = "uuid" -ATTR_USERNAME = "username" -ATTR_PASSWORD = "password" -ATTR_PANELS = "panels" -ATTR_ENABLE = "enable" -ATTR_TITLE = "title" -ATTR_ICON = "icon" +ATTR_ADDONS = "addons" ATTR_ADMIN = "admin" +ATTR_CONFIG = "config" +ATTR_DATA = "data" +ATTR_DISCOVERY = "discovery" +ATTR_ENABLE = "enable" +ATTR_FOLDERS = "folders" +ATTR_HOMEASSISTANT = "homeassistant" +ATTR_ICON = "icon" +ATTR_INPUT = "input" +ATTR_NAME = "name" +ATTR_PANELS = "panels" +ATTR_PASSWORD = "password" +ATTR_SERVICE = "service" +ATTR_SNAPSHOT = "snapshot" +ATTR_TITLE = "title" +ATTR_USERNAME = "username" +ATTR_UUID = "uuid" +ATTR_WS_EVENT = "event" +ATTR_ENDPOINT = "endpoint" +ATTR_METHOD = "method" +ATTR_TIMEOUT = "timeout" + X_HASSIO = "X-Hassio-Key" X_INGRESS_PATH = "X-Ingress-Path" X_HASS_USER_ID = "X-Hass-User-ID" X_HASS_IS_ADMIN = "X-Hass-Is-Admin" + + +WS_TYPE = "type" +WS_ID = "id" + +WS_TYPE_EVENT = "supervisor/event" +WS_TYPE_API = "supervisor/api" + +EVENT_SUPERVISOR_EVENT = "supervisor_event" diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py new file mode 100644 index 00000000000..851404b4b0e --- /dev/null +++ b/homeassistant/components/hassio/websocket_api.py @@ -0,0 +1,84 @@ +"""Websocekt API handlers for the hassio integration.""" +import logging + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.components.websocket_api.connection import ActiveConnection +from homeassistant.core import HomeAssistant, callback +import homeassistant.helpers.config_validation as cv + +from .const import ( + ATTR_DATA, + ATTR_ENDPOINT, + ATTR_METHOD, + ATTR_TIMEOUT, + ATTR_WS_EVENT, + DOMAIN, + EVENT_SUPERVISOR_EVENT, + WS_ID, + WS_TYPE, + WS_TYPE_API, + WS_TYPE_EVENT, +) +from .handler import HassIO + +SCHEMA_WEBSOCKET_EVENT = vol.Schema( + {vol.Required(ATTR_WS_EVENT): cv.string}, + extra=vol.ALLOW_EXTRA, +) + +_LOGGER: logging.Logger = logging.getLogger(__package__) + + +@callback +def async_load_websocket_api(hass: HomeAssistant): + """Set up the websocket API.""" + websocket_api.async_register_command(hass, websocket_supervisor_event) + websocket_api.async_register_command(hass, websocket_supervisor_api) + + +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(WS_TYPE): WS_TYPE_EVENT, + vol.Required(ATTR_DATA): SCHEMA_WEBSOCKET_EVENT, + } +) +async def websocket_supervisor_event( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +): + """Publish events from the Supervisor.""" + hass.bus.async_fire(EVENT_SUPERVISOR_EVENT, msg[ATTR_DATA]) + connection.send_result(msg[WS_ID]) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(WS_TYPE): WS_TYPE_API, + vol.Required(ATTR_ENDPOINT): cv.string, + vol.Required(ATTR_METHOD): cv.string, + vol.Optional(ATTR_DATA): dict, + vol.Optional(ATTR_TIMEOUT): cv.string, + } +) +async def websocket_supervisor_api( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +): + """Websocket handler to call Supervisor API.""" + supervisor: HassIO = hass.data[DOMAIN] + result = False + try: + result = await supervisor.send_command( + msg[ATTR_ENDPOINT], + method=msg[ATTR_METHOD], + timeout=msg.get(ATTR_TIMEOUT, 10), + payload=msg.get(ATTR_DATA, {}), + ) + except hass.components.hassio.HassioAPIError as err: + _LOGGER.error("Failed to to call %s - %s", msg[ATTR_ENDPOINT], err) + connection.send_error(msg[WS_ID], err) + else: + connection.send_result(msg[WS_ID], result[ATTR_DATA]) diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 7ed24dca457..eaeed74fbf7 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -7,8 +7,21 @@ import pytest from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components import frontend from homeassistant.components.hassio import STORAGE_KEY +from homeassistant.components.hassio.const import ( + ATTR_DATA, + ATTR_ENDPOINT, + ATTR_METHOD, + EVENT_SUPERVISOR_EVENT, + WS_ID, + WS_TYPE, + WS_TYPE_API, + WS_TYPE_EVENT, +) +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import async_capture_events + MOCK_ENVIRON = {"HASSIO": "127.0.0.1", "HASSIO_TOKEN": "abcdefgh"} @@ -346,3 +359,58 @@ async def test_service_calls_core(hassio_env, hass, aioclient_mock): assert mock_check_config.called assert aioclient_mock.call_count == 5 + + +async def test_websocket_supervisor_event( + hassio_env, hass: HomeAssistant, hass_ws_client +): + """Test Supervisor websocket event.""" + assert await async_setup_component(hass, "hassio", {}) + websocket_client = await hass_ws_client(hass) + + test_event = async_capture_events(hass, EVENT_SUPERVISOR_EVENT) + + await websocket_client.send_json( + {WS_ID: 1, WS_TYPE: WS_TYPE_EVENT, ATTR_DATA: {"event": "test"}} + ) + + assert await websocket_client.receive_json() + await hass.async_block_till_done() + + assert test_event[0].data == {"event": "test"} + + +async def test_websocket_supervisor_api( + hassio_env, hass: HomeAssistant, hass_ws_client, aioclient_mock +): + """Test Supervisor websocket api.""" + assert await async_setup_component(hass, "hassio", {}) + websocket_client = await hass_ws_client(hass) + aioclient_mock.post( + "http://127.0.0.1/snapshots/new/partial", + json={"result": "ok", "data": {"slug": "sn_slug"}}, + ) + + await websocket_client.send_json( + { + WS_ID: 1, + WS_TYPE: WS_TYPE_API, + ATTR_ENDPOINT: "/snapshots/new/partial", + ATTR_METHOD: "post", + } + ) + + msg = await websocket_client.receive_json() + assert msg["result"]["slug"] == "sn_slug" + + await websocket_client.send_json( + { + WS_ID: 2, + WS_TYPE: WS_TYPE_API, + ATTR_ENDPOINT: "/supervisor/info", + ATTR_METHOD: "get", + } + ) + + msg = await websocket_client.receive_json() + assert msg["result"]["version_latest"] == "1.0.0" From f2ca4acff0e19357a703dfe982ab0e72b5d12c1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Mon, 15 Feb 2021 18:28:28 +0100 Subject: [PATCH 447/796] Limit fronius error messages on failed connection (#45824) * Do not print several error messages on failed connection * Change wrapping of exception, implement available * Simplify exception flow * Remove unnecessary init * Add available property to actual entities * Rebase and formatting --- homeassistant/components/fronius/sensor.py | 28 ++++++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 130a8d55072..02ff760e574 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -2,6 +2,7 @@ import copy from datetime import timedelta import logging +from typing import Dict from pyfronius import Fronius import voluptuous as vol @@ -130,6 +131,7 @@ class FroniusAdapter: self._name = name self._device = device self._fetched = {} + self._available = True self.sensors = set() self._registered_sensors = set() @@ -145,21 +147,32 @@ class FroniusAdapter: """Return the state attributes.""" return self._fetched + @property + def available(self): + """Whether the fronius device is active.""" + return self._available + async def async_update(self): """Retrieve and update latest state.""" - values = {} try: values = await self._update() except ConnectionError: - _LOGGER.error("Failed to update: connection error") + # fronius devices are often powered by self-produced solar energy + # and henced turned off at night. + # Therefore we will not print multiple errors when connection fails + if self._available: + self._available = False + _LOGGER.error("Failed to update: connection error") + return except ValueError: _LOGGER.error( "Failed to update: invalid response returned." "Maybe the configured device is not supported" ) - - if not values: return + + self._available = True # reset connection failure + attributes = self._fetched # Copy data of current fronius device for key, entry in values.items(): @@ -182,7 +195,7 @@ class FroniusAdapter: for sensor in self._registered_sensors: sensor.async_schedule_update_ha_state(True) - async def _update(self): + async def _update(self) -> Dict: """Return values of interest.""" async def register(self, sensor): @@ -268,6 +281,11 @@ class FroniusTemplateSensor(Entity): """Device should not be polled, returns False.""" return False + @property + def available(self): + """Whether the fronius device is active.""" + return self.parent.available + async def async_update(self): """Update the internal state.""" state = self.parent.data.get(self._name) From 89aaeb3c351a5f9c3fe28bbd62066b4ab5cd665e Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 15 Feb 2021 09:52:37 -0800 Subject: [PATCH 448/796] Refactor stream worker responsibilities for segmenting into a separate class (#46563) * Remove stream_worker dependencies on Stream Removee stream_worker dependencies on Stream and split out the logic for writing segments to a stream buffer. * Stop calling internal stream methods * Update homeassistant/components/stream/worker.py Co-authored-by: uvjustin <46082645+uvjustin@users.noreply.github.com> * Reuse self._outputs when creating new streams Co-authored-by: uvjustin <46082645+uvjustin@users.noreply.github.com> --- homeassistant/components/stream/__init__.py | 9 +- homeassistant/components/stream/worker.py | 143 ++++++++++---------- tests/components/stream/test_worker.py | 4 +- 3 files changed, 81 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index e871963d2ba..cdaa0faeb95 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -124,7 +124,6 @@ class Stream: self.access_token = secrets.token_hex() return self.hass.data[DOMAIN][ATTR_ENDPOINTS][fmt].format(self.access_token) - @property def outputs(self): """Return a copy of the stream outputs.""" # A copy is returned so the caller can iterate through the outputs @@ -192,7 +191,7 @@ class Stream: wait_timeout = 0 while not self._thread_quit.wait(timeout=wait_timeout): start_time = time.time() - stream_worker(self.hass, self, self._thread_quit) + stream_worker(self.source, self.options, self.outputs, self._thread_quit) if not self.keepalive or self._thread_quit.is_set(): if self._fast_restart_once: # The stream source is updated, restart without any delay. @@ -219,7 +218,7 @@ class Stream: @callback def remove_outputs(): - for provider in self.outputs.values(): + for provider in self.outputs().values(): self.remove_provider(provider) self.hass.loop.call_soon_threadsafe(remove_outputs) @@ -248,7 +247,7 @@ class Stream: raise HomeAssistantError(f"Can't write {video_path}, no access to path!") # Add recorder - recorder = self.outputs.get("recorder") + recorder = self.outputs().get("recorder") if recorder: raise HomeAssistantError( f"Stream already recording to {recorder.video_path}!" @@ -259,7 +258,7 @@ class Stream: self.start() # Take advantage of lookback - hls = self.outputs.get("hls") + hls = self.outputs().get("hls") if lookback > 0 and hls: num_segments = min(int(lookback // hls.target_duration), MAX_SEGMENTS) # Wait for latest segment, then add the lookback diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 510d0ebd460..2050787a714 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -43,15 +43,78 @@ def create_stream_buffer(stream_output, video_stream, audio_stream, sequence): return StreamBuffer(segment, output, vstream, astream) -def stream_worker(hass, stream, quit_event): +class SegmentBuffer: + """Buffer for writing a sequence of packets to the output as a segment.""" + + def __init__(self, video_stream, audio_stream, outputs_callback) -> None: + """Initialize SegmentBuffer.""" + self._video_stream = video_stream + self._audio_stream = audio_stream + self._outputs_callback = outputs_callback + # tuple of StreamOutput, StreamBuffer + self._outputs = [] + self._sequence = 0 + self._segment_start_pts = None + + def reset(self, video_pts): + """Initialize a new stream segment.""" + # Keep track of the number of segments we've processed + self._sequence += 1 + self._segment_start_pts = video_pts + + # Fetch the latest StreamOutputs, which may have changed since the + # worker started. + self._outputs = [] + for stream_output in self._outputs_callback().values(): + if self._video_stream.name not in stream_output.video_codecs: + continue + buffer = create_stream_buffer( + stream_output, self._video_stream, self._audio_stream, self._sequence + ) + self._outputs.append((buffer, stream_output)) + + def mux_packet(self, packet): + """Mux a packet to the appropriate StreamBuffers.""" + + # Check for end of segment + if packet.stream == self._video_stream and packet.is_keyframe: + duration = (packet.pts - self._segment_start_pts) * packet.time_base + if duration >= MIN_SEGMENT_DURATION: + # Save segment to outputs + self.flush(duration) + + # Reinitialize + self.reset(packet.pts) + + # Mux the packet + for (buffer, _) in self._outputs: + if packet.stream == self._video_stream: + packet.stream = buffer.vstream + elif packet.stream == self._audio_stream: + packet.stream = buffer.astream + else: + continue + buffer.output.mux(packet) + + def flush(self, duration): + """Create a segment from the buffered packets and write to output.""" + for (buffer, stream_output) in self._outputs: + buffer.output.close() + stream_output.put(Segment(self._sequence, buffer.segment, duration)) + + def close(self): + """Close all StreamBuffers.""" + for (buffer, _) in self._outputs: + buffer.output.close() + + +def stream_worker(source, options, outputs_callback, quit_event): """Handle consuming streams.""" try: - container = av.open( - stream.source, options=stream.options, timeout=STREAM_TIMEOUT - ) + container = av.open(source, options=options, timeout=STREAM_TIMEOUT) except av.AVError: - _LOGGER.error("Error opening stream %s", stream.source) + _LOGGER.error("Error opening stream %s", source) return try: video_stream = container.streams.video[0] @@ -78,9 +141,7 @@ def stream_worker(hass, stream, quit_event): # Keep track of consecutive packets without a dts to detect end of stream. missing_dts = 0 # Holds the buffers for each stream provider - outputs = None - # Keep track of the number of segments we've processed - sequence = 0 + segment_buffer = SegmentBuffer(video_stream, audio_stream, outputs_callback) # The video pts at the beginning of the segment segment_start_pts = None # Because of problems 1 and 2 below, we need to store the first few packets and replay them @@ -157,44 +218,11 @@ def stream_worker(hass, stream, quit_event): return False return True - def initialize_segment(video_pts): - """Reset some variables and initialize outputs for each segment.""" - nonlocal outputs, sequence, segment_start_pts - # Clear outputs and increment sequence - outputs = {} - sequence += 1 - segment_start_pts = video_pts - for stream_output in stream.outputs.values(): - if video_stream.name not in stream_output.video_codecs: - continue - buffer = create_stream_buffer( - stream_output, video_stream, audio_stream, sequence - ) - outputs[stream_output.name] = ( - buffer, - {video_stream: buffer.vstream, audio_stream: buffer.astream}, - ) - - def mux_video_packet(packet): - # mux packets to each buffer - for buffer, output_streams in outputs.values(): - # Assign the packet to the new stream & mux - packet.stream = output_streams[video_stream] - buffer.output.mux(packet) - - def mux_audio_packet(packet): - # almost the same as muxing video but add extra check - for buffer, output_streams in outputs.values(): - # Assign the packet to the new stream & mux - if output_streams.get(audio_stream): - packet.stream = output_streams[audio_stream] - buffer.output.mux(packet) - if not peek_first_pts(): container.close() return - initialize_segment(segment_start_pts) + segment_buffer.reset(segment_start_pts) while not quit_event.is_set(): try: @@ -229,34 +257,13 @@ def stream_worker(hass, stream, quit_event): break continue - # Check for end of segment - if packet.stream == video_stream and packet.is_keyframe: - segment_duration = (packet.pts - segment_start_pts) * packet.time_base - if segment_duration >= MIN_SEGMENT_DURATION: - # Save segment to outputs - for fmt, (buffer, _) in outputs.items(): - buffer.output.close() - if stream.outputs.get(fmt): - stream.outputs[fmt].put( - Segment( - sequence, - buffer.segment, - segment_duration, - ), - ) - - # Reinitialize - initialize_segment(packet.pts) - # Update last_dts processed last_dts[packet.stream] = packet.dts - # mux packets - if packet.stream == video_stream: - mux_video_packet(packet) # mutates packet timestamps - else: - mux_audio_packet(packet) # mutates packet timestamps + + # Mux packets, and possibly write a segment to the output stream. + # This mutates packet timestamps and stream + segment_buffer.mux_packet(packet) # Close stream - for buffer, _ in outputs.values(): - buffer.output.close() + segment_buffer.close() container.close() diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 0d5be68d93c..b348d68fc86 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -198,7 +198,7 @@ async def async_decode_stream(hass, packets, py_av=None): "homeassistant.components.stream.core.StreamOutput.put", side_effect=py_av.capture_buffer.capture_output_segment, ): - stream_worker(hass, stream, threading.Event()) + stream_worker(STREAM_SOURCE, {}, stream.outputs, threading.Event()) await hass.async_block_till_done() return py_av.capture_buffer @@ -210,7 +210,7 @@ async def test_stream_open_fails(hass): stream.add_provider(STREAM_OUTPUT_FORMAT) with patch("av.open") as av_open: av_open.side_effect = av.error.InvalidDataError(-2, "error") - stream_worker(hass, stream, threading.Event()) + stream_worker(STREAM_SOURCE, {}, stream.outputs, threading.Event()) await hass.async_block_till_done() av_open.assert_called_once() From 68809e9f43699b2ae414954b6cab0456dc8e40a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 15 Feb 2021 20:08:08 +0100 Subject: [PATCH 449/796] Fix issue with timeout and error response (#46584) --- homeassistant/components/hassio/websocket_api.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py index 851404b4b0e..d2c0bc9ed10 100644 --- a/homeassistant/components/hassio/websocket_api.py +++ b/homeassistant/components/hassio/websocket_api.py @@ -61,7 +61,7 @@ async def websocket_supervisor_event( vol.Required(ATTR_ENDPOINT): cv.string, vol.Required(ATTR_METHOD): cv.string, vol.Optional(ATTR_DATA): dict, - vol.Optional(ATTR_TIMEOUT): cv.string, + vol.Optional(ATTR_TIMEOUT): vol.Any(cv.Number, None), } ) async def websocket_supervisor_api( @@ -79,6 +79,8 @@ async def websocket_supervisor_api( ) except hass.components.hassio.HassioAPIError as err: _LOGGER.error("Failed to to call %s - %s", msg[ATTR_ENDPOINT], err) - connection.send_error(msg[WS_ID], err) + connection.send_error( + msg[WS_ID], code=websocket_api.ERR_UNKNOWN_ERROR, message=str(err) + ) else: - connection.send_result(msg[WS_ID], result[ATTR_DATA]) + connection.send_result(msg[WS_ID], result.get(ATTR_DATA, {})) From 2f9fda73f4e0f843c7a3b48ce7c33beac6cbcab8 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 15 Feb 2021 20:11:27 +0100 Subject: [PATCH 450/796] Add config flow to Xiaomi Miio switch (#46179) --- .coveragerc | 1 + .../components/xiaomi_miio/__init__.py | 36 ++- .../components/xiaomi_miio/config_flow.py | 144 ++++++---- homeassistant/components/xiaomi_miio/const.py | 21 ++ .../components/xiaomi_miio/device.py | 87 ++++++ homeassistant/components/xiaomi_miio/light.py | 13 +- .../components/xiaomi_miio/sensor.py | 3 +- .../components/xiaomi_miio/strings.json | 15 +- .../components/xiaomi_miio/switch.py | 267 +++++++++--------- .../xiaomi_miio/translations/en.json | 19 +- .../xiaomi_miio/test_config_flow.py | 252 ++++++++++++----- 11 files changed, 563 insertions(+), 295 deletions(-) create mode 100644 homeassistant/components/xiaomi_miio/device.py diff --git a/.coveragerc b/.coveragerc index 17dd078d15b..e64bfab280a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1085,6 +1085,7 @@ omit = homeassistant/components/xiaomi_miio/__init__.py homeassistant/components/xiaomi_miio/air_quality.py homeassistant/components/xiaomi_miio/alarm_control_panel.py + homeassistant/components/xiaomi_miio/device.py homeassistant/components/xiaomi_miio/device_tracker.py homeassistant/components/xiaomi_miio/fan.py homeassistant/components/xiaomi_miio/gateway.py diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 7ff1ed999c4..e81c35d39e4 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -3,11 +3,18 @@ from homeassistant import config_entries, core from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.helpers import device_registry as dr -from .config_flow import CONF_FLOW_TYPE, CONF_GATEWAY -from .const import DOMAIN +from .const import ( + CONF_DEVICE, + CONF_FLOW_TYPE, + CONF_GATEWAY, + CONF_MODEL, + DOMAIN, + MODELS_SWITCH, +) from .gateway import ConnectXiaomiGateway GATEWAY_PLATFORMS = ["alarm_control_panel", "sensor", "light"] +SWITCH_PLATFORMS = ["switch"] async def async_setup(hass: core.HomeAssistant, config: dict): @@ -19,10 +26,13 @@ async def async_setup_entry( hass: core.HomeAssistant, entry: config_entries.ConfigEntry ): """Set up the Xiaomi Miio components from a config entry.""" - hass.data[DOMAIN] = {} + hass.data.setdefault(DOMAIN, {}) if entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY: if not await async_setup_gateway_entry(hass, entry): return False + if entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: + if not await async_setup_device_entry(hass, entry): + return False return True @@ -67,3 +77,23 @@ async def async_setup_gateway_entry( ) return True + + +async def async_setup_device_entry( + hass: core.HomeAssistant, entry: config_entries.ConfigEntry +): + """Set up the Xiaomi Miio device component from a config entry.""" + model = entry.data[CONF_MODEL] + + # Identify platforms to setup + if model in MODELS_SWITCH: + platforms = SWITCH_PLATFORMS + else: + return False + + for component in platforms: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 6ed5f422f7c..2a1532eaf9b 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -8,24 +8,27 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN from homeassistant.helpers.device_registry import format_mac # pylint: disable=unused-import -from .const import DOMAIN -from .gateway import ConnectXiaomiGateway +from .const import ( + CONF_DEVICE, + CONF_FLOW_TYPE, + CONF_GATEWAY, + CONF_MAC, + CONF_MODEL, + DOMAIN, + MODELS_GATEWAY, + MODELS_SWITCH, +) +from .device import ConnectXiaomiDevice _LOGGER = logging.getLogger(__name__) -CONF_FLOW_TYPE = "config_flow_device" -CONF_GATEWAY = "gateway" DEFAULT_GATEWAY_NAME = "Xiaomi Gateway" -ZEROCONF_GATEWAY = "lumi-gateway" -ZEROCONF_ACPARTNER = "lumi-acpartner" +DEFAULT_DEVICE_NAME = "Xiaomi Device" -GATEWAY_SETTINGS = { +DEVICE_SETTINGS = { vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)), - vol.Optional(CONF_NAME, default=DEFAULT_GATEWAY_NAME): str, } -GATEWAY_CONFIG = vol.Schema({vol.Required(CONF_HOST): str}).extend(GATEWAY_SETTINGS) - -CONFIG_SCHEMA = vol.Schema({vol.Optional(CONF_GATEWAY, default=False): bool}) +DEVICE_CONFIG = vol.Schema({vol.Required(CONF_HOST): str}).extend(DEVICE_SETTINGS) class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -38,19 +41,13 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Initialize.""" self.host = None + async def async_step_import(self, conf: dict): + """Import a configuration from config.yaml.""" + return await self.async_step_device(user_input=conf) + async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" - errors = {} - if user_input is not None: - # Check which device needs to be connected. - if user_input[CONF_GATEWAY]: - return await self.async_step_gateway() - - errors["base"] = "no_device_selected" - - return self.async_show_form( - step_id="user", data_schema=CONFIG_SCHEMA, errors=errors - ) + return await self.async_step_device() async def async_step_zeroconf(self, discovery_info): """Handle zeroconf discovery.""" @@ -62,16 +59,28 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_xiaomi_miio") # Check which device is discovered. - if name.startswith(ZEROCONF_GATEWAY) or name.startswith(ZEROCONF_ACPARTNER): - unique_id = format_mac(mac_address) - await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured({CONF_HOST: self.host}) + for gateway_model in MODELS_GATEWAY: + if name.startswith(gateway_model.replace(".", "-")): + unique_id = format_mac(mac_address) + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured({CONF_HOST: self.host}) - self.context.update( - {"title_placeholders": {"name": f"Gateway {self.host}"}} - ) + self.context.update( + {"title_placeholders": {"name": f"Gateway {self.host}"}} + ) - return await self.async_step_gateway() + return await self.async_step_device() + for switch_model in MODELS_SWITCH: + if name.startswith(switch_model.replace(".", "-")): + unique_id = format_mac(mac_address) + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured({CONF_HOST: self.host}) + + self.context.update( + {"title_placeholders": {"name": f"Miio Device {self.host}"}} + ) + + return await self.async_step_device() # Discovered device is not yet supported _LOGGER.debug( @@ -81,42 +90,63 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_abort(reason="not_xiaomi_miio") - async def async_step_gateway(self, user_input=None): - """Handle a flow initialized by the user to configure a gateway.""" + async def async_step_device(self, user_input=None): + """Handle a flow initialized by the user to configure a xiaomi miio device.""" errors = {} if user_input is not None: token = user_input[CONF_TOKEN] if user_input.get(CONF_HOST): self.host = user_input[CONF_HOST] - # Try to connect to a Xiaomi Gateway. - connect_gateway_class = ConnectXiaomiGateway(self.hass) - await connect_gateway_class.async_connect_gateway(self.host, token) - gateway_info = connect_gateway_class.gateway_info + # Try to connect to a Xiaomi Device. + connect_device_class = ConnectXiaomiDevice(self.hass) + await connect_device_class.async_connect_device(self.host, token) + device_info = connect_device_class.device_info - if gateway_info is not None: - mac = format_mac(gateway_info.mac_address) - unique_id = mac - await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured() - return self.async_create_entry( - title=user_input[CONF_NAME], - data={ - CONF_FLOW_TYPE: CONF_GATEWAY, - CONF_HOST: self.host, - CONF_TOKEN: token, - "model": gateway_info.model, - "mac": mac, - }, - ) + if device_info is not None: + # Setup Gateways + for gateway_model in MODELS_GATEWAY: + if device_info.model.startswith(gateway_model): + mac = format_mac(device_info.mac_address) + unique_id = mac + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=DEFAULT_GATEWAY_NAME, + data={ + CONF_FLOW_TYPE: CONF_GATEWAY, + CONF_HOST: self.host, + CONF_TOKEN: token, + CONF_MODEL: device_info.model, + CONF_MAC: mac, + }, + ) - errors["base"] = "cannot_connect" + # Setup all other Miio Devices + name = user_input.get(CONF_NAME, DEFAULT_DEVICE_NAME) + + if device_info.model in MODELS_SWITCH: + mac = format_mac(device_info.mac_address) + unique_id = mac + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=name, + data={ + CONF_FLOW_TYPE: CONF_DEVICE, + CONF_HOST: self.host, + CONF_TOKEN: token, + CONF_MODEL: device_info.model, + CONF_MAC: mac, + }, + ) + errors["base"] = "unknown_device" + else: + errors["base"] = "cannot_connect" if self.host: - schema = vol.Schema(GATEWAY_SETTINGS) + schema = vol.Schema(DEVICE_SETTINGS) else: - schema = GATEWAY_CONFIG + schema = DEVICE_CONFIG - return self.async_show_form( - step_id="gateway", data_schema=schema, errors=errors - ) + return self.async_show_form(step_id="device", data_schema=schema, errors=errors) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 8de68cda97f..3726f7f709d 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -1,6 +1,27 @@ """Constants for the Xiaomi Miio component.""" DOMAIN = "xiaomi_miio" +CONF_FLOW_TYPE = "config_flow_device" +CONF_GATEWAY = "gateway" +CONF_DEVICE = "device" +CONF_MODEL = "model" +CONF_MAC = "mac" + +MODELS_GATEWAY = ["lumi.gateway", "lumi.acpartner"] +MODELS_SWITCH = [ + "chuangmi.plug.v1", + "chuangmi.plug.v3", + "chuangmi.plug.hmi208", + "qmi.powerstrip.v1", + "zimi.powerstrip.v2", + "chuangmi.plug.m1", + "chuangmi.plug.m3", + "chuangmi.plug.v2", + "chuangmi.plug.hmi205", + "chuangmi.plug.hmi206", + "lumi.acpartner.v3", +] + # Fan Services SERVICE_SET_BUZZER_ON = "fan_set_buzzer_on" SERVICE_SET_BUZZER_OFF = "fan_set_buzzer_off" diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py new file mode 100644 index 00000000000..48bedbf0cc8 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/device.py @@ -0,0 +1,87 @@ +"""Code to handle a Xiaomi Device.""" +import logging + +from miio import Device, DeviceException + +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import Entity + +from .const import CONF_MAC, CONF_MODEL, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class ConnectXiaomiDevice: + """Class to async connect to a Xiaomi Device.""" + + def __init__(self, hass): + """Initialize the entity.""" + self._hass = hass + self._device = None + self._device_info = None + + @property + def device(self): + """Return the class containing all connections to the device.""" + return self._device + + @property + def device_info(self): + """Return the class containing device info.""" + return self._device_info + + async def async_connect_device(self, host, token): + """Connect to the Xiaomi Device.""" + _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) + try: + self._device = Device(host, token) + # get the device info + self._device_info = await self._hass.async_add_executor_job( + self._device.info + ) + except DeviceException: + _LOGGER.error( + "DeviceException during setup of xiaomi device with host %s", host + ) + return False + _LOGGER.debug( + "%s %s %s detected", + self._device_info.model, + self._device_info.firmware_version, + self._device_info.hardware_version, + ) + return True + + +class XiaomiMiioEntity(Entity): + """Representation of a base Xiaomi Miio Entity.""" + + def __init__(self, name, device, entry, unique_id): + """Initialize the Xiaomi Miio Device.""" + self._device = device + self._model = entry.data[CONF_MODEL] + self._mac = entry.data[CONF_MAC] + self._device_id = entry.unique_id + self._unique_id = unique_id + self._name = name + + @property + def unique_id(self): + """Return an unique ID.""" + return self._unique_id + + @property + def name(self): + """Return the name of this entity, if any.""" + return self._name + + @property + def device_info(self): + """Return the device info.""" + return { + "connections": {(dr.CONNECTION_NETWORK_MAC, self._mac)}, + "identifiers": {(DOMAIN, self._device_id)}, + "manufacturer": "Xiaomi", + "name": self._name, + "model": self._model, + } diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index d1746fcd889..efe67a370c4 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -6,14 +6,8 @@ from functools import partial import logging from math import ceil -from miio import ( # pylint: disable=import-error - Ceil, - Device, - DeviceException, - PhilipsBulb, - PhilipsEyecare, - PhilipsMoonlight, -) +from miio import Ceil, DeviceException, PhilipsBulb, PhilipsEyecare, PhilipsMoonlight +from miio import Device # pylint: disable=import-error from miio.gateway import ( GATEWAY_MODEL_AC_V1, GATEWAY_MODEL_AC_V2, @@ -37,8 +31,9 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util import color, dt -from .config_flow import CONF_FLOW_TYPE, CONF_GATEWAY from .const import ( + CONF_FLOW_TYPE, + CONF_GATEWAY, DOMAIN, SERVICE_EYECARE_MODE_OFF, SERVICE_EYECARE_MODE_ON, diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index d20c2dfac1e..ab4df8cd982 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -31,8 +31,7 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from .config_flow import CONF_FLOW_TYPE, CONF_GATEWAY -from .const import DOMAIN +from .const import CONF_FLOW_TYPE, CONF_GATEWAY, DOMAIN from .gateway import XiaomiGatewayDevice _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json index 68536de76e5..1ab0c6f51c6 100644 --- a/homeassistant/components/xiaomi_miio/strings.json +++ b/homeassistant/components/xiaomi_miio/strings.json @@ -2,26 +2,19 @@ "config": { "flow_title": "Xiaomi Miio: {name}", "step": { - "user": { - "title": "Xiaomi Miio", - "description": "Select to which device you want to connect.", - "data": { - "gateway": "Connect to a Xiaomi Gateway" - } - }, - "gateway": { - "title": "Connect to a Xiaomi Gateway", + "device": { + "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway", "description": "You will need the 32 character [%key:common::config_flow::data::api_token%], see https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instructions. Please note, that this [%key:common::config_flow::data::api_token%] is different from the key used by the Xiaomi Aqara integration.", "data": { "host": "[%key:common::config_flow::data::ip%]", "token": "[%key:common::config_flow::data::api_token%]", - "name": "Name of the Gateway" + "name": "Name of the device" } } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "no_device_selected": "No device selected, please select one device." + "unknown_device": "The device model is not known, not able to setup the device using config flow." }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index b9e90cc5c23..3cc95572e6c 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -3,17 +3,13 @@ import asyncio from functools import partial import logging -from miio import ( # pylint: disable=import-error - AirConditioningCompanionV3, - ChuangmiPlug, - Device, - DeviceException, - PowerStrip, -) +from miio import AirConditioningCompanionV3 # pylint: disable=import-error +from miio import ChuangmiPlug, DeviceException, PowerStrip from miio.powerstrip import PowerMode # pylint: disable=import-error import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, @@ -21,23 +17,25 @@ from homeassistant.const import ( CONF_NAME, CONF_TOKEN, ) -from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from .const import ( + CONF_DEVICE, + CONF_FLOW_TYPE, + CONF_MODEL, DOMAIN, SERVICE_SET_POWER_MODE, SERVICE_SET_POWER_PRICE, SERVICE_SET_WIFI_LED_OFF, SERVICE_SET_WIFI_LED_ON, ) +from .device import XiaomiMiioEntity _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Xiaomi Miio Switch" DATA_KEY = "switch.xiaomi_miio" -CONF_MODEL = "model" MODEL_POWER_STRIP_V2 = "zimi.powerstrip.v2" MODEL_PLUG_V3 = "chuangmi.plug.v3" @@ -114,119 +112,124 @@ SERVICE_TO_METHOD = { async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the switch from config.""" - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {} + """Import Miio configuration from YAML.""" + _LOGGER.warning( + "Loading Xiaomi Miio Switch via platform setup is deprecated. Please remove it from your configuration." + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) - host = config[CONF_HOST] - token = config[CONF_TOKEN] - name = config[CONF_NAME] - model = config.get(CONF_MODEL) - _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the switch from a config entry.""" + entities = [] - devices = [] - unique_id = None + if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = {} - if model is None: - try: - miio_device = Device(host, token) - device_info = await hass.async_add_executor_job(miio_device.info) - model = device_info.model - unique_id = f"{model}-{device_info.mac_address}" - _LOGGER.info( - "%s %s %s detected", - model, - device_info.firmware_version, - device_info.hardware_version, - ) - except DeviceException as ex: - raise PlatformNotReady from ex + host = config_entry.data[CONF_HOST] + token = config_entry.data[CONF_TOKEN] + name = config_entry.title + model = config_entry.data[CONF_MODEL] + unique_id = config_entry.unique_id - if model in ["chuangmi.plug.v1", "chuangmi.plug.v3", "chuangmi.plug.hmi208"]: - plug = ChuangmiPlug(host, token, model=model) + _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) - # The device has two switchable channels (mains and a USB port). - # A switch device per channel will be created. - for channel_usb in [True, False]: - device = ChuangMiPlugSwitch(name, plug, model, unique_id, channel_usb) - devices.append(device) + if model in ["chuangmi.plug.v1", "chuangmi.plug.v3", "chuangmi.plug.hmi208"]: + plug = ChuangmiPlug(host, token, model=model) + + # The device has two switchable channels (mains and a USB port). + # A switch device per channel will be created. + for channel_usb in [True, False]: + if channel_usb: + unique_id_ch = f"{unique_id}-USB" + else: + unique_id_ch = f"{unique_id}-mains" + device = ChuangMiPlugSwitch( + name, plug, config_entry, unique_id_ch, channel_usb + ) + entities.append(device) + hass.data[DATA_KEY][host] = device + elif model in ["qmi.powerstrip.v1", "zimi.powerstrip.v2"]: + plug = PowerStrip(host, token, model=model) + device = XiaomiPowerStripSwitch(name, plug, config_entry, unique_id) + entities.append(device) + hass.data[DATA_KEY][host] = device + elif model in [ + "chuangmi.plug.m1", + "chuangmi.plug.m3", + "chuangmi.plug.v2", + "chuangmi.plug.hmi205", + "chuangmi.plug.hmi206", + ]: + plug = ChuangmiPlug(host, token, model=model) + device = XiaomiPlugGenericSwitch(name, plug, config_entry, unique_id) + entities.append(device) + hass.data[DATA_KEY][host] = device + elif model in ["lumi.acpartner.v3"]: + plug = AirConditioningCompanionV3(host, token) + device = XiaomiAirConditioningCompanionSwitch( + name, plug, config_entry, unique_id + ) + entities.append(device) hass.data[DATA_KEY][host] = device - - elif model in ["qmi.powerstrip.v1", "zimi.powerstrip.v2"]: - plug = PowerStrip(host, token, model=model) - device = XiaomiPowerStripSwitch(name, plug, model, unique_id) - devices.append(device) - hass.data[DATA_KEY][host] = device - elif model in [ - "chuangmi.plug.m1", - "chuangmi.plug.m3", - "chuangmi.plug.v2", - "chuangmi.plug.hmi205", - "chuangmi.plug.hmi206", - ]: - plug = ChuangmiPlug(host, token, model=model) - device = XiaomiPlugGenericSwitch(name, plug, model, unique_id) - devices.append(device) - hass.data[DATA_KEY][host] = device - elif model in ["lumi.acpartner.v3"]: - plug = AirConditioningCompanionV3(host, token) - device = XiaomiAirConditioningCompanionSwitch(name, plug, model, unique_id) - devices.append(device) - hass.data[DATA_KEY][host] = device - else: - _LOGGER.error( - "Unsupported device found! Please create an issue at " - "https://github.com/rytilahti/python-miio/issues " - "and provide the following data: %s", - model, - ) - return False - - async_add_entities(devices, update_before_add=True) - - async def async_service_handler(service): - """Map services to methods on XiaomiPlugGenericSwitch.""" - method = SERVICE_TO_METHOD.get(service.service) - params = { - key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID - } - entity_ids = service.data.get(ATTR_ENTITY_ID) - if entity_ids: - devices = [ - device - for device in hass.data[DATA_KEY].values() - if device.entity_id in entity_ids - ] else: - devices = hass.data[DATA_KEY].values() + _LOGGER.error( + "Unsupported device found! Please create an issue at " + "https://github.com/rytilahti/python-miio/issues " + "and provide the following data: %s", + model, + ) - update_tasks = [] - for device in devices: - if not hasattr(device, method["method"]): - continue - await getattr(device, method["method"])(**params) - update_tasks.append(device.async_update_ha_state(True)) + async def async_service_handler(service): + """Map services to methods on XiaomiPlugGenericSwitch.""" + method = SERVICE_TO_METHOD.get(service.service) + params = { + key: value + for key, value in service.data.items() + if key != ATTR_ENTITY_ID + } + entity_ids = service.data.get(ATTR_ENTITY_ID) + if entity_ids: + devices = [ + device + for device in hass.data[DATA_KEY].values() + if device.entity_id in entity_ids + ] + else: + devices = hass.data[DATA_KEY].values() - if update_tasks: - await asyncio.wait(update_tasks) + update_tasks = [] + for device in devices: + if not hasattr(device, method["method"]): + continue + await getattr(device, method["method"])(**params) + update_tasks.append(device.async_update_ha_state(True)) - for plug_service in SERVICE_TO_METHOD: - schema = SERVICE_TO_METHOD[plug_service].get("schema", SERVICE_SCHEMA) - hass.services.async_register( - DOMAIN, plug_service, async_service_handler, schema=schema - ) + if update_tasks: + await asyncio.wait(update_tasks) + + for plug_service in SERVICE_TO_METHOD: + schema = SERVICE_TO_METHOD[plug_service].get("schema", SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, plug_service, async_service_handler, schema=schema + ) + + async_add_entities(entities, update_before_add=True) -class XiaomiPlugGenericSwitch(SwitchEntity): +class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): """Representation of a Xiaomi Plug Generic.""" - def __init__(self, name, plug, model, unique_id): + def __init__(self, name, device, entry, unique_id): """Initialize the plug switch.""" - self._name = name - self._plug = plug - self._model = model - self._unique_id = unique_id + super().__init__(name, device, entry, unique_id) self._icon = "mdi:power-socket" self._available = False @@ -235,16 +238,6 @@ class XiaomiPlugGenericSwitch(SwitchEntity): self._device_features = FEATURE_FLAGS_GENERIC self._skip_update = False - @property - def unique_id(self): - """Return an unique ID.""" - return self._unique_id - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - @property def icon(self): """Return the icon to use for device if any.""" @@ -288,7 +281,7 @@ class XiaomiPlugGenericSwitch(SwitchEntity): async def async_turn_on(self, **kwargs): """Turn the plug on.""" - result = await self._try_command("Turning the plug on failed.", self._plug.on) + result = await self._try_command("Turning the plug on failed", self._device.on) if result: self._state = True @@ -296,7 +289,9 @@ class XiaomiPlugGenericSwitch(SwitchEntity): async def async_turn_off(self, **kwargs): """Turn the plug off.""" - result = await self._try_command("Turning the plug off failed.", self._plug.off) + result = await self._try_command( + "Turning the plug off failed", self._device.off + ) if result: self._state = False @@ -310,7 +305,7 @@ class XiaomiPlugGenericSwitch(SwitchEntity): return try: - state = await self.hass.async_add_executor_job(self._plug.status) + state = await self.hass.async_add_executor_job(self._device.status) _LOGGER.debug("Got new state: %s", state) self._available = True @@ -328,7 +323,7 @@ class XiaomiPlugGenericSwitch(SwitchEntity): return await self._try_command( - "Turning the wifi led on failed.", self._plug.set_wifi_led, True + "Turning the wifi led on failed", self._device.set_wifi_led, True ) async def async_set_wifi_led_off(self): @@ -337,7 +332,7 @@ class XiaomiPlugGenericSwitch(SwitchEntity): return await self._try_command( - "Turning the wifi led off failed.", self._plug.set_wifi_led, False + "Turning the wifi led off failed", self._device.set_wifi_led, False ) async def async_set_power_price(self, price: int): @@ -346,8 +341,8 @@ class XiaomiPlugGenericSwitch(SwitchEntity): return await self._try_command( - "Setting the power price of the power strip failed.", - self._plug.set_power_price, + "Setting the power price of the power strip failed", + self._device.set_power_price, price, ) @@ -383,7 +378,7 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): return try: - state = await self.hass.async_add_executor_job(self._plug.status) + state = await self.hass.async_add_executor_job(self._device.status) _LOGGER.debug("Got new state: %s", state) self._available = True @@ -415,8 +410,8 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): return await self._try_command( - "Setting the power mode of the power strip failed.", - self._plug.set_power_mode, + "Setting the power mode of the power strip failed", + self._device.set_power_mode, PowerMode(mode), ) @@ -424,14 +419,14 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): """Representation of a Chuang Mi Plug V1 and V3.""" - def __init__(self, name, plug, model, unique_id, channel_usb): + def __init__(self, name, plug, entry, unique_id, channel_usb): """Initialize the plug switch.""" name = f"{name} USB" if channel_usb else name if unique_id is not None and channel_usb: unique_id = f"{unique_id}-usb" - super().__init__(name, plug, model, unique_id) + super().__init__(name, plug, entry, unique_id) self._channel_usb = channel_usb if self._model == MODEL_PLUG_V3: @@ -444,11 +439,11 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): """Turn a channel on.""" if self._channel_usb: result = await self._try_command( - "Turning the plug on failed.", self._plug.usb_on + "Turning the plug on failed", self._device.usb_on ) else: result = await self._try_command( - "Turning the plug on failed.", self._plug.on + "Turning the plug on failed", self._device.on ) if result: @@ -459,11 +454,11 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): """Turn a channel off.""" if self._channel_usb: result = await self._try_command( - "Turning the plug on failed.", self._plug.usb_off + "Turning the plug off failed", self._device.usb_off ) else: result = await self._try_command( - "Turning the plug on failed.", self._plug.off + "Turning the plug off failed", self._device.off ) if result: @@ -478,7 +473,7 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): return try: - state = await self.hass.async_add_executor_job(self._plug.status) + state = await self.hass.async_add_executor_job(self._device.status) _LOGGER.debug("Got new state: %s", state) self._available = True @@ -513,7 +508,7 @@ class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch): async def async_turn_on(self, **kwargs): """Turn the socket on.""" result = await self._try_command( - "Turning the socket on failed.", self._plug.socket_on + "Turning the socket on failed", self._device.socket_on ) if result: @@ -523,7 +518,7 @@ class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch): async def async_turn_off(self, **kwargs): """Turn the socket off.""" result = await self._try_command( - "Turning the socket off failed.", self._plug.socket_off + "Turning the socket off failed", self._device.socket_off ) if result: @@ -538,7 +533,7 @@ class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch): return try: - state = await self.hass.async_add_executor_job(self._plug.status) + state = await self.hass.async_add_executor_job(self._device.status) _LOGGER.debug("Got new state: %s", state) self._available = True diff --git a/homeassistant/components/xiaomi_miio/translations/en.json b/homeassistant/components/xiaomi_miio/translations/en.json index 4d39a6d1137..c8ac63d1ea7 100644 --- a/homeassistant/components/xiaomi_miio/translations/en.json +++ b/homeassistant/components/xiaomi_miio/translations/en.json @@ -6,25 +6,18 @@ }, "error": { "cannot_connect": "Failed to connect", - "no_device_selected": "No device selected, please select one device." + "unknown_device": "The device model is not known, not able to setup the device using config flow." }, "flow_title": "Xiaomi Miio: {name}", "step": { - "gateway": { + "device": { "data": { - "host": "IP Address", - "name": "Name of the Gateway", - "token": "API Token" + "host": "IP Address", + "name": "Name of the device", + "token": "API Token" }, "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.", - "title": "Connect to a Xiaomi Gateway" - }, - "user": { - "data": { - "gateway": "Connect to a Xiaomi Gateway" - }, - "description": "Select to which device you want to connect.", - "title": "Xiaomi Miio" + "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" } } } diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index dbe78957586..220c51034f1 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -5,7 +5,11 @@ from miio import DeviceException from homeassistant import config_entries from homeassistant.components import zeroconf -from homeassistant.components.xiaomi_miio import config_flow, const +from homeassistant.components.xiaomi_miio import const +from homeassistant.components.xiaomi_miio.config_flow import ( + DEFAULT_DEVICE_NAME, + DEFAULT_GATEWAY_NAME, +) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN ZEROCONF_NAME = "name" @@ -15,7 +19,7 @@ ZEROCONF_MAC = "mac" TEST_HOST = "1.2.3.4" TEST_TOKEN = "12345678901234567890123456789012" TEST_NAME = "Test_Gateway" -TEST_MODEL = "model5" +TEST_MODEL = const.MODELS_GATEWAY[0] TEST_MAC = "ab:cd:ef:gh:ij:kl" TEST_GATEWAY_ID = TEST_MAC TEST_HARDWARE_VERSION = "AB123" @@ -40,26 +44,6 @@ def get_mock_info( return gateway_info -async def test_config_flow_step_user_no_device(hass): - """Test config flow, user step with no device selected.""" - result = await hass.config_entries.flow.async_init( - const.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"] == "user" - assert result["errors"] == {"base": "no_device_selected"} - - async def test_config_flow_step_gateway_connect_error(hass): """Test config flow, gateway connection error.""" result = await hass.config_entries.flow.async_init( @@ -67,29 +51,20 @@ async def test_config_flow_step_gateway_connect_error(hass): ) assert result["type"] == "form" - assert result["step_id"] == "user" - assert result["errors"] == {} - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {config_flow.CONF_GATEWAY: True}, - ) - - assert result["type"] == "form" - assert result["step_id"] == "gateway" + assert result["step_id"] == "device" assert result["errors"] == {} with patch( - "homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.info", + "homeassistant.components.xiaomi_miio.device.Device.info", side_effect=DeviceException({}), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: TEST_HOST, CONF_NAME: TEST_NAME, CONF_TOKEN: TEST_TOKEN}, + {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, ) assert result["type"] == "form" - assert result["step_id"] == "gateway" + assert result["step_id"] == "device" assert result["errors"] == {"base": "cannot_connect"} @@ -100,42 +75,30 @@ async def test_config_flow_gateway_success(hass): ) assert result["type"] == "form" - assert result["step_id"] == "user" - assert result["errors"] == {} - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {config_flow.CONF_GATEWAY: True}, - ) - - assert result["type"] == "form" - assert result["step_id"] == "gateway" + assert result["step_id"] == "device" assert result["errors"] == {} mock_info = get_mock_info() with patch( - "homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.info", + "homeassistant.components.xiaomi_miio.device.Device.info", return_value=mock_info, - ), patch( - "homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.discover_devices", - return_value=TEST_SUB_DEVICE_LIST, ), patch( "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: TEST_HOST, CONF_NAME: TEST_NAME, CONF_TOKEN: TEST_TOKEN}, + {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, ) assert result["type"] == "create_entry" - assert result["title"] == TEST_NAME + assert result["title"] == DEFAULT_GATEWAY_NAME assert result["data"] == { - config_flow.CONF_FLOW_TYPE: config_flow.CONF_GATEWAY, + const.CONF_FLOW_TYPE: const.CONF_GATEWAY, CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN, - "model": TEST_MODEL, - "mac": TEST_MAC, + const.CONF_MODEL: TEST_MODEL, + const.CONF_MAC: TEST_MAC, } @@ -152,33 +115,30 @@ async def test_zeroconf_gateway_success(hass): ) assert result["type"] == "form" - assert result["step_id"] == "gateway" + assert result["step_id"] == "device" assert result["errors"] == {} mock_info = get_mock_info() with patch( - "homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.info", + "homeassistant.components.xiaomi_miio.device.Device.info", return_value=mock_info, - ), patch( - "homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.discover_devices", - return_value=TEST_SUB_DEVICE_LIST, ), patch( "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_NAME: TEST_NAME, CONF_TOKEN: TEST_TOKEN}, + {CONF_TOKEN: TEST_TOKEN}, ) assert result["type"] == "create_entry" - assert result["title"] == TEST_NAME + assert result["title"] == DEFAULT_GATEWAY_NAME assert result["data"] == { - config_flow.CONF_FLOW_TYPE: config_flow.CONF_GATEWAY, + const.CONF_FLOW_TYPE: const.CONF_GATEWAY, CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN, - "model": TEST_MODEL, - "mac": TEST_MAC, + const.CONF_MODEL: TEST_MODEL, + const.CONF_MAC: TEST_MAC, } @@ -218,3 +178,167 @@ async def test_zeroconf_missing_data(hass): assert result["type"] == "abort" assert result["reason"] == "not_xiaomi_miio" + + +async def test_config_flow_step_device_connect_error(hass): + """Test config flow, device connection error.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "device" + assert result["errors"] == {} + + with patch( + "homeassistant.components.xiaomi_miio.device.Device.info", + side_effect=DeviceException({}), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "device" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_config_flow_step_unknown_device(hass): + """Test config flow, unknown device error.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "device" + assert result["errors"] == {} + + mock_info = get_mock_info(model="UNKNOWN") + + with patch( + "homeassistant.components.xiaomi_miio.device.Device.info", + return_value=mock_info, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "device" + assert result["errors"] == {"base": "unknown_device"} + + +async def test_import_flow_success(hass): + """Test a successful import form yaml for a device.""" + mock_info = get_mock_info(model=const.MODELS_SWITCH[0]) + + with patch( + "homeassistant.components.xiaomi_miio.device.Device.info", + return_value=mock_info, + ), patch( + "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_NAME: TEST_NAME, CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == TEST_NAME + assert result["data"] == { + const.CONF_FLOW_TYPE: const.CONF_DEVICE, + CONF_HOST: TEST_HOST, + CONF_TOKEN: TEST_TOKEN, + const.CONF_MODEL: const.MODELS_SWITCH[0], + const.CONF_MAC: TEST_MAC, + } + + +async def config_flow_device_success(hass, model_to_test): + """Test a successful config flow for a device (base class).""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "device" + assert result["errors"] == {} + + mock_info = get_mock_info(model=model_to_test) + + with patch( + "homeassistant.components.xiaomi_miio.device.Device.info", + return_value=mock_info, + ), patch( + "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == DEFAULT_DEVICE_NAME + assert result["data"] == { + const.CONF_FLOW_TYPE: const.CONF_DEVICE, + CONF_HOST: TEST_HOST, + CONF_TOKEN: TEST_TOKEN, + const.CONF_MODEL: model_to_test, + const.CONF_MAC: TEST_MAC, + } + + +async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test): + """Test a successful zeroconf discovery of a device (base class).""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + zeroconf.ATTR_HOST: TEST_HOST, + ZEROCONF_NAME: zeroconf_name_to_test, + ZEROCONF_PROP: {ZEROCONF_MAC: TEST_MAC}, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "device" + assert result["errors"] == {} + + mock_info = get_mock_info(model=model_to_test) + + with patch( + "homeassistant.components.xiaomi_miio.device.Device.info", + return_value=mock_info, + ), patch( + "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_TOKEN: TEST_TOKEN}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == DEFAULT_DEVICE_NAME + assert result["data"] == { + const.CONF_FLOW_TYPE: const.CONF_DEVICE, + CONF_HOST: TEST_HOST, + CONF_TOKEN: TEST_TOKEN, + const.CONF_MODEL: model_to_test, + const.CONF_MAC: TEST_MAC, + } + + +async def test_config_flow_plug_success(hass): + """Test a successful config flow for a plug.""" + test_plug_model = const.MODELS_SWITCH[0] + await config_flow_device_success(hass, test_plug_model) + + +async def test_zeroconf_plug_success(hass): + """Test a successful zeroconf discovery of a plug.""" + test_plug_model = const.MODELS_SWITCH[0] + test_zeroconf_name = const.MODELS_SWITCH[0].replace(".", "-") + await zeroconf_device_success(hass, test_zeroconf_name, test_plug_model) From e3ae3cfb83afa94ae06155c20a41c0802e679def Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Mon, 15 Feb 2021 14:24:42 -0500 Subject: [PATCH 451/796] Upgrade blinkpy to 0.17.0 (#46581) --- homeassistant/components/blink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index 17d737bcaf3..1c91f1a2295 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -2,7 +2,7 @@ "domain": "blink", "name": "Blink", "documentation": "https://www.home-assistant.io/integrations/blink", - "requirements": ["blinkpy==0.16.4"], + "requirements": ["blinkpy==0.17.0"], "codeowners": ["@fronzbot"], "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index 342849b291c..86a15b1288f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -351,7 +351,7 @@ bizkaibus==0.1.1 blebox_uniapi==1.3.2 # homeassistant.components.blink -blinkpy==0.16.4 +blinkpy==0.17.0 # homeassistant.components.blinksticklight blinkstick==1.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7b49c63f3c5..9d057cc414a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -201,7 +201,7 @@ bimmer_connected==0.7.14 blebox_uniapi==1.3.2 # homeassistant.components.blink -blinkpy==0.16.4 +blinkpy==0.17.0 # homeassistant.components.bond bond-api==0.1.9 From 6f4df7e52e18159ab412df4d16deaad54dd84599 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Feb 2021 10:35:58 -1000 Subject: [PATCH 452/796] Use shared clientsession for sense (#46419) --- homeassistant/components/emulated_kasa/manifest.json | 2 +- homeassistant/components/sense/__init__.py | 7 ++++++- homeassistant/components/sense/config_flow.py | 6 +++++- homeassistant/components/sense/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 15 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json index c3458093943..bb292b2e7b5 100644 --- a/homeassistant/components/emulated_kasa/manifest.json +++ b/homeassistant/components/emulated_kasa/manifest.json @@ -2,7 +2,7 @@ "domain": "emulated_kasa", "name": "Emulated Kasa", "documentation": "https://www.home-assistant.io/integrations/emulated_kasa", - "requirements": ["sense_energy==0.8.1"], + "requirements": ["sense_energy==0.9.0"], "codeowners": ["@kbickar"], "quality_scale": "internal" } diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index 114c64c390b..2d6c0c41e5b 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -14,6 +14,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval @@ -96,7 +97,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): password = entry_data[CONF_PASSWORD] timeout = entry_data[CONF_TIMEOUT] - gateway = ASyncSenseable(api_timeout=timeout, wss_timeout=timeout) + client_session = async_get_clientsession(hass) + + gateway = ASyncSenseable( + api_timeout=timeout, wss_timeout=timeout, client_session=client_session + ) gateway.rate_limit = ACTIVE_UPDATE_RATE try: diff --git a/homeassistant/components/sense/config_flow.py b/homeassistant/components/sense/config_flow.py index f8b8ede6a4c..866c1683b1e 100644 --- a/homeassistant/components/sense/config_flow.py +++ b/homeassistant/components/sense/config_flow.py @@ -6,6 +6,7 @@ import voluptuous as vol from homeassistant import config_entries, core from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT, SENSE_TIMEOUT_EXCEPTIONS from .const import DOMAIN # pylint:disable=unused-import; pylint:disable=unused-import @@ -27,8 +28,11 @@ async def validate_input(hass: core.HomeAssistant, data): Data has the keys from DATA_SCHEMA with values provided by the user. """ timeout = data[CONF_TIMEOUT] + client_session = async_get_clientsession(hass) - gateway = ASyncSenseable(api_timeout=timeout, wss_timeout=timeout) + gateway = ASyncSenseable( + api_timeout=timeout, wss_timeout=timeout, client_session=client_session + ) gateway.rate_limit = ACTIVE_UPDATE_RATE await gateway.authenticate(data[CONF_EMAIL], data[CONF_PASSWORD]) diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index bd132f1f983..57028ccb395 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -2,7 +2,7 @@ "domain": "sense", "name": "Sense", "documentation": "https://www.home-assistant.io/integrations/sense", - "requirements": ["sense_energy==0.8.1"], + "requirements": ["sense_energy==0.9.0"], "codeowners": ["@kbickar"], "config_flow": true, "dhcp": [{"hostname":"sense-*","macaddress":"009D6B*"}, {"hostname":"sense-*","macaddress":"DCEFCA*"}] diff --git a/requirements_all.txt b/requirements_all.txt index 86a15b1288f..a56447ee251 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2010,7 +2010,7 @@ sense-hat==2.2.0 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.8.1 +sense_energy==0.9.0 # homeassistant.components.sentry sentry-sdk==0.20.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9d057cc414a..1cec8c9a3c2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1026,7 +1026,7 @@ scapy==2.4.4 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.8.1 +sense_energy==0.9.0 # homeassistant.components.sentry sentry-sdk==0.20.1 From ea47e5d8af49de4cdaf2908b010207a2536e1238 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 15 Feb 2021 22:02:58 +0100 Subject: [PATCH 453/796] Upgrade sentry-sdk to 0.20.2 (#46590) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 090d19eb2fc..4c823362aee 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,6 +3,6 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==0.20.1"], + "requirements": ["sentry-sdk==0.20.2"], "codeowners": ["@dcramer", "@frenck"] } diff --git a/requirements_all.txt b/requirements_all.txt index a56447ee251..c31abaa6c1f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2013,7 +2013,7 @@ sense-hat==2.2.0 sense_energy==0.9.0 # homeassistant.components.sentry -sentry-sdk==0.20.1 +sentry-sdk==0.20.2 # homeassistant.components.sharkiq sharkiqpy==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1cec8c9a3c2..66faad75ed7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1029,7 +1029,7 @@ scapy==2.4.4 sense_energy==0.9.0 # homeassistant.components.sentry -sentry-sdk==0.20.1 +sentry-sdk==0.20.2 # homeassistant.components.sharkiq sharkiqpy==0.1.8 From 2a7d2868bea58a2571815ce0096474c10459bc1a Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 15 Feb 2021 18:14:27 -0500 Subject: [PATCH 454/796] Use core constants for xiaomi_aqara (#46551) --- homeassistant/components/xiaomi_aqara/__init__.py | 2 +- homeassistant/components/xiaomi_aqara/config_flow.py | 3 +-- homeassistant/components/xiaomi_aqara/const.py | 1 - tests/components/xiaomi_aqara/test_config_flow.py | 12 ++++++------ 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index c5b74e68af5..f54c262abba 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_HOST, CONF_MAC, CONF_PORT, + CONF_PROTOCOL, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback @@ -26,7 +27,6 @@ from homeassistant.util.dt import utcnow from .const import ( CONF_INTERFACE, CONF_KEY, - CONF_PROTOCOL, CONF_SID, DEFAULT_DISCOVERY_RETRY, DOMAIN, diff --git a/homeassistant/components/xiaomi_aqara/config_flow.py b/homeassistant/components/xiaomi_aqara/config_flow.py index 46d4852abae..8028d16f86a 100644 --- a/homeassistant/components/xiaomi_aqara/config_flow.py +++ b/homeassistant/components/xiaomi_aqara/config_flow.py @@ -6,7 +6,7 @@ import voluptuous as vol from xiaomi_gateway import MULTICAST_PORT, XiaomiGateway, XiaomiGatewayDiscovery from homeassistant import config_entries -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT, CONF_PROTOCOL from homeassistant.core import callback from homeassistant.helpers.device_registry import format_mac @@ -14,7 +14,6 @@ from homeassistant.helpers.device_registry import format_mac from .const import ( CONF_INTERFACE, CONF_KEY, - CONF_PROTOCOL, CONF_SID, DEFAULT_DISCOVERY_RETRY, DOMAIN, diff --git a/homeassistant/components/xiaomi_aqara/const.py b/homeassistant/components/xiaomi_aqara/const.py index 1cc3b2d4633..11706cdb6fb 100644 --- a/homeassistant/components/xiaomi_aqara/const.py +++ b/homeassistant/components/xiaomi_aqara/const.py @@ -9,7 +9,6 @@ ZEROCONF_GATEWAY = "lumi-gateway" ZEROCONF_ACPARTNER = "lumi-acpartner" CONF_INTERFACE = "interface" -CONF_PROTOCOL = "protocol" CONF_KEY = "key" CONF_SID = "sid" diff --git a/tests/components/xiaomi_aqara/test_config_flow.py b/tests/components/xiaomi_aqara/test_config_flow.py index 280775a7130..f52f9b8e64d 100644 --- a/tests/components/xiaomi_aqara/test_config_flow.py +++ b/tests/components/xiaomi_aqara/test_config_flow.py @@ -7,7 +7,7 @@ import pytest from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.xiaomi_aqara import config_flow, const -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT, CONF_PROTOCOL ZEROCONF_NAME = "name" ZEROCONF_PROP = "properties" @@ -107,7 +107,7 @@ async def test_config_flow_user_success(hass): CONF_PORT: TEST_PORT, CONF_MAC: TEST_MAC, const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE, - const.CONF_PROTOCOL: TEST_PROTOCOL, + CONF_PROTOCOL: TEST_PROTOCOL, const.CONF_KEY: TEST_KEY, const.CONF_SID: TEST_SID, } @@ -159,7 +159,7 @@ async def test_config_flow_user_multiple_success(hass): CONF_PORT: TEST_PORT, CONF_MAC: TEST_MAC, const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE, - const.CONF_PROTOCOL: TEST_PROTOCOL, + CONF_PROTOCOL: TEST_PROTOCOL, const.CONF_KEY: TEST_KEY, const.CONF_SID: TEST_SID, } @@ -196,7 +196,7 @@ async def test_config_flow_user_no_key_success(hass): CONF_PORT: TEST_PORT, CONF_MAC: TEST_MAC, const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE, - const.CONF_PROTOCOL: TEST_PROTOCOL, + CONF_PROTOCOL: TEST_PROTOCOL, const.CONF_KEY: None, const.CONF_SID: TEST_SID, } @@ -243,7 +243,7 @@ async def test_config_flow_user_host_mac_success(hass): CONF_PORT: TEST_PORT, CONF_MAC: TEST_MAC, const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE, - const.CONF_PROTOCOL: TEST_PROTOCOL, + CONF_PROTOCOL: TEST_PROTOCOL, const.CONF_KEY: None, const.CONF_SID: TEST_SID, } @@ -433,7 +433,7 @@ async def test_zeroconf_success(hass): CONF_PORT: TEST_PORT, CONF_MAC: TEST_MAC, const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE, - const.CONF_PROTOCOL: TEST_PROTOCOL, + CONF_PROTOCOL: TEST_PROTOCOL, const.CONF_KEY: TEST_KEY, const.CONF_SID: TEST_SID, } From 1bb535aa6784fcb1af69749ec8eea2a1207456b9 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 16 Feb 2021 00:03:57 +0000 Subject: [PATCH 455/796] [ci skip] Translation update --- .../components/abode/translations/it.json | 2 +- .../components/aemet/translations/es.json | 22 +++++++++ .../components/aemet/translations/it.json | 22 +++++++++ .../components/aemet/translations/no.json | 22 +++++++++ .../components/airvisual/translations/de.json | 26 ++++++++++- .../components/airvisual/translations/it.json | 2 +- .../ambiclimate/translations/ru.json | 2 +- .../components/asuswrt/translations/es.json | 17 +++++++ .../components/asuswrt/translations/it.json | 45 +++++++++++++++++++ .../components/asuswrt/translations/no.json | 45 +++++++++++++++++++ .../components/august/translations/it.json | 2 +- .../components/awair/translations/it.json | 2 +- .../azure_devops/translations/it.json | 2 +- .../cloudflare/translations/de.json | 7 ++- .../components/deconz/translations/de.json | 1 + .../fireservicerota/translations/it.json | 2 +- .../components/fritzbox/translations/de.json | 10 ++++- .../components/fritzbox/translations/it.json | 2 +- .../fritzbox_callmonitor/translations/de.json | 41 +++++++++++++++++ .../components/homekit/translations/de.json | 21 ++++++++- .../homekit_controller/translations/de.json | 9 ++++ .../components/hyperion/translations/it.json | 2 +- .../components/icloud/translations/it.json | 4 +- .../components/icloud/translations/ru.json | 2 +- .../keenetic_ndms2/translations/es.json | 20 +++++++++ .../keenetic_ndms2/translations/it.json | 36 +++++++++++++++ .../keenetic_ndms2/translations/no.json | 36 +++++++++++++++ .../keenetic_ndms2/translations/ru.json | 36 +++++++++++++++ .../components/konnected/translations/ru.json | 2 +- .../lutron_caseta/translations/ru.json | 19 ++++++++ .../components/mazda/translations/it.json | 2 +- .../components/neato/translations/it.json | 2 +- .../components/nest/translations/it.json | 4 +- .../philips_js/translations/it.json | 24 ++++++++++ .../components/pi_hole/translations/de.json | 6 +++ .../components/plaato/translations/ru.json | 4 +- .../components/plex/translations/it.json | 2 +- .../components/powerwall/translations/it.json | 8 +++- .../components/sharkiq/translations/it.json | 2 +- .../components/shelly/translations/ru.json | 2 +- .../simplisafe/translations/it.json | 4 +- .../components/sonarr/translations/it.json | 4 +- .../components/spotify/translations/it.json | 2 +- .../components/tesla/translations/it.json | 4 ++ .../components/traccar/translations/ru.json | 2 +- .../components/unifi/translations/it.json | 2 +- .../components/withings/translations/it.json | 2 +- .../xiaomi_aqara/translations/ru.json | 2 +- .../xiaomi_miio/translations/en.json | 23 ++++++++-- .../components/zwave/translations/ru.json | 2 +- 50 files changed, 520 insertions(+), 44 deletions(-) create mode 100644 homeassistant/components/aemet/translations/es.json create mode 100644 homeassistant/components/aemet/translations/it.json create mode 100644 homeassistant/components/aemet/translations/no.json create mode 100644 homeassistant/components/asuswrt/translations/es.json create mode 100644 homeassistant/components/asuswrt/translations/it.json create mode 100644 homeassistant/components/asuswrt/translations/no.json create mode 100644 homeassistant/components/fritzbox_callmonitor/translations/de.json create mode 100644 homeassistant/components/keenetic_ndms2/translations/es.json create mode 100644 homeassistant/components/keenetic_ndms2/translations/it.json create mode 100644 homeassistant/components/keenetic_ndms2/translations/no.json create mode 100644 homeassistant/components/keenetic_ndms2/translations/ru.json create mode 100644 homeassistant/components/philips_js/translations/it.json diff --git a/homeassistant/components/abode/translations/it.json b/homeassistant/components/abode/translations/it.json index a3e5aa4d7a8..6cb571df8e5 100644 --- a/homeassistant/components/abode/translations/it.json +++ b/homeassistant/components/abode/translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "La riautenticazione ha avuto successo", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "error": { diff --git a/homeassistant/components/aemet/translations/es.json b/homeassistant/components/aemet/translations/es.json new file mode 100644 index 00000000000..ffe4d524754 --- /dev/null +++ b/homeassistant/components/aemet/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada" + }, + "error": { + "invalid_api_key": "Clave API no v\u00e1lida" + }, + "step": { + "user": { + "data": { + "api_key": "Clave API", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre de la integraci\u00f3n" + }, + "description": "Configurar la integraci\u00f3n de AEMET OpenData. Para generar la clave API, ve a https://opendata.aemet.es/centrodedescargas/altaUsuario", + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/it.json b/homeassistant/components/aemet/translations/it.json new file mode 100644 index 00000000000..112630028b9 --- /dev/null +++ b/homeassistant/components/aemet/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "La posizione \u00e8 gi\u00e0 configurata" + }, + "error": { + "invalid_api_key": "Chiave API non valida" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "latitude": "Latitudine", + "longitude": "Logitudine", + "name": "Nome dell'integrazione" + }, + "description": "Imposta l'integrazione di AEMET OpenData. Per generare la chiave API, vai su https://opendata.aemet.es/centrodedescargas/altaUsuario", + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/no.json b/homeassistant/components/aemet/translations/no.json new file mode 100644 index 00000000000..48cbc9916ca --- /dev/null +++ b/homeassistant/components/aemet/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Plasseringen er allerede konfigurert" + }, + "error": { + "invalid_api_key": "Ugyldig API-n\u00f8kkel" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "name": "Navnet p\u00e5 integrasjonen" + }, + "description": "Sett opp AEMET OpenData-integrasjon. For \u00e5 generere API-n\u00f8kkel, g\u00e5 til https://opendata.aemet.es/centrodedescargas/altaUsuario", + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/de.json b/homeassistant/components/airvisual/translations/de.json index a16b02915ee..6e2a5f60c6f 100644 --- a/homeassistant/components/airvisual/translations/de.json +++ b/homeassistant/components/airvisual/translations/de.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "general_error": "Unerwarteter Fehler", - "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", + "location_not_found": "Standort nicht gefunden" }, "step": { "geography": { @@ -16,8 +17,28 @@ "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad" }, + "description": "Verwende die AirVisual Cloud API, um einen geografischen Standort zu \u00fcberwachen.", "title": "Konfigurieren Sie eine Geografie" }, + "geography_by_coords": { + "data": { + "api_key": "API-Schl\u00fcssel", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad" + }, + "description": "Verwende die AirVisual Cloud API, um einen L\u00e4ngengrad/Breitengrad zu \u00fcberwachen.", + "title": "Konfiguriere einen Standort" + }, + "geography_by_name": { + "data": { + "api_key": "API-Schl\u00fcssel", + "city": "Stadt", + "country": "Land", + "state": "Bundesland" + }, + "description": "Verwende die AirVisual Cloud API, um ein(e) Stadt/Bundesland/Land zu \u00fcberwachen.", + "title": "Konfiguriere einen Standort" + }, "node_pro": { "data": { "ip_address": "Host", @@ -29,7 +50,8 @@ "reauth_confirm": { "data": { "api_key": "API-Key" - } + }, + "title": "AirVisual erneut authentifizieren" }, "user": { "data": { diff --git a/homeassistant/components/airvisual/translations/it.json b/homeassistant/components/airvisual/translations/it.json index be493669a64..3ce45ff1342 100644 --- a/homeassistant/components/airvisual/translations/it.json +++ b/homeassistant/components/airvisual/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La posizione \u00e8 gi\u00e0 configurata o Node/Pro ID sono gi\u00e0 registrati.", - "reauth_successful": "La riautenticazione ha avuto successo" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/ambiclimate/translations/ru.json b/homeassistant/components/ambiclimate/translations/ru.json index 8c8863c0eec..a1948c45d0f 100644 --- a/homeassistant/components/ambiclimate/translations/ru.json +++ b/homeassistant/components/ambiclimate/translations/ru.json @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 **\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435** \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Ambi Climate, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c**. \n(\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 URL \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 {cb_url})", + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 **\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435** \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Ambi Climate, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c**. \n(\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441 \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 {cb_url})", "title": "Ambi Climate" } } diff --git a/homeassistant/components/asuswrt/translations/es.json b/homeassistant/components/asuswrt/translations/es.json new file mode 100644 index 00000000000..3531e0a3cca --- /dev/null +++ b/homeassistant/components/asuswrt/translations/es.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "mode": "Modo", + "name": "Nombre", + "password": "Contrase\u00f1a", + "port": "Puerto" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/it.json b/homeassistant/components/asuswrt/translations/it.json new file mode 100644 index 00000000000..d266cabbed4 --- /dev/null +++ b/homeassistant/components/asuswrt/translations/it.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_host": "Nome host o indirizzo IP non valido", + "pwd_and_ssh": "Fornire solo la password o il file della chiave SSH", + "pwd_or_ssh": "Si prega di fornire la password o il file della chiave SSH", + "ssh_not_file": "File chiave SSH non trovato", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host", + "mode": "Modalit\u00e0", + "name": "Nome", + "password": "Password", + "port": "Porta", + "protocol": "Protocollo di comunicazione da utilizzare", + "ssh_key": "Percorso del file della chiave SSH (invece della password)", + "username": "Nome utente" + }, + "description": "Imposta il parametro richiesto per collegarti al tuo router", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Secondi di attesa prima di considerare un dispositivo lontano", + "dnsmasq": "La posizione nel router dei file dnsmasq.leases", + "interface": "L'interfaccia da cui si desidera ottenere statistiche (ad esempio eth0, eth1, ecc.)", + "require_ip": "I dispositivi devono avere un IP (per la modalit\u00e0 punto di accesso)", + "track_unknown": "Tieni traccia dei dispositivi sconosciuti / non denominati" + }, + "title": "Opzioni AsusWRT" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/no.json b/homeassistant/components/asuswrt/translations/no.json new file mode 100644 index 00000000000..42c9798d495 --- /dev/null +++ b/homeassistant/components/asuswrt/translations/no.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_host": "Ugyldig vertsnavn eller IP-adresse", + "pwd_and_ssh": "Oppgi bare passord eller SSH-n\u00f8kkelfil", + "pwd_or_ssh": "Oppgi passord eller SSH-n\u00f8kkelfil", + "ssh_not_file": "Finner ikke SSH-n\u00f8kkelfilen", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "mode": "Modus", + "name": "Navn", + "password": "Passord", + "port": "Port", + "protocol": "Kommunikasjonsprotokoll som skal brukes", + "ssh_key": "Bane til SSH-n\u00f8kkelfilen (i stedet for passord)", + "username": "Brukernavn" + }, + "description": "Sett \u00f8nsket parameter for \u00e5 koble til ruteren", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Sekunder \u00e5 vente f\u00f8r du vurderer en enhet borte", + "dnsmasq": "Plasseringen i ruteren til dnsmasq.leases-filene", + "interface": "Grensesnittet du vil ha statistikk fra (f.eks. Eth0, eth1 osv.)", + "require_ip": "Enheter m\u00e5 ha IP (for tilgangspunktmodus)", + "track_unknown": "Spor ukjente / ikke-navngitte enheter" + }, + "title": "AsusWRT-alternativer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/it.json b/homeassistant/components/august/translations/it.json index 08332c29d7e..adc9017a275 100644 --- a/homeassistant/components/august/translations/it.json +++ b/homeassistant/components/august/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "L'account \u00e8 gi\u00e0 configurato", - "reauth_successful": "La riautenticazione ha avuto successo" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/awair/translations/it.json b/homeassistant/components/awair/translations/it.json index 085796f9263..cad2b8555a8 100644 --- a/homeassistant/components/awair/translations/it.json +++ b/homeassistant/components/awair/translations/it.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "L'account \u00e8 gi\u00e0 configurato", "no_devices_found": "Nessun dispositivo trovato sulla rete", - "reauth_successful": "La riautenticazione ha avuto successo" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "invalid_access_token": "Token di accesso non valido", diff --git a/homeassistant/components/azure_devops/translations/it.json b/homeassistant/components/azure_devops/translations/it.json index 849e65b933f..4b2f5e0efae 100644 --- a/homeassistant/components/azure_devops/translations/it.json +++ b/homeassistant/components/azure_devops/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "L'account \u00e8 gi\u00e0 configurato", - "reauth_successful": "La riautenticazione ha avuto successo" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/cloudflare/translations/de.json b/homeassistant/components/cloudflare/translations/de.json index d9858b36f55..21118e106bf 100644 --- a/homeassistant/components/cloudflare/translations/de.json +++ b/homeassistant/components/cloudflare/translations/de.json @@ -14,18 +14,21 @@ "records": { "data": { "records": "Datens\u00e4tze" - } + }, + "title": "W\u00e4hle die Records, die aktualisiert werden sollen" }, "user": { "data": { "api_token": "API Token" }, + "description": "F\u00fcr diese Integration ist ein API-Token erforderlich, der mit Zone: Zone: Lesen und Zone: DNS: Bearbeiten f\u00fcr alle Zonen in deinem Konto erstellt wurde.", "title": "Mit Cloudflare verbinden" }, "zone": { "data": { "zone": "Zone" - } + }, + "title": "W\u00e4hle die Zone, die aktualisiert werden soll" } } } diff --git a/homeassistant/components/deconz/translations/de.json b/homeassistant/components/deconz/translations/de.json index d7553652412..75b807b8848 100644 --- a/homeassistant/components/deconz/translations/de.json +++ b/homeassistant/components/deconz/translations/de.json @@ -66,6 +66,7 @@ "remote_button_quadruple_press": "\"{subtype}\" Taste vierfach geklickt", "remote_button_quintuple_press": "\"{subtype}\" Taste f\u00fcnffach geklickt", "remote_button_rotated": "Button gedreht \"{subtype}\".", + "remote_button_rotated_fast": "Button schnell gedreht \"{subtype}\"", "remote_button_rotation_stopped": "Die Tastendrehung \"{subtype}\" wurde gestoppt", "remote_button_short_press": "\"{subtype}\" Taste gedr\u00fcckt", "remote_button_short_release": "\"{subtype}\" Taste losgelassen", diff --git a/homeassistant/components/fireservicerota/translations/it.json b/homeassistant/components/fireservicerota/translations/it.json index 8fc43f294ec..6960b68b2a2 100644 --- a/homeassistant/components/fireservicerota/translations/it.json +++ b/homeassistant/components/fireservicerota/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "L'account \u00e8 gi\u00e0 configurato", - "reauth_successful": "La riautenticazione ha avuto successo" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "create_entry": { "default": "Autenticazione riuscita" diff --git a/homeassistant/components/fritzbox/translations/de.json b/homeassistant/components/fritzbox/translations/de.json index 9b76ad19ff4..8e79076bda6 100644 --- a/homeassistant/components/fritzbox/translations/de.json +++ b/homeassistant/components/fritzbox/translations/de.json @@ -4,7 +4,8 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", - "not_supported": "Verbunden mit AVM FRITZ! Box, kann jedoch keine Smart Home-Ger\u00e4te steuern." + "not_supported": "Verbunden mit AVM FRITZ! Box, kann jedoch keine Smart Home-Ger\u00e4te steuern.", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "invalid_auth": "Ung\u00fcltige Zugangsdaten" @@ -18,6 +19,13 @@ }, "description": "M\u00f6chtest du {name} einrichten?" }, + "reauth_confirm": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "description": "Aktualisiere deine Anmeldeinformationen f\u00fcr {name} ." + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/fritzbox/translations/it.json b/homeassistant/components/fritzbox/translations/it.json index a420b3f6de7..6aba6a007d7 100644 --- a/homeassistant/components/fritzbox/translations/it.json +++ b/homeassistant/components/fritzbox/translations/it.json @@ -5,7 +5,7 @@ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "no_devices_found": "Nessun dispositivo trovato sulla rete", "not_supported": "Collegato a AVM FRITZ!Box ma non \u00e8 in grado di controllare i dispositivi Smart Home.", - "reauth_successful": "La riautenticazione ha avuto successo" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "invalid_auth": "Autenticazione non valida" diff --git a/homeassistant/components/fritzbox_callmonitor/translations/de.json b/homeassistant/components/fritzbox_callmonitor/translations/de.json new file mode 100644 index 00000000000..a26f301a9bd --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/de.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "insufficient_permissions": "Der Benutzer verf\u00fcgt nicht \u00fcber ausreichende Berechtigungen, um auf die Einstellungen der AVM FRITZ!Box und ihre Telefonb\u00fccher zuzugreifen.", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "flow_title": "AVM FRITZ! Box-Anrufmonitor: {name}", + "step": { + "phonebook": { + "data": { + "phonebook": "Telefonbuch" + } + }, + "user": { + "data": { + "host": "Host", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "Die Pr\u00e4fixe sind fehlerhaft, bitte das Format \u00fcberpr\u00fcfen." + }, + "step": { + "init": { + "data": { + "prefixes": "Pr\u00e4fixe (kommagetrennte Liste)" + }, + "title": "Pr\u00e4fixe konfigurieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/de.json b/homeassistant/components/homekit/translations/de.json index 6d69c498bac..88583d9ca80 100644 --- a/homeassistant/components/homekit/translations/de.json +++ b/homeassistant/components/homekit/translations/de.json @@ -4,12 +4,27 @@ "port_name_in_use": "Eine HomeKit Bridge mit demselben Namen oder Port ist bereits vorhanden." }, "step": { + "accessory_mode": { + "data": { + "entity_id": "Entit\u00e4t" + }, + "description": "W\u00e4hle die Entit\u00e4t aus, die aufgenommen werden soll. Im Zubeh\u00f6rmodus ist nur eine einzelne Entit\u00e4t enthalten.", + "title": "W\u00e4hle die Entit\u00e4t aus, die aufgenommen werden soll" + }, + "bridge_mode": { + "data": { + "include_domains": "Einzubeziehende Domains" + }, + "description": "W\u00e4hle die Domains aus, die aufgenommen werden sollen. Alle unterst\u00fctzten Ger\u00e4te innerhalb der Domain werden aufgenommen.", + "title": "W\u00e4hle die Domains aus, die aufgenommen werden sollen" + }, "pairing": { "title": "HomeKit verbinden" }, "user": { "data": { - "include_domains": "Einzubeziehende Domains" + "include_domains": "Einzubeziehende Domains", + "mode": "Modus" }, "title": "HomeKit aktivieren" } @@ -21,6 +36,7 @@ "data": { "safe_mode": "Abgesicherter Modus (nur aktivieren, wenn das Pairing fehlschl\u00e4gt)" }, + "description": "Diese Einstellungen m\u00fcssen nur angepasst werden, wenn HomeKit nicht funktioniert.", "title": "Erweiterte Konfiguration" }, "cameras": { @@ -33,7 +49,8 @@ "data": { "entities": "Entit\u00e4ten", "mode": "Modus" - } + }, + "title": "W\u00e4hle die Entit\u00e4ten aus, die aufgenommen werden sollen" }, "init": { "data": { diff --git a/homeassistant/components/homekit_controller/translations/de.json b/homeassistant/components/homekit_controller/translations/de.json index 7bab8f30574..49586a23634 100644 --- a/homeassistant/components/homekit_controller/translations/de.json +++ b/homeassistant/components/homekit_controller/translations/de.json @@ -18,6 +18,12 @@ }, "flow_title": "HomeKit-Zubeh\u00f6r: {name}", "step": { + "busy_error": { + "title": "Das Ger\u00e4t wird bereits mit einem anderen Controller gekoppelt" + }, + "max_tries_error": { + "title": "Maximale Authentifizierungsversuche \u00fcberschritten" + }, "pair": { "data": { "pairing_code": "Kopplungscode" @@ -25,6 +31,9 @@ "description": "Gib deinen HomeKit-Kopplungscode ein, um dieses Zubeh\u00f6r zu verwenden", "title": "Mit HomeKit Zubeh\u00f6r koppeln" }, + "protocol_error": { + "title": "Fehler bei der Kommunikation mit dem Zubeh\u00f6r" + }, "user": { "data": { "device": "Ger\u00e4t" diff --git a/homeassistant/components/hyperion/translations/it.json b/homeassistant/components/hyperion/translations/it.json index 6fee49ebe14..b03b368d039 100644 --- a/homeassistant/components/hyperion/translations/it.json +++ b/homeassistant/components/hyperion/translations/it.json @@ -8,7 +8,7 @@ "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", - "reauth_successful": "La riautenticazione ha avuto successo" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/icloud/translations/it.json b/homeassistant/components/icloud/translations/it.json index 32931d96a32..cfb18caee1e 100644 --- a/homeassistant/components/icloud/translations/it.json +++ b/homeassistant/components/icloud/translations/it.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "L'account \u00e8 gi\u00e0 configurato", "no_device": "Nessuno dei tuoi dispositivi ha attivato \"Trova il mio iPhone\"", - "reauth_successful": "La riautenticazione ha avuto successo" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "invalid_auth": "Autenticazione non valida", @@ -16,7 +16,7 @@ "password": "Password" }, "description": "La password inserita in precedenza per {username} non funziona pi\u00f9. Aggiorna la tua password per continuare a utilizzare questa integrazione.", - "title": "Reautenticare l'integrazione" + "title": "Autenticare nuovamente l'integrazione" }, "trusted_device": { "data": { diff --git a/homeassistant/components/icloud/translations/ru.json b/homeassistant/components/icloud/translations/ru.json index d977899d902..797637e1010 100644 --- a/homeassistant/components/icloud/translations/ru.json +++ b/homeassistant/components/icloud/translations/ru.json @@ -8,7 +8,7 @@ "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.", "send_verification_code": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f.", - "validate_verification_code": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438 \u043d\u0430\u0447\u043d\u0438\u0442\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0441\u043d\u043e\u0432\u0430." + "validate_verification_code": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437." }, "step": { "reauth": { diff --git a/homeassistant/components/keenetic_ndms2/translations/es.json b/homeassistant/components/keenetic_ndms2/translations/es.json new file mode 100644 index 00000000000..6b8af4f98af --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada" + }, + "error": { + "cannot_connect": "Fallo de conexi\u00f3n" + }, + "step": { + "user": { + "data": { + "name": "Nombre", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/it.json b/homeassistant/components/keenetic_ndms2/translations/it.json new file mode 100644 index 00000000000..e5a705d14b8 --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/it.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nome", + "password": "Password", + "port": "Porta", + "username": "Nome utente" + }, + "title": "Configurare il router Keenetic NDMS2" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "consider_home": "Considerare in casa nell'intervallo di", + "include_arp": "Usa i dati ARP (ignorati se vengono utilizzati i dati dell'hotspot)", + "include_associated": "Usa i dati delle associazioni WiFi AP (ignorati se si usano i dati dell'hotspot)", + "interfaces": "Scegli le interfacce da scansionare", + "scan_interval": "Intervallo di scansione", + "try_hotspot": "Utilizza i dati \"ip hotspot\" (pi\u00f9 accurato)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/no.json b/homeassistant/components/keenetic_ndms2/translations/no.json new file mode 100644 index 00000000000..6ad2805eb3d --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/no.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "name": "Navn", + "password": "Passord", + "port": "Port", + "username": "Brukernavn" + }, + "title": "Sett opp Keenetic NDMS2 Router" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "consider_home": "Vurder hjemmeintervall", + "include_arp": "Bruk ARP-data (ignorert hvis hotspot-data brukes)", + "include_associated": "Bruk WiFi AP-tilknytningsdata (ignoreres hvis hotspot-data brukes)", + "interfaces": "Velg grensesnitt for \u00e5 skanne", + "scan_interval": "Skanneintervall", + "try_hotspot": "Bruk 'ip hotspot'-data (mest n\u00f8yaktig)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/ru.json b/homeassistant/components/keenetic_ndms2/translations/ru.json new file mode 100644 index 00000000000..bfd7f6407e7 --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/ru.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 Keenetic NDMS2" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "consider_home": "\u0412\u0440\u0435\u043c\u044f \u043e\u0442 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0433\u043e \u0441\u0435\u0430\u043d\u0441\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c (\u0441\u0435\u043a.), \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u041d\u0435 \u0434\u043e\u043c\u0430\"", + "include_arp": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 ARP (\u0438\u0433\u043d\u043e\u0440\u0438\u0440\u0443\u044e\u0442\u0441\u044f, \u0435\u0441\u043b\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0434\u0430\u043d\u043d\u044b\u0435 \u0442\u043e\u0447\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430)", + "include_associated": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u0442\u043e\u0447\u0435\u043a \u0434\u043e\u0441\u0442\u0443\u043f\u0430 WiFi (\u0438\u0433\u043d\u043e\u0440\u0438\u0440\u0443\u044e\u0442\u0441\u044f, \u0435\u0441\u043b\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0434\u0430\u043d\u043d\u044b\u0435 hotspot)", + "interfaces": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u044b \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f", + "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f", + "try_hotspot": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 'ip hotspot' (\u043d\u0430\u0438\u0431\u043e\u043b\u0435\u0435 \u0442\u043e\u0447\u043d\u044b\u0435)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/ru.json b/homeassistant/components/konnected/translations/ru.json index 931f0802dc0..4357c924572 100644 --- a/homeassistant/components/konnected/translations/ru.json +++ b/homeassistant/components/konnected/translations/ru.json @@ -32,7 +32,7 @@ "not_konn_panel": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Konnected.io \u043d\u0435 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u043d\u043e." }, "error": { - "bad_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 URL \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0445\u043e\u0441\u0442\u0430 API." + "bad_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 Override API." }, "step": { "options_binary": { diff --git a/homeassistant/components/lutron_caseta/translations/ru.json b/homeassistant/components/lutron_caseta/translations/ru.json index edda7af8e9a..f54057f464e 100644 --- a/homeassistant/components/lutron_caseta/translations/ru.json +++ b/homeassistant/components/lutron_caseta/translations/ru.json @@ -42,6 +42,25 @@ "group_1_button_2": "\u041f\u0435\u0440\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u0432\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "group_2_button_1": "\u0412\u0442\u043e\u0440\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "group_2_button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u0432\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "lower": "\u041e\u043f\u0443\u0441\u0442\u0438\u0442\u044c", + "lower_1": "\u041e\u043f\u0443\u0441\u0442\u0438\u0442\u044c 1", + "lower_2": "\u041e\u043f\u0443\u0441\u0442\u0438\u0442\u044c 2", + "lower_3": "\u041e\u043f\u0443\u0441\u0442\u0438\u0442\u044c 3", + "lower_4": "\u041e\u043f\u0443\u0441\u0442\u0438\u0442\u044c 4", + "lower_all": "\u041e\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0432\u0441\u0435", + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "open_1": "\u041e\u0442\u043a\u0440\u044b\u0442\u044c 1", + "open_2": "\u041e\u0442\u043a\u0440\u044b\u0442\u044c 2", + "open_3": "\u041e\u0442\u043a\u0440\u044b\u0442\u044c 3", + "open_4": "\u041e\u0442\u043a\u0440\u044b\u0442\u044c 4", + "open_all": "\u041e\u0442\u043a\u0440\u044b\u0442\u044c \u0432\u0441\u0435", + "raise": "\u041f\u043e\u0434\u043d\u044f\u0442\u044c", + "raise_1": "\u041f\u043e\u0434\u043d\u044f\u0442\u044c 1", + "raise_2": "\u041f\u043e\u0434\u043d\u044f\u0442\u044c 2", + "raise_3": "\u041f\u043e\u0434\u043d\u044f\u0442\u044c 3", + "raise_4": "\u041f\u043e\u0434\u043d\u044f\u0442\u044c 4", + "raise_all": "\u041f\u043e\u0434\u043d\u044f\u0442\u044c \u0432\u0441\u0435", "stop": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c (\u043b\u044e\u0431\u0438\u043c\u0430\u044f)", "stop_1": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c 1", "stop_2": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c 2", diff --git a/homeassistant/components/mazda/translations/it.json b/homeassistant/components/mazda/translations/it.json index 5eb995a4dfb..d5a2796ed18 100644 --- a/homeassistant/components/mazda/translations/it.json +++ b/homeassistant/components/mazda/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "L'account \u00e8 gi\u00e0 configurato", - "reauth_successful": "La riautenticazione ha avuto successo" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "account_locked": "Account bloccato. Per favore riprova pi\u00f9 tardi.", diff --git a/homeassistant/components/neato/translations/it.json b/homeassistant/components/neato/translations/it.json index 95866e918c6..b559c23bb1a 100644 --- a/homeassistant/components/neato/translations/it.json +++ b/homeassistant/components/neato/translations/it.json @@ -6,7 +6,7 @@ "invalid_auth": "Autenticazione non valida", "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": "La riautenticazione ha avuto successo" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "create_entry": { "default": "Autenticazione riuscita" diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index 376437d20f0..84c04049946 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -5,7 +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": "La riautenticazione ha avuto successo", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", "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." }, @@ -38,7 +38,7 @@ }, "reauth_confirm": { "description": "L'integrazione di Nest deve autenticare nuovamente il tuo account", - "title": "Reautenticare l'integrazione" + "title": "Autenticare nuovamente l'integrazione" } } }, diff --git a/homeassistant/components/philips_js/translations/it.json b/homeassistant/components/philips_js/translations/it.json new file mode 100644 index 00000000000..216248d8eac --- /dev/null +++ b/homeassistant/components/philips_js/translations/it.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "api_version": "Versione API", + "host": "Host" + } + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "Si richiede l'accensione del dispositivo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/de.json b/homeassistant/components/pi_hole/translations/de.json index 34198fcfebe..6d9518490d5 100644 --- a/homeassistant/components/pi_hole/translations/de.json +++ b/homeassistant/components/pi_hole/translations/de.json @@ -7,6 +7,11 @@ "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { + "api_key": { + "data": { + "api_key": "API-Schl\u00fcssel" + } + }, "user": { "data": { "api_key": "API-Schl\u00fcssel", @@ -15,6 +20,7 @@ "name": "Name", "port": "Port", "ssl": "Nutzt ein SSL-Zertifikat", + "statistics_only": "Nur Statistiken", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" } } diff --git a/homeassistant/components/plaato/translations/ru.json b/homeassistant/components/plaato/translations/ru.json index befce3d7e84..99e1bf94e0d 100644 --- a/homeassistant/components/plaato/translations/ru.json +++ b/homeassistant/components/plaato/translations/ru.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f Webhook-\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Plaato Airlock.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Plaato {device_type} **{device_name}** \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "invalid_webhook_device": "\u0412\u044b \u0432\u044b\u0431\u0440\u0430\u043b\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0443 \u0434\u0430\u043d\u043d\u044b\u0445 \u043d\u0430 Webhook. \u042d\u0442\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0448\u043b\u044e\u0437\u0430.", @@ -28,7 +28,7 @@ "device_type": "\u0422\u0438\u043f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Plaato" }, "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?", - "title": "Plaato Airlock" + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 Plaato" }, "webhook": { "description": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Plaato Airlock.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", diff --git a/homeassistant/components/plex/translations/it.json b/homeassistant/components/plex/translations/it.json index f470c1bc163..f1ec23e2736 100644 --- a/homeassistant/components/plex/translations/it.json +++ b/homeassistant/components/plex/translations/it.json @@ -4,7 +4,7 @@ "all_configured": "Tutti i server collegati sono gi\u00e0 configurati", "already_configured": "Questo server Plex \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", - "reauth_successful": "La riautenticazione ha avuto successo", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", "token_request_timeout": "Timeout per l'ottenimento del token", "unknown": "Errore imprevisto" }, diff --git a/homeassistant/components/powerwall/translations/it.json b/homeassistant/components/powerwall/translations/it.json index 376168f8616..d136c385aca 100644 --- a/homeassistant/components/powerwall/translations/it.json +++ b/homeassistant/components/powerwall/translations/it.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto", "wrong_version": "Il tuo powerwall utilizza una versione del software non supportata. Si prega di considerare l'aggiornamento o la segnalazione di questo problema in modo che possa essere risolto." }, @@ -12,8 +14,10 @@ "step": { "user": { "data": { - "ip_address": "Indirizzo IP" + "ip_address": "Indirizzo IP", + "password": "Password" }, + "description": "La password di solito \u00e8 costituita dagli ultimi 5 caratteri del numero di serie per il Backup Gateway e pu\u00f2 essere trovata nell'app Telsa; oppure dagli ultimi 5 caratteri della password trovata all'interno della porta per il Backup Gateway 2.", "title": "Connessione al Powerwall" } } diff --git a/homeassistant/components/sharkiq/translations/it.json b/homeassistant/components/sharkiq/translations/it.json index 4f7940cb5fc..cfba2066bfc 100644 --- a/homeassistant/components/sharkiq/translations/it.json +++ b/homeassistant/components/sharkiq/translations/it.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "L'account \u00e8 gi\u00e0 configurato", "cannot_connect": "Impossibile connettersi", - "reauth_successful": "La riautenticazione ha avuto successo", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", "unknown": "Errore imprevisto" }, "error": { diff --git a/homeassistant/components/shelly/translations/ru.json b/homeassistant/components/shelly/translations/ru.json index 5a3a40ac9f8..ad5a288cf91 100644 --- a/homeassistant/components/shelly/translations/ru.json +++ b/homeassistant/components/shelly/translations/ru.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "confirm_discovery": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c {model} ({host}) ?\n\n\u041f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0449\u0438\u0435 \u043e\u0442 \u0431\u0430\u0442\u0430\u0440\u0435\u0438, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u0432\u0435\u0441\u0442\u0438 \u0438\u0437 \u0441\u043f\u044f\u0449\u0435\u0433\u043e \u0440\u0435\u0436\u0438\u043c\u0430, \u043d\u0430\u0436\u0430\u0432 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435." + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c {model} ({host})?\n\n\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0442 \u043e\u0442 \u0431\u0430\u0442\u0430\u0440\u0435\u0438 \u0438 \u0437\u0430\u0449\u0438\u0449\u0435\u043d\u044b \u043f\u0430\u0440\u043e\u043b\u0435\u043c, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u0442\u044c, \u043f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443.\n\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0442 \u043e\u0442 \u0431\u0430\u0442\u0430\u0440\u0435\u0438 \u0438 \u043d\u0435 \u0437\u0430\u0449\u0438\u0449\u0435\u043d\u044b \u043f\u0430\u0440\u043e\u043b\u0435\u043c, \u0431\u0443\u0434\u0443\u0442 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u044b, \u043a\u043e\u0433\u0434\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u044b\u0439\u0434\u0435\u0442 \u0438\u0437 \u0441\u043f\u044f\u0449\u0435\u0433\u043e \u0440\u0435\u0436\u0438\u043c\u0430. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0436\u0430\u0442\u044c \u043d\u0430 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435, \u0442\u0435\u043c \u0441\u0430\u043c\u044b\u043c \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u0432 \u0435\u0433\u043e, \u043b\u0438\u0431\u043e \u0434\u043e\u0436\u0434\u0430\u0442\u044c\u0441\u044f \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0433\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." }, "credentials": { "data": { diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json index fdd69b39efc..b5ce2a26702 100644 --- a/homeassistant/components/simplisafe/translations/it.json +++ b/homeassistant/components/simplisafe/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Questo account SimpliSafe \u00e8 gi\u00e0 in uso.", - "reauth_successful": "La riautenticazione ha avuto successo" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "identifier_exists": "Account gi\u00e0 registrato", @@ -20,7 +20,7 @@ "password": "Password" }, "description": "Il token di accesso \u00e8 scaduto o \u00e8 stato revocato. Inserisci la tua password per ricollegare il tuo account.", - "title": "Reautenticare l'integrazione" + "title": "Autenticare nuovamente l'integrazione" }, "user": { "data": { diff --git a/homeassistant/components/sonarr/translations/it.json b/homeassistant/components/sonarr/translations/it.json index 1a383201ab1..71a4cca729e 100644 --- a/homeassistant/components/sonarr/translations/it.json +++ b/homeassistant/components/sonarr/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", - "reauth_successful": "La riautenticazione ha avuto successo", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", "unknown": "Errore imprevisto" }, "error": { @@ -13,7 +13,7 @@ "step": { "reauth_confirm": { "description": "L'integrazione di Sonarr deve essere nuovamente autenticata manualmente con l'API Sonarr ospitata su: {host}", - "title": "Reautenticare l'integrazione" + "title": "Autenticare nuovamente l'integrazione" }, "user": { "data": { diff --git a/homeassistant/components/spotify/translations/it.json b/homeassistant/components/spotify/translations/it.json index 595e13865e1..28d821c81f1 100644 --- a/homeassistant/components/spotify/translations/it.json +++ b/homeassistant/components/spotify/translations/it.json @@ -15,7 +15,7 @@ }, "reauth_confirm": { "description": "L'integrazione di Spotify deve essere nuovamente autenticata con Spotify per l'account: {account}", - "title": "Reautenticare l'integrazione" + "title": "Autenticare nuovamente l'integrazione" } } }, diff --git a/homeassistant/components/tesla/translations/it.json b/homeassistant/components/tesla/translations/it.json index a316b41c29c..3a137da78f1 100644 --- a/homeassistant/components/tesla/translations/it.json +++ b/homeassistant/components/tesla/translations/it.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, "error": { "already_configured": "L'account \u00e8 gi\u00e0 configurato", "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/traccar/translations/ru.json b/homeassistant/components/traccar/translations/ru.json index b35b1e74e1e..dc4cd2cde11 100644 --- a/homeassistant/components/traccar/translations/ru.json +++ b/homeassistant/components/traccar/translations/ru.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f Webhook-\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Traccar.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Traccar.\n\n\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 URL-\u0430\u0434\u0440\u0435\u0441 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438: `{webhook_url}`\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/unifi/translations/it.json b/homeassistant/components/unifi/translations/it.json index d50018227c5..f5311f538c1 100644 --- a/homeassistant/components/unifi/translations/it.json +++ b/homeassistant/components/unifi/translations/it.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Il sito del Controller \u00e8 gi\u00e0 configurato", "configuration_updated": "Configurazione aggiornata.", - "reauth_successful": "La riautenticazione ha avuto successo" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "faulty_credentials": "Autenticazione non valida", diff --git a/homeassistant/components/withings/translations/it.json b/homeassistant/components/withings/translations/it.json index 85baeb1f0e0..8fb4dee9918 100644 --- a/homeassistant/components/withings/translations/it.json +++ b/homeassistant/components/withings/translations/it.json @@ -26,7 +26,7 @@ }, "reauth": { "description": "Il profilo \"{profile}\" deve essere autenticato nuovamente per continuare a ricevere i dati Withings.", - "title": "Reautenticare l'integrazione" + "title": "Autenticare nuovamente l'integrazione" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/ru.json b/homeassistant/components/xiaomi_aqara/translations/ru.json index 96da0a24074..4ede8019a4f 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ru.json +++ b/homeassistant/components/xiaomi_aqara/translations/ru.json @@ -18,7 +18,7 @@ "data": { "select_ip": "IP-\u0430\u0434\u0440\u0435\u0441" }, - "description": "\u0417\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0435\u0449\u0451 \u0440\u0430\u0437, \u0435\u0441\u043b\u0438 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0435\u0449\u0451 \u043e\u0434\u0438\u043d \u0448\u043b\u044e\u0437", + "description": "\u0417\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0435\u0449\u0451 \u0440\u0430\u0437, \u0435\u0441\u043b\u0438 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0435\u0449\u0451 \u043e\u0434\u0438\u043d \u0448\u043b\u044e\u0437.", "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 Xiaomi Aqara" }, "settings": { diff --git a/homeassistant/components/xiaomi_miio/translations/en.json b/homeassistant/components/xiaomi_miio/translations/en.json index c8ac63d1ea7..fe95af5e06c 100644 --- a/homeassistant/components/xiaomi_miio/translations/en.json +++ b/homeassistant/components/xiaomi_miio/translations/en.json @@ -6,18 +6,35 @@ }, "error": { "cannot_connect": "Failed to connect", + "no_device_selected": "No device selected, please select one device.", "unknown_device": "The device model is not known, not able to setup the device using config flow." }, "flow_title": "Xiaomi Miio: {name}", "step": { "device": { "data": { - "host": "IP Address", - "name": "Name of the device", - "token": "API Token" + "host": "IP Address", + "name": "Name of the device", + "token": "API Token" }, "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.", "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + }, + "gateway": { + "data": { + "host": "IP Address", + "name": "Name of the Gateway", + "token": "API Token" + }, + "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.", + "title": "Connect to a Xiaomi Gateway" + }, + "user": { + "data": { + "gateway": "Connect to a Xiaomi Gateway" + }, + "description": "Select to which device you want to connect.", + "title": "Xiaomi Miio" } } } diff --git a/homeassistant/components/zwave/translations/ru.json b/homeassistant/components/zwave/translations/ru.json index 515a47d87a6..5188bb8330e 100644 --- a/homeassistant/components/zwave/translations/ru.json +++ b/homeassistant/components/zwave/translations/ru.json @@ -13,7 +13,7 @@ "network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438)", "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" }, - "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", + "description": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \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 \u0432\u043c\u0435\u0441\u0442\u043e \u043d\u0435\u0451 Z-Wave JS.\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", "title": "Z-Wave" } } From 3c26235e784177d25452f90310d16b114863545d Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Tue, 16 Feb 2021 03:20:45 +0100 Subject: [PATCH 456/796] Bump tuyaha to 0.0.10 and fix set temperature issues (#45732) --- .coveragerc | 9 +- homeassistant/components/tuya/climate.py | 10 +- homeassistant/components/tuya/config_flow.py | 31 ++- homeassistant/components/tuya/const.py | 2 + homeassistant/components/tuya/manifest.json | 2 +- homeassistant/components/tuya/strings.json | 2 + .../components/tuya/translations/en.json | 2 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tuya/common.py | 75 ++++++ tests/components/tuya/test_config_flow.py | 233 +++++++++++++++--- 11 files changed, 318 insertions(+), 52 deletions(-) create mode 100644 tests/components/tuya/common.py diff --git a/.coveragerc b/.coveragerc index e64bfab280a..df9fbac4c58 100644 --- a/.coveragerc +++ b/.coveragerc @@ -993,7 +993,14 @@ omit = homeassistant/components/transmission/const.py homeassistant/components/transmission/errors.py homeassistant/components/travisci/sensor.py - homeassistant/components/tuya/* + homeassistant/components/tuya/__init__.py + homeassistant/components/tuya/climate.py + homeassistant/components/tuya/const.py + homeassistant/components/tuya/cover.py + homeassistant/components/tuya/fan.py + homeassistant/components/tuya/light.py + homeassistant/components/tuya/scene.py + homeassistant/components/tuya/switch.py homeassistant/components/twentemilieu/const.py homeassistant/components/twentemilieu/sensor.py homeassistant/components/twilio_call/notify.py diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 0502273818c..73ba69da797 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -33,7 +33,9 @@ from .const import ( CONF_CURR_TEMP_DIVIDER, CONF_MAX_TEMP, CONF_MIN_TEMP, + CONF_SET_TEMP_DIVIDED, CONF_TEMP_DIVIDER, + CONF_TEMP_STEP_OVERRIDE, DOMAIN, SIGNAL_CONFIG_ENTITY, TUYA_DATA, @@ -103,6 +105,8 @@ class TuyaClimateEntity(TuyaDevice, ClimateEntity): self.operations = [HVAC_MODE_OFF] self._has_operation = False self._def_hvac_mode = HVAC_MODE_AUTO + self._set_temp_divided = True + self._temp_step_override = None self._min_temp = None self._max_temp = None @@ -117,6 +121,8 @@ class TuyaClimateEntity(TuyaDevice, ClimateEntity): self._tuya.set_unit("FAHRENHEIT" if unit == TEMP_FAHRENHEIT else "CELSIUS") self._tuya.temp_divider = config.get(CONF_TEMP_DIVIDER, 0) self._tuya.curr_temp_divider = config.get(CONF_CURR_TEMP_DIVIDER, 0) + self._set_temp_divided = config.get(CONF_SET_TEMP_DIVIDED, True) + self._temp_step_override = config.get(CONF_TEMP_STEP_OVERRIDE) min_temp = config.get(CONF_MIN_TEMP, 0) max_temp = config.get(CONF_MAX_TEMP, 0) if min_temp >= max_temp: @@ -189,6 +195,8 @@ class TuyaClimateEntity(TuyaDevice, ClimateEntity): @property def target_temperature_step(self): """Return the supported step of target temperature.""" + if self._temp_step_override: + return self._temp_step_override return self._tuya.target_temperature_step() @property @@ -204,7 +212,7 @@ class TuyaClimateEntity(TuyaDevice, ClimateEntity): def set_temperature(self, **kwargs): """Set new target temperature.""" if ATTR_TEMPERATURE in kwargs: - self._tuya.set_temperature(kwargs[ATTR_TEMPERATURE]) + self._tuya.set_temperature(kwargs[ATTR_TEMPERATURE], self._set_temp_divided) def set_fan_mode(self, fan_mode): """Set new target fan mode.""" diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index 5d22a83e03e..b705c2c7c36 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -23,7 +23,6 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -# pylint:disable=unused-import from .const import ( CONF_BRIGHTNESS_RANGE_MODE, CONF_COUNTRYCODE, @@ -35,17 +34,19 @@ from .const import ( CONF_MIN_TEMP, CONF_QUERY_DEVICE, CONF_QUERY_INTERVAL, + CONF_SET_TEMP_DIVIDED, CONF_SUPPORT_COLOR, CONF_TEMP_DIVIDER, + CONF_TEMP_STEP_OVERRIDE, CONF_TUYA_MAX_COLTEMP, DEFAULT_DISCOVERY_INTERVAL, DEFAULT_QUERY_INTERVAL, DEFAULT_TUYA_MAX_COLTEMP, - DOMAIN, TUYA_DATA, TUYA_PLATFORMS, TUYA_TYPE_NOT_QUERY, ) +from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) @@ -66,6 +67,7 @@ ERROR_DEV_NOT_FOUND = "dev_not_found" RESULT_AUTH_FAILED = "invalid_auth" RESULT_CONN_ERROR = "cannot_connect" +RESULT_SINGLE_INSTANCE = "single_instance_allowed" RESULT_SUCCESS = "success" RESULT_LOG_MESSAGE = { @@ -123,7 +125,7 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + return self.async_abort(reason=RESULT_SINGLE_INSTANCE) errors = {} @@ -257,7 +259,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if self.config_entry.state != config_entries.ENTRY_STATE_LOADED: _LOGGER.error("Tuya integration not yet loaded") - return self.async_abort(reason="cannot_connect") + return self.async_abort(reason=RESULT_CONN_ERROR) if user_input is not None: dev_ids = user_input.get(CONF_LIST_DEVICES) @@ -323,11 +325,14 @@ class OptionsFlowHandler(config_entries.OptionsFlow): def _get_device_schema(self, device_type, curr_conf, device): """Return option schema for device.""" + if device_type != device.device_type(): + return None + schema = None if device_type == "light": - return self._get_light_schema(curr_conf, device) - if device_type == "climate": - return self._get_climate_schema(curr_conf, device) - return None + schema = self._get_light_schema(curr_conf, device) + elif device_type == "climate": + schema = self._get_climate_schema(curr_conf, device) + return schema @staticmethod def _get_light_schema(curr_conf, device): @@ -374,6 +379,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): """Create option schema for climate device.""" unit = device.temperature_unit() def_unit = TEMP_FAHRENHEIT if unit == "FAHRENHEIT" else TEMP_CELSIUS + supported_steps = device.supported_temperature_steps() + default_step = device.target_temperature_step() config_schema = vol.Schema( { @@ -389,6 +396,14 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_CURR_TEMP_DIVIDER, default=curr_conf.get(CONF_CURR_TEMP_DIVIDER, 0), ): vol.All(vol.Coerce(int), vol.Clamp(min=0)), + vol.Optional( + CONF_SET_TEMP_DIVIDED, + default=curr_conf.get(CONF_SET_TEMP_DIVIDED, True), + ): bool, + vol.Optional( + CONF_TEMP_STEP_OVERRIDE, + default=curr_conf.get(CONF_TEMP_STEP_OVERRIDE, default_step), + ): vol.In(supported_steps), vol.Optional( CONF_MIN_TEMP, default=curr_conf.get(CONF_MIN_TEMP, 0), diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 4f4ec342b15..646bcc077cf 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -10,8 +10,10 @@ CONF_MIN_KELVIN = "min_kelvin" CONF_MIN_TEMP = "min_temp" CONF_QUERY_DEVICE = "query_device" CONF_QUERY_INTERVAL = "query_interval" +CONF_SET_TEMP_DIVIDED = "set_temp_divided" CONF_SUPPORT_COLOR = "support_color" CONF_TEMP_DIVIDER = "temp_divider" +CONF_TEMP_STEP_OVERRIDE = "temp_step_override" CONF_TUYA_MAX_COLTEMP = "tuya_max_coltemp" DEFAULT_DISCOVERY_INTERVAL = 605 diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 7481e56f00a..e72c7c63112 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.9"], + "requirements": ["tuyaha==0.0.10"], "codeowners": ["@ollo69"], "config_flow": true } diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index 444ff0b5c21..23958349b66 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -49,6 +49,8 @@ "unit_of_measurement": "Temperature unit used by device", "temp_divider": "Temperature values divider (0 = use default)", "curr_temp_divider": "Current Temperature value divider (0 = use default)", + "set_temp_divided": "Use divided Temperature value for set temperature command", + "temp_step_override": "Target Temperature step", "min_temp": "Min target temperature (use min and max = 0 for default)", "max_temp": "Max target temperature (use min and max = 0 for default)" } diff --git a/homeassistant/components/tuya/translations/en.json b/homeassistant/components/tuya/translations/en.json index 46756b18cb8..7204d6072a9 100644 --- a/homeassistant/components/tuya/translations/en.json +++ b/homeassistant/components/tuya/translations/en.json @@ -40,8 +40,10 @@ "max_temp": "Max target temperature (use min and max = 0 for default)", "min_kelvin": "Min color temperature supported in kelvin", "min_temp": "Min target temperature (use min and max = 0 for default)", + "set_temp_divided": "Use divided Temperature value for set temperature command", "support_color": "Force color support", "temp_divider": "Temperature values divider (0 = use default)", + "temp_step_override": "Target Temperature step", "tuya_max_coltemp": "Max color temperature reported by device", "unit_of_measurement": "Temperature unit used by device" }, diff --git a/requirements_all.txt b/requirements_all.txt index c31abaa6c1f..b1a18147b25 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2217,7 +2217,7 @@ tp-connected==0.0.4 transmissionrpc==0.11 # homeassistant.components.tuya -tuyaha==0.0.9 +tuyaha==0.0.10 # homeassistant.components.twentemilieu twentemilieu==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 66faad75ed7..bc1e86fc61f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1126,7 +1126,7 @@ total_connect_client==0.55 transmissionrpc==0.11 # homeassistant.components.tuya -tuyaha==0.0.9 +tuyaha==0.0.10 # homeassistant.components.twentemilieu twentemilieu==0.3.0 diff --git a/tests/components/tuya/common.py b/tests/components/tuya/common.py new file mode 100644 index 00000000000..8dcef136b7f --- /dev/null +++ b/tests/components/tuya/common.py @@ -0,0 +1,75 @@ +"""Test code shared between test files.""" + +from tuyaha.devices import climate, light, switch + +CLIMATE_ID = "1" +CLIMATE_DATA = { + "data": {"state": "true", "temp_unit": climate.UNIT_CELSIUS}, + "id": CLIMATE_ID, + "ha_type": "climate", + "name": "TestClimate", + "dev_type": "climate", +} + +LIGHT_ID = "2" +LIGHT_DATA = { + "data": {"state": "true"}, + "id": LIGHT_ID, + "ha_type": "light", + "name": "TestLight", + "dev_type": "light", +} + +SWITCH_ID = "3" +SWITCH_DATA = { + "data": {"state": True}, + "id": SWITCH_ID, + "ha_type": "switch", + "name": "TestSwitch", + "dev_type": "switch", +} + +LIGHT_ID_FAKE1 = "9998" +LIGHT_DATA_FAKE1 = { + "data": {"state": "true"}, + "id": LIGHT_ID_FAKE1, + "ha_type": "light", + "name": "TestLightFake1", + "dev_type": "light", +} + +LIGHT_ID_FAKE2 = "9999" +LIGHT_DATA_FAKE2 = { + "data": {"state": "true"}, + "id": LIGHT_ID_FAKE2, + "ha_type": "light", + "name": "TestLightFake2", + "dev_type": "light", +} + +TUYA_DEVICES = [ + climate.TuyaClimate(CLIMATE_DATA, None), + light.TuyaLight(LIGHT_DATA, None), + switch.TuyaSwitch(SWITCH_DATA, None), + light.TuyaLight(LIGHT_DATA_FAKE1, None), + light.TuyaLight(LIGHT_DATA_FAKE2, None), +] + + +class MockTuya: + """Mock for Tuya devices.""" + + def get_all_devices(self): + """Return all configured devices.""" + return TUYA_DEVICES + + def get_device_by_id(self, dev_id): + """Return configured device with dev id.""" + if dev_id == LIGHT_ID_FAKE1: + return None + if dev_id == LIGHT_ID_FAKE2: + return switch.TuyaSwitch(SWITCH_DATA, None) + for device in TUYA_DEVICES: + if device.object_id() == dev_id: + return device + return None diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index 0055b451e1a..012886fe3b8 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -2,11 +2,47 @@ from unittest.mock import Mock, patch import pytest +from tuyaha.devices.climate import STEP_HALVES from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException -from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.tuya.const import CONF_COUNTRYCODE, DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.tuya.config_flow import ( + CONF_LIST_DEVICES, + ERROR_DEV_MULTI_TYPE, + ERROR_DEV_NOT_CONFIG, + ERROR_DEV_NOT_FOUND, + RESULT_AUTH_FAILED, + RESULT_CONN_ERROR, + RESULT_SINGLE_INSTANCE, +) +from homeassistant.components.tuya.const import ( + CONF_BRIGHTNESS_RANGE_MODE, + CONF_COUNTRYCODE, + CONF_CURR_TEMP_DIVIDER, + CONF_DISCOVERY_INTERVAL, + CONF_MAX_KELVIN, + CONF_MAX_TEMP, + CONF_MIN_KELVIN, + CONF_MIN_TEMP, + CONF_QUERY_DEVICE, + CONF_QUERY_INTERVAL, + CONF_SET_TEMP_DIVIDED, + CONF_SUPPORT_COLOR, + CONF_TEMP_DIVIDER, + CONF_TEMP_STEP_OVERRIDE, + CONF_TUYA_MAX_COLTEMP, + DOMAIN, + TUYA_DATA, +) +from homeassistant.const import ( + CONF_PASSWORD, + CONF_PLATFORM, + CONF_UNIT_OF_MEASUREMENT, + CONF_USERNAME, + TEMP_CELSIUS, +) + +from .common import CLIMATE_ID, LIGHT_ID, LIGHT_ID_FAKE1, LIGHT_ID_FAKE2, MockTuya from tests.common import MockConfigEntry @@ -30,9 +66,15 @@ def tuya_fixture() -> Mock: yield tuya +@pytest.fixture(name="tuya_setup", autouse=True) +def tuya_setup_fixture(): + """Mock tuya entry setup.""" + with patch("homeassistant.components.tuya.async_setup_entry", return_value=True): + yield + + async def test_user(hass, tuya): """Test user config.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -40,15 +82,10 @@ async def test_user(hass, tuya): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.tuya.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.tuya.async_setup_entry", return_value=True - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=TUYA_USER_DATA - ) - await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=TUYA_USER_DATA + ) + await hass.async_block_till_done() assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == USERNAME @@ -58,26 +95,15 @@ async def test_user(hass, tuya): assert result["data"][CONF_PLATFORM] == TUYA_PLATFORM assert not result["result"].unique_id - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - async def test_import(hass, tuya): """Test import step.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - with patch( - "homeassistant.components.tuya.async_setup", - return_value=True, - ) as mock_setup, patch( - "homeassistant.components.tuya.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=TUYA_USER_DATA, - ) - await hass.async_block_till_done() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=TUYA_USER_DATA, + ) + await hass.async_block_till_done() assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == USERNAME @@ -87,9 +113,6 @@ async def test_import(hass, tuya): assert result["data"][CONF_PLATFORM] == TUYA_PLATFORM assert not result["result"].unique_id - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - async def test_abort_if_already_setup(hass, tuya): """Test we abort if Tuya is already setup.""" @@ -101,7 +124,7 @@ async def test_abort_if_already_setup(hass, tuya): ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "single_instance_allowed" + assert result["reason"] == RESULT_SINGLE_INSTANCE # Should fail, config exist (flow) result = await hass.config_entries.flow.async_init( @@ -109,7 +132,7 @@ async def test_abort_if_already_setup(hass, tuya): ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "single_instance_allowed" + assert result["reason"] == RESULT_SINGLE_INSTANCE async def test_abort_on_invalid_credentials(hass, tuya): @@ -121,14 +144,14 @@ async def test_abort_on_invalid_credentials(hass, tuya): ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "invalid_auth"} + assert result["errors"] == {"base": RESULT_AUTH_FAILED} result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TUYA_USER_DATA ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "invalid_auth" + assert result["reason"] == RESULT_AUTH_FAILED async def test_abort_on_connection_error(hass, tuya): @@ -140,11 +163,143 @@ async def test_abort_on_connection_error(hass, tuya): ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "cannot_connect" + assert result["reason"] == RESULT_CONN_ERROR result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TUYA_USER_DATA ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "cannot_connect" + assert result["reason"] == RESULT_CONN_ERROR + + +async def test_options_flow(hass): + """Test config flow options.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=TUYA_USER_DATA, + ) + config_entry.add_to_hass(hass) + + # Test check for integration not loaded + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == RESULT_CONN_ERROR + + # Load integration and enter options + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + hass.data[DOMAIN] = {TUYA_DATA: MockTuya()} + 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" + + # Test dev not found error + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_LIST_DEVICES: [f"light-{LIGHT_ID_FAKE1}"]}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + assert result["errors"] == {"base": ERROR_DEV_NOT_FOUND} + + # Test dev type error + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_LIST_DEVICES: [f"light-{LIGHT_ID_FAKE2}"]}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + assert result["errors"] == {"base": ERROR_DEV_NOT_CONFIG} + + # Test multi dev error + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_LIST_DEVICES: [f"climate-{CLIMATE_ID}", f"light-{LIGHT_ID}"]}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + assert result["errors"] == {"base": ERROR_DEV_MULTI_TYPE} + + # Test climate options form + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_LIST_DEVICES: [f"climate-{CLIMATE_ID}"]} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "device" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + CONF_TEMP_DIVIDER: 10, + CONF_CURR_TEMP_DIVIDER: 5, + CONF_SET_TEMP_DIVIDED: False, + CONF_TEMP_STEP_OVERRIDE: STEP_HALVES, + CONF_MIN_TEMP: 12, + CONF_MAX_TEMP: 22, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + # Test light options form + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_LIST_DEVICES: [f"light-{LIGHT_ID}"]} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "device" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_SUPPORT_COLOR: True, + CONF_BRIGHTNESS_RANGE_MODE: 1, + CONF_MIN_KELVIN: 4000, + CONF_MAX_KELVIN: 5000, + CONF_TUYA_MAX_COLTEMP: 12000, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + # Test common options + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_DISCOVERY_INTERVAL: 100, + CONF_QUERY_INTERVAL: 50, + CONF_QUERY_DEVICE: LIGHT_ID, + }, + ) + + # Verify results + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + climate_options = config_entry.options[CLIMATE_ID] + assert climate_options[CONF_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert climate_options[CONF_TEMP_DIVIDER] == 10 + assert climate_options[CONF_CURR_TEMP_DIVIDER] == 5 + assert climate_options[CONF_SET_TEMP_DIVIDED] is False + assert climate_options[CONF_TEMP_STEP_OVERRIDE] == STEP_HALVES + assert climate_options[CONF_MIN_TEMP] == 12 + assert climate_options[CONF_MAX_TEMP] == 22 + + light_options = config_entry.options[LIGHT_ID] + assert light_options[CONF_SUPPORT_COLOR] is True + assert light_options[CONF_BRIGHTNESS_RANGE_MODE] == 1 + assert light_options[CONF_MIN_KELVIN] == 4000 + assert light_options[CONF_MAX_KELVIN] == 5000 + assert light_options[CONF_TUYA_MAX_COLTEMP] == 12000 + + assert config_entry.options[CONF_DISCOVERY_INTERVAL] == 100 + assert config_entry.options[CONF_QUERY_INTERVAL] == 50 + assert config_entry.options[CONF_QUERY_DEVICE] == LIGHT_ID From 1e172dedf6fd431b20723a097b802da6cea0bce4 Mon Sep 17 00:00:00 2001 From: marecabo <23156476+marecabo@users.noreply.github.com> Date: Tue, 16 Feb 2021 07:19:31 +0100 Subject: [PATCH 457/796] Add device_class attribute to ESPHome sensor entities (#46595) --- homeassistant/components/esphome/manifest.json | 2 +- homeassistant/components/esphome/sensor.py | 11 +++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index c69f4f4d8c6..17ff2ca96ba 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==2.6.4"], + "requirements": ["aioesphomeapi==2.6.5"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter"], "after_dependencies": ["zeroconf", "tag"] diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index a5cc321cb08..68a8c00caed 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -52,6 +52,8 @@ class EsphomeSensor(EsphomeEntity): @property def icon(self) -> str: """Return the icon.""" + if self._static_info.icon == "": + return None return self._static_info.icon @property @@ -71,8 +73,17 @@ class EsphomeSensor(EsphomeEntity): @property def unit_of_measurement(self) -> str: """Return the unit the value is expressed in.""" + if self._static_info.unit_of_measurement == "": + return None return self._static_info.unit_of_measurement + @property + def device_class(self) -> str: + """Return the class of this device, from component DEVICE_CLASSES.""" + if self._static_info.device_class == "": + return None + return self._static_info.device_class + class EsphomeTextSensor(EsphomeEntity): """A text sensor implementation for ESPHome.""" diff --git a/requirements_all.txt b/requirements_all.txt index b1a18147b25..640c2ace4df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -154,7 +154,7 @@ aiodns==2.0.0 aioeafm==0.1.2 # homeassistant.components.esphome -aioesphomeapi==2.6.4 +aioesphomeapi==2.6.5 # homeassistant.components.flo aioflo==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc1e86fc61f..6b77eaafb9d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -91,7 +91,7 @@ aiodns==2.0.0 aioeafm==0.1.2 # homeassistant.components.esphome -aioesphomeapi==2.6.4 +aioesphomeapi==2.6.5 # homeassistant.components.flo aioflo==0.4.1 From 20d93b4b298085547258831a76a2c4a0fac2ed1d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Feb 2021 21:37:43 -1000 Subject: [PATCH 458/796] Remove support for migrating pre-config entry homekit (#46616) HomeKit pairings and accessory ids from versions 0.109 and earlier are no longer migrated on upgrade. Users upgrading directly to 2021.3 from 0.109 and older should upgrade to 2021.2 first if they wish to preserve HomeKit configuration and avoid re-pairing the bridge. This change does not affect upgrades from 0.110 and later. --- homeassistant/components/homekit/__init__.py | 9 --- homeassistant/components/homekit/util.py | 19 ------ tests/components/homekit/test_homekit.py | 71 +------------------- 3 files changed, 1 insertion(+), 98 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 34044742703..396f36f7c03 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -99,7 +99,6 @@ from .const import ( from .util import ( dismiss_setup_message, get_persist_fullpath_for_entry_id, - migrate_filesystem_state_data_for_primary_imported_entry_id, port_is_available, remove_state_files_for_entry_id, show_setup_message, @@ -238,14 +237,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): port = conf[CONF_PORT] _LOGGER.debug("Begin setup HomeKit for %s", name) - if CONF_ENTRY_INDEX in conf and conf[CONF_ENTRY_INDEX] == 0: - _LOGGER.debug("Migrating legacy HomeKit data for %s", name) - await hass.async_add_executor_job( - migrate_filesystem_state_data_for_primary_imported_entry_id, - hass, - entry.entry_id, - ) - aid_storage = AccessoryAidStorage(hass, entry.entry_id) await aid_storage.async_initialize() diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 83a9a0a0353..c23b8c1baaf 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -66,7 +66,6 @@ from .const import ( FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, - HOMEKIT_FILE, HOMEKIT_PAIRING_QR, HOMEKIT_PAIRING_QR_SECRET, TYPE_FAUCET, @@ -410,24 +409,6 @@ def format_sw_version(version): return None -def migrate_filesystem_state_data_for_primary_imported_entry_id( - hass: HomeAssistant, entry_id: str -): - """Migrate the old paths to the storage directory.""" - legacy_persist_file_path = hass.config.path(HOMEKIT_FILE) - if os.path.exists(legacy_persist_file_path): - os.rename( - legacy_persist_file_path, get_persist_fullpath_for_entry_id(hass, entry_id) - ) - - legacy_aid_storage_path = hass.config.path(STORAGE_DIR, "homekit.aids") - if os.path.exists(legacy_aid_storage_path): - os.rename( - legacy_aid_storage_path, - get_aid_storage_fullpath_for_entry_id(hass, entry_id), - ) - - def remove_state_files_for_entry_id(hass: HomeAssistant, entry_id: str): """Remove the state files from disk.""" persist_file_path = get_persist_fullpath_for_entry_id(hass, entry_id) diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 1fff55db195..b0213ee7e8b 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -27,20 +27,15 @@ from homeassistant.components.homekit.const import ( BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, CONF_AUTO_START, - CONF_ENTRY_INDEX, DEFAULT_PORT, DOMAIN, HOMEKIT, - HOMEKIT_FILE, HOMEKIT_MODE_ACCESSORY, HOMEKIT_MODE_BRIDGE, SERVICE_HOMEKIT_RESET_ACCESSORY, SERVICE_HOMEKIT_START, ) -from homeassistant.components.homekit.util import ( - get_aid_storage_fullpath_for_entry_id, - get_persist_fullpath_for_entry_id, -) +from homeassistant.components.homekit.util import get_persist_fullpath_for_entry_id from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -60,7 +55,6 @@ from homeassistant.const import ( from homeassistant.core import State from homeassistant.helpers import device_registry from homeassistant.helpers.entityfilter import generate_filter -from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.setup import async_setup_component from homeassistant.util import json as json_util @@ -909,69 +903,6 @@ async def test_homekit_async_get_integration_fails( ) -async def test_setup_imported(hass, mock_zeroconf): - """Test async_setup with imported config options.""" - legacy_persist_file_path = hass.config.path(HOMEKIT_FILE) - legacy_aid_storage_path = hass.config.path(STORAGE_DIR, "homekit.aids") - legacy_homekit_state_contents = {"homekit.state": 1} - legacy_homekit_aids_contents = {"homekit.aids": 1} - await hass.async_add_executor_job( - _write_data, legacy_persist_file_path, legacy_homekit_state_contents - ) - await hass.async_add_executor_job( - _write_data, legacy_aid_storage_path, legacy_homekit_aids_contents - ) - - entry = MockConfigEntry( - domain=DOMAIN, - source=SOURCE_IMPORT, - data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT, CONF_ENTRY_INDEX: 0}, - options={}, - ) - entry.add_to_hass(hass) - - with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: - mock_homekit.return_value = homekit = Mock() - type(homekit).async_start = AsyncMock() - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - mock_homekit.assert_any_call( - hass, - BRIDGE_NAME, - DEFAULT_PORT, - None, - ANY, - {}, - HOMEKIT_MODE_BRIDGE, - None, - entry.entry_id, - ) - assert mock_homekit().setup.called is True - - # Test auto start enabled - mock_homekit.reset_mock() - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - mock_homekit().async_start.assert_called() - - migrated_persist_file_path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) - assert ( - await hass.async_add_executor_job( - json_util.load_json, migrated_persist_file_path - ) - == legacy_homekit_state_contents - ) - os.unlink(migrated_persist_file_path) - migrated_aid_file_path = get_aid_storage_fullpath_for_entry_id(hass, entry.entry_id) - assert ( - await hass.async_add_executor_job(json_util.load_json, migrated_aid_file_path) - == legacy_homekit_aids_contents - ) - os.unlink(migrated_aid_file_path) - - async def test_yaml_updates_update_config_entry_for_name(hass, mock_zeroconf): """Test async_setup with imported config.""" entry = MockConfigEntry( From 6986fa4eb674c4a9d49af2e4f393dac8be4e6ba5 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 16 Feb 2021 09:35:27 +0100 Subject: [PATCH 459/796] Add target to services.yaml (#46410) Co-authored-by: Franck Nijhof --- homeassistant/components/light/services.yaml | 20 +++++++++++--- homeassistant/components/sonos/services.yaml | 28 +++++++++++++++++++- homeassistant/helpers/service.py | 8 ++++-- script/hassfest/services.py | 8 +++++- 4 files changed, 57 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index a2b71f5632b..dda7396e11b 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -2,13 +2,19 @@ turn_on: description: Turn a light on. + target: fields: - entity_id: - description: Name(s) of entities to turn on - example: "light.kitchen" transition: + name: Transition description: Duration in seconds it takes to get to next state example: 60 + selector: + number: + min: 0 + max: 300 + step: 1 + unit_of_measurement: seconds + mode: slider rgb_color: description: Color for the light in RGB-format. example: "[255, 100, 100]" @@ -34,8 +40,16 @@ turn_on: description: Number between 0..255 indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness supported by the light. example: 120 brightness_pct: + name: Brightness description: Number between 0..100 indicating percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness and 100 is the maximum brightness supported by the light. example: 47 + selector: + number: + min: 0 + max: 100 + step: 1 + unit_of_measurement: "%" + mode: slider brightness_step: description: Change brightness by an amount. Should be between -255..255. example: -25.5 diff --git a/homeassistant/components/sonos/services.yaml b/homeassistant/components/sonos/services.yaml index 8a35e9a7790..04a46940d6a 100644 --- a/homeassistant/components/sonos/services.yaml +++ b/homeassistant/components/sonos/services.yaml @@ -4,6 +4,10 @@ join: master: description: Entity ID of the player that should become the coordinator of the group. example: "media_player.living_room_sonos" + selector: + entity: + integration: sonos + domain: media_player entity_id: description: Name(s) of entities that will join the master. example: "media_player.living_room_sonos" @@ -64,7 +68,13 @@ set_sleep_timer: sleep_time: description: Number of seconds to set the timer. example: "900" - + selector: + number: + min: 0 + max: 3600 + step: 1 + unit_of_measurement: seconds + mode: slider clear_sleep_timer: description: Clear a Sonos timer. fields: @@ -89,12 +99,18 @@ set_option: night_sound: description: Enable Night Sound mode example: "true" + selector: + boolean: speech_enhance: description: Enable Speech Enhancement mode example: "true" + selector: + boolean: status_light: description: Enable Status (LED) Light example: "true" + selector: + boolean: play_queue: description: Starts playing the queue from the first item. @@ -109,6 +125,11 @@ play_queue: queue_position: description: Position of the song in the queue to start playing from. example: "0" + selector: + number: + min: 0 + max: 100000000 + mode: box remove_from_queue: description: Removes an item from the queue. @@ -123,6 +144,11 @@ remove_from_queue: queue_position: description: Position in the queue to remove. example: "0" + selector: + number: + min: 0 + max: 100000000 + mode: box update_alarm: description: Updates an alarm with new time and volume settings. diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index afc354dae56..d4eacbc0503 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -448,12 +448,16 @@ async def async_get_all_descriptions( # Don't warn for missing services, because it triggers false # positives for things like scripts, that register as a service - description = descriptions_cache[cache_key] = { + description = { "description": yaml_description.get("description", ""), - "target": yaml_description.get("target"), "fields": yaml_description.get("fields", {}), } + if "target" in yaml_description: + description["target"] = yaml_description["target"] + + descriptions_cache[cache_key] = description + descriptions[domain][service] = description return descriptions diff --git a/script/hassfest/services.py b/script/hassfest/services.py index c07d3bbc6ef..c0823b672e8 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -24,6 +24,7 @@ def exists(value): FIELD_SCHEMA = vol.Schema( { vol.Required("description"): str, + vol.Optional("name"): str, vol.Optional("example"): exists, vol.Optional("default"): exists, vol.Optional("values"): exists, @@ -35,6 +36,9 @@ FIELD_SCHEMA = vol.Schema( SERVICE_SCHEMA = vol.Schema( { vol.Required("description"): str, + vol.Optional("target"): vol.Any( + selector.TargetSelector.CONFIG_SCHEMA, None # pylint: disable=no-member + ), vol.Optional("fields"): vol.Schema({str: FIELD_SCHEMA}), } ) @@ -60,7 +64,9 @@ def validate_services(integration: Integration): """Validate services.""" # Find if integration uses services has_services = grep_dir( - integration.path, "**/*.py", r"hass\.services\.(register|async_register)" + integration.path, + "**/*.py", + r"(hass\.services\.(register|async_register))|async_register_entity_service", ) if not has_services: From 0bfcd5e1ee44254292c775a7a82f6f1a02c57461 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 16 Feb 2021 10:26:38 +0100 Subject: [PATCH 460/796] Use explicit open/close for covers (#46602) --- .../components/google_assistant/trait.py | 12 ++++++------ tests/components/google_assistant/test_trait.py | 17 +++++++++++------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 9e0da39b58a..8b0bde09010 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1685,17 +1685,17 @@ class OpenCloseTrait(_Trait): else: position = params["openPercent"] - if features & cover.SUPPORT_SET_POSITION: - service = cover.SERVICE_SET_COVER_POSITION - if position > 0: - should_verify = True - svc_params[cover.ATTR_POSITION] = position - elif position == 0: + if position == 0: service = cover.SERVICE_CLOSE_COVER should_verify = False elif position == 100: service = cover.SERVICE_OPEN_COVER should_verify = True + elif features & cover.SUPPORT_SET_POSITION: + service = cover.SERVICE_SET_COVER_POSITION + if position > 0: + should_verify = True + svc_params[cover.ATTR_POSITION] = position else: raise SmartHomeError( ERR_NOT_SUPPORTED, "No support for partial open close" diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 74e8ab21eb0..a9b1e9a97fb 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1933,14 +1933,18 @@ async def test_openclose_cover(hass): assert trt.sync_attributes() == {} assert trt.query_attributes() == {"openPercent": 75} - calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION) + calls_set = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION) + calls_open = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_OPEN_COVER) + await trt.execute(trait.COMMAND_OPENCLOSE, BASIC_DATA, {"openPercent": 50}, {}) await trt.execute( trait.COMMAND_OPENCLOSE_RELATIVE, BASIC_DATA, {"openRelativePercent": 50}, {} ) - assert len(calls) == 2 - assert calls[0].data == {ATTR_ENTITY_ID: "cover.bla", cover.ATTR_POSITION: 50} - assert calls[1].data == {ATTR_ENTITY_ID: "cover.bla", cover.ATTR_POSITION: 100} + assert len(calls_set) == 1 + assert calls_set[0].data == {ATTR_ENTITY_ID: "cover.bla", cover.ATTR_POSITION: 50} + + assert len(calls_open) == 1 + assert calls_open[0].data == {ATTR_ENTITY_ID: "cover.bla"} async def test_openclose_cover_unknown_state(hass): @@ -2111,6 +2115,7 @@ async def test_openclose_cover_secure(hass, device_class): assert trt.query_attributes() == {"openPercent": 75} calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION) + calls_close = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_CLOSE_COVER) # No challenge data with pytest.raises(error.ChallengeNeeded) as err: @@ -2136,8 +2141,8 @@ async def test_openclose_cover_secure(hass, device_class): # no challenge on close await trt.execute(trait.COMMAND_OPENCLOSE, PIN_DATA, {"openPercent": 0}, {}) - assert len(calls) == 2 - assert calls[1].data == {ATTR_ENTITY_ID: "cover.bla", cover.ATTR_POSITION: 0} + assert len(calls_close) == 1 + assert calls_close[0].data == {ATTR_ENTITY_ID: "cover.bla"} @pytest.mark.parametrize( From a6912277eb014571fee3318ec96e8c4a7add831f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 16 Feb 2021 11:00:08 +0100 Subject: [PATCH 461/796] Remove defunct CoinMarketCap integration (#46615) --- .../components/coinmarketcap/__init__.py | 1 - .../components/coinmarketcap/manifest.json | 7 - .../components/coinmarketcap/sensor.py | 164 ------------------ requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/coinmarketcap/__init__.py | 1 - tests/components/coinmarketcap/test_sensor.py | 42 ----- 7 files changed, 221 deletions(-) delete mode 100644 homeassistant/components/coinmarketcap/__init__.py delete mode 100644 homeassistant/components/coinmarketcap/manifest.json delete mode 100644 homeassistant/components/coinmarketcap/sensor.py delete mode 100644 tests/components/coinmarketcap/__init__.py delete mode 100644 tests/components/coinmarketcap/test_sensor.py diff --git a/homeassistant/components/coinmarketcap/__init__.py b/homeassistant/components/coinmarketcap/__init__.py deleted file mode 100644 index 0cdb5a16a4a..00000000000 --- a/homeassistant/components/coinmarketcap/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The coinmarketcap component.""" diff --git a/homeassistant/components/coinmarketcap/manifest.json b/homeassistant/components/coinmarketcap/manifest.json deleted file mode 100644 index e3f827f2718..00000000000 --- a/homeassistant/components/coinmarketcap/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "coinmarketcap", - "name": "CoinMarketCap", - "documentation": "https://www.home-assistant.io/integrations/coinmarketcap", - "requirements": ["coinmarketcap==5.0.3"], - "codeowners": [] -} diff --git a/homeassistant/components/coinmarketcap/sensor.py b/homeassistant/components/coinmarketcap/sensor.py deleted file mode 100644 index f3fe240c0bc..00000000000 --- a/homeassistant/components/coinmarketcap/sensor.py +++ /dev/null @@ -1,164 +0,0 @@ -"""Details about crypto currencies from CoinMarketCap.""" -from datetime import timedelta -import logging -from urllib.error import HTTPError - -from coinmarketcap import Market -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION, CONF_DISPLAY_CURRENCY -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity - -_LOGGER = logging.getLogger(__name__) - -ATTR_VOLUME_24H = "volume_24h" -ATTR_AVAILABLE_SUPPLY = "available_supply" -ATTR_CIRCULATING_SUPPLY = "circulating_supply" -ATTR_MARKET_CAP = "market_cap" -ATTR_PERCENT_CHANGE_24H = "percent_change_24h" -ATTR_PERCENT_CHANGE_7D = "percent_change_7d" -ATTR_PERCENT_CHANGE_1H = "percent_change_1h" -ATTR_PRICE = "price" -ATTR_RANK = "rank" -ATTR_SYMBOL = "symbol" -ATTR_TOTAL_SUPPLY = "total_supply" - -ATTRIBUTION = "Data provided by CoinMarketCap" - -CONF_CURRENCY_ID = "currency_id" -CONF_DISPLAY_CURRENCY_DECIMALS = "display_currency_decimals" - -DEFAULT_CURRENCY_ID = 1 -DEFAULT_DISPLAY_CURRENCY = "USD" -DEFAULT_DISPLAY_CURRENCY_DECIMALS = 2 - -ICON = "mdi:currency-usd" - -SCAN_INTERVAL = timedelta(minutes=15) - - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_CURRENCY_ID, default=DEFAULT_CURRENCY_ID): cv.positive_int, - vol.Optional(CONF_DISPLAY_CURRENCY, default=DEFAULT_DISPLAY_CURRENCY): vol.All( - cv.string, vol.Upper - ), - vol.Optional( - CONF_DISPLAY_CURRENCY_DECIMALS, default=DEFAULT_DISPLAY_CURRENCY_DECIMALS - ): vol.All(vol.Coerce(int), vol.Range(min=1)), - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the CoinMarketCap sensor.""" - currency_id = config[CONF_CURRENCY_ID] - display_currency = config[CONF_DISPLAY_CURRENCY] - display_currency_decimals = config[CONF_DISPLAY_CURRENCY_DECIMALS] - - try: - CoinMarketCapData(currency_id, display_currency).update() - except HTTPError: - _LOGGER.warning( - "Currency ID %s or display currency %s " - "is not available. Using 1 (bitcoin) " - "and USD", - currency_id, - display_currency, - ) - currency_id = DEFAULT_CURRENCY_ID - display_currency = DEFAULT_DISPLAY_CURRENCY - - add_entities( - [ - CoinMarketCapSensor( - CoinMarketCapData(currency_id, display_currency), - display_currency_decimals, - ) - ], - True, - ) - - -class CoinMarketCapSensor(Entity): - """Representation of a CoinMarketCap sensor.""" - - def __init__(self, data, display_currency_decimals): - """Initialize the sensor.""" - self.data = data - self.display_currency_decimals = display_currency_decimals - self._ticker = None - self._unit_of_measurement = self.data.display_currency - - @property - def name(self): - """Return the name of the sensor.""" - return self._ticker.get("name") - - @property - def state(self): - """Return the state of the sensor.""" - return round( - float( - self._ticker.get("quotes").get(self.data.display_currency).get("price") - ), - self.display_currency_decimals, - ) - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit_of_measurement - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return ICON - - @property - def device_state_attributes(self): - """Return the state attributes of the sensor.""" - return { - ATTR_VOLUME_24H: self._ticker.get("quotes") - .get(self.data.display_currency) - .get("volume_24h"), - ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_CIRCULATING_SUPPLY: self._ticker.get("circulating_supply"), - ATTR_MARKET_CAP: self._ticker.get("quotes") - .get(self.data.display_currency) - .get("market_cap"), - ATTR_PERCENT_CHANGE_24H: self._ticker.get("quotes") - .get(self.data.display_currency) - .get("percent_change_24h"), - ATTR_PERCENT_CHANGE_7D: self._ticker.get("quotes") - .get(self.data.display_currency) - .get("percent_change_7d"), - ATTR_PERCENT_CHANGE_1H: self._ticker.get("quotes") - .get(self.data.display_currency) - .get("percent_change_1h"), - ATTR_RANK: self._ticker.get("rank"), - ATTR_SYMBOL: self._ticker.get("symbol"), - ATTR_TOTAL_SUPPLY: self._ticker.get("total_supply"), - } - - def update(self): - """Get the latest data and updates the states.""" - self.data.update() - self._ticker = self.data.ticker.get("data") - - -class CoinMarketCapData: - """Get the latest data and update the states.""" - - def __init__(self, currency_id, display_currency): - """Initialize the data object.""" - self.currency_id = currency_id - self.display_currency = display_currency - self.ticker = None - - def update(self): - """Get the latest data from coinmarketcap.com.""" - - self.ticker = Market().ticker(self.currency_id, convert=self.display_currency) diff --git a/requirements_all.txt b/requirements_all.txt index 640c2ace4df..6eb19726cb3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -427,9 +427,6 @@ co2signal==0.4.2 # homeassistant.components.coinbase coinbase==2.1.0 -# homeassistant.components.coinmarketcap -coinmarketcap==5.0.3 - # homeassistant.scripts.check_config colorlog==4.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6b77eaafb9d..11064b63db8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -224,9 +224,6 @@ buienradar==1.0.4 # homeassistant.components.caldav caldav==0.7.1 -# homeassistant.components.coinmarketcap -coinmarketcap==5.0.3 - # homeassistant.scripts.check_config colorlog==4.7.2 diff --git a/tests/components/coinmarketcap/__init__.py b/tests/components/coinmarketcap/__init__.py deleted file mode 100644 index 9e9b871bbe2..00000000000 --- a/tests/components/coinmarketcap/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the coinmarketcap component.""" diff --git a/tests/components/coinmarketcap/test_sensor.py b/tests/components/coinmarketcap/test_sensor.py deleted file mode 100644 index 369a006f568..00000000000 --- a/tests/components/coinmarketcap/test_sensor.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Tests for the CoinMarketCap sensor platform.""" -import json -from unittest.mock import patch - -import pytest - -from homeassistant.components.sensor import DOMAIN -from homeassistant.setup import async_setup_component - -from tests.common import assert_setup_component, load_fixture - -VALID_CONFIG = { - DOMAIN: { - "platform": "coinmarketcap", - "currency_id": 1027, - "display_currency": "EUR", - "display_currency_decimals": 3, - } -} - - -@pytest.fixture -async def setup_sensor(hass): - """Set up demo sensor component.""" - with assert_setup_component(1, DOMAIN): - with patch( - "coinmarketcap.Market.ticker", - return_value=json.loads(load_fixture("coinmarketcap.json")), - ): - await async_setup_component(hass, DOMAIN, VALID_CONFIG) - await hass.async_block_till_done() - - -async def test_setup(hass, setup_sensor): - """Test the setup with custom settings.""" - state = hass.states.get("sensor.ethereum") - assert state is not None - - assert state.name == "Ethereum" - assert state.state == "493.455" - assert state.attributes.get("symbol") == "ETH" - assert state.attributes.get("unit_of_measurement") == "EUR" From 713544e5ebd4727f28b524981092bf61aa889976 Mon Sep 17 00:00:00 2001 From: David McClosky Date: Tue, 16 Feb 2021 06:32:53 -0500 Subject: [PATCH 462/796] Bump python-vlc-telnet to 2.0.1 (#46608) Includes corresponding Home Assistant changes to avoid introducing regressions. --- CODEOWNERS | 2 +- homeassistant/components/vlc_telnet/manifest.json | 4 ++-- homeassistant/components/vlc_telnet/media_player.py | 9 ++++----- requirements_all.txt | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index ba727de5694..d1bda212d2d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -510,7 +510,7 @@ homeassistant/components/vicare/* @oischinger homeassistant/components/vilfo/* @ManneW homeassistant/components/vivotek/* @HarlemSquirrel homeassistant/components/vizio/* @raman325 -homeassistant/components/vlc_telnet/* @rodripf +homeassistant/components/vlc_telnet/* @rodripf @dmcc homeassistant/components/volkszaehler/* @fabaff homeassistant/components/volumio/* @OnFreund homeassistant/components/waqi/* @andrey-git diff --git a/homeassistant/components/vlc_telnet/manifest.json b/homeassistant/components/vlc_telnet/manifest.json index f6e4aa04521..37941e15458 100644 --- a/homeassistant/components/vlc_telnet/manifest.json +++ b/homeassistant/components/vlc_telnet/manifest.json @@ -2,6 +2,6 @@ "domain": "vlc_telnet", "name": "VLC media player Telnet", "documentation": "https://www.home-assistant.io/integrations/vlc-telnet", - "requirements": ["python-telnet-vlc==1.0.4"], - "codeowners": ["@rodripf"] + "requirements": ["python-telnet-vlc==2.0.1"], + "codeowners": ["@rodripf", "@dmcc"] } diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 1f0d62b6ee8..96690955fcc 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -37,6 +37,7 @@ DOMAIN = "vlc_telnet" DEFAULT_NAME = "VLC-TELNET" DEFAULT_PORT = 4212 +MAX_VOLUME = 500 SUPPORT_VLC = ( SUPPORT_PAUSE @@ -210,17 +211,15 @@ class VlcDevice(MediaPlayerEntity): """Mute the volume.""" if mute: self._volume_bkp = self._volume - self._volume = 0 - self._vlc.set_volume("0") + self.set_volume_level(0) else: - self._vlc.set_volume(str(self._volume_bkp)) - self._volume = self._volume_bkp + self.set_volume_level(self._volume_bkp) self._muted = mute def set_volume_level(self, volume): """Set volume level, range 0..1.""" - self._vlc.set_volume(str(volume * 500)) + self._vlc.set_volume(volume * MAX_VOLUME) self._volume = volume def media_play(self): diff --git a/requirements_all.txt b/requirements_all.txt index 6eb19726cb3..1b2409a8e6f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1822,7 +1822,7 @@ python-tado==0.10.0 python-telegram-bot==13.1 # homeassistant.components.vlc_telnet -python-telnet-vlc==1.0.4 +python-telnet-vlc==2.0.1 # homeassistant.components.twitch python-twitch-client==0.6.0 From add0d9d3eb8243eeca1de8b2ab0e9d6756005386 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Tue, 16 Feb 2021 09:00:09 -0500 Subject: [PATCH 463/796] Use core constants for yeelight (#46552) --- homeassistant/components/yeelight/__init__.py | 2 -- homeassistant/components/yeelight/config_flow.py | 3 +-- homeassistant/components/yeelight/light.py | 2 -- tests/components/yeelight/test_config_flow.py | 3 +-- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index b61df3f810b..f24847a2d54 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -42,7 +42,6 @@ CONF_FLOW_PARAMS = "flow_params" CONF_CUSTOM_EFFECTS = "custom_effects" CONF_NIGHTLIGHT_SWITCH_TYPE = "nightlight_switch_type" CONF_NIGHTLIGHT_SWITCH = "nightlight_switch" -CONF_DEVICE = "device" DATA_CONFIG_ENTRIES = "config_entries" DATA_CUSTOM_EFFECTS = "custom_effects" @@ -423,7 +422,6 @@ class YeelightDevice: Uses brightness as it appears to be supported in both ceiling and other lights. """ - return self._nightlight_brightness is not None @property diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index 52f27932403..f186c897a21 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -5,12 +5,11 @@ import voluptuous as vol import yeelight from homeassistant import config_entries, exceptions -from homeassistant.const import CONF_HOST, CONF_ID, CONF_NAME +from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from . import ( - CONF_DEVICE, CONF_MODE_MUSIC, CONF_MODEL, CONF_NIGHTLIGHT_SWITCH, diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index c256cfb23e0..e4044303ef0 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -261,7 +261,6 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up Yeelight from a config entry.""" - custom_effects = _parse_custom_effects(hass.data[DOMAIN][DATA_CUSTOM_EFFECTS]) device = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE] @@ -563,7 +562,6 @@ class YeelightGenericLight(YeelightEntity, LightEntity): @property def device_state_attributes(self): """Return the device specific state attributes.""" - attributes = { "flowing": self.device.is_color_flow_enabled, "music_mode": self._bulb.music_mode, diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 8fa1ba5c988..6a1508d7896 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -3,7 +3,6 @@ from unittest.mock import MagicMock, patch from homeassistant import config_entries from homeassistant.components.yeelight import ( - CONF_DEVICE, CONF_MODE_MUSIC, CONF_MODEL, CONF_NIGHTLIGHT_SWITCH, @@ -18,7 +17,7 @@ from homeassistant.components.yeelight import ( DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, ) -from homeassistant.const import CONF_HOST, CONF_ID, CONF_NAME +from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_NAME from homeassistant.core import HomeAssistant from . import ( From 9d8ba6af967f86ed3097e83413754ceedd4ce52f Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 16 Feb 2021 15:28:25 +0100 Subject: [PATCH 464/796] Use update coordinator for Xioami Miio subdevices (#46251) Co-authored-by: Martin Hjelmare --- .../components/xiaomi_miio/__init__.py | 35 +++++++++++++++++-- .../xiaomi_miio/alarm_control_panel.py | 4 +-- homeassistant/components/xiaomi_miio/const.py | 2 ++ .../components/xiaomi_miio/gateway.py | 22 +++--------- homeassistant/components/xiaomi_miio/light.py | 2 +- .../components/xiaomi_miio/sensor.py | 29 +++++++-------- 6 files changed, 55 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index e81c35d39e4..273fc53da5a 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -1,7 +1,13 @@ """Support for Xiaomi Miio.""" +from datetime import timedelta +import logging + +from miio.gateway import GatewayException + from homeassistant import config_entries, core from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( CONF_DEVICE, @@ -9,10 +15,13 @@ from .const import ( CONF_GATEWAY, CONF_MODEL, DOMAIN, + KEY_COORDINATOR, MODELS_SWITCH, ) from .gateway import ConnectXiaomiGateway +_LOGGER = logging.getLogger(__name__) + GATEWAY_PLATFORMS = ["alarm_control_panel", "sensor", "light"] SWITCH_PLATFORMS = ["switch"] @@ -56,8 +65,6 @@ async def async_setup_gateway_entry( return False gateway_info = gateway.gateway_info - hass.data[DOMAIN][entry.entry_id] = gateway.gateway_device - gateway_model = f"{gateway_info.model}-{gateway_info.hardware_version}" device_registry = await dr.async_get_registry(hass) @@ -71,6 +78,30 @@ async def async_setup_gateway_entry( sw_version=gateway_info.firmware_version, ) + async def async_update_data(): + """Fetch data from the subdevice.""" + try: + for sub_device in gateway.gateway_device.devices.values(): + await hass.async_add_executor_job(sub_device.update) + except GatewayException as ex: + raise UpdateFailed("Got exception while fetching the state") from ex + + # Create update coordinator + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name=name, + update_method=async_update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=timedelta(seconds=10), + ) + + hass.data[DOMAIN][entry.entry_id] = { + CONF_GATEWAY: gateway.gateway_device, + KEY_COORDINATOR: coordinator, + } + for component in GATEWAY_PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) diff --git a/homeassistant/components/xiaomi_miio/alarm_control_panel.py b/homeassistant/components/xiaomi_miio/alarm_control_panel.py index 6880202cbd6..26421770771 100644 --- a/homeassistant/components/xiaomi_miio/alarm_control_panel.py +++ b/homeassistant/components/xiaomi_miio/alarm_control_panel.py @@ -15,7 +15,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, ) -from .const import DOMAIN +from .const import CONF_GATEWAY, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -27,7 +27,7 @@ XIAOMI_STATE_ARMING_VALUE = "oning" async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Xiaomi Gateway Alarm from a config entry.""" entities = [] - gateway = hass.data[DOMAIN][config_entry.entry_id] + gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY] entity = XiaomiGatewayAlarm( gateway, f"{config_entry.title} Alarm", diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 3726f7f709d..c0ddb698340 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -7,6 +7,8 @@ CONF_DEVICE = "device" CONF_MODEL = "model" CONF_MAC = "mac" +KEY_COORDINATOR = "coordinator" + MODELS_GATEWAY = ["lumi.gateway", "lumi.acpartner"] MODELS_SWITCH = [ "chuangmi.plug.v1", diff --git a/homeassistant/components/xiaomi_miio/gateway.py b/homeassistant/components/xiaomi_miio/gateway.py index eb2f4cdf2eb..356b19dc89a 100644 --- a/homeassistant/components/xiaomi_miio/gateway.py +++ b/homeassistant/components/xiaomi_miio/gateway.py @@ -4,6 +4,7 @@ import logging from miio import DeviceException, gateway from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -56,16 +57,16 @@ class ConnectXiaomiGateway: return True -class XiaomiGatewayDevice(Entity): +class XiaomiGatewayDevice(CoordinatorEntity, Entity): """Representation of a base Xiaomi Gateway Device.""" - def __init__(self, sub_device, entry): + def __init__(self, coordinator, sub_device, entry): """Initialize the Xiaomi Gateway Device.""" + super().__init__(coordinator) self._sub_device = sub_device self._entry = entry self._unique_id = sub_device.sid self._name = f"{sub_device.name} ({sub_device.sid})" - self._available = False @property def unique_id(self): @@ -88,18 +89,3 @@ class XiaomiGatewayDevice(Entity): "model": self._sub_device.model, "sw_version": self._sub_device.firmware_version, } - - @property - def available(self): - """Return true when state is known.""" - return self._available - - async def async_update(self): - """Fetch state from the sub device.""" - try: - await self.hass.async_add_executor_job(self._sub_device.update) - self._available = True - except gateway.GatewayException as ex: - if self._available: - self._available = False - _LOGGER.error("Got exception while fetching the state: %s", ex) diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index efe67a370c4..7f168cf0e3e 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -130,7 +130,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY: - gateway = hass.data[DOMAIN][config_entry.entry_id] + gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY] # Gateway light if gateway.model not in [ GATEWAY_MODEL_AC_V1, diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index ab4df8cd982..821fe164ea9 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -2,13 +2,13 @@ from dataclasses import dataclass import logging -from miio import AirQualityMonitor, DeviceException # pylint: disable=import-error +from miio import AirQualityMonitor # pylint: disable=import-error +from miio import DeviceException from miio.gateway import ( GATEWAY_MODEL_AC_V1, GATEWAY_MODEL_AC_V2, GATEWAY_MODEL_AC_V3, GATEWAY_MODEL_EU, - DeviceType, GatewayException, ) import voluptuous as vol @@ -31,7 +31,7 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from .const import CONF_FLOW_TYPE, CONF_GATEWAY, DOMAIN +from .const import CONF_FLOW_TYPE, CONF_GATEWAY, DOMAIN, KEY_COORDINATOR from .gateway import XiaomiGatewayDevice _LOGGER = logging.getLogger(__name__) @@ -87,7 +87,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY: - gateway = hass.data[DOMAIN][config_entry.entry_id] + gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY] # Gateway illuminance sensor if gateway.model not in [ GATEWAY_MODEL_AC_V1, @@ -102,16 +102,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) # Gateway sub devices sub_devices = gateway.devices + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] for sub_device in sub_devices.values(): - sensor_variables = None - if sub_device.type == DeviceType.SensorHT: - sensor_variables = ["temperature", "humidity"] - if sub_device.type == DeviceType.AqaraHT: - sensor_variables = ["temperature", "humidity", "pressure"] - if sensor_variables is not None: + sensor_variables = set(sub_device.status) & set(GATEWAY_SENSOR_TYPES) + if sensor_variables: entities.extend( [ - XiaomiGatewaySensor(sub_device, config_entry, variable) + XiaomiGatewaySensor( + coordinator, sub_device, config_entry, variable + ) for variable in sensor_variables ] ) @@ -240,9 +239,9 @@ class XiaomiAirQualityMonitor(Entity): class XiaomiGatewaySensor(XiaomiGatewayDevice): """Representation of a XiaomiGatewaySensor.""" - def __init__(self, sub_device, entry, data_key): + def __init__(self, coordinator, sub_device, entry, data_key): """Initialize the XiaomiSensor.""" - super().__init__(sub_device, entry) + super().__init__(coordinator, sub_device, entry) self._data_key = data_key self._unique_id = f"{sub_device.sid}-{data_key}" self._name = f"{data_key} ({sub_device.sid})".capitalize() @@ -288,9 +287,7 @@ class XiaomiGatewayIlluminanceSensor(Entity): @property def device_info(self): """Return the device info of the gateway.""" - return { - "identifiers": {(DOMAIN, self._gateway_device_id)}, - } + return {"identifiers": {(DOMAIN, self._gateway_device_id)}} @property def name(self): From f0e9ef421c264f882b73a9a62897e3982ec5f03d Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 16 Feb 2021 15:48:06 +0100 Subject: [PATCH 465/796] Fix vlc_telnet state update (#46628) * Clean up * Add debug logs * Fix info lookup * Handle more errors * Guard get length and time --- .../components/vlc_telnet/media_player.py | 78 +++++++++++-------- 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 96690955fcc..a7d5ee0a211 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -1,7 +1,13 @@ """Provide functionality to interact with the vlc telnet interface.""" import logging -from python_telnet_vlc import ConnectionError as ConnErr, VLCTelnet +from python_telnet_vlc import ( + CommandError, + ConnectionError as ConnErr, + LuaError, + ParseError, + VLCTelnet, +) import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity @@ -82,7 +88,6 @@ class VlcDevice(MediaPlayerEntity): def __init__(self, name, host, port, passwd): """Initialize the vlc device.""" - self._instance = None self._name = name self._volume = None self._muted = None @@ -94,7 +99,7 @@ class VlcDevice(MediaPlayerEntity): self._port = port self._password = passwd self._vlc = None - self._available = False + self._available = True self._volume_bkp = 0 self._media_artist = "" self._media_title = "" @@ -104,43 +109,54 @@ class VlcDevice(MediaPlayerEntity): if self._vlc is None: try: self._vlc = VLCTelnet(self._host, self._password, self._port) - self._state = STATE_IDLE - self._available = True - except (ConnErr, EOFError): - self._available = False + except (ConnErr, EOFError) as err: + if self._available: + _LOGGER.error("Connection error: %s", err) + self._available = False self._vlc = None - else: - try: - status = self._vlc.status() - if status: - if "volume" in status: - self._volume = int(status["volume"]) / 500.0 - else: - self._volume = None - if "state" in status: - state = status["state"] - if state == "playing": - self._state = STATE_PLAYING - elif state == "paused": - self._state = STATE_PAUSED - else: - self._state = STATE_IDLE + return + + self._state = STATE_IDLE + self._available = True + + try: + status = self._vlc.status() + _LOGGER.debug("Status: %s", status) + + if status: + if "volume" in status: + self._volume = int(status["volume"]) / 500.0 + else: + self._volume = None + if "state" in status: + state = status["state"] + if state == "playing": + self._state = STATE_PLAYING + elif state == "paused": + self._state = STATE_PAUSED else: self._state = STATE_IDLE + else: + self._state = STATE_IDLE + if self._state != STATE_IDLE: self._media_duration = self._vlc.get_length() self._media_position = self._vlc.get_time() - info = self._vlc.info() - if info: - self._media_artist = info[0].get("artist") - self._media_title = info[0].get("title") + info = self._vlc.info() + _LOGGER.debug("Info: %s", info) - except (ConnErr, EOFError): + if info: + self._media_artist = info.get(0, {}).get("artist") + self._media_title = info.get(0, {}).get("title") + + except (CommandError, LuaError, ParseError) as err: + _LOGGER.error("Command error: %s", err) + except (ConnErr, EOFError) as err: + if self._available: + _LOGGER.error("Connection error: %s", err) self._available = False - self._vlc = None - - return True + self._vlc = None @property def name(self): From 68e78a2ddce52a6c6be8c43712d0e4a0280902c0 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Tue, 16 Feb 2021 09:49:31 -0500 Subject: [PATCH 466/796] Remove unnecessary constants from universal (#46537) --- homeassistant/components/universal/media_player.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index e4891dca68a..1834d22855c 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -94,13 +94,10 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.service import async_call_from_config ATTR_ACTIVE_CHILD = "active_child" -ATTR_DATA = "data" CONF_ATTRS = "attributes" CONF_CHILDREN = "children" CONF_COMMANDS = "commands" -CONF_SERVICE = "service" -CONF_SERVICE_DATA = "service_data" OFF_STATES = [STATE_IDLE, STATE_OFF, STATE_UNAVAILABLE] @@ -124,7 +121,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the universal media players.""" - await async_setup_reload_service(hass, "universal", ["media_player"]) player = UniversalMediaPlayer( From 08201d146b372fbfd40d80a7d37fc853461ed6c4 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 16 Feb 2021 06:59:43 -0800 Subject: [PATCH 467/796] Separate HLS logic out of core StreamOutput to prepare for discontinuity (#46610) Separate the HLS stream view logic out of StreamOutput since the hls stream view is about to get more complex to track discontinuities. This makes the idle timeout, shutdown, and coupling between hls and record more explicit. --- homeassistant/components/camera/__init__.py | 14 +- homeassistant/components/stream/__init__.py | 149 ++++++++++---------- homeassistant/components/stream/const.py | 8 +- homeassistant/components/stream/core.py | 93 +----------- homeassistant/components/stream/hls.py | 92 ++++++++---- homeassistant/components/stream/recorder.py | 43 ++---- homeassistant/components/stream/worker.py | 11 +- tests/components/stream/test_hls.py | 22 ++- tests/components/stream/test_recorder.py | 78 +++++----- tests/components/stream/test_worker.py | 9 +- 10 files changed, 225 insertions(+), 294 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index ba61f155473..d0bee4e249f 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -24,7 +24,11 @@ from homeassistant.components.media_player.const import ( SERVICE_PLAY_MEDIA, ) from homeassistant.components.stream import Stream, create_stream -from homeassistant.components.stream.const import FORMAT_CONTENT_TYPE, OUTPUT_FORMATS +from homeassistant.components.stream.const import ( + FORMAT_CONTENT_TYPE, + HLS_OUTPUT, + OUTPUT_FORMATS, +) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_FILENAME, @@ -254,7 +258,7 @@ async def async_setup(hass, config): stream = await camera.create_stream() if not stream: continue - stream.add_provider("hls") + stream.hls_output() stream.start() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, preload_stream) @@ -702,6 +706,8 @@ async def async_handle_play_stream_service(camera, service_call): async def _async_stream_endpoint_url(hass, camera, fmt): + if fmt != HLS_OUTPUT: + raise ValueError("Only format {HLS_OUTPUT} is supported") stream = await camera.create_stream() if not stream: raise HomeAssistantError( @@ -712,9 +718,9 @@ async def _async_stream_endpoint_url(hass, camera, fmt): camera_prefs = hass.data[DATA_CAMERA_PREFS].get(camera.entity_id) stream.keepalive = camera_prefs.preload_stream - stream.add_provider(fmt) + stream.hls_output() stream.start() - return stream.endpoint_url(fmt) + return stream.endpoint_url() async def async_handle_record_service(camera, call): diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index cdaa0faeb95..677d01e5006 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -7,25 +7,25 @@ a new Stream object. Stream manages: - Home Assistant URLs for viewing a stream - Access tokens for URLs for viewing a stream -A Stream consists of a background worker, and one or more output formats each -with their own idle timeout managed by the stream component. When an output -format is no longer in use, the stream component will expire it. When there -are no active output formats, the background worker is shut down and access -tokens are expired. Alternatively, a Stream can be configured with keepalive -to always keep workers active. +A Stream consists of a background worker and multiple output streams (e.g. hls +and recorder). The worker has a callback to retrieve the current active output +streams where it writes the decoded output packets. The HLS stream has an +inactivity idle timeout that expires the access token. When all output streams +are inactive, the background worker is shut down. Alternatively, a Stream +can be configured with keepalive to always keep workers active. """ import logging import secrets import threading import time -from types import MappingProxyType +from typing import List from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from .const import ( - ATTR_ENDPOINTS, + ATTR_HLS_ENDPOINT, ATTR_STREAMS, DOMAIN, MAX_SEGMENTS, @@ -33,8 +33,8 @@ from .const import ( STREAM_RESTART_INCREMENT, STREAM_RESTART_RESET_TIME, ) -from .core import PROVIDERS, IdleTimer -from .hls import async_setup_hls +from .core import IdleTimer, StreamOutput +from .hls import HlsStreamOutput, async_setup_hls _LOGGER = logging.getLogger(__name__) @@ -75,12 +75,10 @@ async def async_setup(hass, config): from .recorder import async_setup_recorder hass.data[DOMAIN] = {} - hass.data[DOMAIN][ATTR_ENDPOINTS] = {} hass.data[DOMAIN][ATTR_STREAMS] = [] # Setup HLS - hls_endpoint = async_setup_hls(hass) - hass.data[DOMAIN][ATTR_ENDPOINTS]["hls"] = hls_endpoint + hass.data[DOMAIN][ATTR_HLS_ENDPOINT] = async_setup_hls(hass) # Setup Recorder async_setup_recorder(hass) @@ -89,7 +87,6 @@ async def async_setup(hass, config): def shutdown(event): """Stop all stream workers.""" for stream in hass.data[DOMAIN][ATTR_STREAMS]: - stream.keepalive = False stream.stop() _LOGGER.info("Stopped stream workers") @@ -110,58 +107,53 @@ class Stream: self.access_token = None self._thread = None self._thread_quit = threading.Event() - self._outputs = {} + self._hls = None + self._hls_timer = None + self._recorder = None self._fast_restart_once = False if self.options is None: self.options = {} - def endpoint_url(self, fmt): - """Start the stream and returns a url for the output format.""" - if fmt not in self._outputs: - raise ValueError(f"Stream is not configured for format '{fmt}'") + def endpoint_url(self) -> str: + """Start the stream and returns a url for the hls endpoint.""" + if not self._hls: + raise ValueError("Stream is not configured for hls") if not self.access_token: self.access_token = secrets.token_hex() - return self.hass.data[DOMAIN][ATTR_ENDPOINTS][fmt].format(self.access_token) + return self.hass.data[DOMAIN][ATTR_HLS_ENDPOINT].format(self.access_token) - def outputs(self): - """Return a copy of the stream outputs.""" - # A copy is returned so the caller can iterate through the outputs - # without concern about self._outputs being modified from another thread. - return MappingProxyType(self._outputs.copy()) + def outputs(self) -> List[StreamOutput]: + """Return the active stream outputs.""" + return [output for output in [self._hls, self._recorder] if output] - def add_provider(self, fmt, timeout=OUTPUT_IDLE_TIMEOUT): - """Add provider output stream.""" - if not self._outputs.get(fmt): + def hls_output(self) -> StreamOutput: + """Return the hls output stream, creating if not already active.""" + if not self._hls: + self._hls = HlsStreamOutput(self.hass) + self._hls_timer = IdleTimer(self.hass, OUTPUT_IDLE_TIMEOUT, self._hls_idle) + self._hls_timer.start() + self._hls_timer.awake() + return self._hls - @callback - def idle_callback(): - if not self.keepalive and fmt in self._outputs: - self.remove_provider(self._outputs[fmt]) - self.check_idle() + @callback + def _hls_idle(self): + """Reset access token and cleanup stream due to inactivity.""" + self.access_token = None + if not self.keepalive: + self._hls.cleanup() + self._hls = None + self._hls_timer = None + self._check_idle() - provider = PROVIDERS[fmt]( - self.hass, IdleTimer(self.hass, timeout, idle_callback) - ) - self._outputs[fmt] = provider - return self._outputs[fmt] - - def remove_provider(self, provider): - """Remove provider output stream.""" - if provider.name in self._outputs: - self._outputs[provider.name].cleanup() - del self._outputs[provider.name] - - if not self._outputs: - self.stop() - - def check_idle(self): - """Reset access token if all providers are idle.""" - if all([p.idle for p in self._outputs.values()]): - self.access_token = None + def _check_idle(self): + """Check if all outputs are idle and shut down worker.""" + if self.keepalive or self.outputs(): + return + self.stop() def start(self): - """Start a stream.""" + """Start stream decode worker.""" if self._thread is None or not self._thread.is_alive(): if self._thread is not None: # The thread must have crashed/exited. Join to clean up the @@ -215,21 +207,18 @@ class Stream: def _worker_finished(self): """Schedule cleanup of all outputs.""" - - @callback - def remove_outputs(): - for provider in self.outputs().values(): - self.remove_provider(provider) - - self.hass.loop.call_soon_threadsafe(remove_outputs) + self.hass.loop.call_soon_threadsafe(self.stop) def stop(self): """Remove outputs and access token.""" - self._outputs = {} self.access_token = None - - if not self.keepalive: - self._stop() + if self._hls: + self._hls.cleanup() + self._hls = None + if self._recorder: + self._recorder.save() + self._recorder = None + self._stop() def _stop(self): """Stop worker thread.""" @@ -242,25 +231,35 @@ class Stream: async def async_record(self, video_path, duration=30, lookback=5): """Make a .mp4 recording from a provided stream.""" + # Keep import here so that we can import stream integration without installing reqs + # pylint: disable=import-outside-toplevel + from .recorder import RecorderOutput + # Check for file access if not self.hass.config.is_allowed_path(video_path): raise HomeAssistantError(f"Can't write {video_path}, no access to path!") # Add recorder - recorder = self.outputs().get("recorder") - if recorder: + if self._recorder: raise HomeAssistantError( - f"Stream already recording to {recorder.video_path}!" + f"Stream already recording to {self._recorder.video_path}!" ) - recorder = self.add_provider("recorder", timeout=duration) - recorder.video_path = video_path - + self._recorder = RecorderOutput(self.hass) + self._recorder.video_path = video_path self.start() # Take advantage of lookback - hls = self.outputs().get("hls") - if lookback > 0 and hls: - num_segments = min(int(lookback // hls.target_duration), MAX_SEGMENTS) + if lookback > 0 and self._hls: + num_segments = min(int(lookback // self._hls.target_duration), MAX_SEGMENTS) # Wait for latest segment, then add the lookback - await hls.recv() - recorder.prepend(list(hls.get_segment())[-num_segments:]) + await self._hls.recv() + self._recorder.prepend(list(self._hls.get_segment())[-num_segments:]) + + @callback + def save_recording(): + if self._recorder: + self._recorder.save() + self._recorder = None + self._check_idle() + + IdleTimer(self.hass, duration, save_recording).start() diff --git a/homeassistant/components/stream/const.py b/homeassistant/components/stream/const.py index 41df806d020..55f447a9a69 100644 --- a/homeassistant/components/stream/const.py +++ b/homeassistant/components/stream/const.py @@ -1,10 +1,14 @@ """Constants for Stream component.""" DOMAIN = "stream" -ATTR_ENDPOINTS = "endpoints" +ATTR_HLS_ENDPOINT = "hls_endpoint" ATTR_STREAMS = "streams" -OUTPUT_FORMATS = ["hls"] +HLS_OUTPUT = "hls" +OUTPUT_FORMATS = [HLS_OUTPUT] +OUTPUT_CONTAINER_FORMAT = "mp4" +OUTPUT_VIDEO_CODECS = {"hevc", "h264"} +OUTPUT_AUDIO_CODECS = {"aac", "mp3"} FORMAT_CONTENT_TYPE = {"hls": "application/vnd.apple.mpegurl"} diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 31c7940b8e1..4fc70eb856f 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -1,8 +1,7 @@ """Provides core stream functionality.""" -import asyncio -from collections import deque +import abc import io -from typing import Any, Callable, List +from typing import Callable from aiohttp import web import attr @@ -10,11 +9,8 @@ import attr from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.event import async_call_later -from homeassistant.util.decorator import Registry -from .const import ATTR_STREAMS, DOMAIN, MAX_SEGMENTS - -PROVIDERS = Registry() +from .const import ATTR_STREAMS, DOMAIN @attr.s @@ -78,86 +74,18 @@ class IdleTimer: self._callback() -class StreamOutput: +class StreamOutput(abc.ABC): """Represents a stream output.""" - def __init__(self, hass: HomeAssistant, idle_timer: IdleTimer) -> None: + def __init__(self, hass: HomeAssistant): """Initialize a stream output.""" self._hass = hass - self._idle_timer = idle_timer - self._cursor = None - self._event = asyncio.Event() - self._segments = deque(maxlen=MAX_SEGMENTS) - - @property - def name(self) -> str: - """Return provider name.""" - return None - - @property - def idle(self) -> bool: - """Return True if the output is idle.""" - return self._idle_timer.idle - - @property - def format(self) -> str: - """Return container format.""" - return None - - @property - def audio_codecs(self) -> str: - """Return desired audio codecs.""" - return None - - @property - def video_codecs(self) -> tuple: - """Return desired video codecs.""" - return None @property def container_options(self) -> Callable[[int], dict]: """Return Callable which takes a sequence number and returns container options.""" return None - @property - def segments(self) -> List[int]: - """Return current sequence from segments.""" - return [s.sequence for s in self._segments] - - @property - def target_duration(self) -> int: - """Return the max duration of any given segment in seconds.""" - segment_length = len(self._segments) - if not segment_length: - return 1 - durations = [s.duration for s in self._segments] - return round(max(durations)) or 1 - - def get_segment(self, sequence: int = None) -> Any: - """Retrieve a specific segment, or the whole list.""" - self._idle_timer.awake() - - if not sequence: - return self._segments - - for segment in self._segments: - if segment.sequence == sequence: - return segment - return None - - async def recv(self) -> Segment: - """Wait for and retrieve the latest segment.""" - last_segment = max(self.segments, default=0) - if self._cursor is None or self._cursor <= last_segment: - await self._event.wait() - - if not self._segments: - return None - - segment = self.get_segment()[-1] - self._cursor = segment.sequence - return segment - def put(self, segment: Segment) -> None: """Store output.""" self._hass.loop.call_soon_threadsafe(self._async_put, segment) @@ -165,17 +93,6 @@ class StreamOutput: @callback def _async_put(self, segment: Segment) -> None: """Store output from event loop.""" - # Start idle timeout when we start receiving data - self._idle_timer.start() - self._segments.append(segment) - self._event.set() - self._event.clear() - - def cleanup(self): - """Handle cleanup.""" - self._event.set() - self._idle_timer.clear() - self._segments = deque(maxlen=MAX_SEGMENTS) class StreamView(HomeAssistantView): diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index bd5fbd5e9ae..57894d17711 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -1,13 +1,15 @@ """Provide functionality to stream HLS.""" +import asyncio +from collections import deque import io -from typing import Callable +from typing import Any, Callable, List from aiohttp import web from homeassistant.core import callback -from .const import FORMAT_CONTENT_TYPE, NUM_PLAYLIST_SEGMENTS -from .core import PROVIDERS, StreamOutput, StreamView +from .const import FORMAT_CONTENT_TYPE, MAX_SEGMENTS, NUM_PLAYLIST_SEGMENTS +from .core import Segment, StreamOutput, StreamView from .fmp4utils import get_codec_string, get_init, get_m4s @@ -48,8 +50,7 @@ class HlsMasterPlaylistView(StreamView): async def handle(self, request, stream, sequence): """Return m3u8 playlist.""" - track = stream.add_provider("hls") - stream.start() + track = stream.hls_output() # Wait for a segment to be ready if not track.segments: if not await track.recv(): @@ -102,8 +103,7 @@ class HlsPlaylistView(StreamView): async def handle(self, request, stream, sequence): """Return m3u8 playlist.""" - track = stream.add_provider("hls") - stream.start() + track = stream.hls_output() # Wait for a segment to be ready if not track.segments: if not await track.recv(): @@ -121,7 +121,7 @@ class HlsInitView(StreamView): async def handle(self, request, stream, sequence): """Return init.mp4.""" - track = stream.add_provider("hls") + track = stream.hls_output() segments = track.get_segment() if not segments: return web.HTTPNotFound() @@ -138,7 +138,7 @@ class HlsSegmentView(StreamView): async def handle(self, request, stream, sequence): """Return fmp4 segment.""" - track = stream.add_provider("hls") + track = stream.hls_output() segment = track.get_segment(int(sequence)) if not segment: return web.HTTPNotFound() @@ -149,29 +149,15 @@ class HlsSegmentView(StreamView): ) -@PROVIDERS.register("hls") class HlsStreamOutput(StreamOutput): """Represents HLS Output formats.""" - @property - def name(self) -> str: - """Return provider name.""" - return "hls" - - @property - def format(self) -> str: - """Return container format.""" - return "mp4" - - @property - def audio_codecs(self) -> str: - """Return desired audio codecs.""" - return {"aac", "mp3"} - - @property - def video_codecs(self) -> tuple: - """Return desired video codecs.""" - return {"hevc", "h264"} + def __init__(self, hass) -> None: + """Initialize HlsStreamOutput.""" + super().__init__(hass) + self._cursor = None + self._event = asyncio.Event() + self._segments = deque(maxlen=MAX_SEGMENTS) @property def container_options(self) -> Callable[[int], dict]: @@ -182,3 +168,51 @@ class HlsStreamOutput(StreamOutput): "avoid_negative_ts": "make_non_negative", "fragment_index": str(sequence), } + + @property + def segments(self) -> List[int]: + """Return current sequence from segments.""" + return [s.sequence for s in self._segments] + + @property + def target_duration(self) -> int: + """Return the max duration of any given segment in seconds.""" + segment_length = len(self._segments) + if not segment_length: + return 1 + durations = [s.duration for s in self._segments] + return round(max(durations)) or 1 + + def get_segment(self, sequence: int = None) -> Any: + """Retrieve a specific segment, or the whole list.""" + if not sequence: + return self._segments + + for segment in self._segments: + if segment.sequence == sequence: + return segment + return None + + async def recv(self) -> Segment: + """Wait for and retrieve the latest segment.""" + last_segment = max(self.segments, default=0) + if self._cursor is None or self._cursor <= last_segment: + await self._event.wait() + + if not self._segments: + return None + + segment = self.get_segment()[-1] + self._cursor = segment.sequence + return segment + + def _async_put(self, segment: Segment) -> None: + """Store output from event loop.""" + self._segments.append(segment) + self._event.set() + self._event.clear() + + def cleanup(self): + """Handle cleanup.""" + self._event.set() + self._segments = deque(maxlen=MAX_SEGMENTS) diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index 7db9997f870..0fc3d84b1b9 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -6,9 +6,10 @@ from typing import List import av -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback -from .core import PROVIDERS, IdleTimer, Segment, StreamOutput +from .const import OUTPUT_CONTAINER_FORMAT +from .core import Segment, StreamOutput _LOGGER = logging.getLogger(__name__) @@ -18,7 +19,7 @@ def async_setup_recorder(hass): """Only here so Provider Registry works.""" -def recorder_save_worker(file_out: str, segments: List[Segment], container_format: str): +def recorder_save_worker(file_out: str, segments: List[Segment], container_format): """Handle saving stream.""" if not os.path.exists(os.path.dirname(file_out)): os.makedirs(os.path.dirname(file_out), exist_ok=True) @@ -68,51 +69,31 @@ def recorder_save_worker(file_out: str, segments: List[Segment], container_forma output.close() -@PROVIDERS.register("recorder") class RecorderOutput(StreamOutput): """Represents HLS Output formats.""" - def __init__(self, hass: HomeAssistant, idle_timer: IdleTimer) -> None: + def __init__(self, hass) -> None: """Initialize recorder output.""" - super().__init__(hass, idle_timer) + super().__init__(hass) self.video_path = None self._segments = [] - @property - def name(self) -> str: - """Return provider name.""" - return "recorder" - - @property - def format(self) -> str: - """Return container format.""" - return "mp4" - - @property - def audio_codecs(self) -> str: - """Return desired audio codec.""" - return {"aac", "mp3"} - - @property - def video_codecs(self) -> tuple: - """Return desired video codecs.""" - return {"hevc", "h264"} + def _async_put(self, segment: Segment) -> None: + """Store output.""" + self._segments.append(segment) def prepend(self, segments: List[Segment]) -> None: """Prepend segments to existing list.""" - own_segments = self.segments - segments = [s for s in segments if s.sequence not in own_segments] + segments = [s for s in segments if s.sequence not in self._segments] self._segments = segments + self._segments - def cleanup(self): + def save(self): """Write recording and clean up.""" _LOGGER.debug("Starting recorder worker thread") thread = threading.Thread( name="recorder_save_worker", target=recorder_save_worker, - args=(self.video_path, self._segments, self.format), + args=(self.video_path, self._segments, OUTPUT_CONTAINER_FORMAT), ) thread.start() - - super().cleanup() self._segments = [] diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 2050787a714..41cb4bafd90 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -9,6 +9,9 @@ from .const import ( MAX_MISSING_DTS, MAX_TIMESTAMP_GAP, MIN_SEGMENT_DURATION, + OUTPUT_AUDIO_CODECS, + OUTPUT_CONTAINER_FORMAT, + OUTPUT_VIDEO_CODECS, PACKETS_TO_WAIT_FOR_AUDIO, STREAM_TIMEOUT, ) @@ -29,7 +32,7 @@ def create_stream_buffer(stream_output, video_stream, audio_stream, sequence): output = av.open( segment, mode="w", - format=stream_output.format, + format=OUTPUT_CONTAINER_FORMAT, container_options={ "video_track_timescale": str(int(1 / video_stream.time_base)), **container_options, @@ -38,7 +41,7 @@ def create_stream_buffer(stream_output, video_stream, audio_stream, sequence): vstream = output.add_stream(template=video_stream) # Check if audio is requested astream = None - if audio_stream and audio_stream.name in stream_output.audio_codecs: + if audio_stream and audio_stream.name in OUTPUT_AUDIO_CODECS: astream = output.add_stream(template=audio_stream) return StreamBuffer(segment, output, vstream, astream) @@ -65,8 +68,8 @@ class SegmentBuffer: # Fetch the latest StreamOutputs, which may have changed since the # worker started. self._outputs = [] - for stream_output in self._outputs_callback().values(): - if self._video_stream.name not in stream_output.video_codecs: + for stream_output in self._outputs_callback(): + if self._video_stream.name not in OUTPUT_VIDEO_CODECS: continue buffer = create_stream_buffer( stream_output, self._video_stream, self._audio_stream, self._sequence diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 7811cac2a2a..2a53e2c5169 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -45,7 +45,7 @@ def hls_stream(hass, hass_client): async def create_client_for_stream(stream): http_client = await hass_client() - parsed_url = urlparse(stream.endpoint_url("hls")) + parsed_url = urlparse(stream.endpoint_url()) return HlsClient(http_client, parsed_url) return create_client_for_stream @@ -87,7 +87,7 @@ async def test_hls_stream(hass, hls_stream, stream_worker_sync): stream = create_stream(hass, source) # Request stream - stream.add_provider("hls") + stream.hls_output() stream.start() hls_client = await hls_stream(stream) @@ -128,9 +128,9 @@ async def test_stream_timeout(hass, hass_client, stream_worker_sync): stream = create_stream(hass, source) # Request stream - stream.add_provider("hls") + stream.hls_output() stream.start() - url = stream.endpoint_url("hls") + url = stream.endpoint_url() http_client = await hass_client() @@ -168,12 +168,10 @@ async def test_stream_ended(hass, stream_worker_sync): # Setup demo HLS track source = generate_h264_video() stream = create_stream(hass, source) - track = stream.add_provider("hls") # Request stream - stream.add_provider("hls") + track = stream.hls_output() stream.start() - stream.endpoint_url("hls") # Run it dead while True: @@ -199,7 +197,7 @@ async def test_stream_keepalive(hass): # Setup demo HLS track source = "test_stream_keepalive_source" stream = create_stream(hass, source) - track = stream.add_provider("hls") + track = stream.hls_output() track.num_segments = 2 stream.start() @@ -230,12 +228,12 @@ async def test_stream_keepalive(hass): stream.stop() -async def test_hls_playlist_view_no_output(hass, hass_client, hls_stream): +async def test_hls_playlist_view_no_output(hass, hls_stream): """Test rendering the hls playlist with no output segments.""" await async_setup_component(hass, "stream", {"stream": {}}) stream = create_stream(hass, STREAM_SOURCE) - stream.add_provider("hls") + stream.hls_output() hls_client = await hls_stream(stream) @@ -250,7 +248,7 @@ async def test_hls_playlist_view(hass, hls_stream, stream_worker_sync): stream = create_stream(hass, STREAM_SOURCE) stream_worker_sync.pause() - hls = stream.add_provider("hls") + hls = stream.hls_output() hls.put(Segment(1, SEQUENCE_BYTES, DURATION)) await hass.async_block_till_done() @@ -277,7 +275,7 @@ async def test_hls_max_segments(hass, hls_stream, stream_worker_sync): stream = create_stream(hass, STREAM_SOURCE) stream_worker_sync.pause() - hls = stream.add_provider("hls") + hls = stream.hls_output() hls_client = await hls_stream(stream) diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index 9d418c360b1..3930a5e237d 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -1,10 +1,12 @@ """The tests for hls streams.""" +import asyncio from datetime import timedelta import logging import os import threading from unittest.mock import patch +import async_timeout import av import pytest @@ -32,23 +34,30 @@ class SaveRecordWorkerSync: def __init__(self): """Initialize SaveRecordWorkerSync.""" self.reset() + self._segments = None - def recorder_save_worker(self, *args, **kwargs): + def recorder_save_worker(self, file_out, segments, container_format): """Mock method for patch.""" logging.debug("recorder_save_worker thread started") + self._segments = segments assert self._save_thread is None self._save_thread = threading.current_thread() self._save_event.set() + async def get_segments(self): + """Verify save worker thread was invoked and return saved segments.""" + with async_timeout.timeout(TEST_TIMEOUT): + assert await self._save_event.wait() + return self._segments + def join(self): - """Verify save worker was invoked and block on shutdown.""" - assert self._save_event.wait(timeout=TEST_TIMEOUT) + """Block until the record worker thread exist to ensure cleanup.""" self._save_thread.join() def reset(self): """Reset callback state for reuse in tests.""" self._save_thread = None - self._save_event = threading.Event() + self._save_event = asyncio.Event() @pytest.fixture() @@ -63,7 +72,7 @@ def record_worker_sync(hass): yield sync -async def test_record_stream(hass, hass_client, stream_worker_sync, record_worker_sync): +async def test_record_stream(hass, hass_client, record_worker_sync): """ Test record stream. @@ -73,28 +82,14 @@ async def test_record_stream(hass, hass_client, stream_worker_sync, record_worke """ await async_setup_component(hass, "stream", {"stream": {}}) - stream_worker_sync.pause() - # Setup demo track source = generate_h264_video() stream = create_stream(hass, source) with patch.object(hass.config, "is_allowed_path", return_value=True): await stream.async_record("/example/path") - recorder = stream.add_provider("recorder") - while True: - segment = await recorder.recv() - if not segment: - break - segments = segment.sequence - if segments > 1: - stream_worker_sync.resume() - - stream.stop() - assert segments > 1 - - # Verify that the save worker was invoked, then block until its - # thread completes and is shutdown completely to avoid thread leaks. + segments = await record_worker_sync.get_segments() + assert len(segments) > 1 record_worker_sync.join() @@ -107,19 +102,24 @@ async def test_record_lookback( source = generate_h264_video() stream = create_stream(hass, source) + # Don't let the stream finish (and clean itself up) until the test has had + # a chance to perform lookback + stream_worker_sync.pause() + # Start an HLS feed to enable lookback - stream.add_provider("hls") - stream.start() + stream.hls_output() with patch.object(hass.config, "is_allowed_path", return_value=True): await stream.async_record("/example/path", lookback=4) # This test does not need recorder cleanup since it is not fully exercised - + stream_worker_sync.resume() stream.stop() -async def test_recorder_timeout(hass, hass_client, stream_worker_sync): +async def test_recorder_timeout( + hass, hass_client, stream_worker_sync, record_worker_sync +): """ Test recorder timeout. @@ -137,9 +137,8 @@ async def test_recorder_timeout(hass, hass_client, stream_worker_sync): stream = create_stream(hass, source) with patch.object(hass.config, "is_allowed_path", return_value=True): await stream.async_record("/example/path") - recorder = stream.add_provider("recorder") - await recorder.recv() + assert not mock_timeout.called # Wait a minute future = dt_util.utcnow() + timedelta(minutes=1) @@ -149,9 +148,11 @@ async def test_recorder_timeout(hass, hass_client, stream_worker_sync): assert mock_timeout.called stream_worker_sync.resume() + # Verify worker is invoked, and do clean shutdown of worker thread + await record_worker_sync.get_segments() + record_worker_sync.join() + stream.stop() - await hass.async_block_till_done() - await hass.async_block_till_done() async def test_record_path_not_allowed(hass, hass_client): @@ -180,9 +181,7 @@ async def test_recorder_save(tmpdir): assert os.path.exists(filename) -async def test_record_stream_audio( - hass, hass_client, stream_worker_sync, record_worker_sync -): +async def test_record_stream_audio(hass, hass_client, record_worker_sync): """ Test treatment of different audio inputs. @@ -198,7 +197,6 @@ async def test_record_stream_audio( (None, 0), # no audio stream ): record_worker_sync.reset() - stream_worker_sync.pause() # Setup demo track source = generate_h264_video( @@ -207,22 +205,14 @@ async def test_record_stream_audio( stream = create_stream(hass, source) with patch.object(hass.config, "is_allowed_path", return_value=True): await stream.async_record("/example/path") - recorder = stream.add_provider("recorder") - while True: - segment = await recorder.recv() - if not segment: - break - last_segment = segment - stream_worker_sync.resume() + segments = await record_worker_sync.get_segments() + last_segment = segments[-1] result = av.open(last_segment.segment, "r", format="mp4") assert len(result.streams.audio) == expected_audio_streams result.close() - stream.stop() - await hass.async_block_till_done() - # Verify that the save worker was invoked, then block until its - # thread completes and is shutdown completely to avoid thread leaks. + stream.stop() record_worker_sync.join() diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index b348d68fc86..d9006c81ad5 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -31,7 +31,6 @@ from homeassistant.components.stream.worker import stream_worker STREAM_SOURCE = "some-stream-source" # Formats here are arbitrary, not exercised by tests -STREAM_OUTPUT_FORMAT = "hls" AUDIO_STREAM_FORMAT = "mp3" VIDEO_STREAM_FORMAT = "h264" VIDEO_FRAME_RATE = 12 @@ -188,7 +187,7 @@ class MockPyAv: async def async_decode_stream(hass, packets, py_av=None): """Start a stream worker that decodes incoming stream packets into output segments.""" stream = Stream(hass, STREAM_SOURCE) - stream.add_provider(STREAM_OUTPUT_FORMAT) + stream.hls_output() if not py_av: py_av = MockPyAv() @@ -207,7 +206,7 @@ async def async_decode_stream(hass, packets, py_av=None): async def test_stream_open_fails(hass): """Test failure on stream open.""" stream = Stream(hass, STREAM_SOURCE) - stream.add_provider(STREAM_OUTPUT_FORMAT) + stream.hls_output() with patch("av.open") as av_open: av_open.side_effect = av.error.InvalidDataError(-2, "error") stream_worker(STREAM_SOURCE, {}, stream.outputs, threading.Event()) @@ -483,7 +482,7 @@ async def test_stream_stopped_while_decoding(hass): worker_wake = threading.Event() stream = Stream(hass, STREAM_SOURCE) - stream.add_provider(STREAM_OUTPUT_FORMAT) + stream.hls_output() py_av = MockPyAv() py_av.container.packets = PacketSequence(TEST_SEQUENCE_LENGTH) @@ -510,7 +509,7 @@ async def test_update_stream_source(hass): worker_wake = threading.Event() stream = Stream(hass, STREAM_SOURCE) - stream.add_provider(STREAM_OUTPUT_FORMAT) + stream.hls_output() # Note that keepalive is not set here. The stream is "restarted" even though # it is not stopping due to failure. From 960b5b7d86dcb32a720b8e87e5403df9e5ae466b Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Tue, 16 Feb 2021 12:06:20 -0500 Subject: [PATCH 468/796] Fix climate hold bug in ecobee (#46613) --- homeassistant/components/ecobee/climate.py | 15 +++++++++++---- homeassistant/components/ecobee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ecobee/test_climate.py | 10 +++++----- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index c61428cbc78..089f0950854 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -559,7 +559,7 @@ class Thermostat(ClimateEntity): if preset_mode == PRESET_AWAY: self.data.ecobee.set_climate_hold( - self.thermostat_index, "away", "indefinite" + self.thermostat_index, "away", "indefinite", self.hold_hours() ) elif preset_mode == PRESET_TEMPERATURE: @@ -570,6 +570,7 @@ class Thermostat(ClimateEntity): self.thermostat_index, PRESET_TO_ECOBEE_HOLD[preset_mode], self.hold_preference(), + self.hold_hours(), ) elif preset_mode == PRESET_NONE: @@ -585,14 +586,20 @@ class Thermostat(ClimateEntity): if climate_ref is not None: self.data.ecobee.set_climate_hold( - self.thermostat_index, climate_ref, self.hold_preference() + self.thermostat_index, + climate_ref, + self.hold_preference(), + self.hold_hours(), ) else: _LOGGER.warning("Received unknown preset mode: %s", preset_mode) else: self.data.ecobee.set_climate_hold( - self.thermostat_index, preset_mode, self.hold_preference() + self.thermostat_index, + preset_mode, + self.hold_preference(), + self.hold_hours(), ) @property @@ -743,7 +750,7 @@ class Thermostat(ClimateEntity): "useEndTime2hour": 2, "useEndTime4hour": 4, } - return hold_hours_map.get(device_preference, 0) + return hold_hours_map.get(device_preference) def create_vacation(self, service_data): """Create a vacation with user-specified parameters.""" diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index 040744b27aa..de7a7d325b3 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.8"], + "requirements": ["python-ecobee-api==0.2.10"], "codeowners": ["@marthoc"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1b2409a8e6f..24bb508c713 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1747,7 +1747,7 @@ python-clementine-remote==1.0.1 python-digitalocean==1.13.2 # homeassistant.components.ecobee -python-ecobee-api==0.2.8 +python-ecobee-api==0.2.10 # homeassistant.components.eq3btsmart # python-eq3bt==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 11064b63db8..e22ab3fa0e2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -914,7 +914,7 @@ pysqueezebox==0.5.5 pysyncthru==0.7.0 # homeassistant.components.ecobee -python-ecobee-api==0.2.8 +python-ecobee-api==0.2.10 # homeassistant.components.darksky python-forecastio==1.4.0 diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index a9b9165d713..975fabcf9ab 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -209,14 +209,14 @@ async def test_set_temperature(ecobee_fixture, thermostat, data): data.reset_mock() thermostat.set_temperature(target_temp_low=20, target_temp_high=30) data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 30, 20, "nextTransition", 0)] + [mock.call(1, 30, 20, "nextTransition", None)] ) # Auto -> Hold data.reset_mock() thermostat.set_temperature(temperature=20) data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 25, 15, "nextTransition", 0)] + [mock.call(1, 25, 15, "nextTransition", None)] ) # Cool -> Hold @@ -224,7 +224,7 @@ async def test_set_temperature(ecobee_fixture, thermostat, data): ecobee_fixture["settings"]["hvacMode"] = "cool" thermostat.set_temperature(temperature=20.5) data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 20.5, 20.5, "nextTransition", 0)] + [mock.call(1, 20.5, 20.5, "nextTransition", None)] ) # Heat -> Hold @@ -232,7 +232,7 @@ async def test_set_temperature(ecobee_fixture, thermostat, data): ecobee_fixture["settings"]["hvacMode"] = "heat" thermostat.set_temperature(temperature=20) data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 20, 20, "nextTransition", 0)] + [mock.call(1, 20, 20, "nextTransition", None)] ) # Heat -> Auto @@ -311,7 +311,7 @@ def test_hold_hours(ecobee_fixture, thermostat): "askMe", ]: ecobee_fixture["settings"]["holdAction"] = action - assert thermostat.hold_hours() == 0 + assert thermostat.hold_hours() is None async def test_set_fan_mode_on(thermostat, data): From c45ce86e530e5fcda4f96c035ab1bc98f4d98a58 Mon Sep 17 00:00:00 2001 From: marecabo <23156476+marecabo@users.noreply.github.com> Date: Tue, 16 Feb 2021 20:36:32 +0100 Subject: [PATCH 469/796] Do not provide icon if device class is set in ESPHome config (#46650) --- homeassistant/components/esphome/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 68a8c00caed..ad23afd4744 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -52,7 +52,7 @@ class EsphomeSensor(EsphomeEntity): @property def icon(self) -> str: """Return the icon.""" - if self._static_info.icon == "": + if not self._static_info.icon or self._static_info.device_class: return None return self._static_info.icon @@ -73,14 +73,14 @@ class EsphomeSensor(EsphomeEntity): @property def unit_of_measurement(self) -> str: """Return the unit the value is expressed in.""" - if self._static_info.unit_of_measurement == "": + if not self._static_info.unit_of_measurement: return None return self._static_info.unit_of_measurement @property def device_class(self) -> str: """Return the class of this device, from component DEVICE_CLASSES.""" - if self._static_info.device_class == "": + if not self._static_info.device_class: return None return self._static_info.device_class From aaecd91407b631dc67bbb3e827b01802bb1a7e19 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 16 Feb 2021 12:10:26 -0800 Subject: [PATCH 470/796] Fix exception in stream idle callback (#46642) * Fix exception in stream idle callback Fix bug where idle timeout callback fails if the stream previously exited. * Add a test for stream idle timeout after stream is stopped. * Add clarifying comment to idle timer clear method * Clear hls timer only on stop --- homeassistant/components/stream/__init__.py | 3 ++ homeassistant/components/stream/core.py | 2 +- tests/components/stream/test_hls.py | 32 +++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 677d01e5006..9b5afb38ed0 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -212,6 +212,9 @@ class Stream: def stop(self): """Remove outputs and access token.""" self.access_token = None + if self._hls_timer: + self._hls_timer.clear() + self._hls_timer = None if self._hls: self._hls.cleanup() self._hls = None diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 4fc70eb856f..f7beb3aa754 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -63,7 +63,7 @@ class IdleTimer: self._unsub = async_call_later(self._hass, self._timeout, self.fire) def clear(self): - """Clear and disable the timer.""" + """Clear and disable the timer if it has not already fired.""" if self._unsub is not None: self._unsub() diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 2a53e2c5169..55b79684b7b 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -159,6 +159,38 @@ async def test_stream_timeout(hass, hass_client, stream_worker_sync): assert fail_response.status == HTTP_NOT_FOUND +async def test_stream_timeout_after_stop(hass, hass_client, stream_worker_sync): + """Test hls stream timeout after the stream has been stopped already.""" + await async_setup_component(hass, "stream", {"stream": {}}) + + stream_worker_sync.pause() + + # Setup demo HLS track + source = generate_h264_video() + stream = create_stream(hass, source) + + # Request stream + stream.hls_output() + stream.start() + url = stream.endpoint_url() + + http_client = await hass_client() + + # Fetch playlist + parsed_url = urlparse(url) + playlist_response = await http_client.get(parsed_url.path) + assert playlist_response.status == 200 + + stream_worker_sync.resume() + stream.stop() + + # Wait 5 minutes and fire callback. Stream should already have been + # stopped so this is a no-op. + future = dt_util.utcnow() + timedelta(minutes=5) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + async def test_stream_ended(hass, stream_worker_sync): """Test hls stream packets ended.""" await async_setup_component(hass, "stream", {"stream": {}}) From e4496ed1e344ce9f560e54e096ceb6b508b58763 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Tue, 16 Feb 2021 13:07:18 -0800 Subject: [PATCH 471/796] Fix KeyError in comfoconnect percentage (#46654) --- homeassistant/components/comfoconnect/fan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index 1d7b9c1c0af..7457d0ffad2 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -96,7 +96,7 @@ class ComfoConnectFan(FanEntity): @property def percentage(self) -> str: """Return the current speed percentage.""" - speed = self._ccb.data[SENSOR_FAN_SPEED_MODE] + speed = self._ccb.data.get(SENSOR_FAN_SPEED_MODE) if speed is None: return None return ranged_value_to_percentage(SPEED_RANGE, speed) From b38af0821b99f5b47a6c5ac944c30f79777ffe82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 17 Feb 2021 00:26:41 +0200 Subject: [PATCH 472/796] Fix version of pip in tox (#46656) --- script/bootstrap | 2 +- tox.ini | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/script/bootstrap b/script/bootstrap index 3ffac11852b..b641ec7e8c0 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -8,4 +8,4 @@ cd "$(dirname "$0")/.." echo "Installing development dependencies..." python3 -m pip install wheel --constraint homeassistant/package_constraints.txt -python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) $(grep stdlib-list requirements_test.txt) $(grep tqdm requirements_test.txt) $(grep pipdeptree requirements_test.txt) $(grep awesomeversion requirements.txt) --constraint homeassistant/package_constraints.txt +python3 -m pip install tox tox-pip-version colorlog pre-commit $(grep mypy requirements_test.txt) $(grep stdlib-list requirements_test.txt) $(grep tqdm requirements_test.txt) $(grep pipdeptree requirements_test.txt) $(grep awesomeversion requirements.txt) --constraint homeassistant/package_constraints.txt diff --git a/tox.ini b/tox.ini index 9c9963c28ee..a82c5afda79 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,8 @@ ignore_basepython_conflict = True [testenv] basepython = {env:PYTHON3_PATH:python3} +# pip version duplicated in homeassistant/package_constraints.txt +pip_version = pip>=8.0.3,<20.3 commands = pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar {posargs} {toxinidir}/script/check_dirty From 318cbf29136f71f569e0be95482d2e86ea9cdfdb Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 17 Feb 2021 00:08:46 +0000 Subject: [PATCH 473/796] [ci skip] Translation update --- .../components/asuswrt/translations/pl.json | 4 +-- .../binary_sensor/translations/zh-Hans.json | 4 +-- .../keenetic_ndms2/translations/pl.json | 36 +++++++++++++++++++ .../keenetic_ndms2/translations/zh-Hant.json | 36 +++++++++++++++++++ .../components/plaato/translations/et.json | 2 +- .../components/tuya/translations/ca.json | 2 ++ .../components/tuya/translations/et.json | 2 ++ .../components/tuya/translations/no.json | 2 ++ .../components/tuya/translations/pl.json | 1 + .../components/tuya/translations/ru.json | 2 ++ .../components/tuya/translations/zh-Hant.json | 2 ++ .../xiaomi_miio/translations/ca.json | 12 ++++++- .../xiaomi_miio/translations/et.json | 12 ++++++- .../xiaomi_miio/translations/no.json | 12 ++++++- .../xiaomi_miio/translations/pl.json | 12 ++++++- .../xiaomi_miio/translations/ru.json | 12 ++++++- .../xiaomi_miio/translations/zh-Hant.json | 12 ++++++- 17 files changed, 154 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/keenetic_ndms2/translations/pl.json create mode 100644 homeassistant/components/keenetic_ndms2/translations/zh-Hant.json diff --git a/homeassistant/components/asuswrt/translations/pl.json b/homeassistant/components/asuswrt/translations/pl.json index b646c8e4503..9fd5d00b1c4 100644 --- a/homeassistant/components/asuswrt/translations/pl.json +++ b/homeassistant/components/asuswrt/translations/pl.json @@ -32,9 +32,9 @@ "step": { "init": { "data": { - "consider_home": "Czas w sekundach, zanim urz\u0105dzenie zostanie uznane za \"nieobecne\"", + "consider_home": "Czas w sekundach, zanim urz\u0105dzenie utrzyma stan \"poza domem\"", "dnsmasq": "Lokalizacja w routerze plik\u00f3w dnsmasq.leases", - "interface": "Interfejs, z kt\u00f3rego chcesz uzyska\u0107 statystyki (np. Eth0, eth1 itp.)", + "interface": "Interfejs, z kt\u00f3rego chcesz uzyska\u0107 statystyki (np. eth0, eth1 itp.)", "require_ip": "Urz\u0105dzenia musz\u0105 mie\u0107 adres IP (w trybie punktu dost\u0119pu)", "track_unknown": "\u015aled\u017a nieznane / nienazwane urz\u0105dzenia" }, diff --git a/homeassistant/components/binary_sensor/translations/zh-Hans.json b/homeassistant/components/binary_sensor/translations/zh-Hans.json index 9254f667a48..a44e16d78e2 100644 --- a/homeassistant/components/binary_sensor/translations/zh-Hans.json +++ b/homeassistant/components/binary_sensor/translations/zh-Hans.json @@ -100,8 +100,8 @@ "on": "\u8fc7\u70ed" }, "light": { - "off": "\u6ca1\u6709\u5149\u7ebf", - "on": "\u68c0\u6d4b\u5230\u5149\u7ebf" + "off": "\u65e0\u5149", + "on": "\u6709\u5149" }, "lock": { "off": "\u4e0a\u9501", diff --git a/homeassistant/components/keenetic_ndms2/translations/pl.json b/homeassistant/components/keenetic_ndms2/translations/pl.json new file mode 100644 index 00000000000..30e7b9a4fae --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/pl.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "name": "Nazwa", + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika" + }, + "title": "Konfiguracja routera Keenetic NDMS2" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "consider_home": "Czas przed oznaczeniem \"poza domem\"", + "include_arp": "U\u017cyj danych ARP (ignorowane, je\u015bli u\u017cywane s\u0105 dane hotspotu)", + "include_associated": "U\u017cyj danych skojarze\u0144 WiFi AP (ignorowane, je\u015bli u\u017cywane s\u0105 dane hotspotu)", + "interfaces": "Wybierz interfejsy do skanowania", + "scan_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania", + "try_hotspot": "U\u017cyj danych \u201eip hotspot\u201d (najdok\u0142adniejsze)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/zh-Hant.json b/homeassistant/components/keenetic_ndms2/translations/zh-Hant.json new file mode 100644 index 00000000000..7900f3a8854 --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/zh-Hant.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "name": "\u540d\u7a31", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "title": "\u8a2d\u5b9a Keenetic NDMS2 \u8def\u7531\u5668" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "consider_home": "\u5224\u5b9a\u5728\u5bb6\u9593\u9694", + "include_arp": "\u4f7f\u7528 ARP \u8cc7\u6599\uff08\u5047\u5982\u5df2\u4f7f\u7528 hotspot \u8cc7\u6599\u5247\u5ffd\u7565\uff09", + "include_associated": "\u4f7f\u7528 WiFi AP \u95dc\u806f\u8cc7\u6599\uff08\u5047\u5982\u5df2\u4f7f\u7528 hotspot \u8cc7\u6599\u5247\u5ffd\u7565\uff09", + "interfaces": "\u9078\u64c7\u6383\u63cf\u4ecb\u9762", + "scan_interval": "\u6383\u63cf\u9593\u8ddd", + "try_hotspot": "\u4f7f\u7528 'ip hotspot' \u8cc7\u6599\uff08\u6700\u7cbe\u6e96\uff09" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/et.json b/homeassistant/components/plaato/translations/et.json index ec7b7e4b1a4..66a7b252a87 100644 --- a/homeassistant/components/plaato/translations/et.json +++ b/homeassistant/components/plaato/translations/et.json @@ -24,7 +24,7 @@ }, "user": { "data": { - "device_name": "Pang oma seadmele nimi", + "device_name": "Pane oma seadmele nimi", "device_type": "Plaato seadme t\u00fc\u00fcp" }, "description": "Kas alustan seadistamist?", diff --git a/homeassistant/components/tuya/translations/ca.json b/homeassistant/components/tuya/translations/ca.json index 908cf287eeb..a00d9683141 100644 --- a/homeassistant/components/tuya/translations/ca.json +++ b/homeassistant/components/tuya/translations/ca.json @@ -40,8 +40,10 @@ "max_temp": "Temperatura desitjada m\u00e0xima (utilitza min i max = 0 per defecte)", "min_kelvin": "Temperatura del color m\u00ednima suportada, en Kelvin", "min_temp": "Temperatura desitjada m\u00ednima (utilitza min i max = 0 per defecte)", + "set_temp_divided": "Utilitza el valor de temperatura dividit per a ordres de configuraci\u00f3 de temperatura", "support_color": "For\u00e7a el suport de color", "temp_divider": "Divisor del valor de temperatura (0 = predeterminat)", + "temp_step_override": "Pas de temperatura objectiu", "tuya_max_coltemp": "Temperatura de color m\u00e0xima enviada pel dispositiu", "unit_of_measurement": "Unitat de temperatura utilitzada pel dispositiu" }, diff --git a/homeassistant/components/tuya/translations/et.json b/homeassistant/components/tuya/translations/et.json index 967b38cdb82..0fc1297ce7c 100644 --- a/homeassistant/components/tuya/translations/et.json +++ b/homeassistant/components/tuya/translations/et.json @@ -40,8 +40,10 @@ "max_temp": "Maksimaalne sihttemperatuur (vaikimisi kasuta min ja max = 0)", "min_kelvin": "Minimaalne v\u00f5imalik v\u00e4rvitemperatuur (Kelvinites)", "min_temp": "Minimaalne sihttemperatuur (vaikimisi kasuta min ja max = 0)", + "set_temp_divided": "M\u00e4\u00e4ratud temperatuuri k\u00e4su jaoks kasuta jagatud temperatuuri v\u00e4\u00e4rtust", "support_color": "Luba v\u00e4rvuse juhtimine", "temp_divider": "Temperatuuri v\u00e4\u00e4rtuse eraldaja (0 = kasuta vaikev\u00e4\u00e4rtust)", + "temp_step_override": "Sihttemperatuuri samm", "tuya_max_coltemp": "Seadme teatatud maksimaalne v\u00e4rvitemperatuur", "unit_of_measurement": "Seadme temperatuuri\u00fchik" }, diff --git a/homeassistant/components/tuya/translations/no.json b/homeassistant/components/tuya/translations/no.json index d0c1a3ca188..d02a88f4097 100644 --- a/homeassistant/components/tuya/translations/no.json +++ b/homeassistant/components/tuya/translations/no.json @@ -40,8 +40,10 @@ "max_temp": "Maks m\u00e5ltemperatur (bruk min og maks = 0 for standard)", "min_kelvin": "Min fargetemperatur st\u00f8ttet i kelvin", "min_temp": "Min m\u00e5ltemperatur (bruk min og maks = 0 for standard)", + "set_temp_divided": "Bruk delt temperaturverdi for innstilt temperaturkommando", "support_color": "Tving fargest\u00f8tte", "temp_divider": "Deler temperaturverdier (0 = bruk standard)", + "temp_step_override": "Trinn for m\u00e5ltemperatur", "tuya_max_coltemp": "Maks fargetemperatur rapportert av enheten", "unit_of_measurement": "Temperaturenhet som brukes av enheten" }, diff --git a/homeassistant/components/tuya/translations/pl.json b/homeassistant/components/tuya/translations/pl.json index dc57f064d57..742ac600c62 100644 --- a/homeassistant/components/tuya/translations/pl.json +++ b/homeassistant/components/tuya/translations/pl.json @@ -40,6 +40,7 @@ "max_temp": "Maksymalna temperatura docelowa (u\u017cyj min i max = 0 dla warto\u015bci domy\u015blnej)", "min_kelvin": "Minimalna obs\u0142ugiwana temperatura barwy w kelwinach", "min_temp": "Minimalna temperatura docelowa (u\u017cyj min i max = 0 dla warto\u015bci domy\u015blnej)", + "set_temp_divided": "U\u017cyj podzielonej warto\u015bci temperatury dla polecenia ustawienia temperatury", "support_color": "Wymu\u015b obs\u0142ug\u0119 kolor\u00f3w", "temp_divider": "Dzielnik warto\u015bci temperatury (0 = u\u017cyj warto\u015bci domy\u015blnej)", "tuya_max_coltemp": "Maksymalna temperatura barwy raportowana przez urz\u0105dzenie", diff --git a/homeassistant/components/tuya/translations/ru.json b/homeassistant/components/tuya/translations/ru.json index b98c6c8e9cd..5d887710230 100644 --- a/homeassistant/components/tuya/translations/ru.json +++ b/homeassistant/components/tuya/translations/ru.json @@ -40,8 +40,10 @@ "max_temp": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u0446\u0435\u043b\u0435\u0432\u0430\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 min \u0438 max = 0)", "min_kelvin": "\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0446\u0432\u0435\u0442\u043e\u0432\u0430\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0432 \u043a\u0435\u043b\u044c\u0432\u0438\u043d\u0430\u0445)", "min_temp": "\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u0446\u0435\u043b\u0435\u0432\u0430\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 min \u0438 max = 0)", + "set_temp_divided": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b", "support_color": "\u041f\u0440\u0438\u043d\u0443\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 \u0446\u0432\u0435\u0442\u0430", "temp_divider": "\u0414\u0435\u043b\u0438\u0442\u0435\u043b\u044c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b (0 = \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e)", + "temp_step_override": "\u0428\u0430\u0433 \u0446\u0435\u043b\u0435\u0432\u043e\u0439 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b", "tuya_max_coltemp": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u0446\u0432\u0435\u0442\u043e\u0432\u0430\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430, \u0441\u043e\u043e\u0431\u0449\u0430\u0435\u043c\u0430\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c", "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c" }, diff --git a/homeassistant/components/tuya/translations/zh-Hant.json b/homeassistant/components/tuya/translations/zh-Hant.json index 08871c3108e..7221c86eb63 100644 --- a/homeassistant/components/tuya/translations/zh-Hant.json +++ b/homeassistant/components/tuya/translations/zh-Hant.json @@ -40,8 +40,10 @@ "max_temp": "\u6700\u9ad8\u76ee\u6a19\u8272\u6eab\uff08\u4f7f\u7528\u6700\u4f4e\u8207\u6700\u9ad8 = 0 \u4f7f\u7528\u9810\u8a2d\uff09", "min_kelvin": "Kelvin \u652f\u63f4\u6700\u4f4e\u8272\u6eab", "min_temp": "\u6700\u4f4e\u76ee\u6a19\u8272\u6eab\uff08\u4f7f\u7528\u6700\u4f4e\u8207\u6700\u9ad8 = 0 \u4f7f\u7528\u9810\u8a2d\uff09", + "set_temp_divided": "\u4f7f\u7528\u5206\u9694\u865f\u6eab\u5ea6\u503c\u4ee5\u57f7\u884c\u8a2d\u5b9a\u6eab\u5ea6\u6307\u4ee4", "support_color": "\u5f37\u5236\u8272\u6eab\u652f\u63f4", "temp_divider": "\u8272\u6eab\u503c\u5206\u914d\u5668\uff080 = \u4f7f\u7528\u9810\u8a2d\uff09", + "temp_step_override": "\u76ee\u6a19\u6eab\u5ea6\u8a2d\u5b9a", "tuya_max_coltemp": "\u88dd\u7f6e\u56de\u5831\u6700\u9ad8\u8272\u6eab", "unit_of_measurement": "\u88dd\u7f6e\u6240\u4f7f\u7528\u4e4b\u6eab\u5ea6\u55ae\u4f4d" }, diff --git a/homeassistant/components/xiaomi_miio/translations/ca.json b/homeassistant/components/xiaomi_miio/translations/ca.json index 5183157371b..3c666fe4ef1 100644 --- a/homeassistant/components/xiaomi_miio/translations/ca.json +++ b/homeassistant/components/xiaomi_miio/translations/ca.json @@ -6,10 +6,20 @@ }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", - "no_device_selected": "No hi ha cap dispositiu seleccionat, selecciona'n un." + "no_device_selected": "No hi ha cap dispositiu seleccionat, selecciona'n un.", + "unknown_device": "No es reconeix el model del dispositiu, no es pot configurar el dispositiu mitjan\u00e7ant el flux de configuraci\u00f3." }, "flow_title": "Xiaomi Miio: {name}", "step": { + "device": { + "data": { + "host": "Adre\u00e7a IP", + "name": "Nom del dispositiu", + "token": "Token d'API" + }, + "description": "Necessitar\u00e0s el Token d'API de 32 car\u00e0cters, consulta les instruccions a https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token. Tingues en compte que aquest Token d'API \u00e9s diferent a la clau utilitzada per la integraci\u00f3 Xiaomi Aqara.", + "title": "Connexi\u00f3 amb un dispositiu Xiaomi Miio o una passarel\u00b7la de Xiaomi" + }, "gateway": { "data": { "host": "Adre\u00e7a IP", diff --git a/homeassistant/components/xiaomi_miio/translations/et.json b/homeassistant/components/xiaomi_miio/translations/et.json index f6bd3218e14..a5975ab05dc 100644 --- a/homeassistant/components/xiaomi_miio/translations/et.json +++ b/homeassistant/components/xiaomi_miio/translations/et.json @@ -6,10 +6,20 @@ }, "error": { "cannot_connect": "\u00dchendus nurjus", - "no_device_selected": "Seadmeid pole valitud, vali \u00fcks seade." + "no_device_selected": "Seadmeid pole valitud, vali \u00fcks seade.", + "unknown_device": "Seadme mudel pole teada, seadet ei saa seadistamisvoo abil seadistada." }, "flow_title": "Xiaomi Miio: {name}", "step": { + "device": { + "data": { + "host": "IP-aadress", + "name": "Seadme nimi", + "token": "API v\u00f5ti" + }, + "description": "Vaja on 32 t\u00e4hem\u00e4rgilist v\u00f5tit API v\u00f5ti , juhiste saamiseks vaata https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token. Pane t\u00e4hele, et see v\u00f5ti API v\u00f5ti erineb Xiaomi Aqara sidumises kasutatavast v\u00f5tmest.", + "title": "\u00dchenda Xiaomi Miio seade v\u00f5i Xiaomi Gateway" + }, "gateway": { "data": { "host": "IP aadress", diff --git a/homeassistant/components/xiaomi_miio/translations/no.json b/homeassistant/components/xiaomi_miio/translations/no.json index c7128900fc7..3375cac31d5 100644 --- a/homeassistant/components/xiaomi_miio/translations/no.json +++ b/homeassistant/components/xiaomi_miio/translations/no.json @@ -6,10 +6,20 @@ }, "error": { "cannot_connect": "Tilkobling mislyktes", - "no_device_selected": "Ingen enhet valgt, vennligst velg en enhet." + "no_device_selected": "Ingen enhet valgt, vennligst velg en enhet.", + "unknown_device": "Enhetsmodellen er ikke kjent, kan ikke konfigurere enheten ved hjelp av konfigurasjonsflyt." }, "flow_title": "", "step": { + "device": { + "data": { + "host": "IP adresse", + "name": "Navnet p\u00e5 enheten", + "token": "API-token" + }, + "description": "Du trenger 32 tegn API-token , se https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instruksjoner. V\u00e6r oppmerksom p\u00e5 at denne API-token er forskjellig fra n\u00f8kkelen som brukes av Xiaomi Aqara-integrasjonen.", + "title": "Koble til en Xiaomi Miio-enhet eller Xiaomi Gateway" + }, "gateway": { "data": { "host": "IP adresse", diff --git a/homeassistant/components/xiaomi_miio/translations/pl.json b/homeassistant/components/xiaomi_miio/translations/pl.json index 82f7f958905..50eb4d16887 100644 --- a/homeassistant/components/xiaomi_miio/translations/pl.json +++ b/homeassistant/components/xiaomi_miio/translations/pl.json @@ -6,10 +6,20 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "no_device_selected": "Nie wybrano \u017cadnego urz\u0105dzenia, wybierz jedno urz\u0105dzenie" + "no_device_selected": "Nie wybrano \u017cadnego urz\u0105dzenia, wybierz jedno urz\u0105dzenie", + "unknown_device": "Model urz\u0105dzenia nie jest znany, nie mo\u017cna skonfigurowa\u0107 urz\u0105dzenia przy u\u017cyciu interfejsu u\u017cytkownika." }, "flow_title": "Xiaomi Miio: {name}", "step": { + "device": { + "data": { + "host": "Adres IP", + "name": "Nazwa urz\u0105dzenia", + "token": "Token API" + }, + "description": "B\u0119dziesz potrzebowa\u0107 tokenu API (32 znaki), odwied\u017a https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token, aby uzyska\u0107 instrukcje. Zauwa\u017c i\u017c jest to inny token ni\u017c w integracji Xiaomi Aqara.", + "title": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi b\u0105d\u017a innym urz\u0105dzeniem Xiaomi Miio" + }, "gateway": { "data": { "host": "Adres IP", diff --git a/homeassistant/components/xiaomi_miio/translations/ru.json b/homeassistant/components/xiaomi_miio/translations/ru.json index be02fd14f3e..5113128cac5 100644 --- a/homeassistant/components/xiaomi_miio/translations/ru.json +++ b/homeassistant/components/xiaomi_miio/translations/ru.json @@ -6,10 +6,20 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "no_device_selected": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u043d\u043e \u0438\u0437 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432." + "no_device_selected": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u043d\u043e \u0438\u0437 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.", + "unknown_device": "\u041c\u043e\u0434\u0435\u043b\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430, \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043c\u0430\u0441\u0442\u0435\u0440\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." }, "flow_title": "Xiaomi Miio: {name}", "step": { + "device": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "token": "\u0422\u043e\u043a\u0435\u043d API" + }, + "description": "\u0414\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f 32-\u0445 \u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u0422\u043e\u043a\u0435\u043d API. \u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0437\u0434\u0435\u0441\u044c: \nhttps://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.\n\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u044d\u0442\u043e\u0442 \u0442\u043e\u043a\u0435\u043d \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043a\u043b\u044e\u0447\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0433\u043e \u043f\u0440\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Xiaomi Aqara.", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Xiaomi Miio \u0438\u043b\u0438 \u0448\u043b\u044e\u0437\u0443 Xiaomi" + }, "gateway": { "data": { "host": "IP-\u0430\u0434\u0440\u0435\u0441", diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json index 95499fb7b82..43e3e10df20 100644 --- a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json @@ -6,10 +6,20 @@ }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "no_device_selected": "\u672a\u9078\u64c7\u88dd\u7f6e\uff0c\u8acb\u9078\u64c7\u4e00\u9805\u88dd\u7f6e\u3002" + "no_device_selected": "\u672a\u9078\u64c7\u88dd\u7f6e\uff0c\u8acb\u9078\u64c7\u4e00\u9805\u88dd\u7f6e\u3002", + "unknown_device": "\u88dd\u7f6e\u578b\u865f\u672a\u77e5\uff0c\u7121\u6cd5\u4f7f\u7528\u8a2d\u5b9a\u6d41\u7a0b\u3002" }, "flow_title": "Xiaomi Miio\uff1a{name}", "step": { + "device": { + "data": { + "host": "IP \u4f4d\u5740", + "name": "\u88dd\u7f6e\u540d\u7a31", + "token": "API \u5bc6\u9470" + }, + "description": "\u5c07\u9700\u8981\u8f38\u5165 32 \u4f4d\u5b57\u5143 API \u5bc6\u9470\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u4ee5\u7372\u5f97\u7372\u53d6\u5bc6\u9470\u7684\u6559\u5b78\u3002\u8acb\u6ce8\u610f\uff1a\u6b64API \u5bc6\u9470\u8207 Xiaomi Aqara \u6574\u5408\u6240\u4f7f\u7528\u4e4b\u5bc6\u9470\u4e0d\u540c\u3002", + "title": "\u9023\u7dda\u81f3\u5c0f\u7c73 MIIO \u88dd\u7f6e\u6216\u5c0f\u7c73\u7db2\u95dc" + }, "gateway": { "data": { "host": "IP \u4f4d\u5740", From 4236f6e5d499e38e21f2d54a9da6ffdb6061b538 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 16 Feb 2021 20:46:44 -0800 Subject: [PATCH 474/796] Fix stream keepalive on startup (#46640) This was accidentally dropped in a previous PR that refactored the interaction between stream and camera. This is difficult to test from the existing preload stream tests, so leaving untested for now. --- homeassistant/components/camera/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index d0bee4e249f..4c5d89030b4 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -258,6 +258,7 @@ async def async_setup(hass, config): stream = await camera.create_stream() if not stream: continue + stream.keepalive = True stream.hls_output() stream.start() From 58499946edf050218f57bde729883cf269f17533 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Tue, 16 Feb 2021 21:37:56 -0800 Subject: [PATCH 475/796] Add SmartTub integration (#37775) Co-authored-by: J. Nick Koston --- CODEOWNERS | 1 + homeassistant/components/smarttub/__init__.py | 54 ++++++++ homeassistant/components/smarttub/climate.py | 116 ++++++++++++++++++ .../components/smarttub/config_flow.py | 53 ++++++++ homeassistant/components/smarttub/const.py | 14 +++ .../components/smarttub/controller.py | 110 +++++++++++++++++ homeassistant/components/smarttub/entity.py | 64 ++++++++++ homeassistant/components/smarttub/helpers.py | 8 ++ .../components/smarttub/manifest.json | 12 ++ .../components/smarttub/strings.json | 22 ++++ .../components/smarttub/translations/en.json | 22 ++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/smarttub/__init__.py | 1 + tests/components/smarttub/conftest.py | 86 +++++++++++++ tests/components/smarttub/test_climate.py | 74 +++++++++++ tests/components/smarttub/test_config_flow.py | 64 ++++++++++ tests/components/smarttub/test_controller.py | 37 ++++++ tests/components/smarttub/test_entity.py | 18 +++ tests/components/smarttub/test_init.py | 60 +++++++++ 21 files changed, 823 insertions(+) create mode 100644 homeassistant/components/smarttub/__init__.py create mode 100644 homeassistant/components/smarttub/climate.py create mode 100644 homeassistant/components/smarttub/config_flow.py create mode 100644 homeassistant/components/smarttub/const.py create mode 100644 homeassistant/components/smarttub/controller.py create mode 100644 homeassistant/components/smarttub/entity.py create mode 100644 homeassistant/components/smarttub/helpers.py create mode 100644 homeassistant/components/smarttub/manifest.json create mode 100644 homeassistant/components/smarttub/strings.json create mode 100644 homeassistant/components/smarttub/translations/en.json create mode 100644 tests/components/smarttub/__init__.py create mode 100644 tests/components/smarttub/conftest.py create mode 100644 tests/components/smarttub/test_climate.py create mode 100644 tests/components/smarttub/test_config_flow.py create mode 100644 tests/components/smarttub/test_controller.py create mode 100644 tests/components/smarttub/test_entity.py create mode 100644 tests/components/smarttub/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index d1bda212d2d..8a2ea23ded9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -421,6 +421,7 @@ homeassistant/components/smappee/* @bsmappee homeassistant/components/smart_meter_texas/* @grahamwetzler homeassistant/components/smarthab/* @outadoc homeassistant/components/smartthings/* @andrewsayre +homeassistant/components/smarttub/* @mdz homeassistant/components/smarty/* @z0mbieprocess homeassistant/components/sms/* @ocalvo homeassistant/components/smtp/* @fabaff diff --git a/homeassistant/components/smarttub/__init__.py b/homeassistant/components/smarttub/__init__.py new file mode 100644 index 00000000000..85298a75f65 --- /dev/null +++ b/homeassistant/components/smarttub/__init__.py @@ -0,0 +1,54 @@ +"""SmartTub integration.""" +import asyncio +import logging + +from .const import DOMAIN, SMARTTUB_CONTROLLER +from .controller import SmartTubController + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["climate"] + + +async def async_setup(hass, _config): + """Set up smarttub component.""" + + hass.data.setdefault(DOMAIN, {}) + + return True + + +async def async_setup_entry(hass, entry): + """Set up a smarttub config entry.""" + + controller = SmartTubController(hass) + hass.data[DOMAIN][entry.entry_id] = { + SMARTTUB_CONTROLLER: controller, + } + + if not await controller.async_setup_entry(entry): + return False + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + return True + + +async def async_unload_entry(hass, entry): + """Remove a smarttub config entry.""" + if not all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + ) + ): + return False + + hass.data[DOMAIN].pop(entry.entry_id) + + return True diff --git a/homeassistant/components/smarttub/climate.py b/homeassistant/components/smarttub/climate.py new file mode 100644 index 00000000000..5f16bea8cf7 --- /dev/null +++ b/homeassistant/components/smarttub/climate.py @@ -0,0 +1,116 @@ +"""Platform for climate integration.""" +import logging + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + HVAC_MODE_HEAT, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.util.temperature import convert as convert_temperature + +from .const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, SMARTTUB_CONTROLLER +from .entity import SmartTubEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up climate entity for the thermostat in the tub.""" + + controller = hass.data[DOMAIN][entry.entry_id][SMARTTUB_CONTROLLER] + + entities = [ + SmartTubThermostat(controller.coordinator, spa) for spa in controller.spas + ] + + async_add_entities(entities) + + +class SmartTubThermostat(SmartTubEntity, ClimateEntity): + """The target water temperature for the spa.""" + + def __init__(self, coordinator, spa): + """Initialize the entity.""" + super().__init__(coordinator, spa, "thermostat") + + @property + def unique_id(self) -> str: + """Return a unique id for the entity.""" + return f"{self.spa.id}-{self._entity_type}" + + @property + def temperature_unit(self): + """Return the unit of measurement used by the platform.""" + return TEMP_CELSIUS + + @property + def hvac_action(self): + """Return the current running hvac operation.""" + heater_status = self.get_spa_status("heater") + if heater_status == "ON": + return CURRENT_HVAC_HEAT + if heater_status == "OFF": + return CURRENT_HVAC_IDLE + return None + + @property + def hvac_modes(self): + """Return the list of available hvac operation modes.""" + return [HVAC_MODE_HEAT] + + @property + def hvac_mode(self): + """Return the current hvac mode. + + SmartTub devices don't seem to have the option of disabling the heater, + so this is always HVAC_MODE_HEAT. + """ + return HVAC_MODE_HEAT + + async def async_set_hvac_mode(self, hvac_mode: str): + """Set new target hvac mode. + + As with hvac_mode, we don't really have an option here. + """ + if hvac_mode == HVAC_MODE_HEAT: + return + raise NotImplementedError(hvac_mode) + + @property + def min_temp(self): + """Return the minimum temperature.""" + min_temp = DEFAULT_MIN_TEMP + return convert_temperature(min_temp, TEMP_CELSIUS, self.temperature_unit) + + @property + def max_temp(self): + """Return the maximum temperature.""" + max_temp = DEFAULT_MAX_TEMP + return convert_temperature(max_temp, TEMP_CELSIUS, self.temperature_unit) + + @property + def supported_features(self): + """Return the set of supported features. + + Only target temperature is supported. + """ + return SUPPORT_TARGET_TEMPERATURE + + @property + def current_temperature(self): + """Return the current water temperature.""" + return self.get_spa_status("water.temperature") + + @property + def target_temperature(self): + """Return the target water temperature.""" + return self.get_spa_status("setTemperature") + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + temperature = kwargs[ATTR_TEMPERATURE] + await self.spa.set_temperature(temperature) + await self.coordinator.async_refresh() diff --git a/homeassistant/components/smarttub/config_flow.py b/homeassistant/components/smarttub/config_flow.py new file mode 100644 index 00000000000..8f3ed17f93a --- /dev/null +++ b/homeassistant/components/smarttub/config_flow.py @@ -0,0 +1,53 @@ +"""Config flow to configure the SmartTub integration.""" +import logging + +from smarttub import LoginFailed +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD + +from .const import DOMAIN # pylint: disable=unused-import +from .controller import SmartTubController + +DATA_SCHEMA = vol.Schema( + {vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str} +) + + +_LOGGER = logging.getLogger(__name__) + + +class SmartTubConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """SmartTub configuration flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + errors = {} + + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + controller = SmartTubController(self.hass) + try: + account = await controller.login( + user_input[CONF_EMAIL], user_input[CONF_PASSWORD] + ) + except LoginFailed: + errors["base"] = "invalid_auth" + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + existing_entry = await self.async_set_unique_id(account.id) + if existing_entry: + self.hass.config_entries.async_update_entry(existing_entry, data=user_input) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_create_entry(title=user_input[CONF_EMAIL], data=user_input) diff --git a/homeassistant/components/smarttub/const.py b/homeassistant/components/smarttub/const.py new file mode 100644 index 00000000000..99e7f21f86d --- /dev/null +++ b/homeassistant/components/smarttub/const.py @@ -0,0 +1,14 @@ +"""smarttub constants.""" + +DOMAIN = "smarttub" + +EVENT_SMARTTUB = "smarttub" + +SMARTTUB_CONTROLLER = "smarttub_controller" + +SCAN_INTERVAL = 60 + +POLLING_TIMEOUT = 10 + +DEFAULT_MIN_TEMP = 18.5 +DEFAULT_MAX_TEMP = 40 diff --git a/homeassistant/components/smarttub/controller.py b/homeassistant/components/smarttub/controller.py new file mode 100644 index 00000000000..31fe71d14b6 --- /dev/null +++ b/homeassistant/components/smarttub/controller.py @@ -0,0 +1,110 @@ +"""Interface to the SmartTub API.""" + +import asyncio +from datetime import timedelta +import logging + +from aiohttp import client_exceptions +import async_timeout +from smarttub import APIError, LoginFailed, SmartTub +from smarttub.api import Account + +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, POLLING_TIMEOUT, SCAN_INTERVAL +from .helpers import get_spa_name + +_LOGGER = logging.getLogger(__name__) + + +class SmartTubController: + """Interface between Home Assistant and the SmartTub API.""" + + def __init__(self, hass): + """Initialize an interface to SmartTub.""" + self._hass = hass + self._account = None + self.spas = set() + self._spa_devices = {} + + self.coordinator = None + + async def async_setup_entry(self, entry): + """Perform initial setup. + + Authenticate, query static state, set up polling, and otherwise make + ready for normal operations . + """ + + try: + self._account = await self.login( + entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD] + ) + except LoginFailed: + # credentials were changed or invalidated, we need new ones + + return False + except ( + asyncio.TimeoutError, + client_exceptions.ClientOSError, + client_exceptions.ServerDisconnectedError, + client_exceptions.ContentTypeError, + ) as err: + raise ConfigEntryNotReady from err + + self.spas = await self._account.get_spas() + + self.coordinator = DataUpdateCoordinator( + self._hass, + _LOGGER, + name=DOMAIN, + update_method=self.async_update_data, + update_interval=timedelta(seconds=SCAN_INTERVAL), + ) + + await self.coordinator.async_refresh() + + await self.async_register_devices(entry) + + return True + + async def async_update_data(self): + """Query the API and return the new state.""" + + data = {} + try: + async with async_timeout.timeout(POLLING_TIMEOUT): + for spa in self.spas: + data[spa.id] = {"status": await spa.get_status()} + except APIError as err: + raise UpdateFailed(err) from err + + return data + + async def async_register_devices(self, entry): + """Register devices with the device registry for all spas.""" + device_registry = await dr.async_get_registry(self._hass) + for spa in self.spas: + device = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, spa.id)}, + manufacturer=spa.brand, + name=get_spa_name(spa), + model=spa.model, + ) + self._spa_devices[spa.id] = device + + async def login(self, email, password) -> Account: + """Retrieve the account corresponding to the specified email and password. + + Returns None if the credentials are invalid. + """ + + api = SmartTub(async_get_clientsession(self._hass)) + + await api.login(email, password) + return await api.get_account() diff --git a/homeassistant/components/smarttub/entity.py b/homeassistant/components/smarttub/entity.py new file mode 100644 index 00000000000..95d89971cb7 --- /dev/null +++ b/homeassistant/components/smarttub/entity.py @@ -0,0 +1,64 @@ +"""SmartTub integration.""" +import logging + +import smarttub + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DOMAIN +from .helpers import get_spa_name + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["climate"] + + +class SmartTubEntity(CoordinatorEntity): + """Base class for SmartTub entities.""" + + def __init__( + self, coordinator: DataUpdateCoordinator, spa: smarttub.Spa, entity_type + ): + """Initialize the entity. + + Given a spa id and a short name for the entity, we provide basic device + info, name, unique id, etc. for all derived entities. + """ + + super().__init__(coordinator) + self.spa = spa + self._entity_type = entity_type + + @property + def device_info(self) -> str: + """Return device info.""" + return { + "identifiers": {(DOMAIN, self.spa.id)}, + "manufacturer": self.spa.brand, + "model": self.spa.model, + } + + @property + def name(self) -> str: + """Return the name of the entity.""" + spa_name = get_spa_name(self.spa) + return f"{spa_name} {self._entity_type}" + + def get_spa_status(self, path): + """Retrieve a value from the data returned by Spa.get_status(). + + Nested keys can be specified by a dotted path, e.g. + status['foo']['bar'] is 'foo.bar'. + """ + + status = self.coordinator.data[self.spa.id].get("status") + if status is None: + return None + + for key in path.split("."): + status = status[key] + + return status diff --git a/homeassistant/components/smarttub/helpers.py b/homeassistant/components/smarttub/helpers.py new file mode 100644 index 00000000000..a6f2d09c38f --- /dev/null +++ b/homeassistant/components/smarttub/helpers.py @@ -0,0 +1,8 @@ +"""Helper functions for SmartTub integration.""" + +import smarttub + + +def get_spa_name(spa: smarttub.Spa) -> str: + """Return the name of the specified spa.""" + return f"{spa.brand} {spa.model}" diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json new file mode 100644 index 00000000000..9735a3753b4 --- /dev/null +++ b/homeassistant/components/smarttub/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "smarttub", + "name": "SmartTub", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/smarttub", + "dependencies": [], + "codeowners": ["@mdz"], + "requirements": [ + "python-smarttub==0.0.6" + ], + "quality_scale": "platinum" +} diff --git a/homeassistant/components/smarttub/strings.json b/homeassistant/components/smarttub/strings.json new file mode 100644 index 00000000000..0d52673a469 --- /dev/null +++ b/homeassistant/components/smarttub/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "user": { + "title": "Login", + "description": "Enter your SmartTub email address and password to login", + "data": { + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + } +} diff --git a/homeassistant/components/smarttub/translations/en.json b/homeassistant/components/smarttub/translations/en.json new file mode 100644 index 00000000000..4cf93091887 --- /dev/null +++ b/homeassistant/components/smarttub/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Password" + }, + "description": "Enter your SmartTub email address and password to login", + "title": "Login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 6ff72cf5572..400f2f2352b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -197,6 +197,7 @@ FLOWS = [ "smart_meter_texas", "smarthab", "smartthings", + "smarttub", "smhi", "sms", "solaredge", diff --git a/requirements_all.txt b/requirements_all.txt index 24bb508c713..de6c34f4feb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1809,6 +1809,9 @@ python-qbittorrent==0.4.2 # homeassistant.components.ripple python-ripple-api==0.0.3 +# homeassistant.components.smarttub +python-smarttub==0.0.6 + # homeassistant.components.sochain python-sochain-api==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e22ab3fa0e2..4f15becca1a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -934,6 +934,9 @@ python-nest==4.1.0 # homeassistant.components.ozw python-openzwave-mqtt[mqtt-client]==1.4.0 +# homeassistant.components.smarttub +python-smarttub==0.0.6 + # homeassistant.components.songpal python-songpal==0.12 diff --git a/tests/components/smarttub/__init__.py b/tests/components/smarttub/__init__.py new file mode 100644 index 00000000000..afbf271eb63 --- /dev/null +++ b/tests/components/smarttub/__init__.py @@ -0,0 +1 @@ +"""Tests for the smarttub integration.""" diff --git a/tests/components/smarttub/conftest.py b/tests/components/smarttub/conftest.py new file mode 100644 index 00000000000..fec74a2b30a --- /dev/null +++ b/tests/components/smarttub/conftest.py @@ -0,0 +1,86 @@ +"""Common fixtures for smarttub tests.""" + +from unittest.mock import create_autospec, patch + +import pytest +import smarttub + +from homeassistant.components.smarttub.const import DOMAIN +from homeassistant.components.smarttub.controller import SmartTubController +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD + +from tests.common import MockConfigEntry + + +@pytest.fixture +def config_data(): + """Provide configuration data for tests.""" + return {CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"} + + +@pytest.fixture +def config_entry(config_data): + """Create a mock config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data=config_data, + options={}, + ) + + +@pytest.fixture(name="spa") +def mock_spa(): + """Mock a SmartTub.Spa.""" + + mock_spa = create_autospec(smarttub.Spa, instance=True) + mock_spa.id = "mockspa1" + mock_spa.brand = "mockbrand1" + mock_spa.model = "mockmodel1" + mock_spa.get_status.return_value = { + "setTemperature": 39, + "water": {"temperature": 38}, + "heater": "ON", + } + return mock_spa + + +@pytest.fixture(name="account") +def mock_account(spa): + """Mock a SmartTub.Account.""" + + mock_account = create_autospec(smarttub.Account, instance=True) + mock_account.id = "mockaccount1" + mock_account.get_spas.return_value = [spa] + return mock_account + + +@pytest.fixture(name="smarttub_api") +def mock_api(account, spa): + """Mock the SmartTub API.""" + + with patch( + "homeassistant.components.smarttub.controller.SmartTub", + autospec=True, + ) as api_class_mock: + api_mock = api_class_mock.return_value + api_mock.get_account.return_value = account + yield api_mock + + +@pytest.fixture +async def controller(smarttub_api, hass, config_entry): + """Instantiate controller for testing.""" + + controller = SmartTubController(hass) + assert len(controller.spas) == 0 + assert await controller.async_setup_entry(config_entry) + + assert len(controller.spas) > 0 + + return controller + + +@pytest.fixture +async def coordinator(controller): + """Provide convenient access to the coordinator via the controller.""" + return controller.coordinator diff --git a/tests/components/smarttub/test_climate.py b/tests/components/smarttub/test_climate.py new file mode 100644 index 00000000000..f0e6ced4abd --- /dev/null +++ b/tests/components/smarttub/test_climate.py @@ -0,0 +1,74 @@ +"""Test the SmartTub climate platform.""" + +from homeassistant.components.climate.const import ( + ATTR_CURRENT_TEMPERATURE, + ATTR_HVAC_ACTION, + ATTR_HVAC_MODE, + ATTR_HVAC_MODES, + ATTR_MAX_TEMP, + ATTR_MIN_TEMP, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + DOMAIN as CLIMATE_DOMAIN, + HVAC_MODE_HEAT, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.components.smarttub.const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + ATTR_TEMPERATURE, +) + + +async def test_thermostat(coordinator, spa, hass, config_entry): + """Test the thermostat entity.""" + + spa.get_status.return_value = { + "heater": "ON", + "water": { + "temperature": 38, + }, + "setTemperature": 39, + } + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + entity_id = f"climate.{spa.brand}_{spa.model}_thermostat" + state = hass.states.get(entity_id) + assert state + + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT + + spa.get_status.return_value["heater"] = "OFF" + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + + assert set(state.attributes[ATTR_HVAC_MODES]) == {HVAC_MODE_HEAT} + assert state.state == HVAC_MODE_HEAT + assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_TARGET_TEMPERATURE + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 38 + assert state.attributes[ATTR_TEMPERATURE] == 39 + assert state.attributes[ATTR_MAX_TEMP] == DEFAULT_MAX_TEMP + assert state.attributes[ATTR_MIN_TEMP] == DEFAULT_MIN_TEMP + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 37}, + blocking=True, + ) + spa.set_temperature.assert_called_with(37) + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: HVAC_MODE_HEAT}, + blocking=True, + ) + # does nothing diff --git a/tests/components/smarttub/test_config_flow.py b/tests/components/smarttub/test_config_flow.py new file mode 100644 index 00000000000..a57eb43eef7 --- /dev/null +++ b/tests/components/smarttub/test_config_flow.py @@ -0,0 +1,64 @@ +"""Test the smarttub config flow.""" +from unittest.mock import patch + +from smarttub import LoginFailed + +from homeassistant import config_entries +from homeassistant.components.smarttub.const import DOMAIN + + +async def test_form(hass, smarttub_api): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.smarttub.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.smarttub.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"email": "test-email", "password": "test-password"}, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "test-email" + assert result2["data"] == { + "email": "test-email", + "password": "test-password", + } + await hass.async_block_till_done() + mock_setup.assert_called_once() + mock_setup_entry.assert_called_once() + + 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"], {"email": "test-email2", "password": "test-password2"} + ) + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + + +async def test_form_invalid_auth(hass, smarttub_api): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + smarttub_api.login.side_effect = LoginFailed + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"email": "test-email", "password": "test-password"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} diff --git a/tests/components/smarttub/test_controller.py b/tests/components/smarttub/test_controller.py new file mode 100644 index 00000000000..e59ad86c09e --- /dev/null +++ b/tests/components/smarttub/test_controller.py @@ -0,0 +1,37 @@ +"""Test the SmartTub controller.""" + +import pytest +import smarttub + +from homeassistant.components.smarttub.controller import SmartTubController +from homeassistant.helpers.update_coordinator import UpdateFailed + + +async def test_invalid_credentials(hass, controller, smarttub_api, config_entry): + """Check that we return False if the configured credentials are invalid. + + This should mean that the user changed their SmartTub password. + """ + + smarttub_api.login.side_effect = smarttub.LoginFailed + controller = SmartTubController(hass) + ret = await controller.async_setup_entry(config_entry) + assert ret is False + + +async def test_update(controller, spa): + """Test data updates from API.""" + data = await controller.async_update_data() + assert data[spa.id] == {"status": spa.get_status.return_value} + + spa.get_status.side_effect = smarttub.APIError + with pytest.raises(UpdateFailed): + data = await controller.async_update_data() + + +async def test_login(controller, smarttub_api, account): + """Test SmartTubController.login.""" + smarttub_api.get_account.return_value.id = "account-id1" + account = await controller.login("test-email1", "test-password1") + smarttub_api.login.assert_called() + assert account == account diff --git a/tests/components/smarttub/test_entity.py b/tests/components/smarttub/test_entity.py new file mode 100644 index 00000000000..4a19b265090 --- /dev/null +++ b/tests/components/smarttub/test_entity.py @@ -0,0 +1,18 @@ +"""Test SmartTubEntity.""" + +from homeassistant.components.smarttub.entity import SmartTubEntity + + +async def test_entity(coordinator, spa): + """Test SmartTubEntity.""" + + entity = SmartTubEntity(coordinator, spa, "entity1") + + assert entity.device_info + assert entity.name + + coordinator.data[spa.id] = {} + assert entity.get_spa_status("foo") is None + coordinator.data[spa.id]["status"] = {"foo": "foo1", "bar": {"baz": "barbaz1"}} + assert entity.get_spa_status("foo") == "foo1" + assert entity.get_spa_status("bar.baz") == "barbaz1" diff --git a/tests/components/smarttub/test_init.py b/tests/components/smarttub/test_init.py new file mode 100644 index 00000000000..aa22780e9b8 --- /dev/null +++ b/tests/components/smarttub/test_init.py @@ -0,0 +1,60 @@ +"""Test smarttub setup process.""" + +import asyncio +from unittest.mock import patch + +import pytest +from smarttub import LoginFailed + +from homeassistant.components import smarttub +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.setup import async_setup_component + + +async def test_setup_with_no_config(hass): + """Test that we do not discover anything.""" + assert await async_setup_component(hass, smarttub.DOMAIN, {}) is True + + # No flows started + assert len(hass.config_entries.flow.async_progress()) == 0 + + assert smarttub.const.SMARTTUB_CONTROLLER not in hass.data[smarttub.DOMAIN] + + +async def test_setup_entry_not_ready(hass, config_entry, smarttub_api): + """Test setup when the entry is not ready.""" + assert await async_setup_component(hass, smarttub.DOMAIN, {}) is True + smarttub_api.login.side_effect = asyncio.TimeoutError + + with pytest.raises(ConfigEntryNotReady): + await smarttub.async_setup_entry(hass, config_entry) + + +async def test_setup_auth_failed(hass, config_entry, smarttub_api): + """Test setup when the credentials are invalid.""" + assert await async_setup_component(hass, smarttub.DOMAIN, {}) is True + smarttub_api.login.side_effect = LoginFailed + + assert await smarttub.async_setup_entry(hass, config_entry) is False + + +async def test_config_passed_to_config_entry(hass, config_entry, config_data): + """Test that configured options are loaded via config entry.""" + config_entry.add_to_hass(hass) + ret = await async_setup_component(hass, smarttub.DOMAIN, config_data) + assert ret is True + + +async def test_unload_entry(hass, config_entry, smarttub_api): + """Test being able to unload an entry.""" + config_entry.add_to_hass(hass) + + assert await async_setup_component(hass, smarttub.DOMAIN, {}) is True + + assert await smarttub.async_unload_entry(hass, config_entry) + + # test failure of platform unload + assert await async_setup_component(hass, smarttub.DOMAIN, {}) is True + with patch.object(hass.config_entries, "async_forward_entry_unload") as mock: + mock.return_value = False + assert await smarttub.async_unload_entry(hass, config_entry) is False From 58f6db0127c8436fe5bc0ca3acb0dae13e1c1e05 Mon Sep 17 00:00:00 2001 From: David McClosky Date: Wed, 17 Feb 2021 00:39:46 -0500 Subject: [PATCH 476/796] Fix media_pause in vlc_telnet (#46675) The underlying python-telnet-vlc pause() function toggles play/pause, so we need to check our state before calling it. --- homeassistant/components/vlc_telnet/media_player.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index a7d5ee0a211..68b3c373c7a 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -245,7 +245,11 @@ class VlcDevice(MediaPlayerEntity): def media_pause(self): """Send pause command.""" - self._vlc.pause() + current_state = self._vlc.status().get("state") + if current_state != "paused": + # Make sure we're not already paused since VLCTelnet.pause() toggles + # pause. + self._vlc.pause() self._state = STATE_PAUSED def media_stop(self): From b956a571f4a136d0dac6ec46326cf640f7a8fc02 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 16 Feb 2021 21:49:53 -0800 Subject: [PATCH 477/796] Fix Cloud Google/Alexa check (#46681) --- homeassistant/components/cloud/alexa_config.py | 6 +++++- homeassistant/components/cloud/google_config.py | 12 +++++++++--- tests/components/cloud/conftest.py | 15 ++++++++++++++- tests/components/cloud/test_alexa_config.py | 13 +++++++++++++ tests/components/cloud/test_google_config.py | 13 +++++++++++++ 5 files changed, 54 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 7abbefe85ff..2d4714b4c81 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -62,7 +62,11 @@ class AlexaConfig(alexa_config.AbstractConfig): @property def enabled(self): """Return if Alexa is enabled.""" - return self._prefs.alexa_enabled + return ( + self._cloud.is_logged_in + and not self._cloud.subscription_expired + and self._prefs.alexa_enabled + ) @property def supports_auth(self): diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 2ac0bc40252..dffa1e2f306 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -2,7 +2,7 @@ import asyncio import logging -from hass_nabucasa import cloud_api +from hass_nabucasa import Cloud, cloud_api from hass_nabucasa.google_report_state import ErrorResponse from homeassistant.components.google_assistant.helpers import AbstractConfig @@ -28,7 +28,9 @@ _LOGGER = logging.getLogger(__name__) class CloudGoogleConfig(AbstractConfig): """HA Cloud Configuration for Google Assistant.""" - def __init__(self, hass, config, cloud_user: str, prefs: CloudPreferences, cloud): + def __init__( + self, hass, config, cloud_user: str, prefs: CloudPreferences, cloud: Cloud + ): """Initialize the Google config.""" super().__init__(hass) self._config = config @@ -43,7 +45,11 @@ class CloudGoogleConfig(AbstractConfig): @property def enabled(self): """Return if Google is enabled.""" - return self._cloud.is_logged_in and self._prefs.google_enabled + return ( + self._cloud.is_logged_in + and not self._cloud.subscription_expired + and self._prefs.google_enabled + ) @property def entity_config(self): diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index 4755d470418..75276a9f2e2 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -43,7 +43,20 @@ def mock_cloud_login(hass, mock_cloud_setup): hass.data[const.DOMAIN].id_token = jwt.encode( { "email": "hello@home-assistant.io", - "custom:sub-exp": "2018-01-03", + "custom:sub-exp": "2300-01-03", + "cognito:username": "abcdefghjkl", + }, + "test", + ) + + +@pytest.fixture +def mock_expired_cloud_login(hass, mock_cloud_setup): + """Mock cloud is logged in.""" + hass.data[const.DOMAIN].id_token = jwt.encode( + { + "email": "hello@home-assistant.io", + "custom:sub-exp": "2018-01-01", "cognito:username": "abcdefghjkl", }, "test", diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index 966ef4b0af3..8e104f641b2 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -215,3 +215,16 @@ async def test_alexa_update_report_state(hass, cloud_prefs): await hass.async_block_till_done() assert len(mock_sync.mock_calls) == 1 + + +def test_enabled_requires_valid_sub(hass, mock_expired_cloud_login, cloud_prefs): + """Test that alexa config enabled requires a valid Cloud sub.""" + assert cloud_prefs.alexa_enabled + assert hass.data["cloud"].is_logged_in + assert hass.data["cloud"].subscription_expired + + config = alexa_config.AlexaConfig( + hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] + ) + + assert not config.enabled diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index f58ea1a415b..e1da6bbe0a8 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -192,3 +192,16 @@ async def test_google_config_expose_entity_prefs(mock_conf, cloud_prefs): google_default_expose=["sensor"], ) assert not mock_conf.should_expose(state) + + +def test_enabled_requires_valid_sub(hass, mock_expired_cloud_login, cloud_prefs): + """Test that google config enabled requires a valid Cloud sub.""" + assert cloud_prefs.google_enabled + assert hass.data["cloud"].is_logged_in + assert hass.data["cloud"].subscription_expired + + config = CloudGoogleConfig( + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] + ) + + assert not config.enabled From 56f32196bdc3ae65da29a0a282fc5135ae8f0c0c Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 16 Feb 2021 22:05:20 -0800 Subject: [PATCH 478/796] Add back block_until_done calls removed in PR 46610 (#46680) --- tests/components/stream/test_recorder.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index 3930a5e237d..e8ff540ba41 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -153,6 +153,8 @@ async def test_recorder_timeout( record_worker_sync.join() stream.stop() + await hass.async_block_till_done() + await hass.async_block_till_done() async def test_record_path_not_allowed(hass, hass_client): From 8c72cb616394cafdea881024c18ee101851328a9 Mon Sep 17 00:00:00 2001 From: Ilja Leiko Date: Wed, 17 Feb 2021 09:04:11 +0100 Subject: [PATCH 479/796] Add sensors to fetch Habitica tasks (#38910) * Adding sensors to fetch habitica tasks * PR changes and rebase * Fixing pylint * Fixing failed test dependancy * Generating requirements * Apply suggestions from code review Co-authored-by: Martin Hjelmare * PR changes * Update homeassistant/components/habitica/config_flow.py Thank you, @MartinHjelmare Co-authored-by: Martin Hjelmare * PR Changes * Fix failing test * Update tests/components/habitica/test_config_flow.py Co-authored-by: Martin Hjelmare * Fixing linting and imports Co-authored-by: Martin Hjelmare --- .coveragerc | 4 +- CODEOWNERS | 1 + homeassistant/components/habitica/__init__.py | 173 +++++++++-------- .../components/habitica/config_flow.py | 85 +++++++++ homeassistant/components/habitica/const.py | 14 ++ .../components/habitica/manifest.json | 11 +- homeassistant/components/habitica/sensor.py | 177 ++++++++++++++++-- .../components/habitica/strings.json | 20 ++ .../components/habitica/translations/en.json | 20 ++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/habitica/__init__.py | 1 + tests/components/habitica/test_config_flow.py | 135 +++++++++++++ 13 files changed, 549 insertions(+), 96 deletions(-) create mode 100644 homeassistant/components/habitica/config_flow.py create mode 100644 homeassistant/components/habitica/const.py create mode 100644 homeassistant/components/habitica/strings.json create mode 100644 homeassistant/components/habitica/translations/en.json create mode 100644 tests/components/habitica/__init__.py create mode 100644 tests/components/habitica/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index df9fbac4c58..51a539b4aba 100644 --- a/.coveragerc +++ b/.coveragerc @@ -361,7 +361,9 @@ omit = homeassistant/components/guardian/sensor.py homeassistant/components/guardian/switch.py homeassistant/components/guardian/util.py - homeassistant/components/habitica/* + homeassistant/components/habitica/__init__.py + homeassistant/components/habitica/const.py + homeassistant/components/habitica/sensor.py homeassistant/components/hangouts/* homeassistant/components/hangouts/__init__.py homeassistant/components/hangouts/const.py diff --git a/CODEOWNERS b/CODEOWNERS index 8a2ea23ded9..81f43da58e7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -182,6 +182,7 @@ homeassistant/components/griddy/* @bdraco homeassistant/components/group/* @home-assistant/core homeassistant/components/growatt_server/* @indykoning homeassistant/components/guardian/* @bachya +homeassistant/components/habitica/* @ASMfreaK @leikoilja homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco @mkeesey homeassistant/components/hassio/* @home-assistant/supervisor homeassistant/components/hdmi_cec/* @newAM diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index b2c3fb16831..36e50db6c20 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -1,55 +1,47 @@ -"""Support for Habitica devices.""" -from collections import namedtuple +"""The habitica integration.""" +import asyncio import logging from habitipy.aio import HabitipyAsync import voluptuous as vol -from homeassistant.const import ( - CONF_API_KEY, - CONF_NAME, - CONF_PATH, - CONF_SENSORS, - CONF_URL, -) -from homeassistant.helpers import config_validation as cv, discovery +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_SENSORS, CONF_URL +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +from .const import ( + ATTR_ARGS, + ATTR_NAME, + ATTR_PATH, + CONF_API_USER, + DEFAULT_URL, + DOMAIN, + EVENT_API_CALL_SUCCESS, + SERVICE_API_CALL, +) +from .sensor import SENSORS_TYPES + _LOGGER = logging.getLogger(__name__) -CONF_API_USER = "api_user" - -DEFAULT_URL = "https://habitica.com" -DOMAIN = "habitica" - -ST = SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"]) - -SENSORS_TYPES = { - "name": ST("Name", None, "", ["profile", "name"]), - "hp": ST("HP", "mdi:heart", "HP", ["stats", "hp"]), - "maxHealth": ST("max HP", "mdi:heart", "HP", ["stats", "maxHealth"]), - "mp": ST("Mana", "mdi:auto-fix", "MP", ["stats", "mp"]), - "maxMP": ST("max Mana", "mdi:auto-fix", "MP", ["stats", "maxMP"]), - "exp": ST("EXP", "mdi:star", "EXP", ["stats", "exp"]), - "toNextLevel": ST("Next Lvl", "mdi:star", "EXP", ["stats", "toNextLevel"]), - "lvl": ST("Lvl", "mdi:arrow-up-bold-circle-outline", "Lvl", ["stats", "lvl"]), - "gp": ST("Gold", "mdi:currency-usd-circle", "Gold", ["stats", "gp"]), - "class": ST("Class", "mdi:sword", "", ["stats", "class"]), -} - -INSTANCE_SCHEMA = vol.Schema( - { - vol.Optional(CONF_URL, default=DEFAULT_URL): cv.url, - vol.Optional(CONF_NAME): cv.string, - vol.Required(CONF_API_USER): cv.string, - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_SENSORS, default=list(SENSORS_TYPES)): vol.All( - cv.ensure_list, vol.Unique(), [vol.In(list(SENSORS_TYPES))] - ), - } +INSTANCE_SCHEMA = vol.All( + cv.deprecated(CONF_SENSORS), + vol.Schema( + { + vol.Optional(CONF_URL, default=DEFAULT_URL): cv.url, + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_API_USER): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_SENSORS, default=list(SENSORS_TYPES)): vol.All( + cv.ensure_list, vol.Unique(), [vol.In(list(SENSORS_TYPES))] + ), + } + ), ) -has_unique_values = vol.Schema(vol.Unique()) +has_unique_values = vol.Schema(vol.Unique()) # pylint: disable=invalid-name # because we want a handy alias @@ -73,14 +65,9 @@ def has_all_unique_users_names(value): INSTANCE_LIST_SCHEMA = vol.All( cv.ensure_list, has_all_unique_users, has_all_unique_users_names, [INSTANCE_SCHEMA] ) - CONFIG_SCHEMA = vol.Schema({DOMAIN: INSTANCE_LIST_SCHEMA}, extra=vol.ALLOW_EXTRA) -SERVICE_API_CALL = "api_call" -ATTR_NAME = CONF_NAME -ATTR_PATH = CONF_PATH -ATTR_ARGS = "args" -EVENT_API_CALL_SUCCESS = f"{DOMAIN}_{SERVICE_API_CALL}_success" +PLATFORMS = ["sensor"] SERVICE_API_CALL_SCHEMA = vol.Schema( { @@ -91,12 +78,25 @@ SERVICE_API_CALL_SCHEMA = vol.Schema( ) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up the Habitica service.""" + configs = config.get(DOMAIN, []) - conf = config[DOMAIN] - data = hass.data[DOMAIN] = {} - websession = async_get_clientsession(hass) + for conf in configs: + if conf.get(CONF_URL) is None: + conf[CONF_URL] = DEFAULT_URL + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up habitica from a config entry.""" class HAHabitipyAsync(HabitipyAsync): """Closure API class to hold session.""" @@ -104,28 +104,6 @@ async def async_setup(hass, config): def __call__(self, **kwargs): return super().__call__(websession, **kwargs) - for instance in conf: - url = instance[CONF_URL] - username = instance[CONF_API_USER] - password = instance[CONF_API_KEY] - name = instance.get(CONF_NAME) - config_dict = {"url": url, "login": username, "password": password} - api = HAHabitipyAsync(config_dict) - user = await api.user.get() - if name is None: - name = user["profile"]["name"] - data[name] = api - if CONF_SENSORS in instance: - hass.async_create_task( - discovery.async_load_platform( - hass, - "sensor", - DOMAIN, - {"name": name, "sensors": instance[CONF_SENSORS]}, - config, - ) - ) - async def handle_api_call(call): name = call.data[ATTR_NAME] path = call.data[ATTR_PATH] @@ -147,7 +125,50 @@ async def async_setup(hass, config): EVENT_API_CALL_SUCCESS, {"name": name, "path": path, "data": data} ) - hass.services.async_register( - DOMAIN, SERVICE_API_CALL, handle_api_call, schema=SERVICE_API_CALL_SCHEMA - ) + data = hass.data.setdefault(DOMAIN, {}) + config = config_entry.data + websession = async_get_clientsession(hass) + url = config[CONF_URL] + username = config[CONF_API_USER] + password = config[CONF_API_KEY] + name = config.get(CONF_NAME) + config_dict = {"url": url, "login": username, "password": password} + api = HAHabitipyAsync(config_dict) + user = await api.user.get() + if name is None: + name = user["profile"]["name"] + hass.config_entries.async_update_entry( + config_entry, + data={**config_entry.data, CONF_NAME: name}, + ) + data[config_entry.entry_id] = api + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, component) + ) + + if not hass.services.has_service(DOMAIN, SERVICE_API_CALL): + hass.services.async_register( + DOMAIN, SERVICE_API_CALL, handle_api_call, schema=SERVICE_API_CALL_SCHEMA + ) + 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) + + if len(hass.config_entries.async_entries) == 1: + hass.components.webhook.async_unregister(SERVICE_API_CALL) + return unload_ok diff --git a/homeassistant/components/habitica/config_flow.py b/homeassistant/components/habitica/config_flow.py new file mode 100644 index 00000000000..6e3311ea9b5 --- /dev/null +++ b/homeassistant/components/habitica/config_flow.py @@ -0,0 +1,85 @@ +"""Config flow for habitica integration.""" +import logging +from typing import Dict + +from aiohttp import ClientResponseError +from habitipy.aio import HabitipyAsync +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_URL +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CONF_API_USER, DEFAULT_URL, DOMAIN # pylint: disable=unused-import + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_USER): str, + vol.Required(CONF_API_KEY): str, + vol.Optional(CONF_NAME): str, + vol.Optional(CONF_URL, default=DEFAULT_URL): str, + } +) + +_LOGGER = logging.getLogger(__name__) + + +async def validate_input( + hass: core.HomeAssistant, data: Dict[str, str] +) -> Dict[str, str]: + """Validate the user input allows us to connect.""" + + websession = async_get_clientsession(hass) + api = HabitipyAsync( + conf={ + "login": data[CONF_API_USER], + "password": data[CONF_API_KEY], + "url": data[CONF_URL] or DEFAULT_URL, + } + ) + try: + await api.user.get(session=websession) + return { + "title": f"{data.get('name', 'Default username')}", + CONF_API_USER: data[CONF_API_USER], + } + except ClientResponseError as ex: + raise InvalidAuth() from ex + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for habitica.""" + + 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: + try: + info = await validate_input(self.hass, user_input) + except InvalidAuth: + errors = {"base": "invalid_credentials"} + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors = {"base": "unknown"} + else: + await self.async_set_unique_id(info[CONF_API_USER]) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=info["title"], data=user_input) + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors=errors, + description_placeholders={}, + ) + + async def async_step_import(self, import_data): + """Import habitica config from configuration.yaml.""" + return await self.async_step_user(import_data) + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/habitica/const.py b/homeassistant/components/habitica/const.py new file mode 100644 index 00000000000..438bcec9d94 --- /dev/null +++ b/homeassistant/components/habitica/const.py @@ -0,0 +1,14 @@ +"""Constants for the habitica integration.""" + +from homeassistant.const import CONF_NAME, CONF_PATH + +CONF_API_USER = "api_user" + +DEFAULT_URL = "https://habitica.com" +DOMAIN = "habitica" + +SERVICE_API_CALL = "api_call" +ATTR_NAME = CONF_NAME +ATTR_PATH = CONF_PATH +ATTR_ARGS = "args" +EVENT_API_CALL_SUCCESS = f"{DOMAIN}_{SERVICE_API_CALL}_success" diff --git a/homeassistant/components/habitica/manifest.json b/homeassistant/components/habitica/manifest.json index 50664d862ad..0779a2d3248 100644 --- a/homeassistant/components/habitica/manifest.json +++ b/homeassistant/components/habitica/manifest.json @@ -1,7 +1,8 @@ { - "domain": "habitica", - "name": "Habitica", - "documentation": "https://www.home-assistant.io/integrations/habitica", - "requirements": ["habitipy==0.2.0"], - "codeowners": [] + "domain": "habitica", + "name": "Habitica", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/habitica", + "requirements": ["habitipy==0.2.0"], + "codeowners": ["@ASMfreaK", "@leikoilja"] } diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index f885aa832c7..29e494d89ee 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -1,25 +1,82 @@ """Support for Habitica sensors.""" +from collections import namedtuple from datetime import timedelta +import logging -from homeassistant.components import habitica +from aiohttp import ClientResponseError + +from homeassistant.const import CONF_NAME, HTTP_TOO_MANY_REQUESTS from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) +ST = SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"]) -async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the habitica platform.""" - if discovery_info is None: - return +SENSORS_TYPES = { + "name": ST("Name", None, "", ["profile", "name"]), + "hp": ST("HP", "mdi:heart", "HP", ["stats", "hp"]), + "maxHealth": ST("max HP", "mdi:heart", "HP", ["stats", "maxHealth"]), + "mp": ST("Mana", "mdi:auto-fix", "MP", ["stats", "mp"]), + "maxMP": ST("max Mana", "mdi:auto-fix", "MP", ["stats", "maxMP"]), + "exp": ST("EXP", "mdi:star", "EXP", ["stats", "exp"]), + "toNextLevel": ST("Next Lvl", "mdi:star", "EXP", ["stats", "toNextLevel"]), + "lvl": ST("Lvl", "mdi:arrow-up-bold-circle-outline", "Lvl", ["stats", "lvl"]), + "gp": ST("Gold", "mdi:currency-usd-circle", "Gold", ["stats", "gp"]), + "class": ST("Class", "mdi:sword", "", ["stats", "class"]), +} - name = discovery_info[habitica.CONF_NAME] - sensors = discovery_info[habitica.CONF_SENSORS] - sensor_data = HabitipyData(hass.data[habitica.DOMAIN][name]) +TASKS_TYPES = { + "habits": ST("Habits", "mdi:clipboard-list-outline", "n_of_tasks", ["habits"]), + "dailys": ST("Dailys", "mdi:clipboard-list-outline", "n_of_tasks", ["dailys"]), + "todos": ST("TODOs", "mdi:clipboard-list-outline", "n_of_tasks", ["todos"]), + "rewards": ST("Rewards", "mdi:clipboard-list-outline", "n_of_tasks", ["rewards"]), +} + +TASKS_MAP_ID = "id" +TASKS_MAP = { + "repeat": "repeat", + "challenge": "challenge", + "group": "group", + "frequency": "frequency", + "every_x": "everyX", + "streak": "streak", + "counter_up": "counterUp", + "counter_down": "counterDown", + "next_due": "nextDue", + "yester_daily": "yesterDaily", + "completed": "completed", + "collapse_checklist": "collapseChecklist", + "type": "type", + "notes": "notes", + "tags": "tags", + "value": "value", + "priority": "priority", + "start_date": "startDate", + "days_of_month": "daysOfMonth", + "weeks_of_month": "weeksOfMonth", + "created_at": "createdAt", + "text": "text", + "is_due": "isDue", +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the habitica sensors.""" + + entities = [] + name = config_entry.data[CONF_NAME] + sensor_data = HabitipyData(hass.data[DOMAIN][config_entry.entry_id]) await sensor_data.update() - async_add_devices( - [HabitipySensor(name, sensor, sensor_data) for sensor in sensors], True - ) + for sensor_type in SENSORS_TYPES: + entities.append(HabitipySensor(name, sensor_type, sensor_data)) + for task_type in TASKS_TYPES: + entities.append(HabitipyTaskSensor(name, task_type, sensor_data)) + async_add_entities(entities, True) class HabitipyData: @@ -29,11 +86,43 @@ class HabitipyData: """Habitica API user data cache.""" self.api = api self.data = None + self.tasks = {} @Throttle(MIN_TIME_BETWEEN_UPDATES) async def update(self): """Get a new fix from Habitica servers.""" - self.data = await self.api.user.get() + try: + self.data = await self.api.user.get() + except ClientResponseError as error: + if error.status == HTTP_TOO_MANY_REQUESTS: + _LOGGER.warning( + "Sensor data update for %s has too many API requests." + " Skipping the update.", + DOMAIN, + ) + else: + _LOGGER.error( + "Count not update sensor data for %s (%s)", + DOMAIN, + error, + ) + + for task_type in TASKS_TYPES: + try: + self.tasks[task_type] = await self.api.tasks.user.get(type=task_type) + except ClientResponseError as error: + if error.status == HTTP_TOO_MANY_REQUESTS: + _LOGGER.warning( + "Sensor data update for %s has too many API requests." + " Skipping the update.", + DOMAIN, + ) + else: + _LOGGER.error( + "Count not update sensor data for %s (%s)", + DOMAIN, + error, + ) class HabitipySensor(Entity): @@ -43,7 +132,7 @@ class HabitipySensor(Entity): """Initialize a generic Habitica sensor.""" self._name = name self._sensor_name = sensor_name - self._sensor_type = habitica.SENSORS_TYPES[sensor_name] + self._sensor_type = SENSORS_TYPES[sensor_name] self._state = None self._updater = updater @@ -63,7 +152,7 @@ class HabitipySensor(Entity): @property def name(self): """Return the name of the sensor.""" - return f"{habitica.DOMAIN}_{self._name}_{self._sensor_name}" + return f"{DOMAIN}_{self._name}_{self._sensor_name}" @property def state(self): @@ -74,3 +163,63 @@ class HabitipySensor(Entity): def unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._sensor_type.unit + + +class HabitipyTaskSensor(Entity): + """A Habitica task sensor.""" + + def __init__(self, name, task_name, updater): + """Initialize a generic Habitica task.""" + self._name = name + self._task_name = task_name + self._task_type = TASKS_TYPES[task_name] + self._state = None + self._updater = updater + + async def async_update(self): + """Update Condition and Forecast.""" + await self._updater.update() + all_tasks = self._updater.tasks + for element in self._task_type.path: + tasks_length = len(all_tasks[element]) + self._state = tasks_length + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return self._task_type.icon + + @property + def name(self): + """Return the name of the task.""" + return f"{DOMAIN}_{self._name}_{self._task_name}" + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes of all user tasks.""" + if self._updater.tasks is not None: + all_received_tasks = self._updater.tasks + for element in self._task_type.path: + received_tasks = all_received_tasks[element] + attrs = {} + + # Map tasks to TASKS_MAP + for received_task in received_tasks: + task_id = received_task[TASKS_MAP_ID] + task = {} + for map_key, map_value in TASKS_MAP.items(): + value = received_task.get(map_value) + if value: + task[map_key] = value + attrs[task_id] = task + return attrs + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._task_type.unit diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json new file mode 100644 index 00000000000..868d024b02e --- /dev/null +++ b/homeassistant/components/habitica/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "invalid_credentials": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "step": { + "user": { + "data": { + "url": "[%key:common::config_flow::data::url%]", + "name": "Override for Habitica’s username. Will be used for service calls", + "api_user": "Habitica’s API user ID", + "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "description": "Connect your Habitica profile to allow monitoring of your user's profile and tasks. Note that api_id and api_key must be gotten from https://habitica.com/user/settings/api" + } + } + }, + "title": "Habitica" +} diff --git a/homeassistant/components/habitica/translations/en.json b/homeassistant/components/habitica/translations/en.json new file mode 100644 index 00000000000..fa571d0d72a --- /dev/null +++ b/homeassistant/components/habitica/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "invalid_credentials": "Invalid credentials", + "unknown": "Unknown error" + }, + "step": { + "user": { + "data": { + "url": "Habitica URL", + "name": "Override for Habitica’s username. Will be used for service calls", + "api_user": "Habitica’s API user ID", + "api_key": "Habitica's API user KEY" + }, + "description": "Connect your Habitica profile to allow monitoring of your user's profile and tasks. Note that api_id and api_key must be grabed from https://habitica.com/user/settings/api" + } + } + }, + "title": "Habitica" +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 400f2f2352b..53d3c8294d2 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -86,6 +86,7 @@ FLOWS = [ "gree", "griddy", "guardian", + "habitica", "hangouts", "harmony", "heos", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f15becca1a..0e863e23bb1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -383,6 +383,9 @@ ha-ffmpeg==3.0.2 # homeassistant.components.philips_js ha-philipsjs==0.1.0 +# homeassistant.components.habitica +habitipy==0.2.0 + # homeassistant.components.hangouts hangups==0.4.11 diff --git a/tests/components/habitica/__init__.py b/tests/components/habitica/__init__.py new file mode 100644 index 00000000000..a7f62afff8f --- /dev/null +++ b/tests/components/habitica/__init__.py @@ -0,0 +1 @@ +"""Tests for the habitica integration.""" diff --git a/tests/components/habitica/test_config_flow.py b/tests/components/habitica/test_config_flow.py new file mode 100644 index 00000000000..8ae92bcc0e2 --- /dev/null +++ b/tests/components/habitica/test_config_flow.py @@ -0,0 +1,135 @@ +"""Test the habitica config flow.""" +from asyncio import Future +from unittest.mock import AsyncMock, MagicMock, patch + +from homeassistant import config_entries, setup +from homeassistant.components.habitica.config_flow import InvalidAuth +from homeassistant.components.habitica.const import DEFAULT_URL, DOMAIN +from homeassistant.const import HTTP_OK + +from tests.common import MockConfigEntry + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + request_mock = MagicMock() + type(request_mock).status_code = HTTP_OK + + mock_obj = MagicMock() + mock_obj.user.get.return_value = Future() + mock_obj.user.get.return_value.set_result(None) + + with patch( + "homeassistant.components.habitica.config_flow.HabitipyAsync", + return_value=mock_obj, + ), patch( + "homeassistant.components.habitica.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.habitica.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"api_user": "test-api-user", "api_key": "test-api-key"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Default username" + assert result2["data"] == { + "url": DEFAULT_URL, + "api_user": "test-api-user", + "api_key": "test-api-key", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_credentials(hass): + """Test we handle invalid credentials error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "habitipy.aio.HabitipyAsync", + side_effect=InvalidAuth, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": DEFAULT_URL, + "api_user": "test-api-user", + "api_key": "test-api-key", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_credentials"} + + +async def test_form_unexpected_exception(hass): + """Test we handle unexpected exception error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.habitica.config_flow.HabitipyAsync", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": DEFAULT_URL, + "api_user": "test-api-user", + "api_key": "test-api-key", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +async def test_manual_flow_config_exist(hass, aioclient_mock): + """Test config flow discovers only already configured config.""" + MockConfigEntry( + domain=DOMAIN, + unique_id="test-api-user", + data={"api_user": "test-api-user", "api_key": "test-api-key"}, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + mock_obj = MagicMock() + mock_obj.user.get.side_effect = AsyncMock( + return_value={"api_user": "test-api-user"} + ) + + with patch( + "homeassistant.components.habitica.config_flow.HabitipyAsync", + return_value=mock_obj, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": DEFAULT_URL, + "api_user": "test-api-user", + "api_key": "test-api-key", + }, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" From 94131df5e01ae9be572da534ab2f8bda000c4eab Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Wed, 17 Feb 2021 00:07:22 -0800 Subject: [PATCH 480/796] Remove exception handling for AttributeError in wemo (#46674) --- homeassistant/components/wemo/binary_sensor.py | 2 +- homeassistant/components/wemo/fan.py | 2 +- homeassistant/components/wemo/light.py | 4 ++-- homeassistant/components/wemo/switch.py | 2 +- tests/components/wemo/entity_test_helpers.py | 4 +++- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index b6690ed6d28..2ea5f2d0b07 100644 --- a/homeassistant/components/wemo/binary_sensor.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -41,7 +41,7 @@ class WemoBinarySensor(WemoSubscriptionEntity, BinarySensorEntity): if not self._available: _LOGGER.info("Reconnected to %s", self.name) self._available = True - except (AttributeError, ActionException) as err: + except ActionException as err: _LOGGER.warning("Could not update status for %s (%s)", self.name, err) self._available = False self.wemo.reconnect_with_device() diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index cdbcc89fae6..faf1b50f1cd 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -155,7 +155,7 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity): if not self._available: _LOGGER.info("Reconnected to %s", self.name) self._available = True - except (AttributeError, ActionException) as err: + except ActionException as err: _LOGGER.warning("Could not update status for %s (%s)", self.name, err) self._available = False self.wemo.reconnect_with_device() diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 1362c7d483c..9aa7c945671 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -191,7 +191,7 @@ class WemoLight(WemoEntity, LightEntity): try: self._update_lights(no_throttle=force_update) self._state = self.wemo.state - except (AttributeError, ActionException) as err: + except ActionException as err: _LOGGER.warning("Could not update status for %s (%s)", self.name, err) self._available = False self.wemo.bridge.reconnect_with_device() @@ -238,7 +238,7 @@ class WemoDimmer(WemoSubscriptionEntity, LightEntity): if not self._available: _LOGGER.info("Reconnected to %s", self.name) self._available = True - except (AttributeError, ActionException) as err: + except ActionException as err: _LOGGER.warning("Could not update status for %s (%s)", self.name, err) self._available = False self.wemo.reconnect_with_device() diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index 50926e07a11..4d2b9c007d4 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -177,7 +177,7 @@ class WemoSwitch(WemoSubscriptionEntity, SwitchEntity): if not self._available: _LOGGER.info("Reconnected to %s", self.name) self._available = True - except (AttributeError, ActionException) as err: + except ActionException as err: _LOGGER.warning("Could not update status for %s (%s)", self.name, err) self._available = False self.wemo.reconnect_with_device() diff --git a/tests/components/wemo/entity_test_helpers.py b/tests/components/wemo/entity_test_helpers.py index 0ecfc46d526..87bc6fd7f40 100644 --- a/tests/components/wemo/entity_test_helpers.py +++ b/tests/components/wemo/entity_test_helpers.py @@ -6,6 +6,8 @@ import asyncio import threading from unittest.mock import patch +from pywemo.ouimeaux_device.api.service import ActionException + from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, @@ -127,7 +129,7 @@ async def test_async_locked_update_with_exception( 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 + update_polling_method.side_effect = ActionException await hass.services.async_call( HA_DOMAIN, From ddf1f88b650b7a96293ab322be51657b98c444ce Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 17 Feb 2021 09:25:00 +0100 Subject: [PATCH 481/796] Fix multiple motion blinds gateways (#46622) local variable multicast was undefined for a second or more gateway that was setup. --- homeassistant/components/motion_blinds/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index e10f1655d2f..5d02d5a14a8 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -54,6 +54,7 @@ async def async_setup_entry( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_motion_multicast) # Connect to motion gateway + multicast = hass.data[DOMAIN][KEY_MULTICAST_LISTENER] connect_gateway_class = ConnectMotionGateway(hass, multicast) if not await connect_gateway_class.async_connect_gateway(host, key): raise ConfigEntryNotReady From 971e27dd80dbbd78ddd5ff2f504994a75ceceef2 Mon Sep 17 00:00:00 2001 From: badguy99 <61918526+badguy99@users.noreply.github.com> Date: Wed, 17 Feb 2021 08:44:37 +0000 Subject: [PATCH 482/796] Home connect use consts (#46659) * Use more consts * black * re-black with black, version 20.8b1 --- homeassistant/components/home_connect/api.py | 82 ++++++++++++------- .../components/home_connect/binary_sensor.py | 18 ++-- .../components/home_connect/const.py | 12 +++ .../components/home_connect/light.py | 28 +++---- .../components/home_connect/sensor.py | 12 +-- .../components/home_connect/switch.py | 20 ++--- 6 files changed, 102 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/home_connect/api.py b/homeassistant/components/home_connect/api.py index 44669ed200f..da5f1df20c6 100644 --- a/homeassistant/components/home_connect/api.py +++ b/homeassistant/components/home_connect/api.py @@ -7,11 +7,27 @@ import homeconnect from homeconnect.api import HomeConnectError from homeassistant import config_entries, core -from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE, TIME_SECONDS +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + CONF_DEVICE, + CONF_ENTITIES, + DEVICE_CLASS_TIMESTAMP, + PERCENTAGE, + TIME_SECONDS, +) from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.dispatcher import dispatcher_send from .const import ( + ATTR_AMBIENT, + ATTR_DESC, + ATTR_DEVICE, + ATTR_KEY, + ATTR_SENSOR_TYPE, + ATTR_SIGN, + ATTR_UNIT, + ATTR_VALUE, BSH_ACTIVE_PROGRAM, BSH_OPERATION_STATE, BSH_POWER_OFF, @@ -72,7 +88,9 @@ class ConfigEntryAuth(homeconnect.HomeConnectAPI): else: _LOGGER.warning("Appliance type %s not implemented", app.type) continue - devices.append({"device": device, "entities": device.get_entity_info()}) + devices.append( + {CONF_DEVICE: device, CONF_ENTITIES: device.get_entity_info()} + ) self.devices = devices return devices @@ -104,8 +122,10 @@ class HomeConnectDevice: except (HomeConnectError, ValueError): _LOGGER.debug("Unable to fetch active programs. Probably offline") program_active = None - if program_active and "key" in program_active: - self.appliance.status[BSH_ACTIVE_PROGRAM] = {"value": program_active["key"]} + if program_active and ATTR_KEY in program_active: + self.appliance.status[BSH_ACTIVE_PROGRAM] = { + ATTR_VALUE: program_active[ATTR_KEY] + } self.appliance.listen_events(callback=self.event_callback) def event_callback(self, appliance): @@ -130,7 +150,7 @@ class DeviceWithPrograms(HomeConnectDevice): There will be one switch for each program. """ programs = self.get_programs_available() - return [{"device": self, "program_name": p["name"]} for p in programs] + return [{ATTR_DEVICE: self, "program_name": p["name"]} for p in programs] def get_program_sensors(self): """Get a dictionary with info about program sensors. @@ -145,13 +165,13 @@ class DeviceWithPrograms(HomeConnectDevice): } return [ { - "device": self, - "desc": k, - "unit": unit, - "key": "BSH.Common.Option.{}".format(k.replace(" ", "")), - "icon": icon, - "device_class": device_class, - "sign": sign, + ATTR_DEVICE: self, + ATTR_DESC: k, + ATTR_UNIT: unit, + ATTR_KEY: "BSH.Common.Option.{}".format(k.replace(" ", "")), + ATTR_ICON: icon, + ATTR_DEVICE_CLASS: device_class, + ATTR_SIGN: sign, } for k, (unit, icon, device_class, sign) in sensors.items() ] @@ -165,13 +185,13 @@ class DeviceWithOpState(HomeConnectDevice): return [ { - "device": self, - "desc": "Operation State", - "unit": None, - "key": BSH_OPERATION_STATE, - "icon": "mdi:state-machine", - "device_class": None, - "sign": 1, + ATTR_DEVICE: self, + ATTR_DESC: "Operation State", + ATTR_UNIT: None, + ATTR_KEY: BSH_OPERATION_STATE, + ATTR_ICON: "mdi:state-machine", + ATTR_DEVICE_CLASS: None, + ATTR_SIGN: 1, } ] @@ -182,10 +202,10 @@ class DeviceWithDoor(HomeConnectDevice): def get_door_entity(self): """Get a dictionary with info about the door binary sensor.""" return { - "device": self, - "desc": "Door", - "sensor_type": "door", - "device_class": "door", + ATTR_DEVICE: self, + ATTR_DESC: "Door", + ATTR_SENSOR_TYPE: "door", + ATTR_DEVICE_CLASS: "door", } @@ -194,7 +214,7 @@ class DeviceWithLight(HomeConnectDevice): def get_light_entity(self): """Get a dictionary with info about the lighting.""" - return {"device": self, "desc": "Light", "ambient": None} + return {ATTR_DEVICE: self, ATTR_DESC: "Light", ATTR_AMBIENT: None} class DeviceWithAmbientLight(HomeConnectDevice): @@ -202,7 +222,7 @@ class DeviceWithAmbientLight(HomeConnectDevice): def get_ambientlight_entity(self): """Get a dictionary with info about the ambient lighting.""" - return {"device": self, "desc": "AmbientLight", "ambient": True} + return {ATTR_DEVICE: self, ATTR_DESC: "AmbientLight", ATTR_AMBIENT: True} class DeviceWithRemoteControl(HomeConnectDevice): @@ -211,9 +231,9 @@ class DeviceWithRemoteControl(HomeConnectDevice): def get_remote_control(self): """Get a dictionary with info about the remote control sensor.""" return { - "device": self, - "desc": "Remote Control", - "sensor_type": "remote_control", + ATTR_DEVICE: self, + ATTR_DESC: "Remote Control", + ATTR_SENSOR_TYPE: "remote_control", } @@ -222,7 +242,11 @@ class DeviceWithRemoteStart(HomeConnectDevice): def get_remote_start(self): """Get a dictionary with info about the remote start sensor.""" - return {"device": self, "desc": "Remote Start", "sensor_type": "remote_start"} + return { + ATTR_DEVICE: self, + ATTR_DESC: "Remote Start", + ATTR_SENSOR_TYPE: "remote_start", + } class Dryer( diff --git a/homeassistant/components/home_connect/binary_sensor.py b/homeassistant/components/home_connect/binary_sensor.py index 1713d34809a..4dc21f2fd58 100644 --- a/homeassistant/components/home_connect/binary_sensor.py +++ b/homeassistant/components/home_connect/binary_sensor.py @@ -2,9 +2,14 @@ import logging from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.const import CONF_ENTITIES from .const import ( + ATTR_VALUE, BSH_DOOR_STATE, + BSH_DOOR_STATE_CLOSED, + BSH_DOOR_STATE_LOCKED, + BSH_DOOR_STATE_OPEN, BSH_REMOTE_CONTROL_ACTIVATION_STATE, BSH_REMOTE_START_ALLOWANCE_STATE, DOMAIN, @@ -21,7 +26,7 @@ async def async_setup_entry(hass, config_entry, async_add_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("binary_sensor", []) + entity_dicts = device_dict.get(CONF_ENTITIES, {}).get("binary_sensor", []) entities += [HomeConnectBinarySensor(**d) for d in entity_dicts] return entities @@ -39,11 +44,8 @@ class HomeConnectBinarySensor(HomeConnectEntity, BinarySensorEntity): self._type = sensor_type if self._type == "door": self._update_key = BSH_DOOR_STATE - self._false_value_list = ( - "BSH.Common.EnumType.DoorState.Closed", - "BSH.Common.EnumType.DoorState.Locked", - ) - self._true_value_list = ["BSH.Common.EnumType.DoorState.Open"] + self._false_value_list = (BSH_DOOR_STATE_CLOSED, BSH_DOOR_STATE_LOCKED) + self._true_value_list = [BSH_DOOR_STATE_OPEN] elif self._type == "remote_control": self._update_key = BSH_REMOTE_CONTROL_ACTIVATION_STATE self._false_value_list = [False] @@ -68,9 +70,9 @@ class HomeConnectBinarySensor(HomeConnectEntity, BinarySensorEntity): state = self.device.appliance.status.get(self._update_key, {}) if not state: self._state = None - elif state.get("value") in self._false_value_list: + elif state.get(ATTR_VALUE) in self._false_value_list: self._state = False - elif state.get("value") in self._true_value_list: + elif state.get(ATTR_VALUE) in self._true_value_list: self._state = True else: _LOGGER.warning( diff --git a/homeassistant/components/home_connect/const.py b/homeassistant/components/home_connect/const.py index 98dd8d383bd..438ee5ace16 100644 --- a/homeassistant/components/home_connect/const.py +++ b/homeassistant/components/home_connect/const.py @@ -26,5 +26,17 @@ BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR = ( BSH_AMBIENT_LIGHT_CUSTOM_COLOR = "BSH.Common.Setting.AmbientLightCustomColor" BSH_DOOR_STATE = "BSH.Common.Status.DoorState" +BSH_DOOR_STATE_CLOSED = "BSH.Common.EnumType.DoorState.Closed" +BSH_DOOR_STATE_LOCKED = "BSH.Common.EnumType.DoorState.Locked" +BSH_DOOR_STATE_OPEN = "BSH.Common.EnumType.DoorState.Open" SIGNAL_UPDATE_ENTITIES = "home_connect.update_entities" + +ATTR_AMBIENT = "ambient" +ATTR_DESC = "desc" +ATTR_DEVICE = "device" +ATTR_KEY = "key" +ATTR_SENSOR_TYPE = "sensor_type" +ATTR_SIGN = "sign" +ATTR_UNIT = "unit" +ATTR_VALUE = "value" diff --git a/homeassistant/components/home_connect/light.py b/homeassistant/components/home_connect/light.py index 814e3b0ed03..dc176ba90f2 100644 --- a/homeassistant/components/home_connect/light.py +++ b/homeassistant/components/home_connect/light.py @@ -11,9 +11,11 @@ from homeassistant.components.light import ( SUPPORT_COLOR, LightEntity, ) +from homeassistant.const import CONF_ENTITIES import homeassistant.util.color as color_util from .const import ( + ATTR_VALUE, BSH_AMBIENT_LIGHT_BRIGHTNESS, BSH_AMBIENT_LIGHT_COLOR, BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR, @@ -36,7 +38,7 @@ async def async_setup_entry(hass, config_entry, async_add_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_dicts = device_dict.get(CONF_ENTITIES, {}).get("light", []) entity_list = [HomeConnectLight(**d) for d in entity_dicts] entities += entity_list return entities @@ -93,9 +95,7 @@ class HomeConnectLight(HomeConnectEntity, LightEntity): _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, + self.device.appliance.set_setting, self._key, True ) except HomeConnectError as err: _LOGGER.error("Error while trying to turn on ambient light: %s", err) @@ -135,9 +135,7 @@ class HomeConnectLight(HomeConnectEntity, LightEntity): 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, + self.device.appliance.set_setting, self._brightness_key, brightness ) except HomeConnectError as err: _LOGGER.error("Error while trying set the brightness: %s", err) @@ -145,9 +143,7 @@ class HomeConnectLight(HomeConnectEntity, LightEntity): _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, + self.device.appliance.set_setting, self._key, True ) except HomeConnectError as err: _LOGGER.error("Error while trying to turn on light: %s", err) @@ -159,9 +155,7 @@ class HomeConnectLight(HomeConnectEntity, LightEntity): _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, + self.device.appliance.set_setting, self._key, False ) except HomeConnectError as err: _LOGGER.error("Error while trying to turn off light: %s", err) @@ -169,9 +163,9 @@ class HomeConnectLight(HomeConnectEntity, LightEntity): async def async_update(self): """Update the light's status.""" - if self.device.appliance.status.get(self._key, {}).get("value") is True: + if self.device.appliance.status.get(self._key, {}).get(ATTR_VALUE) is True: self._state = True - elif self.device.appliance.status.get(self._key, {}).get("value") is False: + elif self.device.appliance.status.get(self._key, {}).get(ATTR_VALUE) is False: self._state = False else: self._state = None @@ -185,7 +179,7 @@ class HomeConnectLight(HomeConnectEntity, LightEntity): self._hs_color = None self._brightness = None else: - colorvalue = color.get("value")[1:] + colorvalue = color.get(ATTR_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]] @@ -197,5 +191,5 @@ class HomeConnectLight(HomeConnectEntity, LightEntity): if brightness is None: self._brightness = None else: - self._brightness = ceil((brightness.get("value") - 10) * 255 / 90) + self._brightness = ceil((brightness.get(ATTR_VALUE) - 10) * 255 / 90) _LOGGER.debug("Updated, new brightness: %s", self._brightness) diff --git a/homeassistant/components/home_connect/sensor.py b/homeassistant/components/home_connect/sensor.py index e51efe06057..064ae033fb0 100644 --- a/homeassistant/components/home_connect/sensor.py +++ b/homeassistant/components/home_connect/sensor.py @@ -3,10 +3,10 @@ from datetime import timedelta import logging -from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from homeassistant.const import CONF_ENTITIES, DEVICE_CLASS_TIMESTAMP import homeassistant.util.dt as dt_util -from .const import BSH_OPERATION_STATE, DOMAIN +from .const import ATTR_VALUE, BSH_OPERATION_STATE, DOMAIN from .entity import HomeConnectEntity _LOGGER = logging.getLogger(__name__) @@ -20,7 +20,7 @@ async def async_setup_entry(hass, config_entry, async_add_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("sensor", []) + entity_dicts = device_dict.get(CONF_ENTITIES, {}).get("sensor", []) entities += [HomeConnectSensor(**d) for d in entity_dicts] return entities @@ -57,7 +57,7 @@ class HomeConnectSensor(HomeConnectEntity): self._state = None else: if self.device_class == DEVICE_CLASS_TIMESTAMP: - if "value" not in status[self._key]: + if ATTR_VALUE not in status[self._key]: self._state = None elif ( self._state is not None @@ -68,12 +68,12 @@ class HomeConnectSensor(HomeConnectEntity): # already past it, set state to None. self._state = None else: - seconds = self._sign * float(status[self._key]["value"]) + seconds = self._sign * float(status[self._key][ATTR_VALUE]) self._state = ( dt_util.utcnow() + timedelta(seconds=seconds) ).isoformat() else: - self._state = status[self._key].get("value") + self._state = status[self._key].get(ATTR_VALUE) if self._key == BSH_OPERATION_STATE: # Value comes back as an enum, we only really care about the # last part, so split it off diff --git a/homeassistant/components/home_connect/switch.py b/homeassistant/components/home_connect/switch.py index 346e739e5ff..5e12d724a5e 100644 --- a/homeassistant/components/home_connect/switch.py +++ b/homeassistant/components/home_connect/switch.py @@ -4,8 +4,10 @@ import logging from homeconnect.api import HomeConnectError from homeassistant.components.switch import SwitchEntity +from homeassistant.const import CONF_DEVICE, CONF_ENTITIES from .const import ( + ATTR_VALUE, BSH_ACTIVE_PROGRAM, BSH_OPERATION_STATE, BSH_POWER_ON, @@ -25,9 +27,9 @@ async def async_setup_entry(hass, config_entry, async_add_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("switch", []) + entity_dicts = device_dict.get(CONF_ENTITIES, {}).get("switch", []) entity_list = [HomeConnectProgramSwitch(**d) for d in entity_dicts] - entity_list += [HomeConnectPowerSwitch(device_dict["device"])] + entity_list += [HomeConnectPowerSwitch(device_dict[CONF_DEVICE])] entities += entity_list return entities @@ -78,7 +80,7 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity): async def async_update(self): """Update the switch's status.""" state = self.device.appliance.status.get(BSH_ACTIVE_PROGRAM, {}) - if state.get("value") == self.program_name: + if state.get(ATTR_VALUE) == self.program_name: self._state = True else: self._state = False @@ -103,9 +105,7 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity): _LOGGER.debug("Tried to switch on %s", self.name) try: await self.hass.async_add_executor_job( - self.device.appliance.set_setting, - BSH_POWER_STATE, - BSH_POWER_ON, + self.device.appliance.set_setting, BSH_POWER_STATE, BSH_POWER_ON ) except HomeConnectError as err: _LOGGER.error("Error while trying to turn on device: %s", err) @@ -129,17 +129,17 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity): async def async_update(self): """Update the switch's status.""" if ( - self.device.appliance.status.get(BSH_POWER_STATE, {}).get("value") + self.device.appliance.status.get(BSH_POWER_STATE, {}).get(ATTR_VALUE) == BSH_POWER_ON ): self._state = True elif ( - self.device.appliance.status.get(BSH_POWER_STATE, {}).get("value") + self.device.appliance.status.get(BSH_POWER_STATE, {}).get(ATTR_VALUE) == self.device.power_off_state ): self._state = False elif self.device.appliance.status.get(BSH_OPERATION_STATE, {}).get( - "value", None + ATTR_VALUE, None ) in [ "BSH.Common.EnumType.OperationState.Ready", "BSH.Common.EnumType.OperationState.DelayedStart", @@ -151,7 +151,7 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity): ]: self._state = True elif ( - self.device.appliance.status.get(BSH_OPERATION_STATE, {}).get("value") + self.device.appliance.status.get(BSH_OPERATION_STATE, {}).get(ATTR_VALUE) == "BSH.Common.EnumType.OperationState.Inactive" ): self._state = False From efb172cedd8a54a2a508e27b4110e3b7f6dc7bbd Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 17 Feb 2021 01:05:50 -0800 Subject: [PATCH 483/796] Fix flaky stream tests due to race in idle timeout callback (#46687) --- homeassistant/components/stream/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 9b5afb38ed0..7f88885ac0b 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -141,8 +141,9 @@ class Stream: """Reset access token and cleanup stream due to inactivity.""" self.access_token = None if not self.keepalive: - self._hls.cleanup() - self._hls = None + if self._hls: + self._hls.cleanup() + self._hls = None self._hls_timer = None self._check_idle() From fb73768164a0d885204838a8ec14a181d81c9431 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Wed, 17 Feb 2021 10:43:12 +0100 Subject: [PATCH 484/796] Fix Tuya Option Flow tests (#46651) --- tests/components/tuya/test_config_flow.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index 012886fe3b8..ede6e5ac1db 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -181,6 +181,15 @@ async def test_options_flow(hass): ) config_entry.add_to_hass(hass) + # Set up the integration to make sure the config flow module is loaded. + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Unload the integration to prepare for the test. + with patch("homeassistant.components.tuya.async_unload_entry", return_value=True): + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + # Test check for integration not loaded result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT From facbd73130e1e19026de344ea8d7cd568f02d1c6 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Wed, 17 Feb 2021 07:15:13 -0500 Subject: [PATCH 485/796] Clean up xbee (#46549) --- homeassistant/components/xbee/__init__.py | 5 ++--- homeassistant/components/xbee/binary_sensor.py | 8 ++------ homeassistant/components/xbee/const.py | 5 +++++ homeassistant/components/xbee/light.py | 8 ++------ homeassistant/components/xbee/sensor.py | 3 +-- homeassistant/components/xbee/switch.py | 9 ++------- 6 files changed, 14 insertions(+), 24 deletions(-) create mode 100644 homeassistant/components/xbee/const.py diff --git a/homeassistant/components/xbee/__init__.py b/homeassistant/components/xbee/__init__.py index e6175a4dccf..6373cfa7535 100644 --- a/homeassistant/components/xbee/__init__.py +++ b/homeassistant/components/xbee/__init__.py @@ -21,9 +21,9 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN -DOMAIN = "xbee" +_LOGGER = logging.getLogger(__name__) SIGNAL_XBEE_FRAME_RECEIVED = "xbee_frame_received" @@ -59,7 +59,6 @@ PLATFORM_SCHEMA = vol.Schema( def setup(hass, config): """Set up the connection to the XBee Zigbee device.""" - usb_device = config[DOMAIN].get(CONF_DEVICE, DEFAULT_DEVICE) baud = int(config[DOMAIN].get(CONF_BAUD, DEFAULT_BAUD)) try: diff --git a/homeassistant/components/xbee/binary_sensor.py b/homeassistant/components/xbee/binary_sensor.py index 47c7515ddc7..01095822d1f 100644 --- a/homeassistant/components/xbee/binary_sensor.py +++ b/homeassistant/components/xbee/binary_sensor.py @@ -3,12 +3,8 @@ import voluptuous as vol from homeassistant.components.binary_sensor import BinarySensorEntity -from . import DOMAIN, PLATFORM_SCHEMA, XBeeDigitalIn, XBeeDigitalInConfig - -CONF_ON_STATE = "on_state" - -DEFAULT_ON_STATE = "high" -STATES = ["high", "low"] +from . import PLATFORM_SCHEMA, XBeeDigitalIn, XBeeDigitalInConfig +from .const import CONF_ON_STATE, DOMAIN, STATES PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_ON_STATE): vol.In(STATES)}) diff --git a/homeassistant/components/xbee/const.py b/homeassistant/components/xbee/const.py new file mode 100644 index 00000000000..a77e71e92f5 --- /dev/null +++ b/homeassistant/components/xbee/const.py @@ -0,0 +1,5 @@ +"""Constants for the xbee integration.""" +CONF_ON_STATE = "on_state" +DEFAULT_ON_STATE = "high" +DOMAIN = "xbee" +STATES = ["high", "low"] diff --git a/homeassistant/components/xbee/light.py b/homeassistant/components/xbee/light.py index 76ed8120166..859feee495b 100644 --- a/homeassistant/components/xbee/light.py +++ b/homeassistant/components/xbee/light.py @@ -3,12 +3,8 @@ import voluptuous as vol from homeassistant.components.light import LightEntity -from . import DOMAIN, PLATFORM_SCHEMA, XBeeDigitalOut, XBeeDigitalOutConfig - -CONF_ON_STATE = "on_state" - -DEFAULT_ON_STATE = "high" -STATES = ["high", "low"] +from . import PLATFORM_SCHEMA, XBeeDigitalOut, XBeeDigitalOutConfig +from .const import CONF_ON_STATE, DEFAULT_ON_STATE, DOMAIN, STATES PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( {vol.Optional(CONF_ON_STATE, default=DEFAULT_ON_STATE): vol.In(STATES)} diff --git a/homeassistant/components/xbee/sensor.py b/homeassistant/components/xbee/sensor.py index 4a392691032..4d9f9ca518b 100644 --- a/homeassistant/components/xbee/sensor.py +++ b/homeassistant/components/xbee/sensor.py @@ -5,14 +5,13 @@ import logging import voluptuous as vol from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import CONF_TYPE, TEMP_CELSIUS from homeassistant.helpers.entity import Entity from . import DOMAIN, PLATFORM_SCHEMA, XBeeAnalogIn, XBeeAnalogInConfig, XBeeConfig _LOGGER = logging.getLogger(__name__) -CONF_TYPE = "type" CONF_MAX_VOLTS = "max_volts" DEFAULT_VOLTS = 1.2 diff --git a/homeassistant/components/xbee/switch.py b/homeassistant/components/xbee/switch.py index cdb0d2677c5..b97d9f315d5 100644 --- a/homeassistant/components/xbee/switch.py +++ b/homeassistant/components/xbee/switch.py @@ -3,13 +3,8 @@ import voluptuous as vol from homeassistant.components.switch import SwitchEntity -from . import DOMAIN, PLATFORM_SCHEMA, XBeeDigitalOut, XBeeDigitalOutConfig - -CONF_ON_STATE = "on_state" - -DEFAULT_ON_STATE = "high" - -STATES = ["high", "low"] +from . import PLATFORM_SCHEMA, XBeeDigitalOut, XBeeDigitalOutConfig +from .const import CONF_ON_STATE, DOMAIN, STATES PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_ON_STATE): vol.In(STATES)}) From f7c0fc55530adc4f0dd0cc3efb23ffd4a6477804 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Wed, 17 Feb 2021 04:17:16 -0800 Subject: [PATCH 486/796] Increase async_timeout for wemo update polling (#46649) --- homeassistant/components/wemo/entity.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index e7c0712272c..2278cc854b2 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -49,16 +49,16 @@ class WemoEntity(Entity): """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. + minute to return. If we don't get a state within the scan interval, + 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): + with async_timeout.timeout(self.platform.scan_interval.seconds - 0.1): await asyncio.shield(self._async_locked_update(True)) except asyncio.TimeoutError: _LOGGER.warning("Lost connection to %s", self.name) From eb3e5cb67f1a0d64cf65c22c5f668448168e4009 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Wed, 17 Feb 2021 04:17:31 -0800 Subject: [PATCH 487/796] Remove calls to wemo.reconnect_with_device (#46646) --- homeassistant/components/wemo/binary_sensor.py | 1 - homeassistant/components/wemo/fan.py | 1 - homeassistant/components/wemo/light.py | 2 -- homeassistant/components/wemo/switch.py | 1 - tests/components/wemo/entity_test_helpers.py | 1 - 5 files changed, 6 deletions(-) diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index 2ea5f2d0b07..0d7f2532057 100644 --- a/homeassistant/components/wemo/binary_sensor.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -44,4 +44,3 @@ class WemoBinarySensor(WemoSubscriptionEntity, BinarySensorEntity): except ActionException as err: _LOGGER.warning("Could not update status for %s (%s)", self.name, err) self._available = False - self.wemo.reconnect_with_device() diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index faf1b50f1cd..678fd93fe05 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -158,7 +158,6 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity): except ActionException as err: _LOGGER.warning("Could not update status for %s (%s)", self.name, err) self._available = False - self.wemo.reconnect_with_device() def turn_on( self, diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 9aa7c945671..169534bf0c5 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -194,7 +194,6 @@ class WemoLight(WemoEntity, LightEntity): except ActionException as err: _LOGGER.warning("Could not update status for %s (%s)", self.name, err) self._available = False - self.wemo.bridge.reconnect_with_device() else: self._is_on = self._state.get("onoff") != WEMO_OFF self._brightness = self._state.get("level", 255) @@ -241,7 +240,6 @@ class WemoDimmer(WemoSubscriptionEntity, LightEntity): except ActionException as err: _LOGGER.warning("Could not update status for %s (%s)", self.name, err) self._available = False - self.wemo.reconnect_with_device() def turn_on(self, **kwargs): """Turn the dimmer on.""" diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index 4d2b9c007d4..f925aad3f72 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -180,4 +180,3 @@ class WemoSwitch(WemoSubscriptionEntity, SwitchEntity): except ActionException as err: _LOGGER.warning("Could not update status for %s (%s)", self.name, err) self._available = False - self.wemo.reconnect_with_device() diff --git a/tests/components/wemo/entity_test_helpers.py b/tests/components/wemo/entity_test_helpers.py index 87bc6fd7f40..4c92640572b 100644 --- a/tests/components/wemo/entity_test_helpers.py +++ b/tests/components/wemo/entity_test_helpers.py @@ -139,7 +139,6 @@ async def test_async_locked_update_with_exception( ) 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): From 1e9483a0e90e14a7a6d0fcb21f37fd3be031de51 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 17 Feb 2021 13:23:15 +0100 Subject: [PATCH 488/796] Bump RMVtransport to v0.3.0 (#46691) --- homeassistant/components/rmvtransport/manifest.json | 2 +- homeassistant/components/rmvtransport/sensor.py | 7 +++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/rmvtransport/manifest.json b/homeassistant/components/rmvtransport/manifest.json index 595e2d4834a..30aef1a0616 100644 --- a/homeassistant/components/rmvtransport/manifest.json +++ b/homeassistant/components/rmvtransport/manifest.json @@ -3,7 +3,7 @@ "name": "RMV", "documentation": "https://www.home-assistant.io/integrations/rmvtransport", "requirements": [ - "PyRMVtransport==0.2.10" + "PyRMVtransport==0.3.0" ], "codeowners": [ "@cgtobi" diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index da42c0cc927..ad1ceea3d86 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -4,7 +4,10 @@ from datetime import timedelta import logging from RMVtransport import RMVtransport -from RMVtransport.rmvtransport import RMVtransportApiConnectionError +from RMVtransport.rmvtransport import ( + RMVtransportApiConnectionError, + RMVtransportDataError, +) import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -229,7 +232,7 @@ class RMVDepartureData: max_journeys=50, ) - except RMVtransportApiConnectionError: + except (RMVtransportApiConnectionError, RMVtransportDataError): self.departures = [] _LOGGER.warning("Could not retrieve data from rmv.de") return diff --git a/requirements_all.txt b/requirements_all.txt index de6c34f4feb..f02ef7b2bb1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -49,7 +49,7 @@ PyNaCl==1.3.0 PyQRCode==1.2.1 # homeassistant.components.rmvtransport -PyRMVtransport==0.2.10 +PyRMVtransport==0.3.0 # homeassistant.components.telegram_bot PySocks==1.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0e863e23bb1..efba3a67c91 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -21,7 +21,7 @@ PyNaCl==1.3.0 PyQRCode==1.2.1 # homeassistant.components.rmvtransport -PyRMVtransport==0.2.10 +PyRMVtransport==0.3.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 4ab0151fb1cbefb31def23ba850e197da0a5027f Mon Sep 17 00:00:00 2001 From: Gaetan Semet Date: Wed, 17 Feb 2021 15:06:16 +0100 Subject: [PATCH 489/796] Discover HRT4-ZW / SRT321 SetPoint in zwave_js (#46625) Missing a specific class to allow discovery of the setpoint command. Fix #46570 Signed-off-by: Gaetan Semet --- homeassistant/components/zwave_js/discovery.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 1aa70ea2fa0..2022258e6ea 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -226,6 +226,7 @@ DISCOVERY_SCHEMAS = [ device_class_generic={"Thermostat"}, device_class_specific={ "Setpoint Thermostat", + "Unused", }, primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.THERMOSTAT_SETPOINT}, From b2df9aaaf1060916e8484260d0914ec3872581e3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Feb 2021 08:03:11 -1000 Subject: [PATCH 490/796] Update zha to use new fan entity model (#45758) * Update zha to use new fan entity model * fixes * tweaks for zha * pylint * augment test cover --- homeassistant/components/zha/fan.py | 169 +++++++++++++++++----------- tests/components/zha/test_fan.py | 123 +++++++++++++++++++- 2 files changed, 220 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index bc5714ef08c..1cd66f94686 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -1,22 +1,26 @@ """Fans on Zigbee Home Automation networks.""" +from abc import abstractmethod import functools +import math from typing import List, Optional from zigpy.exceptions import ZigbeeException import zigpy.zcl.clusters.hvac as hvac from homeassistant.components.fan import ( + ATTR_PERCENTAGE, + ATTR_PRESET_MODE, DOMAIN, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, SUPPORT_SET_SPEED, FanEntity, ) from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import State, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) from .core import discovery from .core.const import ( @@ -32,24 +36,20 @@ from .entity import ZhaEntity, ZhaGroupEntity # Additional speeds in zigbee's ZCL # Spec is unclear as to what this value means. On King Of Fans HBUniversal # receiver, this means Very High. -SPEED_ON = "on" +PRESET_MODE_ON = "on" # The fan speed is self-regulated -SPEED_AUTO = "auto" +PRESET_MODE_AUTO = "auto" # When the heated/cooled space is occupied, the fan is always on -SPEED_SMART = "smart" +PRESET_MODE_SMART = "smart" -SPEED_LIST = [ - SPEED_OFF, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_HIGH, - SPEED_ON, - SPEED_AUTO, - SPEED_SMART, -] +SPEED_RANGE = (1, 3) # off is not included +PRESET_MODES_TO_NAME = {4: PRESET_MODE_ON, 5: PRESET_MODE_AUTO, 6: PRESET_MODE_SMART} + +NAME_TO_PRESET_MODE = {v: k for k, v in PRESET_MODES_TO_NAME.items()} +PRESET_MODES = list(NAME_TO_PRESET_MODE) + +DEFAULT_ON_PERCENTAGE = 50 -VALUE_TO_SPEED = dict(enumerate(SPEED_LIST)) -SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)} STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, DOMAIN) @@ -74,51 +74,41 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class BaseFan(FanEntity): """Base representation of a ZHA fan.""" - def __init__(self, *args, **kwargs): - """Initialize the fan.""" - super().__init__(*args, **kwargs) - self._state = None - self._fan_channel = None - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return SPEED_LIST - - @property - def speed(self) -> str: - """Return the current speed.""" - return self._state + def preset_modes(self) -> str: + """Return the available preset modes.""" + return PRESET_MODES @property def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_SET_SPEED - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # async def async_turn_on( self, speed=None, percentage=None, preset_mode=None, **kwargs ) -> None: """Turn the entity on.""" - if speed is None: - speed = SPEED_MEDIUM - - await self.async_set_speed(speed) + await self.async_set_percentage(percentage) async def async_turn_off(self, **kwargs) -> None: """Turn the entity off.""" - await self.async_set_speed(SPEED_OFF) + await self.async_set_percentage(0) - async def async_set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - await self._fan_channel.async_set_speed(SPEED_TO_VALUE[speed]) - self.async_set_state(0, "fan_mode", speed) + async def async_set_percentage(self, percentage: Optional[int]) -> None: + """Set the speed percenage of the fan.""" + if percentage is None: + percentage = DEFAULT_ON_PERCENTAGE + fan_mode = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) + await self._async_set_fan_mode(fan_mode) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the speed percenage of the fan.""" + fan_mode = NAME_TO_PRESET_MODE.get(preset_mode) + await self._async_set_fan_mode(fan_mode) + + @abstractmethod + async def _async_set_fan_mode(self, fan_mode: int) -> None: + """Set the fan mode for the fan.""" @callback def async_set_state(self, attr_id, attr_name, value): @@ -142,15 +132,32 @@ class ZhaFan(BaseFan, ZhaEntity): ) @property - def speed(self) -> Optional[str]: - """Return the current speed.""" - return VALUE_TO_SPEED.get(self._fan_channel.fan_mode) + def percentage(self) -> str: + """Return the current speed percentage.""" + if ( + self._fan_channel.fan_mode is None + or self._fan_channel.fan_mode > SPEED_RANGE[1] + ): + return None + if self._fan_channel.fan_mode == 0: + return 0 + return ranged_value_to_percentage(SPEED_RANGE, self._fan_channel.fan_mode) + + @property + def preset_mode(self) -> str: + """Return the current preset mode.""" + return PRESET_MODES_TO_NAME.get(self._fan_channel.fan_mode) @callback def async_set_state(self, attr_id, attr_name, value): """Handle state update from channel.""" self.async_write_ha_state() + async def _async_set_fan_mode(self, fan_mode: int) -> None: + """Set the fan mode for the fan.""" + await self._fan_channel.async_set_speed(fan_mode) + self.async_set_state(0, "fan_mode", fan_mode) + @GROUP_MATCH() class FanGroup(BaseFan, ZhaGroupEntity): @@ -164,30 +171,60 @@ class FanGroup(BaseFan, ZhaGroupEntity): self._available: bool = False group = self.zha_device.gateway.get_group(self._group_id) self._fan_channel = group.endpoint[hvac.Fan.cluster_id] + self._percentage = None + self._preset_mode = None - # what should we do with this hack? - async def async_set_speed(value) -> None: - """Set the speed of the fan.""" - try: - await self._fan_channel.write_attributes({"fan_mode": value}) - except ZigbeeException as ex: - self.error("Could not set speed: %s", ex) - return + @property + def percentage(self) -> str: + """Return the current speed percentage.""" + return self._percentage - self._fan_channel.async_set_speed = async_set_speed + @property + def preset_mode(self) -> str: + """Return the current preset mode.""" + return self._preset_mode + + async def async_set_percentage(self, percentage: Optional[int]) -> None: + """Set the speed percenage of the fan.""" + if percentage is None: + percentage = DEFAULT_ON_PERCENTAGE + fan_mode = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) + await self._async_set_fan_mode(fan_mode) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the speed percenage of the fan.""" + fan_mode = NAME_TO_PRESET_MODE.get(preset_mode) + await self._async_set_fan_mode(fan_mode) + + async def _async_set_fan_mode(self, fan_mode: int) -> None: + """Set the fan mode for the group.""" + try: + await self._fan_channel.write_attributes({"fan_mode": fan_mode}) + except ZigbeeException as ex: + self.error("Could not set fan mode: %s", ex) + self.async_set_state(0, "fan_mode", fan_mode) async def async_update(self): """Attempt to retrieve on off state from the fan.""" 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] - + percentage_states: List[State] = [ + state for state in states if state.attributes.get(ATTR_PERCENTAGE) + ] + preset_mode_states: List[State] = [ + state for state in states if state.attributes.get(ATTR_PRESET_MODE) + ] 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 + + if percentage_states: + self._percentage = percentage_states[0].attributes[ATTR_PERCENTAGE] + self._preset_mode = None + elif preset_mode_states: + self._preset_mode = preset_mode_states[0].attributes[ATTR_PRESET_MODE] + self._percentage = None else: - self._state = on_states[0].state + self._percentage = None + self._preset_mode = None async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 61828c135bc..b6347ac6568 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, call, patch import pytest +from zigpy.exceptions import ZigbeeException import zigpy.profiles.zha as zha import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.hvac as hvac @@ -9,8 +10,11 @@ import zigpy.zcl.foundation as zcl_f from homeassistant.components import fan from homeassistant.components.fan import ( + ATTR_PERCENTAGE, + ATTR_PRESET_MODE, ATTR_SPEED, DOMAIN, + SERVICE_SET_PRESET_MODE, SERVICE_SET_SPEED, SPEED_HIGH, SPEED_LOW, @@ -20,6 +24,11 @@ from homeassistant.components.fan import ( 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.components.zha.fan import ( + PRESET_MODE_AUTO, + PRESET_MODE_ON, + PRESET_MODE_SMART, +) from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, @@ -173,6 +182,12 @@ async def test_fan(hass, zha_device_joined_restored, zigpy_device): assert len(cluster.write_attributes.mock_calls) == 1 assert cluster.write_attributes.call_args == call({"fan_mode": 3}) + # change preset_mode from HA + cluster.write_attributes.reset_mock() + await async_set_preset_mode(hass, entity_id, preset_mode=PRESET_MODE_ON) + assert len(cluster.write_attributes.mock_calls) == 1 + assert cluster.write_attributes.call_args == call({"fan_mode": 4}) + # test adding new fan to the network and HA await async_test_rejoin(hass, zigpy_device, [cluster], (1,)) @@ -206,6 +221,17 @@ 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_set_preset_mode(hass, entity_id, preset_mode=None): + """Set preset_mode for specified fan.""" + data = { + key: value + for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_PRESET_MODE, preset_mode)] + if value is not None + } + + await hass.services.async_call(DOMAIN, SERVICE_SET_PRESET_MODE, data, blocking=True) + + @patch( "zigpy.zcl.clusters.hvac.Fan.write_attributes", new=AsyncMock(return_value=zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]), @@ -276,6 +302,24 @@ async def test_zha_group_fan_entity(hass, device_fan_1, device_fan_2, coordinato assert len(group_fan_cluster.write_attributes.mock_calls) == 1 assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 3} + # change preset mode from HA + group_fan_cluster.write_attributes.reset_mock() + await async_set_preset_mode(hass, entity_id, preset_mode=PRESET_MODE_ON) + assert len(group_fan_cluster.write_attributes.mock_calls) == 1 + assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 4} + + # change preset mode from HA + group_fan_cluster.write_attributes.reset_mock() + await async_set_preset_mode(hass, entity_id, preset_mode=PRESET_MODE_AUTO) + assert len(group_fan_cluster.write_attributes.mock_calls) == 1 + assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 5} + + # change preset mode from HA + group_fan_cluster.write_attributes.reset_mock() + await async_set_preset_mode(hass, entity_id, preset_mode=PRESET_MODE_SMART) + assert len(group_fan_cluster.write_attributes.mock_calls) == 1 + assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 6} + # test some of the group logic to make sure we key off states correctly await send_attributes_report(hass, dev1_fan_cluster, {0: 0}) await send_attributes_report(hass, dev2_fan_cluster, {0: 0}) @@ -296,14 +340,74 @@ async def test_zha_group_fan_entity(hass, device_fan_1, device_fan_2, coordinato assert hass.states.get(entity_id).state == STATE_OFF +@patch( + "zigpy.zcl.clusters.hvac.Fan.write_attributes", + new=AsyncMock(side_effect=ZigbeeException), +) +async def test_zha_group_fan_entity_failure_state( + hass, device_fan_1, device_fan_2, coordinator, caplog +): + """Test the fan entity for a ZHA group when writing attributes generates an exception.""" + zha_gateway = get_zha_gateway(hass) + assert zha_gateway is not None + zha_gateway.coordinator_zha_device = coordinator + coordinator._zha_gateway = zha_gateway + 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", 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.device.ieee in member_ieee_addresses + assert member.group == zha_group + assert member.endpoint is not None + + entity_domains = GROUP_PROBE.determine_entity_domains(hass, zha_group) + assert len(entity_domains) == 2 + + assert LIGHT_DOMAIN in entity_domains + assert DOMAIN in entity_domains + + entity_id = async_find_group_entity_id(hass, DOMAIN, zha_group) + assert hass.states.get(entity_id) is not None + + group_fan_cluster = zha_group.endpoint[hvac.Fan.cluster_id] + + 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, [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 + + # 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[0][0] == {"fan_mode": 2} + + assert "Could not set fan mode" in caplog.text + + @pytest.mark.parametrize( - "plug_read, expected_state, expected_speed", + "plug_read, expected_state, expected_speed, expected_percentage", ( - (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), + (None, STATE_OFF, None, None), + ({"fan_mode": 0}, STATE_OFF, SPEED_OFF, 0), + ({"fan_mode": 1}, STATE_ON, SPEED_LOW, 33), + ({"fan_mode": 2}, STATE_ON, SPEED_MEDIUM, 66), + ({"fan_mode": 3}, STATE_ON, SPEED_HIGH, 100), ), ) async def test_fan_init( @@ -313,6 +417,7 @@ async def test_fan_init( plug_read, expected_state, expected_speed, + expected_percentage, ): """Test zha fan platform.""" @@ -324,6 +429,8 @@ async def test_fan_init( 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 + assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == expected_percentage + assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None async def test_fan_update_entity( @@ -341,6 +448,8 @@ async def test_fan_update_entity( 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 hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == 0 + assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None assert cluster.read_attributes.await_count == 1 await async_setup_component(hass, "homeassistant", {}) @@ -358,5 +467,7 @@ async def test_fan_update_entity( "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_PERCENTAGE] == 33 assert hass.states.get(entity_id).attributes[ATTR_SPEED] == SPEED_LOW + assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None assert cluster.read_attributes.await_count == 3 From 8bee3cda375b9a25d27c360364abadc4e393e9b0 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Wed, 17 Feb 2021 14:36:39 -0800 Subject: [PATCH 491/796] Centralize wemo exception handling (#46705) --- .../components/wemo/binary_sensor.py | 11 +----- homeassistant/components/wemo/entity.py | 33 +++++++++++++++- homeassistant/components/wemo/fan.py | 36 +++-------------- homeassistant/components/wemo/light.py | 39 ++++--------------- homeassistant/components/wemo/switch.py | 21 ++-------- 5 files changed, 48 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index 0d7f2532057..94d5a587c17 100644 --- a/homeassistant/components/wemo/binary_sensor.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -2,8 +2,6 @@ import asyncio import logging -from pywemo.ouimeaux_device.api.service import ActionException - from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -35,12 +33,5 @@ class WemoBinarySensor(WemoSubscriptionEntity, BinarySensorEntity): def _update(self, force_update=True): """Update the sensor state.""" - try: + with self._wemo_exception_handler("update status"): self._state = self.wemo.get_state(force_update) - - if not self._available: - _LOGGER.info("Reconnected to %s", self.name) - self._available = True - except ActionException as err: - _LOGGER.warning("Could not update status for %s (%s)", self.name, err) - self._available = False diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index 2278cc854b2..91470d0cd5c 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -1,10 +1,12 @@ """Classes shared among Wemo entities.""" import asyncio +import contextlib import logging -from typing import Any, Dict, Optional +from typing import Any, Dict, Generator, Optional import async_timeout from pywemo import WeMoDevice +from pywemo.ouimeaux_device.api.service import ActionException from homeassistant.helpers.entity import Entity @@ -13,6 +15,18 @@ from .const import DOMAIN as WEMO_DOMAIN _LOGGER = logging.getLogger(__name__) +class ExceptionHandlerStatus: + """Exit status from the _wemo_exception_handler context manager.""" + + # An exception if one was raised in the _wemo_exception_handler. + exception: Optional[Exception] = None + + @property + def success(self) -> bool: + """Return True if the handler completed with no exception.""" + return self.exception is None + + class WemoEntity(Entity): """Common methods for Wemo entities. @@ -36,6 +50,23 @@ class WemoEntity(Entity): """Return true if switch is available.""" return self._available + @contextlib.contextmanager + def _wemo_exception_handler( + self, message: str + ) -> Generator[ExceptionHandlerStatus, None, None]: + """Wrap device calls to set `_available` when wemo exceptions happen.""" + status = ExceptionHandlerStatus() + try: + yield status + except ActionException as err: + status.exception = err + _LOGGER.warning("Could not %s for %s (%s)", message, self.name, err) + self._available = False + else: + if not self._available: + _LOGGER.info("Reconnected to %s", self.name) + self._available = True + def _update(self, force_update: Optional[bool] = True): """Update the device state.""" raise NotImplementedError() diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 678fd93fe05..94dab468a69 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -4,7 +4,6 @@ from datetime import timedelta import logging import math -from pywemo.ouimeaux_device.api.service import ActionException import voluptuous as vol from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity @@ -138,7 +137,7 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity): def _update(self, force_update=True): """Update the device state.""" - try: + with self._wemo_exception_handler("update status"): self._state = self.wemo.get_state(force_update) self._fan_mode = self.wemo.fan_mode @@ -152,13 +151,6 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity): if self.wemo.fan_mode != WEMO_FAN_OFF: self._last_fan_on_mode = self.wemo.fan_mode - if not self._available: - _LOGGER.info("Reconnected to %s", self.name) - self._available = True - except ActionException as err: - _LOGGER.warning("Could not update status for %s (%s)", self.name, err) - self._available = False - def turn_on( self, speed: str = None, @@ -171,11 +163,8 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity): def turn_off(self, **kwargs) -> None: """Turn the switch off.""" - try: + with self._wemo_exception_handler("turn off"): self.wemo.set_state(WEMO_FAN_OFF) - except ActionException as err: - _LOGGER.warning("Error while turning off device %s (%s)", self.name, err) - self._available = False self.schedule_update_ha_state() @@ -188,13 +177,8 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity): else: named_speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) - try: + with self._wemo_exception_handler("set speed"): self.wemo.set_state(named_speed) - except ActionException as err: - _LOGGER.warning( - "Error while setting speed of device %s (%s)", self.name, err - ) - self._available = False self.schedule_update_ha_state() @@ -211,24 +195,14 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity): elif target_humidity >= 100: pywemo_humidity = WEMO_HUMIDITY_100 - try: + with self._wemo_exception_handler("set humidity"): self.wemo.set_humidity(pywemo_humidity) - except ActionException as err: - _LOGGER.warning( - "Error while setting humidity of device: %s (%s)", self.name, err - ) - self._available = False self.schedule_update_ha_state() def reset_filter_life(self) -> None: """Reset the filter life to 100%.""" - try: + with self._wemo_exception_handler("reset filter life"): self.wemo.reset_filter_life() - except ActionException as err: - _LOGGER.warning( - "Error while resetting filter life on device: %s (%s)", self.name, err - ) - self._available = False self.schedule_update_ha_state() diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 169534bf0c5..bbcdafaf351 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -3,8 +3,6 @@ import asyncio from datetime import timedelta import logging -from pywemo.ouimeaux_device.api.service import ActionException - from homeassistant import util from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -158,7 +156,7 @@ class WemoLight(WemoEntity, LightEntity): "force_update": False, } - try: + with self._wemo_exception_handler("turn on"): if xy_color is not None: self.wemo.set_color(xy_color, transition=transition_time) @@ -167,9 +165,6 @@ class WemoLight(WemoEntity, LightEntity): if self.wemo.turn_on(**turn_on_kwargs): self._state["onoff"] = WEMO_ON - except ActionException as err: - _LOGGER.warning("Error while turning on device %s (%s)", self.name, err) - self._available = False self.schedule_update_ha_state() @@ -177,28 +172,21 @@ class WemoLight(WemoEntity, LightEntity): """Turn the light off.""" transition_time = int(kwargs.get(ATTR_TRANSITION, 0)) - try: + with self._wemo_exception_handler("turn off"): if self.wemo.turn_off(transition=transition_time): self._state["onoff"] = WEMO_OFF - except ActionException as err: - _LOGGER.warning("Error while turning off device %s (%s)", self.name, err) - self._available = False self.schedule_update_ha_state() def _update(self, force_update=True): """Synchronize state with bridge.""" - try: + with self._wemo_exception_handler("update status") as handler: self._update_lights(no_throttle=force_update) self._state = self.wemo.state - except ActionException as err: - _LOGGER.warning("Could not update status for %s (%s)", self.name, err) - self._available = False - else: + if handler.success: self._is_on = self._state.get("onoff") != WEMO_OFF self._brightness = self._state.get("level", 255) self._color_temp = self._state.get("temperature_mireds") - self._available = True xy_color = self._state.get("color_xy") @@ -228,19 +216,12 @@ class WemoDimmer(WemoSubscriptionEntity, LightEntity): def _update(self, force_update=True): """Update the device state.""" - try: + with self._wemo_exception_handler("update status"): self._state = self.wemo.get_state(force_update) wemobrightness = int(self.wemo.get_brightness(force_update)) self._brightness = int((wemobrightness * 255) / 100) - if not self._available: - _LOGGER.info("Reconnected to %s", self.name) - self._available = True - except ActionException as err: - _LOGGER.warning("Could not update status for %s (%s)", self.name, err) - self._available = False - def turn_on(self, **kwargs): """Turn the dimmer on.""" # Wemo dimmer switches use a range of [0, 100] to control @@ -251,24 +232,18 @@ class WemoDimmer(WemoSubscriptionEntity, LightEntity): else: brightness = 255 - try: + with self._wemo_exception_handler("turn on"): if self.wemo.on(): self._state = WEMO_ON self.wemo.set_brightness(brightness) - except ActionException as err: - _LOGGER.warning("Error while turning on device %s (%s)", self.name, err) - self._available = False self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the dimmer off.""" - try: + with self._wemo_exception_handler("turn off"): if self.wemo.off(): self._state = WEMO_OFF - except ActionException as err: - _LOGGER.warning("Error while turning on device %s (%s)", self.name, err) - self._available = False self.schedule_update_ha_state() diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index f925aad3f72..15b38550b93 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -3,8 +3,6 @@ import asyncio from datetime import datetime, timedelta import logging -from pywemo.ouimeaux_device.api.service import ActionException - from homeassistant.components.switch import SwitchEntity from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -140,29 +138,23 @@ class WemoSwitch(WemoSubscriptionEntity, SwitchEntity): def turn_on(self, **kwargs): """Turn the switch on.""" - try: + with self._wemo_exception_handler("turn on"): if self.wemo.on(): self._state = WEMO_ON - except ActionException as err: - _LOGGER.warning("Error while turning on device %s (%s)", self.name, err) - self._available = False self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the switch off.""" - try: + with self._wemo_exception_handler("turn off"): if self.wemo.off(): self._state = WEMO_OFF - except ActionException as err: - _LOGGER.warning("Error while turning off device %s (%s)", self.name, err) - self._available = False self.schedule_update_ha_state() def _update(self, force_update=True): """Update the device state.""" - try: + with self._wemo_exception_handler("update status"): self._state = self.wemo.get_state(force_update) if self.wemo.model_name == "Insight": @@ -173,10 +165,3 @@ class WemoSwitch(WemoSubscriptionEntity, SwitchEntity): elif self.wemo.model_name == "CoffeeMaker": self.coffeemaker_mode = self.wemo.mode self._mode_string = self.wemo.mode_string - - if not self._available: - _LOGGER.info("Reconnected to %s", self.name) - self._available = True - except ActionException as err: - _LOGGER.warning("Could not update status for %s (%s)", self.name, err) - self._available = False From 2ac075bb3762e93e92be7b0bc7ab96185559a64b Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Wed, 17 Feb 2021 15:38:08 -0800 Subject: [PATCH 492/796] Perform wemo state update quickly after a timeout (#46702) * Perform state update quickly after a timeout * with async_timeout.timeout(...) -> async with async_timeout.timeout(...) --- homeassistant/components/wemo/entity.py | 19 ++++++++++++---- tests/components/wemo/entity_test_helpers.py | 24 +++++++++++++------- tests/components/wemo/test_light_bridge.py | 7 +++--- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index 91470d0cd5c..d9d90c5508b 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -89,16 +89,28 @@ class WemoEntity(Entity): return try: - with async_timeout.timeout(self.platform.scan_interval.seconds - 0.1): - await asyncio.shield(self._async_locked_update(True)) + async with async_timeout.timeout( + self.platform.scan_interval.seconds - 0.1 + ) as timeout: + await asyncio.shield(self._async_locked_update(True, timeout)) except asyncio.TimeoutError: _LOGGER.warning("Lost connection to %s", self.name) self._available = False - async def _async_locked_update(self, force_update: bool) -> None: + async def _async_locked_update( + self, force_update: bool, timeout: Optional[async_timeout.timeout] = None + ) -> None: """Try updating within an async lock.""" async with self._update_lock: await self.hass.async_add_executor_job(self._update, force_update) + # When the timeout expires HomeAssistant is no longer waiting for an + # update from the device. Instead, the state needs to be updated + # asynchronously. This also handles the case where an update came + # directly from the device (device push). In that case no polling + # update was involved and the state also needs to be updated + # asynchronously. + if not timeout or timeout.expired: + self.async_write_ha_state() class WemoSubscriptionEntity(WemoEntity): @@ -152,4 +164,3 @@ class WemoSubscriptionEntity(WemoEntity): return await self._async_locked_update(force_update) - self.async_write_ha_state() diff --git a/tests/components/wemo/entity_test_helpers.py b/tests/components/wemo/entity_test_helpers.py index 4c92640572b..e584cb5fb39 100644 --- a/tests/components/wemo/entity_test_helpers.py +++ b/tests/components/wemo/entity_test_helpers.py @@ -6,6 +6,7 @@ import asyncio import threading from unittest.mock import patch +import async_timeout from pywemo.ouimeaux_device.api.service import ActionException from homeassistant.components.homeassistant import ( @@ -146,7 +147,19 @@ async def test_async_update_with_timeout_and_recovery(hass, wemo_entity, pywemo_ 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): + event = threading.Event() + + def get_state(*args): + event.wait() + return 0 + + if hasattr(pywemo_device, "bridge_update"): + pywemo_device.bridge_update.side_effect = get_state + else: + pywemo_device.get_state.side_effect = get_state + timeout = async_timeout.timeout(0) + + with patch("async_timeout.timeout", return_value=timeout): await hass.services.async_call( HA_DOMAIN, SERVICE_UPDATE_ENTITY, @@ -157,11 +170,6 @@ async def test_async_update_with_timeout_and_recovery(hass, wemo_entity, pywemo_ 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, - ) - + event.set() + await hass.async_block_till_done() assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF diff --git a/tests/components/wemo/test_light_bridge.py b/tests/components/wemo/test_light_bridge.py index 3e7f79200c6..573f75a66d9 100644 --- a/tests/components/wemo/test_light_bridge.py +++ b/tests/components/wemo/test_light_bridge.py @@ -69,9 +69,10 @@ 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 - ) + with _bypass_throttling(): + await entity_test_helpers.test_async_update_with_timeout_and_recovery( + hass, wemo_entity, pywemo_device + ) async def test_async_locked_update_with_exception( From 28ffa97635cfa2e668430273e66f3f671bee6c16 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 18 Feb 2021 00:07:45 +0000 Subject: [PATCH 493/796] [ci skip] Translation update --- .../components/habitica/translations/ca.json | 20 +++++++++++++++++ .../components/habitica/translations/en.json | 16 +++++++------- .../components/habitica/translations/et.json | 20 +++++++++++++++++ .../components/habitica/translations/no.json | 20 +++++++++++++++++ .../habitica/translations/zh-Hant.json | 20 +++++++++++++++++ .../components/smarttub/translations/ca.json | 22 +++++++++++++++++++ .../components/smarttub/translations/et.json | 22 +++++++++++++++++++ .../components/smarttub/translations/no.json | 22 +++++++++++++++++++ .../smarttub/translations/zh-Hant.json | 22 +++++++++++++++++++ 9 files changed, 176 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/habitica/translations/ca.json create mode 100644 homeassistant/components/habitica/translations/et.json create mode 100644 homeassistant/components/habitica/translations/no.json create mode 100644 homeassistant/components/habitica/translations/zh-Hant.json create mode 100644 homeassistant/components/smarttub/translations/ca.json create mode 100644 homeassistant/components/smarttub/translations/et.json create mode 100644 homeassistant/components/smarttub/translations/no.json create mode 100644 homeassistant/components/smarttub/translations/zh-Hant.json diff --git a/homeassistant/components/habitica/translations/ca.json b/homeassistant/components/habitica/translations/ca.json new file mode 100644 index 00000000000..675fc33db8c --- /dev/null +++ b/homeassistant/components/habitica/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "invalid_credentials": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API", + "api_user": "ID d'usuari de l'API d'Habitica", + "name": "Substitueix el nom d'usuari d'Habitica. S'utilitzar\u00e0 per a crides de servei", + "url": "URL" + }, + "description": "Connecta el perfil d'Habitica per permetre el seguiment del teu perfil i tasques d'usuari. Tingues en compte que l'api_id i l'api_key els has d'obtenir des de https://habitica.com/user/settings/api" + } + } + }, + "title": "Habitica" +} \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/en.json b/homeassistant/components/habitica/translations/en.json index fa571d0d72a..ffbbd2de840 100644 --- a/homeassistant/components/habitica/translations/en.json +++ b/homeassistant/components/habitica/translations/en.json @@ -1,20 +1,20 @@ { "config": { "error": { - "invalid_credentials": "Invalid credentials", - "unknown": "Unknown error" + "invalid_credentials": "Invalid authentication", + "unknown": "Unexpected error" }, "step": { "user": { "data": { - "url": "Habitica URL", - "name": "Override for Habitica’s username. Will be used for service calls", - "api_user": "Habitica’s API user ID", - "api_key": "Habitica's API user KEY" + "api_key": "API Key", + "api_user": "Habitica\u2019s API user ID", + "name": "Override for Habitica\u2019s username. Will be used for service calls", + "url": "URL" }, - "description": "Connect your Habitica profile to allow monitoring of your user's profile and tasks. Note that api_id and api_key must be grabed from https://habitica.com/user/settings/api" + "description": "Connect your Habitica profile to allow monitoring of your user's profile and tasks. Note that api_id and api_key must be gotten from https://habitica.com/user/settings/api" } } }, "title": "Habitica" -} +} \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/et.json b/homeassistant/components/habitica/translations/et.json new file mode 100644 index 00000000000..cfc2bcf898c --- /dev/null +++ b/homeassistant/components/habitica/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "invalid_credentials": "Vigane autentimine", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "api_user": "Habitica API kasutaja ID", + "name": "Habitica kasutajanime alistamine. Kasutatakse teenuste kutsumiseks", + "url": "URL" + }, + "description": "\u00dchenda oma Habitica profiil, et saaksid j\u00e4lgida oma kasutaja profiili ja \u00fclesandeid. Pane t\u00e4hele, et api_id ja api_key tuleb hankida aadressilt https://habitica.com/user/settings/api" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/no.json b/homeassistant/components/habitica/translations/no.json new file mode 100644 index 00000000000..cdb72d3c3d6 --- /dev/null +++ b/homeassistant/components/habitica/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "invalid_credentials": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "api_user": "Habiticas API-bruker-ID", + "name": "Overstyr for Habiticas brukernavn. Blir brukt til serviceanrop", + "url": "URL" + }, + "description": "Koble til Habitica-profilen din for \u00e5 tillate overv\u00e5king av brukerens profil og oppgaver. Merk at api_id og api_key m\u00e5 hentes fra https://habitica.com/user/settings/api" + } + } + }, + "title": "Habitica" +} \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/zh-Hant.json b/homeassistant/components/habitica/translations/zh-Hant.json new file mode 100644 index 00000000000..001682b5c88 --- /dev/null +++ b/homeassistant/components/habitica/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "invalid_credentials": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470", + "api_user": "Habitica \u4e4b API \u4f7f\u7528\u8005 ID", + "name": "\u8986\u5beb Habitica \u4f7f\u7528\u8005\u540d\u7a31\u3001\u7528\u4ee5\u670d\u52d9\u547c\u53eb", + "url": "\u7db2\u5740" + }, + "description": "\u9023\u7dda\u81f3 Habitica \u8a2d\u5b9a\u6a94\u4ee5\u4f9b\u76e3\u63a7\u500b\u4eba\u8a2d\u5b9a\u8207\u4efb\u52d9\u3002\u6ce8\u610f\uff1a\u5fc5\u9808\u7531 https://habitica.com/user/settings/api \u53d6\u5f97 api_id \u8207 api_key" + } + } + }, + "title": "Habitica" +} \ No newline at end of file diff --git a/homeassistant/components/smarttub/translations/ca.json b/homeassistant/components/smarttub/translations/ca.json new file mode 100644 index 00000000000..6d882abeee6 --- /dev/null +++ b/homeassistant/components/smarttub/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "email": "Correu electr\u00f2nic", + "password": "Contrasenya" + }, + "description": "Introdueix el correu electr\u00f2nic i la contrasenya de SmartTub per iniciar sessi\u00f3", + "title": "Inici de sessi\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarttub/translations/et.json b/homeassistant/components/smarttub/translations/et.json new file mode 100644 index 00000000000..676edee1584 --- /dev/null +++ b/homeassistant/components/smarttub/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "invalid_auth": "Vigane autentimine", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "email": "E-posti aadress", + "password": "Salas\u00f5na" + }, + "description": "Sisselogimiseks sisesta oma SmartTubi e-posti aadress ja salas\u00f5na", + "title": "Sisselogimine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarttub/translations/no.json b/homeassistant/components/smarttub/translations/no.json new file mode 100644 index 00000000000..7f1c5982d28 --- /dev/null +++ b/homeassistant/components/smarttub/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "email": "E-post", + "password": "Passord" + }, + "description": "Skriv inn din SmartTub e-postadresse og passord for \u00e5 logge p\u00e5", + "title": "P\u00e5logging" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarttub/translations/zh-Hant.json b/homeassistant/components/smarttub/translations/zh-Hant.json new file mode 100644 index 00000000000..9491e7d2f25 --- /dev/null +++ b/homeassistant/components/smarttub/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "email": "\u96fb\u5b50\u90f5\u4ef6", + "password": "\u5bc6\u78bc" + }, + "description": "\u8acb\u8f38\u5165\u767b\u5165 SmartTub \u4e4b Email \u5730\u5740\u8207\u5bc6\u78bc\u3002", + "title": "\u767b\u5165" + } + } + } +} \ No newline at end of file From e9334347eb8354795cdb17f1401a80ef3abfb269 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Thu, 18 Feb 2021 14:54:10 +0800 Subject: [PATCH 494/796] Bump pylutron 0.2.7 (#46717) --- homeassistant/components/lutron/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json index 2dbeb51da58..fdd47d9005d 100644 --- a/homeassistant/components/lutron/manifest.json +++ b/homeassistant/components/lutron/manifest.json @@ -2,6 +2,6 @@ "domain": "lutron", "name": "Lutron", "documentation": "https://www.home-assistant.io/integrations/lutron", - "requirements": ["pylutron==0.2.5"], + "requirements": ["pylutron==0.2.7"], "codeowners": ["@JonGilmore"] } diff --git a/requirements_all.txt b/requirements_all.txt index f02ef7b2bb1..aba43a17ee0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1503,7 +1503,7 @@ pyloopenergy==0.2.1 pylutron-caseta==0.9.0 # homeassistant.components.lutron -pylutron==0.2.5 +pylutron==0.2.7 # homeassistant.components.mailgun pymailgunner==1.4 From 39785c5cef194bcaa0432f7fd0e17450e72e20a8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Feb 2021 00:00:11 -1000 Subject: [PATCH 495/796] Switch ssdp to be async by using async_upnp_client for scanning (#46554) SSDP scans no longer runs in the executor This is an interim step that converts the async_upnp_client response to netdisco's object to ensure fully backwards compatibility --- homeassistant/components/ssdp/__init__.py | 31 ++++++---- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/package_constraints.txt | 1 + requirements_all.txt | 1 + requirements_test_all.txt | 1 + tests/components/ssdp/test_init.py | 65 ++++++++++++++++----- 6 files changed, 73 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index f07e88d811a..8cad4a74bf8 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -1,14 +1,16 @@ """The SSDP integration.""" import asyncio from datetime import timedelta -import itertools import logging +from typing import Any, Mapping import aiohttp +from async_upnp_client.search import async_search from defusedxml import ElementTree from netdisco import ssdp, util from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import callback from homeassistant.helpers.event import async_track_time_interval from homeassistant.loader import async_get_ssdp @@ -51,12 +53,6 @@ async def async_setup(hass, config): return True -def _run_ssdp_scans(): - _LOGGER.debug("Scanning") - # Run 3 times as packets can get lost - return itertools.chain.from_iterable([ssdp.scan() for _ in range(3)]) - - class Scanner: """Class to manage SSDP scanning.""" @@ -64,25 +60,38 @@ class Scanner: """Initialize class.""" self.hass = hass self.seen = set() + self._entries = [] self._integration_matchers = integration_matchers self._description_cache = {} + async def _on_ssdp_response(self, data: Mapping[str, Any]) -> None: + """Process an ssdp response.""" + self.async_store_entry( + ssdp.UPNPEntry({key.lower(): item for key, item in data.items()}) + ) + + @callback + def async_store_entry(self, entry): + """Save an entry for later processing.""" + self._entries.append(entry) + async def async_scan(self, _): """Scan for new entries.""" - entries = await self.hass.async_add_executor_job(_run_ssdp_scans) - await self._process_entries(entries) + await async_search(async_callback=self._on_ssdp_response) + await self._process_entries() # We clear the cache after each run. We track discovered entries # so will never need a description twice. self._description_cache.clear() + self._entries.clear() - async def _process_entries(self, entries): + async def _process_entries(self): """Process SSDP entries.""" entries_to_process = [] unseen_locations = set() - for entry in entries: + for entry in self._entries: key = (entry.st, entry.location) if key in self.seen: diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index ed20ae9ead6..931119e2398 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["defusedxml==0.6.0", "netdisco==2.8.2"], + "requirements": ["defusedxml==0.6.0", "netdisco==2.8.2", "async-upnp-client==0.14.13"], "after_dependencies": ["zeroconf"], "codeowners": [] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d926471660e..1a1163c7a88 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -3,6 +3,7 @@ PyNaCl==1.3.0 aiohttp==3.7.3 aiohttp_cors==0.7.0 astral==1.10.1 +async-upnp-client==0.14.13 async_timeout==3.0.1 attrs==19.3.0 awesomeversion==21.2.2 diff --git a/requirements_all.txt b/requirements_all.txt index aba43a17ee0..0f96ab51235 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -284,6 +284,7 @@ asmog==0.0.6 asterisk_mbox==0.5.0 # homeassistant.components.dlna_dmr +# homeassistant.components.ssdp # homeassistant.components.upnp async-upnp-client==0.14.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index efba3a67c91..148b66e9ac1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -173,6 +173,7 @@ aprslib==0.6.46 arcam-fmj==0.5.3 # homeassistant.components.dlna_dmr +# homeassistant.components.ssdp # homeassistant.components.upnp async-upnp-client==0.14.13 diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index bba809aedbb..8ca82e93bfc 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -14,15 +14,18 @@ async def test_scan_match_st(hass, caplog): """Test matching based on ST.""" scanner = ssdp.Scanner(hass, {"mock-domain": [{"st": "mock-st"}]}) - with patch( - "netdisco.ssdp.scan", - return_value=[ + async def _inject_entry(*args, **kwargs): + scanner.async_store_entry( Mock( st="mock-st", location=None, values={"usn": "mock-usn", "server": "mock-server", "ext": ""}, ) - ], + ) + + with patch( + "homeassistant.components.ssdp.async_search", + side_effect=_inject_entry, ), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: @@ -58,9 +61,14 @@ async def test_scan_match_upnp_devicedesc(hass, aioclient_mock, key): ) scanner = ssdp.Scanner(hass, {"mock-domain": [{key: "Paulus"}]}) + async def _inject_entry(*args, **kwargs): + scanner.async_store_entry( + Mock(st="mock-st", location="http://1.1.1.1", values={}) + ) + with patch( - "netdisco.ssdp.scan", - return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})], + "homeassistant.components.ssdp.async_search", + side_effect=_inject_entry, ), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: @@ -95,9 +103,14 @@ async def test_scan_not_all_present(hass, aioclient_mock): }, ) + async def _inject_entry(*args, **kwargs): + scanner.async_store_entry( + Mock(st="mock-st", location="http://1.1.1.1", values={}) + ) + with patch( - "netdisco.ssdp.scan", - return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})], + "homeassistant.components.ssdp.async_search", + side_effect=_inject_entry, ), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: @@ -131,9 +144,14 @@ async def test_scan_not_all_match(hass, aioclient_mock): }, ) + async def _inject_entry(*args, **kwargs): + scanner.async_store_entry( + Mock(st="mock-st", location="http://1.1.1.1", values={}) + ) + with patch( - "netdisco.ssdp.scan", - return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})], + "homeassistant.components.ssdp.async_search", + side_effect=_inject_entry, ), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: @@ -148,9 +166,14 @@ async def test_scan_description_fetch_fail(hass, aioclient_mock, exc): aioclient_mock.get("http://1.1.1.1", exc=exc) scanner = ssdp.Scanner(hass, {}) + async def _inject_entry(*args, **kwargs): + scanner.async_store_entry( + Mock(st="mock-st", location="http://1.1.1.1", values={}) + ) + with patch( - "netdisco.ssdp.scan", - return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})], + "homeassistant.components.ssdp.async_search", + side_effect=_inject_entry, ): await scanner.async_scan(None) @@ -165,9 +188,14 @@ async def test_scan_description_parse_fail(hass, aioclient_mock): ) scanner = ssdp.Scanner(hass, {}) + async def _inject_entry(*args, **kwargs): + scanner.async_store_entry( + Mock(st="mock-st", location="http://1.1.1.1", values={}) + ) + with patch( - "netdisco.ssdp.scan", - return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})], + "homeassistant.components.ssdp.async_search", + side_effect=_inject_entry, ): await scanner.async_scan(None) @@ -196,9 +224,14 @@ async def test_invalid_characters(hass, aioclient_mock): }, ) + async def _inject_entry(*args, **kwargs): + scanner.async_store_entry( + Mock(st="mock-st", location="http://1.1.1.1", values={}) + ) + with patch( - "netdisco.ssdp.scan", - return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})], + "homeassistant.components.ssdp.async_search", + side_effect=_inject_entry, ), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: From da662c48901e8443966d06880fb31da8fafa190b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 12:23:30 +0100 Subject: [PATCH 496/796] Add selectors to Input * service definitions (#46652) --- .../components/input_boolean/services.yaml | 26 +++----- .../components/input_datetime/services.yaml | 27 ++++++-- .../components/input_number/services.yaml | 30 +++++---- .../components/input_select/services.yaml | 64 +++++++++++-------- .../components/input_text/services.yaml | 11 ++-- 5 files changed, 90 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/input_boolean/services.yaml b/homeassistant/components/input_boolean/services.yaml index 5ab5e7a9b82..8cefe2b4974 100644 --- a/homeassistant/components/input_boolean/services.yaml +++ b/homeassistant/components/input_boolean/services.yaml @@ -1,20 +1,14 @@ toggle: - description: Toggles an input boolean. - fields: - entity_id: - description: Entity id of the input boolean to toggle. - example: input_boolean.notify_alerts + description: Toggle an input boolean + target: + turn_off: - description: Turns off an input boolean - fields: - entity_id: - description: Entity id of the input boolean to turn off. - example: input_boolean.notify_alerts + description: Turn off an input boolean + target: + turn_on: - description: Turns on an input boolean. - fields: - entity_id: - description: Entity id of the input boolean to turn on. - example: input_boolean.notify_alerts + description: Turn on an input boolean + target: + reload: - description: Reload the input_boolean configuration. + description: Reload the input_boolean configuration diff --git a/homeassistant/components/input_datetime/services.yaml b/homeassistant/components/input_datetime/services.yaml index bcbadc45aad..ef4cb9556c8 100644 --- a/homeassistant/components/input_datetime/services.yaml +++ b/homeassistant/components/input_datetime/services.yaml @@ -1,21 +1,36 @@ set_datetime: - description: This can be used to dynamically set the date and/or time. Use date/time, datetime or timestamp. + description: This can be used to dynamically set the date and/or time + target: fields: - entity_id: - description: Entity id of the input datetime to set the new value. - example: input_datetime.test_date_time date: + name: Date description: The target date the entity should be set to. example: '"2019-04-20"' + selector: + text: time: + name: Time description: The target time the entity should be set to. example: '"05:04:20"' + selector: + time: datetime: + name: Date & Time description: The target date & time the entity should be set to. example: '"2019-04-20 05:04:20"' + selector: + text: timestamp: - description: The target date & time the entity should be set to as expressed by a UNIX timestamp. + name: Timestamp + description: + The target date & time the entity should be set to as expressed by a + UNIX timestamp. example: 1598027400 + selector: + number: + min: 0 + max: 9223372036854775807 + mode: box reload: - description: Reload the input_datetime configuration. + description: Reload the input_datetime configuration diff --git a/homeassistant/components/input_number/services.yaml b/homeassistant/components/input_number/services.yaml index 4d69bf72eda..700a2c28144 100644 --- a/homeassistant/components/input_number/services.yaml +++ b/homeassistant/components/input_number/services.yaml @@ -1,23 +1,25 @@ decrement: - description: Decrement the value of an input number entity by its stepping. - fields: - entity_id: - description: Entity id of the input number that should be decremented. - example: input_number.threshold + description: Decrement the value of an input number entity by its stepping + target: + increment: - description: Increment the value of an input number entity by its stepping. - fields: - entity_id: - description: Entity id of the input number that should be incremented. - example: input_number.threshold + description: Increment the value of an input number entity by its stepping + target: + set_value: - description: Set the value of an input number entity. + description: Set the value of an input number entity + target: fields: - entity_id: - description: Entity id of the input number to set the new value. - example: input_number.threshold value: + name: Value description: The target value the entity should be set to. + required: true example: 42 + selector: + number: + min: 0 + max: 9223372036854775807 + step: 0.001 + mode: box reload: description: Reload the input_number configuration. diff --git a/homeassistant/components/input_select/services.yaml b/homeassistant/components/input_select/services.yaml index 0eddb158d34..bf1b9e81033 100644 --- a/homeassistant/components/input_select/services.yaml +++ b/homeassistant/components/input_select/services.yaml @@ -1,50 +1,58 @@ select_next: - description: Select the next options of an input select entity. + description: Select the next options of an input select entity + target: fields: - entity_id: - description: Entity id of the input select to select the next value for. - example: input_select.my_select cycle: - description: If the option should cycle from the last to the first (defaults to true). + name: Cycle + description: If the option should cycle from the last to the first. + default: true example: true + selector: + boolean: + select_option: - description: Select an option of an input select entity. + description: Select an option of an input select entity + target: fields: - entity_id: - description: Entity id of the input select to select the value. - example: input_select.my_select option: + name: Option description: Option to be selected. + required: true example: '"Item A"' + selector: + text: + select_previous: - description: Select the previous options of an input select entity. + description: Select the previous options of an input select entity + target: fields: - entity_id: - description: Entity id of the input select to select the previous value for. - example: input_select.my_select cycle: - description: If the option should cycle from the first to the last (defaults to true). + name: Cycle + description: If the option should cycle from the first to the last. + default: true example: true + selector: + boolean: + select_first: - description: Select the first option of an input select entity. - fields: - entity_id: - description: Entity id of the input select to select the first value for. - example: input_select.my_select + description: Select the first option of an input select entity + target: + select_last: - description: Select the last option of an input select entity. - fields: - entity_id: - description: Entity id of the input select to select the last value for. - example: input_select.my_select + description: Select the last option of an input select entity + target: + set_options: - description: Set the options of an input select entity. + description: Set the options of an input select entity + target: fields: - entity_id: - description: Entity id of the input select to set the new options for. - example: input_select.my_select options: + name: Options description: Options for the input select entity. + required: true example: '["Item A", "Item B", "Item C"]' + selector: + object: + reload: description: Reload the input_select configuration. diff --git a/homeassistant/components/input_text/services.yaml b/homeassistant/components/input_text/services.yaml index 0f74cd8940e..b5ac97f837a 100644 --- a/homeassistant/components/input_text/services.yaml +++ b/homeassistant/components/input_text/services.yaml @@ -1,11 +1,14 @@ set_value: - description: Set the value of an input text entity. + description: Set the value of an input text entity + target: fields: - entity_id: - description: Entity id of the input text to set the new value. - example: input_text.text1 value: + name: Value description: The target value the entity should be set to. + required: true example: This is an example text + selector: + text: + reload: description: Reload the input_text configuration. From 9f3fdb1b68de84a6d441f04bc2d251c0c504856e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 12:23:50 +0100 Subject: [PATCH 497/796] Add selectors to Alert service definitions (#46627) --- homeassistant/components/alert/services.yaml | 23 +++++++------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/alert/services.yaml b/homeassistant/components/alert/services.yaml index 99530200546..c8c1d5d814a 100644 --- a/homeassistant/components/alert/services.yaml +++ b/homeassistant/components/alert/services.yaml @@ -1,18 +1,11 @@ toggle: - description: Toggle alert's notifications. - fields: - entity_id: - description: Name of the alert to toggle. - example: alert.garage_door_open + description: Toggle alert's notifications + target: + turn_off: - description: Silence alert's notifications. - fields: - entity_id: - description: Name of the alert to silence. - example: alert.garage_door_open + description: Silence alert's notifications + target: + turn_on: - description: Reset alert's notifications. - fields: - entity_id: - description: Name of the alert to reset. - example: alert.garage_door_open + description: Reset alert's notifications + target: From dec2eb36fd5070fd4a6a9a860eab14d08e3ee615 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 12:24:04 +0100 Subject: [PATCH 498/796] Add selectors to Camera service definitions (#46630) --- homeassistant/components/camera/services.yaml | 92 +++++++++++-------- 1 file changed, 56 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/camera/services.yaml b/homeassistant/components/camera/services.yaml index 70d33da884c..3ae5b650304 100644 --- a/homeassistant/components/camera/services.yaml +++ b/homeassistant/components/camera/services.yaml @@ -1,69 +1,89 @@ # Describes the format for available camera services turn_off: - description: Turn off camera. - fields: - entity_id: - description: Entity id. - example: "camera.living_room" + description: Turn off camera + target: turn_on: - description: Turn on camera. - fields: - entity_id: - description: Entity id. - example: "camera.living_room" + description: Turn on camera + target: enable_motion_detection: - description: Enable the motion detection in a camera. - fields: - entity_id: - description: Name(s) of entities to enable motion detection. - example: "camera.living_room_camera" + description: Enable the motion detection in a camera + target: disable_motion_detection: - description: Disable the motion detection in a camera. - fields: - entity_id: - description: Name(s) of entities to disable motion detection. - example: "camera.living_room_camera" + description: Disable the motion detection in a camera + target: snapshot: - description: Take a snapshot from a camera. + description: Take a snapshot from a camera + target: fields: - entity_id: - description: Name(s) of entities to create snapshots from. - example: "camera.living_room_camera" filename: + name: Filename description: Template of a Filename. Variable is entity_id. + required: true example: "/tmp/snapshot_{{ entity_id.name }}.jpg" + selector: + text: play_stream: - description: Play camera stream on supported media player. + description: Play camera stream on supported media player + target: fields: - entity_id: - description: Name(s) of entities to stream from. - example: "camera.living_room_camera" media_player: + name: Media Player description: Name(s) of media player to stream to. + required: true example: "media_player.living_room_tv" + selector: + entity: + domain: media_player format: - description: (Optional) Stream format supported by media player. + name: Format + description: Stream format supported by media player. + default: "hls" example: "hls" + selector: + select: + options: + - "hls" record: - description: Record live camera feed. + description: Record live camera feed + target: fields: - entity_id: - description: Name of entities to record. - example: "camera.living_room_camera" filename: - description: Template of a Filename. Variable is entity_id. Must be mp4. + name: Filename + description: Template of a Filename. Variable is entity_id. Must be mp4. + required: true example: "/tmp/snapshot_{{ entity_id.name }}.mp4" + selector: + text: duration: - description: (Optional) Target recording length (in seconds). + name: Duration + description: Target recording length. default: 30 example: 30 + selector: + number: + min: 1 + max: 3600 + step: 1 + unit_of_measurement: seconds + mode: slider lookback: - description: (Optional) Target lookback period (in seconds) to include in addition to duration. Only available if there is currently an active HLS stream. + name: Lookback + description: + Target lookback period to include in addition to duration. Only + available if there is currently an active HLS stream. + default: 0 example: 4 + selector: + number: + min: 0 + max: 300 + step: 1 + unit_of_measurement: seconds + mode: slider From c8c3ce4172323684691d110c359cd313a81dd8d6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 12:24:42 +0100 Subject: [PATCH 499/796] Add selectors to Switch service definitions (#46635) --- homeassistant/components/switch/services.yaml | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/switch/services.yaml b/homeassistant/components/switch/services.yaml index 74dda2ddf4f..de45995797f 100644 --- a/homeassistant/components/switch/services.yaml +++ b/homeassistant/components/switch/services.yaml @@ -1,22 +1,13 @@ # Describes the format for available switch services turn_on: - description: Turn a switch on. - fields: - entity_id: - description: Name(s) of entities to turn on - example: "switch.living_room" + description: Turn a switch on + target: turn_off: - description: Turn a switch off. - fields: - entity_id: - description: Name(s) of entities to turn off. - example: "switch.living_room" + description: Turn a switch off + target: toggle: - description: Toggles a switch state. - fields: - entity_id: - description: Name(s) of entities to toggle. - example: "switch.living_room" + description: Toggles a switch state + target: From 4e93a0c774793f9ecc7981b66fd5d5313af6d2cd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 12:24:50 +0100 Subject: [PATCH 500/796] Add selectors to Downloader service definitions (#46638) --- .../components/downloader/services.yaml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/downloader/services.yaml b/homeassistant/components/downloader/services.yaml index 6e16e00432f..5ac383fc4f6 100644 --- a/homeassistant/components/downloader/services.yaml +++ b/homeassistant/components/downloader/services.yaml @@ -1,15 +1,28 @@ download_file: - description: Downloads a file to the download location. + description: Downloads a file to the download location fields: url: + name: URL description: The URL of the file to download. + required: true example: "http://example.org/myfile" + selector: + text: subdir: + name: Subdirectory description: Download into subdirectory. example: "download_dir" + selector: + text: filename: + name: Filename description: Determine the filename. example: "my_file_name" + selector: + text: overwrite: + name: Overwrite description: Whether to overwrite the file or not. - example: "false" + default: false + selector: + boolean: From 2dfbd4fbcf1069e18328bf63328ebe8f5942e11c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 12:24:58 +0100 Subject: [PATCH 501/796] Add selectors to Fan service definitions (#46639) --- homeassistant/components/fan/services.yaml | 106 +++++++++++++-------- 1 file changed, 64 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index 760aaabcf4a..2f5802b69f7 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -1,80 +1,102 @@ # Describes the format for available fan services set_speed: - description: Sets fan speed. + description: Set fan speed + target: fields: - entity_id: - description: Name(s) of the entities to set - example: "fan.living_room" speed: - description: Speed setting + name: Speed + description: Speed setting. + required: true example: "low" + selector: + text: set_preset_mode: - description: Set preset mode for a fan device. + description: Set preset mode for a fan device + target: fields: - entity_id: - description: Name(s) of entities to change. - example: "fan.kitchen" preset_mode: - description: New value of preset mode + name: Preset mode + description: New value of preset mode. + required: true example: "auto" + selector: + text: set_percentage: - description: Sets fan speed percentage. + description: Set fan speed percentage + target: fields: - entity_id: - description: Name(s) of the entities to set - example: "fan.living_room" percentage: - description: Percentage speed setting + name: Percentage + description: Percentage speed setting. + required: true example: 25 + selector: + number: + min: 0 + max: 100 + step: 1 + unit_of_measurement: "%" + mode: slider turn_on: - description: Turns fan on. + description: Turn fan on + target: fields: - entity_id: - description: Names(s) of the entities to turn on - example: "fan.living_room" speed: - description: Speed setting + name: Speed + description: Speed setting. example: "high" percentage: - description: Percentage speed setting + name: Percentage + description: Percentage speed setting. example: 75 + selector: + number: + min: 0 + max: 100 + step: 1 + unit_of_measurement: "%" + mode: slider preset_mode: - description: Preset mode setting + name: Preset mode + description: Preset mode setting. example: "auto" + selector: + text: turn_off: - description: Turns fan off. - fields: - entity_id: - description: Names(s) of the entities to turn off - example: "fan.living_room" + description: Turn fan off + target: oscillate: - description: Oscillates the fan. + description: Oscillate the fan + target: fields: - entity_id: - description: Name(s) of the entities to oscillate - example: "fan.desk_fan" oscillating: - description: Flag to turn on/off oscillation + name: Oscillating + description: Flag to turn on/off oscillation. + required: true example: true + selector: + boolean: toggle: - description: Toggle the fan on/off. - fields: - entity_id: - description: Name(s) of the entities to toggle - example: "fan.living_room" + description: Toggle the fan on/off + target: set_direction: - description: Set the fan rotation. + description: Set the fan rotation + target: fields: - entity_id: - description: Name(s) of the entities to set - example: "fan.living_room" direction: - description: The direction to rotate. Either 'forward' or 'reverse' + name: Direction + description: The direction to rotate. + required: true example: "forward" + selector: + select: + options: + - "forward" + - "reverse" From e45fc4562b80a68ede88643107e50abf42a4f380 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 12:25:16 +0100 Subject: [PATCH 502/796] Add selectors to Cover service definitions (#46634) Co-authored-by: Tobias Sauerwein --- homeassistant/components/cover/services.yaml | 90 +++++++++----------- 1 file changed, 40 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/cover/services.yaml b/homeassistant/components/cover/services.yaml index 604955aa199..173674193cb 100644 --- a/homeassistant/components/cover/services.yaml +++ b/homeassistant/components/cover/services.yaml @@ -1,77 +1,67 @@ # Describes the format for available cover services open_cover: - description: Open all or specified cover. - fields: - entity_id: - description: Name(s) of cover(s) to open. - example: "cover.living_room" + description: Open all or specified cover + target: close_cover: - description: Close all or specified cover. - fields: - entity_id: - description: Name(s) of cover(s) to close. - example: "cover.living_room" + description: Close all or specified cover + target: toggle: - description: Toggles a cover open/closed. - fields: - entity_id: - description: Name(s) of cover(s) to toggle. - example: "cover.garage_door" + description: Toggles a cover open/closed + target: set_cover_position: - description: Move to specific position all or specified cover. + description: Move to specific position all or specified cover + target: fields: - entity_id: - description: Name(s) of cover(s) to set cover position. - example: "cover.living_room" position: - description: Position of the cover (0 to 100). + name: Position + description: Position of the cover + required: true example: 30 + selector: + number: + min: 0 + max: 100 + step: 1 + unit_of_measurement: "%" + mode: slider stop_cover: - description: Stop all or specified cover. - fields: - entity_id: - description: Name(s) of cover(s) to stop. - example: "cover.living_room" + description: Stop all or specified cover + target: open_cover_tilt: - description: Open all or specified cover tilt. - fields: - entity_id: - description: Name(s) of cover(s) tilt to open. - example: "cover.living_room_blinds" + description: Open all or specified cover tilt + target: close_cover_tilt: - description: Close all or specified cover tilt. - fields: - entity_id: - description: Name(s) of cover(s) to close tilt. - example: "cover.living_room_blinds" + description: Close all or specified cover tilt + target: toggle_cover_tilt: - description: Toggles a cover tilt open/closed. - fields: - entity_id: - description: Name(s) of cover(s) to toggle tilt. - example: "cover.living_room_blinds" + description: Toggle a cover tilt open/closed + target: set_cover_tilt_position: - description: Move to specific position all or specified cover tilt. + description: Move to specific position all or specified cover tilt + target: fields: - entity_id: - description: Name(s) of cover(s) to set cover tilt position. - example: "cover.living_room_blinds" tilt_position: - description: Tilt position of the cover (0 to 100). + name: Tilt position + description: Tilt position of the cover. + required: true example: 30 + selector: + number: + min: 0 + max: 100 + step: 1 + unit_of_measurement: "%" + mode: slider stop_cover_tilt: - description: Stop all or specified cover. - fields: - entity_id: - description: Name(s) of cover(s) to stop. - example: "cover.living_room_blinds" + description: Stop all or specified cover + target: From bc1cb8f0a044c3cd9d33ea0e44963e4781375add Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 12:25:25 +0100 Subject: [PATCH 503/796] Add selectors to Automation service definitions (#46629) --- .../components/automation/services.yaml | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/automation/services.yaml b/homeassistant/components/automation/services.yaml index 2f5b0a231e4..d8380914ce6 100644 --- a/homeassistant/components/automation/services.yaml +++ b/homeassistant/components/automation/services.yaml @@ -1,37 +1,35 @@ # Describes the format for available automation services turn_on: - description: Enable an automation. - fields: - entity_id: - description: Name of the automation to turn on. - example: "automation.notify_home" + description: Enable an automation + target: turn_off: - description: Disable an automation. + description: Disable an automation + target: fields: - entity_id: - description: Name of the automation to turn off. - example: "automation.notify_home" stop_actions: - description: Stop currently running actions (defaults to true). - example: false + name: Stop actions + description: Stop currently running actions. + default: true + example: true + selector: + boolean: toggle: - description: Toggle an automation. - fields: - entity_id: - description: Name of the automation to toggle on/off. - example: "automation.notify_home" + description: Toggle an automation + target: trigger: - description: Trigger the action of an automation. + description: Trigger the action of an automation + target: fields: - entity_id: - description: Name of the automation to trigger. - example: "automation.notify_home" skip_condition: - description: Whether or not the condition will be skipped (defaults to true). + name: Skip conditions + description: Whether or not the condition will be skipped. + default: true example: true + selector: + boolean: reload: - description: Reload the automation configuration. + description: Reload the automation configuration From ae643bfaf3e6e155fcd29caf0a9ec2773c4aa5b1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 12:25:55 +0100 Subject: [PATCH 504/796] Add selectors to Climate service definitions (#46632) --- .../components/climate/services.yaml | 121 ++++++++++++------ 1 file changed, 79 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index 99964081277..ea90cbf30f6 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -1,93 +1,130 @@ # Describes the format for available climate services set_aux_heat: - description: Turn auxiliary heater on/off for climate device. + description: Turn auxiliary heater on/off for climate device + target: fields: - entity_id: - description: Name(s) of entities to change. - example: "climate.kitchen" aux_heat: - description: New value of axillary heater. + name: Auxiliary heating + description: New value of auxiliary heater. + required: true example: true + selector: + boolean: set_preset_mode: - description: Set preset mode for climate device. + description: Set preset mode for climate device + target: fields: - entity_id: - description: Name(s) of entities to change. - example: "climate.kitchen" preset_mode: - description: New value of preset mode + name: Preset mode + description: New value of preset mode. + required: true example: "away" + selector: + text: set_temperature: - description: Set target temperature of climate device. + description: Set target temperature of climate device + target: fields: - entity_id: - description: Name(s) of entities to change. - example: "climate.kitchen" temperature: + name: Temperature description: New target temperature for HVAC. example: 25 + selector: + number: + min: 0 + max: 250 + step: 0.1 + mode: box target_temp_high: + name: Target temperature high description: New target high temperature for HVAC. example: 26 target_temp_low: + name: Target temperature low description: New target low temperature for HVAC. example: 20 hvac_mode: + name: HVAC mode description: HVAC operation mode to set temperature to. example: "heat" + selector: + select: + options: + - "off" + - "auto" + - "cool" + - "dry" + - "fan_only" + - "heat_cool" + - "heat" set_humidity: - description: Set target humidity of climate device. + description: Set target humidity of climate device + target: fields: - entity_id: - description: Name(s) of entities to change. - example: "climate.kitchen" humidity: + name: Humidity description: New target humidity for climate device. + required: true example: 60 + selector: + number: + min: 30 + max: 99 + step: 1 + unit_of_measurement: "%" + mode: slider set_fan_mode: - description: Set fan operation for climate device. + description: Set fan operation for climate device + target: fields: - entity_id: - description: Name(s) of entities to change. - example: "climate.nest" fan_mode: + name: Fan mode description: New value of fan mode. - example: On Low + required: true + example: "low" + selector: + text: set_hvac_mode: - description: Set HVAC operation mode for climate device. + description: Set HVAC operation mode for climate device + target: fields: - entity_id: - description: Name(s) of entities to change. - example: "climate.nest" hvac_mode: + name: HVAC mode description: New value of operation mode. - example: heat + example: "heat" + selector: + select: + options: + - "off" + - "auto" + - "cool" + - "dry" + - "fan_only" + - "heat_cool" + - "heat" set_swing_mode: - description: Set swing operation for climate device. + description: Set swing operation for climate device + target: fields: - entity_id: - description: Name(s) of entities to change. - example: "climate.nest" swing_mode: + name: Swing mode description: New value of swing mode. + required: true + example: "horizontal" + selector: + text: turn_on: - description: Turn climate device on. - fields: - entity_id: - description: Name(s) of entities to change. - example: "climate.kitchen" + description: Turn climate device on + target: turn_off: - description: Turn climate device off. - fields: - entity_id: - description: Name(s) of entities to change. - example: "climate.kitchen" + description: Turn climate device off + target: From 399777cfa8ad9c157bff7d5c555b981b70a1a2fe Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 12:26:05 +0100 Subject: [PATCH 505/796] Add selectors to Alarm Control Panel service definitions (#46626) --- .../alarm_control_panel/services.yaml | 57 +++++++++++-------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index ee1e8c1fcf6..a6151c58db0 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -1,61 +1,68 @@ # Describes the format for available alarm control panel services alarm_disarm: - description: Send the alarm the command for disarm. + description: Send the alarm the command for disarm + target: fields: - entity_id: - description: Name of alarm control panel to disarm. - example: "alarm_control_panel.downstairs" code: + name: Code description: An optional code to disarm the alarm control panel with. example: "1234" + selector: + text: alarm_arm_custom_bypass: - description: Send arm custom bypass command. + description: Send arm custom bypass command + target: fields: - entity_id: - description: Name of alarm control panel to arm custom bypass. - example: "alarm_control_panel.downstairs" code: - description: An optional code to arm custom bypass the alarm control panel with. + name: Code + description: + An optional code to arm custom bypass the alarm control panel with. example: "1234" + selector: + text: alarm_arm_home: - description: Send the alarm the command for arm home. + description: Send the alarm the command for arm home + target: fields: - entity_id: - description: Name of alarm control panel to arm home. - example: "alarm_control_panel.downstairs" code: + name: Code description: An optional code to arm home the alarm control panel with. example: "1234" + selector: + text: alarm_arm_away: - description: Send the alarm the command for arm away. + description: Send the alarm the command for arm away + target: fields: - entity_id: - description: Name of alarm control panel to arm away. - example: "alarm_control_panel.downstairs" code: + name: Code description: An optional code to arm away the alarm control panel with. example: "1234" + selector: + text: alarm_arm_night: - description: Send the alarm the command for arm night. + description: Send the alarm the command for arm night + target: fields: - entity_id: - description: Name of alarm control panel to arm night. - example: "alarm_control_panel.downstairs" code: + name: Code description: An optional code to arm night the alarm control panel with. example: "1234" + selector: + text: alarm_trigger: - description: Send the alarm the command for trigger. + description: Send the alarm the command for trigger + target: fields: - entity_id: - description: Name of alarm control panel to trigger. - example: "alarm_control_panel.downstairs" code: + name: Code description: An optional code to trigger the alarm control panel with. example: "1234" + selector: + text: From 0181cbb3127475588c060a0dbfea50243cf92bc8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 12:31:07 +0100 Subject: [PATCH 506/796] Upgrade and constrain httplib2>=0.19.0 (#46725) --- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/remember_the_milk/manifest.json | 2 +- homeassistant/package_constraints.txt | 5 +++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/gen_requirements_all.py | 5 +++-- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 6df116effa5..859f1b33296 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/google", "requirements": [ "google-api-python-client==1.6.4", - "httplib2==0.18.1", + "httplib2==0.19.0", "oauth2client==4.0.0" ], "codeowners": [] diff --git a/homeassistant/components/remember_the_milk/manifest.json b/homeassistant/components/remember_the_milk/manifest.json index f03f88023ae..8ce8cb98e5b 100644 --- a/homeassistant/components/remember_the_milk/manifest.json +++ b/homeassistant/components/remember_the_milk/manifest.json @@ -2,7 +2,7 @@ "domain": "remember_the_milk", "name": "Remember The Milk", "documentation": "https://www.home-assistant.io/integrations/remember_the_milk", - "requirements": ["RtmAPI==0.7.2", "httplib2==0.18.1"], + "requirements": ["RtmAPI==0.7.2", "httplib2==0.19.0"], "dependencies": ["configurator"], "codeowners": [] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1a1163c7a88..7db311b56d5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -46,8 +46,9 @@ h11>=0.12.0 # https://github.com/encode/httpcore/issues/239 httpcore>=0.12.3 -# Constrain httplib2 to protect against CVE-2020-11078 -httplib2>=0.18.0 +# Constrain httplib2 to protect against GHSA-93xj-8mrv-444m +# https://github.com/advisories/GHSA-93xj-8mrv-444m +httplib2>=0.19.0 # gRPC 1.32+ currently causes issues on ARMv7, see: # https://github.com/home-assistant/core/issues/40148 diff --git a/requirements_all.txt b/requirements_all.txt index 0f96ab51235..fdddefc44a6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -779,7 +779,7 @@ horimote==0.4.1 # homeassistant.components.google # homeassistant.components.remember_the_milk -httplib2==0.18.1 +httplib2==0.19.0 # homeassistant.components.huawei_lte huawei-lte-api==1.4.17 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 148b66e9ac1..cf1b25c9350 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -425,7 +425,7 @@ homematicip==0.13.1 # homeassistant.components.google # homeassistant.components.remember_the_milk -httplib2==0.18.1 +httplib2==0.19.0 # homeassistant.components.huawei_lte huawei-lte-api==1.4.17 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 52820bfa572..7dd4924dac8 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -71,8 +71,9 @@ h11>=0.12.0 # https://github.com/encode/httpcore/issues/239 httpcore>=0.12.3 -# Constrain httplib2 to protect against CVE-2020-11078 -httplib2>=0.18.0 +# Constrain httplib2 to protect against GHSA-93xj-8mrv-444m +# https://github.com/advisories/GHSA-93xj-8mrv-444m +httplib2>=0.19.0 # gRPC 1.32+ currently causes issues on ARMv7, see: # https://github.com/home-assistant/core/issues/40148 From 4083b90138386dace66a38ea2363bdf16daf9731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 18 Feb 2021 12:33:21 +0100 Subject: [PATCH 507/796] ubus: switch to pypi library (#46690) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas --- CODEOWNERS | 1 + .../components/ubus/device_tracker.py | 89 +++---------------- homeassistant/components/ubus/manifest.json | 3 +- requirements_all.txt | 3 + 4 files changed, 19 insertions(+), 77 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 81f43da58e7..75faf90eb75 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -491,6 +491,7 @@ homeassistant/components/tts/* @pvizeli homeassistant/components/tuya/* @ollo69 homeassistant/components/twentemilieu/* @frenck homeassistant/components/twinkly/* @dr1rrb +homeassistant/components/ubus/* @noltari homeassistant/components/unifi/* @Kane610 homeassistant/components/unifiled/* @florisvdk homeassistant/components/upb/* @gwww diff --git a/homeassistant/components/ubus/device_tracker.py b/homeassistant/components/ubus/device_tracker.py index 4cefefc2f96..12c986d57bb 100644 --- a/homeassistant/components/ubus/device_tracker.py +++ b/homeassistant/components/ubus/device_tracker.py @@ -1,9 +1,9 @@ """Support for OpenWRT (ubus) routers.""" -import json + import logging import re -import requests +from openwrt.ubus import Ubus import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -11,8 +11,7 @@ from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, HTTP_OK -from homeassistant.exceptions import HomeAssistantError +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -58,7 +57,7 @@ def _refresh_on_access_denied(func): "Invalid session detected." " Trying to refresh session_id and re-run RPC" ) - self.session_id = _get_session_id(self.url, self.username, self.password) + self.ubus.connect() return func(self, *args, **kwargs) @@ -82,10 +81,10 @@ class UbusDeviceScanner(DeviceScanner): self.last_results = {} self.url = f"http://{host}/ubus" - self.session_id = _get_session_id(self.url, self.username, self.password) + self.ubus = Ubus(self.url, self.username, self.password) self.hostapd = [] self.mac2name = None - self.success_init = self.session_id is not None + self.success_init = self.ubus.connect() is not None def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" @@ -119,16 +118,14 @@ class UbusDeviceScanner(DeviceScanner): _LOGGER.info("Checking hostapd") if not self.hostapd: - hostapd = _req_json_rpc(self.url, self.session_id, "list", "hostapd.*", "") + hostapd = self.ubus.get_hostapd() self.hostapd.extend(hostapd.keys()) self.last_results = [] results = 0 # for each access point for hostapd in self.hostapd: - result = _req_json_rpc( - self.url, self.session_id, "call", hostapd, "get_clients" - ) + result = self.ubus.get_hostapd_clients(hostapd) if result: results = results + 1 @@ -151,31 +148,21 @@ class DnsmasqUbusDeviceScanner(UbusDeviceScanner): def _generate_mac2name(self): if self.leasefile is None: - result = _req_json_rpc( - self.url, - self.session_id, - "call", - "uci", - "get", - config="dhcp", - type="dnsmasq", - ) + result = self.ubus.get_uci_config("dhcp", "dnsmasq") if result: values = result["values"].values() self.leasefile = next(iter(values))["leasefile"] else: return - result = _req_json_rpc( - self.url, self.session_id, "call", "file", "read", path=self.leasefile - ) + result = self.ubus.file_read(self.leasefile) if result: self.mac2name = {} for line in result["data"].splitlines(): hosts = line.split(" ") self.mac2name[hosts[1].upper()] = hosts[3] else: - # Error, handled in the _req_json_rpc + # Error, handled in the ubus.file_read() return @@ -183,7 +170,7 @@ class OdhcpdUbusDeviceScanner(UbusDeviceScanner): """Implement the Ubus device scanning for the odhcp DHCP server.""" def _generate_mac2name(self): - result = _req_json_rpc(self.url, self.session_id, "call", "dhcp", "ipv4leases") + result = self.ubus.get_dhcp_method("ipv4leases") if result: self.mac2name = {} for device in result["device"].values(): @@ -193,55 +180,5 @@ class OdhcpdUbusDeviceScanner(UbusDeviceScanner): mac = ":".join(mac[i : i + 2] for i in range(0, len(mac), 2)) self.mac2name[mac.upper()] = lease["hostname"] else: - # Error, handled in the _req_json_rpc + # Error, handled in the ubus.get_dhcp_method() return - - -def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params): - """Perform one JSON RPC operation.""" - data = json.dumps( - { - "jsonrpc": "2.0", - "id": 1, - "method": rpcmethod, - "params": [session_id, subsystem, method, params], - } - ) - - try: - res = requests.post(url, data=data, timeout=5) - - except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): - return - - if res.status_code == HTTP_OK: - response = res.json() - if "error" in response: - if ( - "message" in response["error"] - and response["error"]["message"] == "Access denied" - ): - raise PermissionError(response["error"]["message"]) - raise HomeAssistantError(response["error"]["message"]) - - if rpcmethod == "call": - try: - return response["result"][1] - except IndexError: - return - else: - return response["result"] - - -def _get_session_id(url, username, password): - """Get the authentication token for the given host+username+password.""" - res = _req_json_rpc( - url, - "00000000000000000000000000000000", - "call", - "session", - "login", - username=username, - password=password, - ) - return res["ubus_rpc_session"] diff --git a/homeassistant/components/ubus/manifest.json b/homeassistant/components/ubus/manifest.json index af7fb50b6c4..68452f98f7d 100644 --- a/homeassistant/components/ubus/manifest.json +++ b/homeassistant/components/ubus/manifest.json @@ -2,5 +2,6 @@ "domain": "ubus", "name": "OpenWrt (ubus)", "documentation": "https://www.home-assistant.io/integrations/ubus", - "codeowners": [] + "requirements": ["openwrt-ubus-rpc==0.0.2"], + "codeowners": ["@noltari"] } diff --git a/requirements_all.txt b/requirements_all.txt index fdddefc44a6..815484aa700 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1070,6 +1070,9 @@ openwebifpy==3.2.7 # homeassistant.components.luci openwrt-luci-rpc==1.1.6 +# homeassistant.components.ubus +openwrt-ubus-rpc==0.0.2 + # homeassistant.components.oru oru==0.1.11 From 82934b31f8219f1b44ffdb7fe65dc95b8a0ac940 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 12:59:29 +0100 Subject: [PATCH 508/796] Add selectors to Counter service definitions (#46633) --- .../components/counter/services.yaml | 68 ++++++++++++------- 1 file changed, 45 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/counter/services.yaml b/homeassistant/components/counter/services.yaml index 960424df0ca..16010f6e2f4 100644 --- a/homeassistant/components/counter/services.yaml +++ b/homeassistant/components/counter/services.yaml @@ -1,41 +1,63 @@ # Describes the format for available counter services decrement: - description: Decrement a counter. - fields: - entity_id: - description: Entity id of the counter to decrement. - example: "counter.count0" + description: Decrement a counter + target: + increment: - description: Increment a counter. - fields: - entity_id: - description: Entity id of the counter to increment. - example: "counter.count0" + description: Increment a counter + target: + reset: - description: Reset a counter. - fields: - entity_id: - description: Entity id of the counter to reset. - example: "counter.count0" + description: Reset a counter + target: + configure: description: Change counter parameters + target: fields: - entity_id: - description: Entity id of the counter to change. - example: "counter.count0" minimum: - description: New minimum value for the counter or None to remove minimum + name: Minimum + description: New minimum value for the counter or None to remove minimum. example: 0 + selector: + number: + min: -9223372036854775807 + max: 9223372036854775807 + mode: box maximum: - description: New maximum value for the counter or None to remove maximum + name: Maximum + description: New maximum value for the counter or None to remove maximum. example: 100 + selector: + number: + min: -9223372036854775807 + max: 9223372036854775807 + mode: box step: - description: New value for step + name: Step + description: New value for step. example: 2 + selector: + number: + min: 1 + max: 9223372036854775807 + mode: box initial: - description: New value for initial + name: Initial + description: New value for initial. example: 6 + selector: + number: + min: 0 + max: 9223372036854775807 + mode: box value: - description: New state value + name: Value + description: New state value. example: 3 + selector: + number: + min: 0 + max: 9223372036854775807 + mode: box From 62cfe24ed431365d837821788e8405d68fed1468 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 12:59:46 +0100 Subject: [PATCH 509/796] Add advanced service parameter flag (#46727) --- homeassistant/components/light/services.yaml | 37 ++++++++++++++++---- script/hassfest/services.py | 1 + 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index dda7396e11b..202899db7bf 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -22,7 +22,8 @@ turn_on: description: A human readable color name. example: "red" hs_color: - description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100. + description: + Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100. example: "[300, 70]" xy_color: description: Color for the light in XY-format. @@ -37,11 +38,25 @@ turn_on: description: Number between 0..255 indicating level of white. example: "250" brightness: - description: Number between 0..255 indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness supported by the light. + name: Brightness value + description: + Number between 0..255 indicating brightness, where 0 turns the light + off, 1 is the minimum brightness and 255 is the maximum brightness + supported by the light. + advanced: true example: 120 + selector: + number: + min: 0 + max: 255 + step: 1 + mode: slider brightness_pct: name: Brightness - description: Number between 0..100 indicating percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness and 100 is the maximum brightness supported by the light. + description: + Number between 0..100 indicating percentage of full brightness, where 0 + turns the light off, 1 is the minimum brightness and 100 is the maximum + brightness supported by the light. example: 47 selector: number: @@ -54,7 +69,8 @@ turn_on: description: Change brightness by an amount. Should be between -255..255. example: -25.5 brightness_step_pct: - description: Change brightness by a percentage. Should be between -100..100. + description: + Change brightness by a percentage. Should be between -100..100. example: -10 profile: description: Name of a light profile to use. @@ -104,7 +120,8 @@ toggle: description: A human readable color name. example: "red" hs_color: - description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100. + description: + Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100. example: "[300, 70]" xy_color: description: Color for the light in XY-format. @@ -119,10 +136,16 @@ toggle: description: Number between 0..255 indicating level of white. example: "250" brightness: - description: Number between 0..255 indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness supported by the light. + description: + Number between 0..255 indicating brightness, where 0 turns the light + off, 1 is the minimum brightness and 255 is the maximum brightness + supported by the light. example: 120 brightness_pct: - description: Number between 0..100 indicating percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness and 100 is the maximum brightness supported by the light. + description: + Number between 0..100 indicating percentage of full brightness, where 0 + turns the light off, 1 is the minimum brightness and 100 is the maximum + brightness supported by the light. example: 47 profile: description: Name of a light profile to use. diff --git a/script/hassfest/services.py b/script/hassfest/services.py index c0823b672e8..9037b0bb45e 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -29,6 +29,7 @@ FIELD_SCHEMA = vol.Schema( vol.Optional("default"): exists, vol.Optional("values"): exists, vol.Optional("required"): bool, + vol.Optional("advanced"): bool, vol.Optional(CONF_SELECTOR): selector.validate_selector, } ) From 88d143a644fc606cdea5ab512cf4396dd223bff2 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 18 Feb 2021 04:26:02 -0800 Subject: [PATCH 510/796] Add discontinuity support to HLS streams and fix nest expiring stream urls (#46683) * Support HLS stream discontinuity. * Clarify discontinuity comments * Signal a stream discontinuity on restart due to stream error * Apply suggestions from code review Co-authored-by: uvjustin <46082645+uvjustin@users.noreply.github.com> * Simplify stream discontinuity logic --- homeassistant/components/stream/__init__.py | 8 +- homeassistant/components/stream/core.py | 2 + homeassistant/components/stream/hls.py | 14 ++- homeassistant/components/stream/worker.py | 28 ++++-- tests/components/stream/test_hls.py | 101 +++++++++++++++++--- tests/components/stream/test_worker.py | 8 +- 6 files changed, 132 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 7f88885ac0b..6c3f0104ad0 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -170,7 +170,7 @@ class Stream: def update_source(self, new_source): """Restart the stream with a new stream source.""" - _LOGGER.debug("Updating stream source %s", self.source) + _LOGGER.debug("Updating stream source %s", new_source) self.source = new_source self._fast_restart_once = True self._thread_quit.set() @@ -179,12 +179,14 @@ class Stream: """Handle consuming streams and restart keepalive streams.""" # Keep import here so that we can import stream integration without installing reqs # pylint: disable=import-outside-toplevel - from .worker import stream_worker + from .worker import SegmentBuffer, stream_worker + segment_buffer = SegmentBuffer(self.outputs) wait_timeout = 0 while not self._thread_quit.wait(timeout=wait_timeout): start_time = time.time() - stream_worker(self.source, self.options, self.outputs, self._thread_quit) + stream_worker(self.source, self.options, segment_buffer, self._thread_quit) + segment_buffer.discontinuity() if not self.keepalive or self._thread_quit.is_set(): if self._fast_restart_once: # The stream source is updated, restart without any delay. diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index f7beb3aa754..7a46de547d7 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -30,6 +30,8 @@ class Segment: sequence: int = attr.ib() segment: io.BytesIO = attr.ib() duration: float = attr.ib() + # For detecting discontinuities across stream restarts + stream_id: int = attr.ib(default=0) class IdleTimer: diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index 57894d17711..85102d208e7 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -78,21 +78,27 @@ class HlsPlaylistView(StreamView): @staticmethod def render_playlist(track): """Render playlist.""" - segments = track.segments[-NUM_PLAYLIST_SEGMENTS:] + segments = list(track.get_segment())[-NUM_PLAYLIST_SEGMENTS:] if not segments: return [] - playlist = ["#EXT-X-MEDIA-SEQUENCE:{}".format(segments[0])] + playlist = [ + "#EXT-X-MEDIA-SEQUENCE:{}".format(segments[0].sequence), + "#EXT-X-DISCONTINUITY-SEQUENCE:{}".format(segments[0].stream_id), + ] - for sequence in segments: - segment = track.get_segment(sequence) + last_stream_id = segments[0].stream_id + for segment in segments: + if last_stream_id != segment.stream_id: + playlist.append("#EXT-X-DISCONTINUITY") playlist.extend( [ "#EXTINF:{:.04f},".format(float(segment.duration)), f"./segment/{segment.sequence}.m4s", ] ) + last_stream_id = segment.stream_id return playlist diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 41cb4bafd90..2592a74584e 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -49,16 +49,22 @@ def create_stream_buffer(stream_output, video_stream, audio_stream, sequence): class SegmentBuffer: """Buffer for writing a sequence of packets to the output as a segment.""" - def __init__(self, video_stream, audio_stream, outputs_callback) -> None: + def __init__(self, outputs_callback) -> None: """Initialize SegmentBuffer.""" - self._video_stream = video_stream - self._audio_stream = audio_stream + self._stream_id = 0 + self._video_stream = None + self._audio_stream = None self._outputs_callback = outputs_callback # tuple of StreamOutput, StreamBuffer self._outputs = [] self._sequence = 0 self._segment_start_pts = None + def set_streams(self, video_stream, audio_stream): + """Initialize output buffer with streams from container.""" + self._video_stream = video_stream + self._audio_stream = audio_stream + def reset(self, video_pts): """Initialize a new stream segment.""" # Keep track of the number of segments we've processed @@ -103,7 +109,16 @@ class SegmentBuffer: """Create a segment from the buffered packets and write to output.""" for (buffer, stream_output) in self._outputs: buffer.output.close() - stream_output.put(Segment(self._sequence, buffer.segment, duration)) + stream_output.put( + Segment(self._sequence, buffer.segment, duration, self._stream_id) + ) + + def discontinuity(self): + """Mark the stream as having been restarted.""" + # Preserving sequence and stream_id here keep the HLS playlist logic + # simple to check for discontinuity at output time, and to determine + # the discontinuity sequence number. + self._stream_id += 1 def close(self): """Close all StreamBuffers.""" @@ -111,7 +126,7 @@ class SegmentBuffer: buffer.output.close() -def stream_worker(source, options, outputs_callback, quit_event): +def stream_worker(source, options, segment_buffer, quit_event): """Handle consuming streams.""" try: @@ -143,8 +158,6 @@ def stream_worker(source, options, outputs_callback, quit_event): last_dts = {video_stream: float("-inf"), audio_stream: float("-inf")} # Keep track of consecutive packets without a dts to detect end of stream. missing_dts = 0 - # Holds the buffers for each stream provider - segment_buffer = SegmentBuffer(video_stream, audio_stream, outputs_callback) # The video pts at the beginning of the segment segment_start_pts = None # Because of problems 1 and 2 below, we need to store the first few packets and replay them @@ -225,6 +238,7 @@ def stream_worker(source, options, outputs_callback, quit_event): container.close() return + segment_buffer.set_streams(video_stream, audio_stream) segment_buffer.reset(segment_start_pts) while not quit_event.is_set(): diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 55b79684b7b..ffe32d13c61 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -51,7 +51,16 @@ def hls_stream(hass, hass_client): return create_client_for_stream -def playlist_response(sequence, segments): +def make_segment(segment, discontinuity=False): + """Create a playlist response for a segment.""" + response = [] + if discontinuity: + response.append("#EXT-X-DISCONTINUITY") + response.extend(["#EXTINF:10.0000,", f"./segment/{segment}.m4s"]), + return "\n".join(response) + + +def make_playlist(sequence, discontinuity_sequence=0, segments=[]): """Create a an hls playlist response for tests to assert on.""" response = [ "#EXTM3U", @@ -59,14 +68,9 @@ def playlist_response(sequence, segments): "#EXT-X-TARGETDURATION:10", '#EXT-X-MAP:URI="init.mp4"', f"#EXT-X-MEDIA-SEQUENCE:{sequence}", + f"#EXT-X-DISCONTINUITY-SEQUENCE:{discontinuity_sequence}", ] - for segment in segments: - response.extend( - [ - "#EXTINF:10.0000,", - f"./segment/{segment}.m4s", - ] - ) + response.extend(segments) response.append("") return "\n".join(response) @@ -289,13 +293,15 @@ async def test_hls_playlist_view(hass, hls_stream, stream_worker_sync): resp = await hls_client.get("/playlist.m3u8") assert resp.status == 200 - assert await resp.text() == playlist_response(sequence=1, segments=[1]) + assert await resp.text() == make_playlist(sequence=1, segments=[make_segment(1)]) hls.put(Segment(2, SEQUENCE_BYTES, DURATION)) await hass.async_block_till_done() resp = await hls_client.get("/playlist.m3u8") assert resp.status == 200 - assert await resp.text() == playlist_response(sequence=1, segments=[1, 2]) + assert await resp.text() == make_playlist( + sequence=1, segments=[make_segment(1), make_segment(2)] + ) stream_worker_sync.resume() stream.stop() @@ -321,8 +327,12 @@ async def test_hls_max_segments(hass, hls_stream, stream_worker_sync): # Only NUM_PLAYLIST_SEGMENTS are returned in the playlist. start = MAX_SEGMENTS + 2 - NUM_PLAYLIST_SEGMENTS - assert await resp.text() == playlist_response( - sequence=start, segments=range(start, MAX_SEGMENTS + 2) + segments = [] + for sequence in range(start, MAX_SEGMENTS + 2): + segments.append(make_segment(sequence)) + assert await resp.text() == make_playlist( + sequence=start, + segments=segments, ) # Fetch the actual segments with a fake byte payload @@ -340,3 +350,70 @@ async def test_hls_max_segments(hass, hls_stream, stream_worker_sync): stream_worker_sync.resume() stream.stop() + + +async def test_hls_playlist_view_discontinuity(hass, hls_stream, stream_worker_sync): + """Test a discontinuity across segments in the stream with 3 segments.""" + await async_setup_component(hass, "stream", {"stream": {}}) + + stream = create_stream(hass, STREAM_SOURCE) + stream_worker_sync.pause() + hls = stream.hls_output() + + hls.put(Segment(1, SEQUENCE_BYTES, DURATION, stream_id=0)) + hls.put(Segment(2, SEQUENCE_BYTES, DURATION, stream_id=0)) + hls.put(Segment(3, SEQUENCE_BYTES, DURATION, stream_id=1)) + await hass.async_block_till_done() + + hls_client = await hls_stream(stream) + + resp = await hls_client.get("/playlist.m3u8") + assert resp.status == 200 + assert await resp.text() == make_playlist( + sequence=1, + segments=[ + make_segment(1), + make_segment(2), + make_segment(3, discontinuity=True), + ], + ) + + stream_worker_sync.resume() + stream.stop() + + +async def test_hls_max_segments_discontinuity(hass, hls_stream, stream_worker_sync): + """Test a discontinuity with more segments than the segment deque can hold.""" + await async_setup_component(hass, "stream", {"stream": {}}) + + stream = create_stream(hass, STREAM_SOURCE) + stream_worker_sync.pause() + hls = stream.hls_output() + + hls_client = await hls_stream(stream) + + hls.put(Segment(1, SEQUENCE_BYTES, DURATION, stream_id=0)) + + # Produce enough segments to overfill the output buffer by one + for sequence in range(1, MAX_SEGMENTS + 2): + hls.put(Segment(sequence, SEQUENCE_BYTES, DURATION, stream_id=1)) + await hass.async_block_till_done() + + resp = await hls_client.get("/playlist.m3u8") + assert resp.status == 200 + + # Only NUM_PLAYLIST_SEGMENTS are returned in the playlist causing the + # EXT-X-DISCONTINUITY tag to be omitted and EXT-X-DISCONTINUITY-SEQUENCE + # returned instead. + start = MAX_SEGMENTS + 2 - NUM_PLAYLIST_SEGMENTS + segments = [] + for sequence in range(start, MAX_SEGMENTS + 2): + segments.append(make_segment(sequence)) + assert await resp.text() == make_playlist( + sequence=start, + discontinuity_sequence=1, + segments=segments, + ) + + stream_worker_sync.resume() + stream.stop() diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index d9006c81ad5..f7952b7db44 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -27,7 +27,7 @@ from homeassistant.components.stream.const import ( MIN_SEGMENT_DURATION, PACKETS_TO_WAIT_FOR_AUDIO, ) -from homeassistant.components.stream.worker import stream_worker +from homeassistant.components.stream.worker import SegmentBuffer, stream_worker STREAM_SOURCE = "some-stream-source" # Formats here are arbitrary, not exercised by tests @@ -197,7 +197,8 @@ async def async_decode_stream(hass, packets, py_av=None): "homeassistant.components.stream.core.StreamOutput.put", side_effect=py_av.capture_buffer.capture_output_segment, ): - stream_worker(STREAM_SOURCE, {}, stream.outputs, threading.Event()) + segment_buffer = SegmentBuffer(stream.outputs) + stream_worker(STREAM_SOURCE, {}, segment_buffer, threading.Event()) await hass.async_block_till_done() return py_av.capture_buffer @@ -209,7 +210,8 @@ async def test_stream_open_fails(hass): stream.hls_output() with patch("av.open") as av_open: av_open.side_effect = av.error.InvalidDataError(-2, "error") - stream_worker(STREAM_SOURCE, {}, stream.outputs, threading.Event()) + segment_buffer = SegmentBuffer(stream.outputs) + stream_worker(STREAM_SOURCE, {}, segment_buffer, threading.Event()) await hass.async_block_till_done() av_open.assert_called_once() From 616e8f6a65f88e60093c6c48bc27381d6adaa7f7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 13:56:15 +0100 Subject: [PATCH 511/796] Add selectors to Scene service definitions (#46729) --- homeassistant/components/scene/services.yaml | 52 +++++++++++++++----- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/scene/services.yaml b/homeassistant/components/scene/services.yaml index 29fa11e9367..dc9e9e07ab4 100644 --- a/homeassistant/components/scene/services.yaml +++ b/homeassistant/components/scene/services.yaml @@ -1,53 +1,79 @@ # Describes the format for available scene services turn_on: - description: Activate a scene. + description: Activate a scene + target: fields: transition: + name: Transition description: Transition duration in seconds it takes to bring devices to the state defined in the scene. example: 2.5 - entity_id: - description: Name(s) of scenes to turn on - example: "scene.romantic" + selector: + number: + min: 0 + max: 300 + step: 1 + unit_of_measurement: seconds + mode: slider reload: description: Reload the scene configuration apply: - description: - Activate a scene. Takes same data as the entities field from a single scene - in the config. + description: Activate a scene with configuration fields: - transition: - description: - Transition duration in seconds it takes to bring devices to the state - defined in the scene. - example: 2.5 entities: + name: Entities state description: The entities and the state that they need to be. + required: true example: light.kitchen: "on" light.ceiling: state: "on" brightness: 80 + selector: + object: + transition: + name: Transition + description: + Transition duration in seconds it takes to bring devices to the state + defined in the scene. + example: 2.5 + selector: + number: + min: 0 + max: 300 + step: 1 + unit_of_measurement: seconds + mode: slider create: - description: Creates a new scene. + description: Creates a new scene fields: scene_id: + name: Scene entity ID description: The entity_id of the new scene. + required: true example: all_lights + selector: + text: entities: + name: Entities state description: The entities to control with the scene. example: light.tv_back_light: "on" light.ceiling: state: "on" brightness: 200 + selector: + object: snapshot_entities: + name: Snapshot entities description: The entities of which a snapshot is to be taken example: - light.ceiling - light.kitchen + selector: + object: From 12477c5e4686d5d17d12fe65ca3013da9e084a61 Mon Sep 17 00:00:00 2001 From: Jesse Campbell Date: Thu, 18 Feb 2021 07:58:43 -0500 Subject: [PATCH 512/796] Fix missing color switch specific device class for Z-Wave JS driver >6.3 (#46718) --- homeassistant/components/zwave_js/discovery.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 2022258e6ea..7231d18e186 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -242,6 +242,7 @@ DISCOVERY_SCHEMAS = [ device_class_specific={ "Tunable Color Light", "Binary Tunable Color Light", + "Tunable Color Switch", "Multilevel Remote Switch", "Multilevel Power Switch", "Multilevel Scene Switch", From bfa171f80291dcfa9345cda5356258dfa4b0f88a Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 18 Feb 2021 15:01:54 +0100 Subject: [PATCH 513/796] Add selectors to Netatmo services (#46574) * Add selectors * Fix schedul selector * Update homeassistant/components/netatmo/services.yaml Co-authored-by: Bram Kragten * Update homeassistant/components/netatmo/services.yaml Co-authored-by: Bram Kragten * Update services.yaml * Added required field Co-authored-by: Bram Kragten --- .../components/netatmo/services.yaml | 52 +++++++++++++------ 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml index 459ef23b0e0..11f83830dff 100644 --- a/homeassistant/components/netatmo/services.yaml +++ b/homeassistant/components/netatmo/services.yaml @@ -1,43 +1,63 @@ # Describes the format for available Netatmo services set_camera_light: - description: Set the camera light mode. + description: Set the camera light mode + target: + entity: + integration: netatmo + domain: light fields: camera_light_mode: + name: Camera light mode description: Outdoor camera light mode (on/off/auto) example: auto - entity_id: - description: Entity id of the camera. - example: camera.netatmo_entrance + required: true + selector: + select: + options: + - "on" + - "off" + - "auto" set_schedule: - description: Set the heating schedule. + description: Set the heating schedule + target: + entity: + integration: netatmo + domain: climate fields: schedule_name: description: Schedule name example: Standard - entity_id: - description: Entity id of the climate device. - example: climate.netatmo_livingroom + required: true + selector: + text: set_persons_home: - description: Set a list of persons as at home. Person's name must match a name known by the Welcome Camera. + description: Set a list of persons as at home. Person's name must match a name known by the Welcome Camera + target: + entity: + integration: netatmo + domain: camera fields: persons: description: List of names example: Bob - entity_id: - description: Entity id of the camera. - example: camera.netatmo_entrance + required: true + selector: + text: set_person_away: - description: Set a person away. If no person is set the home will be marked as empty. Person's name must match a name known by the Welcome Camera. + description: Set a person away. If no person is set the home will be marked as empty. Person's name must match a name known by the Welcome Camera + target: + entity: + integration: netatmo + domain: camera fields: person: description: Person's name (optional) example: Bob - entity_id: - description: Entity id of the camera. - example: camera.netatmo_entrance + selector: + text: register_webhook: description: Register webhook From 74720d4afd1b7affda1f040fbcb91eec3e4fd5c4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 16:13:52 +0100 Subject: [PATCH 514/796] Add selectors to Vacuum service definitions (#46728) --- homeassistant/components/vacuum/services.yaml | 91 ++++++++----------- 1 file changed, 37 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/vacuum/services.yaml b/homeassistant/components/vacuum/services.yaml index 3287eafe7f2..a60ce9ee658 100644 --- a/homeassistant/components/vacuum/services.yaml +++ b/homeassistant/components/vacuum/services.yaml @@ -1,87 +1,70 @@ # Describes the format for available vacuum services turn_on: - description: Start a new cleaning task. - fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" + description: Start a new cleaning task + target: turn_off: - description: Stop the current cleaning task and return to home. - fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" + description: Stop the current cleaning task and return to home + target: stop: - description: Stop the current cleaning task. - fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" + description: Stop the current cleaning task + target: locate: - description: Locate the vacuum cleaner robot. - fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" + description: Locate the vacuum cleaner robot + target: start_pause: - description: Start, pause, or resume the cleaning task. - fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" + description: Start, pause, or resume the cleaning task + target: start: - description: Start or resume the cleaning task. - fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" + description: Start or resume the cleaning task + target: pause: - description: Pause the cleaning task. - fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" + description: Pause the cleaning task + target: return_to_base: - description: Tell the vacuum cleaner to return to its dock. - fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" + description: Tell the vacuum cleaner to return to its dock + target: clean_spot: - description: Tell the vacuum cleaner to do a spot clean-up. - fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" + description: Tell the vacuum cleaner to do a spot clean-up + target: send_command: - description: Send a raw command to the vacuum cleaner. + description: Send a raw command to the vacuum cleaner + target: fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" command: + name: Command description: Command to execute. + required: true example: "set_dnd_timer" + selector: + text: + params: + name: Parameters description: Parameters for the command. example: '{ "key": "value" }' + selector: + object: set_fan_speed: - description: Set the fan speed of the vacuum cleaner. + description: Set the fan speed of the vacuum cleaner + target: fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" fan_speed: - description: Platform dependent vacuum cleaner fan speed, with speed steps, like 'medium' or by percentage, between 0 and 100. + name: Fan speed + description: + Platform dependent vacuum cleaner fan speed, with speed steps, like + 'medium' or by percentage, between 0 and 100. + required: true example: "low" + selector: + text: From 1d62bf88750ca1d2513d894c70f876d99f376ac0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 16:14:16 +0100 Subject: [PATCH 515/796] Add selectors to Script service definitions (#46730) --- homeassistant/components/script/services.yaml | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/script/services.yaml b/homeassistant/components/script/services.yaml index 1347f760b54..5af81734a9e 100644 --- a/homeassistant/components/script/services.yaml +++ b/homeassistant/components/script/services.yaml @@ -5,21 +5,12 @@ reload: turn_on: description: Turn on script - fields: - entity_id: - description: Name(s) of script to be turned on. - example: "script.arrive_home" + target: turn_off: description: Turn off script - fields: - entity_id: - description: Name(s) of script to be turned off. - example: "script.arrive_home" + target: toggle: description: Toggle script - fields: - entity_id: - description: Name(s) of script to be toggled. - example: "script.arrive_home" + target: From c0cdc0fe795640904f4e704c95e48bf71a52eb7c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 16:15:16 +0100 Subject: [PATCH 516/796] Add advanced selectors to Light service definitions (#46732) --- homeassistant/components/light/services.yaml | 483 ++++++++++++++++++- 1 file changed, 472 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 202899db7bf..f777cd3d348 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -1,12 +1,12 @@ # Describes the format for available light services turn_on: - description: Turn a light on. + description: Turn a light on target: fields: transition: name: Transition - description: Duration in seconds it takes to get to next state + description: Duration in seconds it takes to get to next state. example: 60 selector: number: @@ -16,27 +16,218 @@ turn_on: unit_of_measurement: seconds mode: slider rgb_color: + name: RGB-color description: Color for the light in RGB-format. + advanced: true example: "[255, 100, 100]" + selector: + object: color_name: + name: Color name description: A human readable color name. + advanced: true example: "red" + selector: + select: + options: + - "aliceblue" + - "antiquewhite" + - "aqua" + - "aquamarine" + - "azure" + - "beige" + - "bisque" + - "black" + - "blanchedalmond" + - "blue" + - "blueviolet" + - "brown" + - "burlywood" + - "cadetblue" + - "chartreuse" + - "chocolate" + - "coral" + - "cornflowerblue" + - "cornsilk" + - "crimson" + - "cyan" + - "darkblue" + - "darkcyan" + - "darkgoldenrod" + - "darkgray" + - "darkgreen" + - "darkgrey" + - "darkkhaki" + - "darkmagenta" + - "darkolivegreen" + - "darkorange" + - "darkorchid" + - "darkred" + - "darksalmon" + - "darkseagreen" + - "darkslateblue" + - "darkslategray" + - "darkslategrey" + - "darkturquoise" + - "darkviolet" + - "deeppink" + - "deepskyblue" + - "dimgray" + - "dimgrey" + - "dodgerblue" + - "firebrick" + - "floralwhite" + - "forestgreen" + - "fuchsia" + - "gainsboro" + - "ghostwhite" + - "gold" + - "goldenrod" + - "gray" + - "green" + - "greenyellow" + - "grey" + - "honeydew" + - "hotpink" + - "indianred" + - "indigo" + - "ivory" + - "khaki" + - "lavender" + - "lavenderblush" + - "lawngreen" + - "lemonchiffon" + - "lightblue" + - "lightcoral" + - "lightcyan" + - "lightgoldenrodyellow" + - "lightgray" + - "lightgreen" + - "lightgrey" + - "lightpink" + - "lightsalmon" + - "lightseagreen" + - "lightskyblue" + - "lightslategray" + - "lightslategrey" + - "lightsteelblue" + - "lightyellow" + - "lime" + - "limegreen" + - "linen" + - "magenta" + - "maroon" + - "mediumaquamarine" + - "mediumblue" + - "mediumorchid" + - "mediumpurple" + - "mediumseagreen" + - "mediumslateblue" + - "mediumspringgreen" + - "mediumturquoise" + - "mediumvioletred" + - "midnightblue" + - "mintcream" + - "mistyrose" + - "moccasin" + - "navajowhite" + - "navy" + - "navyblue" + - "oldlace" + - "olive" + - "olivedrab" + - "orange" + - "orangered" + - "orchid" + - "palegoldenrod" + - "palegreen" + - "paleturquoise" + - "palevioletred" + - "papayawhip" + - "peachpuff" + - "peru" + - "pink" + - "plum" + - "powderblue" + - "purple" + - "red" + - "rosybrown" + - "royalblue" + - "saddlebrown" + - "salmon" + - "sandybrown" + - "seagreen" + - "seashell" + - "sienna" + - "silver" + - "skyblue" + - "slateblue" + - "slategray" + - "slategrey" + - "snow" + - "springgreen" + - "steelblue" + - "tan" + - "teal" + - "thistle" + - "tomato" + - "turquoise" + - "violet" + - "wheat" + - "white" + - "whitesmoke" + - "yellow" + - "yellowgreen" hs_color: + name: Hue/Sat color description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100. + advanced: true example: "[300, 70]" + selector: + object: xy_color: + name: XY-color description: Color for the light in XY-format. + advanced: true example: "[0.52, 0.43]" + selector: + object: color_temp: + name: Color temperature (mireds) description: Color temperature for the light in mireds. + advanced: true example: 250 + selector: + number: + min: 153 + max: 500 + step: 1 + unit_of_measurement: mireds + mode: slider kelvin: + name: Color temperature (Kelvin) description: Color temperature for the light in Kelvin. + advanced: true example: 4000 + selector: + number: + min: 2000 + max: 6500 + step: 100 + unit_of_measurement: K + mode: slider white_value: + name: White level description: Number between 0..255 indicating level of white. + advanced: true example: "250" + selector: + number: + min: 0 + max: 255 + step: 1 + mode: slider brightness: name: Brightness value description: @@ -66,99 +257,369 @@ turn_on: unit_of_measurement: "%" mode: slider brightness_step: + name: Brightness step value description: Change brightness by an amount. Should be between -255..255. + advanced: true example: -25.5 + selector: + number: + min: -225 + max: 255 + step: 1 + mode: slider brightness_step_pct: + name: Brightness step description: Change brightness by a percentage. Should be between -100..100. example: -10 + selector: + number: + min: -100 + max: 100 + step: 1 + unit_of_measurement: "%" + mode: slider profile: + name: Profile description: Name of a light profile to use. + advanced: true example: relax + selector: + text: flash: + name: Flash description: If the light should flash. Valid values are short and long. + advanced: true example: short values: - short - long + selector: + select: + options: + - long + - short effect: + name: Effect description: Light effect. example: random values: - colorloop - random + selector: + text: turn_off: - description: Turn a light off. + description: Turn a light off + target: fields: - entity_id: - description: Name(s) of entities to turn off. - example: "light.kitchen" transition: + name: Transition description: Duration in seconds it takes to get to next state. example: 60 + selector: + number: + min: 0 + max: 300 + step: 1 + unit_of_measurement: seconds + mode: slider flash: + name: Flash description: If the light should flash. Valid values are short and long. + advanced: true example: short values: - short - long + selector: + select: + options: + - long + - short toggle: - description: Toggles a light. + description: Toggles a light + target: fields: - entity_id: - description: Name(s) of entities to turn on - example: "light.kitchen" transition: - description: Duration in seconds it takes to get to next state + name: Transition + description: Duration in seconds it takes to get to next state. example: 60 + selector: + number: + min: 0 + max: 300 + step: 1 + unit_of_measurement: seconds + mode: slider rgb_color: + name: RGB-color description: Color for the light in RGB-format. + advanced: true example: "[255, 100, 100]" + selector: + object: color_name: + name: Color name description: A human readable color name. + advanced: true example: "red" + selector: + select: + options: + - "aliceblue" + - "antiquewhite" + - "aqua" + - "aquamarine" + - "azure" + - "beige" + - "bisque" + - "black" + - "blanchedalmond" + - "blue" + - "blueviolet" + - "brown" + - "burlywood" + - "cadetblue" + - "chartreuse" + - "chocolate" + - "coral" + - "cornflowerblue" + - "cornsilk" + - "crimson" + - "cyan" + - "darkblue" + - "darkcyan" + - "darkgoldenrod" + - "darkgray" + - "darkgreen" + - "darkgrey" + - "darkkhaki" + - "darkmagenta" + - "darkolivegreen" + - "darkorange" + - "darkorchid" + - "darkred" + - "darksalmon" + - "darkseagreen" + - "darkslateblue" + - "darkslategray" + - "darkslategrey" + - "darkturquoise" + - "darkviolet" + - "deeppink" + - "deepskyblue" + - "dimgray" + - "dimgrey" + - "dodgerblue" + - "firebrick" + - "floralwhite" + - "forestgreen" + - "fuchsia" + - "gainsboro" + - "ghostwhite" + - "gold" + - "goldenrod" + - "gray" + - "green" + - "greenyellow" + - "grey" + - "honeydew" + - "hotpink" + - "indianred" + - "indigo" + - "ivory" + - "khaki" + - "lavender" + - "lavenderblush" + - "lawngreen" + - "lemonchiffon" + - "lightblue" + - "lightcoral" + - "lightcyan" + - "lightgoldenrodyellow" + - "lightgray" + - "lightgreen" + - "lightgrey" + - "lightpink" + - "lightsalmon" + - "lightseagreen" + - "lightskyblue" + - "lightslategray" + - "lightslategrey" + - "lightsteelblue" + - "lightyellow" + - "lime" + - "limegreen" + - "linen" + - "magenta" + - "maroon" + - "mediumaquamarine" + - "mediumblue" + - "mediumorchid" + - "mediumpurple" + - "mediumseagreen" + - "mediumslateblue" + - "mediumspringgreen" + - "mediumturquoise" + - "mediumvioletred" + - "midnightblue" + - "mintcream" + - "mistyrose" + - "moccasin" + - "navajowhite" + - "navy" + - "navyblue" + - "oldlace" + - "olive" + - "olivedrab" + - "orange" + - "orangered" + - "orchid" + - "palegoldenrod" + - "palegreen" + - "paleturquoise" + - "palevioletred" + - "papayawhip" + - "peachpuff" + - "peru" + - "pink" + - "plum" + - "powderblue" + - "purple" + - "red" + - "rosybrown" + - "royalblue" + - "saddlebrown" + - "salmon" + - "sandybrown" + - "seagreen" + - "seashell" + - "sienna" + - "silver" + - "skyblue" + - "slateblue" + - "slategray" + - "slategrey" + - "snow" + - "springgreen" + - "steelblue" + - "tan" + - "teal" + - "thistle" + - "tomato" + - "turquoise" + - "violet" + - "wheat" + - "white" + - "whitesmoke" + - "yellow" + - "yellowgreen" hs_color: + name: Hue/Sat color description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100. + advanced: true example: "[300, 70]" + selector: + object: xy_color: + name: XY-color description: Color for the light in XY-format. + advanced: true example: "[0.52, 0.43]" + selector: + object: color_temp: + name: Color temperature (mireds) description: Color temperature for the light in mireds. + advanced: true example: 250 + selector: + number: + min: 153 + max: 500 + step: 1 + unit_of_measurement: mireds + mode: slider kelvin: + name: Color temperature (Kelvin) description: Color temperature for the light in Kelvin. + advanced: true example: 4000 + selector: + number: + min: 2000 + max: 6500 + step: 100 + unit_of_measurement: K + mode: slider white_value: + name: White level description: Number between 0..255 indicating level of white. + advanced: true example: "250" + selector: + number: + min: 0 + max: 255 + step: 1 + mode: slider brightness: + name: Brightness value description: Number between 0..255 indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness supported by the light. + advanced: true example: 120 + selector: + number: + min: 0 + max: 255 + step: 1 + mode: slider brightness_pct: + name: Brightness description: Number between 0..100 indicating percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness and 100 is the maximum brightness supported by the light. example: 47 + selector: + number: + min: 0 + max: 100 + step: 1 + unit_of_measurement: "%" + mode: slider profile: + name: Profile description: Name of a light profile to use. + advanced: true example: relax + selector: + text: flash: + name: Flash description: If the light should flash. Valid values are short and long. + advanced: true example: short values: - short - long + selector: + select: + options: + - long + - short effect: + name: Effect description: Light effect. example: random values: - colorloop - random + selector: + text: From b4136c35853bdcc786d965bbe8a9e3ff7816deda Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 16:16:13 +0100 Subject: [PATCH 517/796] Add selectors to WLED service definitions (#46731) --- homeassistant/components/wled/services.yaml | 42 +++++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/wled/services.yaml b/homeassistant/components/wled/services.yaml index 1f1fa1b809d..827e8b5fb36 100644 --- a/homeassistant/components/wled/services.yaml +++ b/homeassistant/components/wled/services.yaml @@ -1,30 +1,58 @@ effect: description: Controls the effect settings of WLED + target: fields: - entity_id: - description: Name of the WLED light entity. - example: "light.wled" effect: + name: Effect description: Name or ID of the WLED light effect. example: "Rainbow" + selector: + text: intensity: + name: Effect intensity description: Intensity of the effect. Number between 0 and 255. example: 100 + selector: + number: + min: 0 + max: 255 + step: 1 + mode: slider palette: + name: Color palette description: Name or ID of the WLED light palette. example: "Tiamat" + selector: + text: speed: + name: Effect speed description: Speed of the effect. Number between 0 (slow) and 255 (fast). example: 150 + selector: + number: + min: 0 + max: 255 + step: 1 + mode: slider reverse: - description: Reverse the effect. Either true to reverse or false otherwise. + name: Reverse effect + description: + Reverse the effect. Either true to reverse or false otherwise. + default: false example: false + selector: + boolean: + preset: description: Calls a preset on the WLED device + target: fields: - entity_id: - description: Name of the WLED light entity. - example: "light.wled" preset: + name: Preset ID description: ID of the WLED preset example: 6 + selector: + number: + min: -1 + max: 65535 + mode: box From 0f4433ce1414a071d7a7f548d72726355fe55442 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 16:23:10 +0100 Subject: [PATCH 518/796] Add advanced selectors to Climate service definitions (#46736) --- homeassistant/components/climate/services.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index ea90cbf30f6..d333260202f 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -41,11 +41,25 @@ set_temperature: target_temp_high: name: Target temperature high description: New target high temperature for HVAC. + advanced: true example: 26 + selector: + number: + min: 0 + max: 250 + step: 0.1 + mode: box target_temp_low: name: Target temperature low description: New target low temperature for HVAC. + advanced: true example: 20 + selector: + number: + min: 0 + max: 250 + step: 0.1 + mode: box hvac_mode: name: HVAC mode description: HVAC operation mode to set temperature to. From 62e0949ea9c26501e72783dfd6c2b1c4abf03365 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 16:24:04 +0100 Subject: [PATCH 519/796] Add selectors to Z-Wave JS service definitions (#46737) --- .../components/zwave_js/services.yaml | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml index cc81da7ed58..edb1f8b1ba4 100644 --- a/homeassistant/components/zwave_js/services.yaml +++ b/homeassistant/components/zwave_js/services.yaml @@ -1,24 +1,31 @@ # Describes the format for available Z-Wave services clear_lock_usercode: - description: Clear a usercode from lock. + description: Clear a usercode from lock + target: fields: - entity_id: - description: Lock entity_id. - example: lock.front_door_locked code_slot: - description: Code slot to clear code from. + name: Code slot + description: Code slot to clear code from + required: true example: 1 + selector: + text: set_lock_usercode: - description: Set a usercode to lock. + description: Set a usercode to lock + target: fields: - entity_id: - description: Lock entity_id. - example: lock.front_door_locked code_slot: + name: Code slot description: Code slot to set the code. example: 1 + selector: + text: usercode: + name: Code description: Code to set. + required: true example: 1234 + selector: + text: From 3f96ebeae53a935ba0d215e5d937e4d400be2049 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 16:33:29 +0100 Subject: [PATCH 520/796] Add selectors to Logger, System Log & Logbook service definitions (#46740) --- .../components/logbook/services.yaml | 16 ++++++++- homeassistant/components/logger/services.yaml | 35 +++++++++++++++---- .../components/system_log/services.yaml | 29 ++++++++++++--- 3 files changed, 67 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/logbook/services.yaml b/homeassistant/components/logbook/services.yaml index fb1736d7784..252b0b6a39b 100644 --- a/homeassistant/components/logbook/services.yaml +++ b/homeassistant/components/logbook/services.yaml @@ -1,15 +1,29 @@ log: - description: Create a custom entry in your logbook. + description: Create a custom entry in your logbook fields: name: + name: Name description: Custom name for an entity, can be referenced with entity_id + required: true example: "Kitchen" + selector: + text: message: + name: Message description: Message of the custom logbook entry + required: true example: "is being used" + selector: + text: entity_id: + name: Entity ID description: Entity to reference in custom logbook entry [Optional] example: "light.kitchen" + selector: + entity: domain: + name: Domain description: Icon of domain to display in custom logbook entry [Optional] example: "light" + selector: + text: diff --git a/homeassistant/components/logger/services.yaml b/homeassistant/components/logger/services.yaml index 514aac4c71c..4bd46b4b01e 100644 --- a/homeassistant/components/logger/services.yaml +++ b/homeassistant/components/logger/services.yaml @@ -1,22 +1,43 @@ set_default_level: - description: Set the default log level for components. + description: Set the default log level for components fields: level: - description: "Default severity level. Possible values are debug, info, warn, warning, error, fatal, critical" + name: Level + description: + "Default severity level. Possible values are debug, info, warn, warning, + error, fatal, critical" example: debug + selector: + select: + options: + - debug + - info + - warning + - error + - fatal + - critical set_level: - description: Set log level for components. + description: Set log level for components fields: homeassistant.core: - description: "Example on how to change the logging level for a Home Assistant core components. Possible values are debug, info, warn, warning, error, fatal, critical" + description: + "Example on how to change the logging level for a Home Assistant core + components. Possible values are debug, info, warn, warning, error, + fatal, critical" example: debug homeassistant.components.mqtt: - description: "Example on how to change the logging level for an Integration. Possible values are debug, info, warn, warning, error, fatal, critical" + description: + "Example on how to change the logging level for an Integration. Possible + values are debug, info, warn, warning, error, fatal, critical" example: warning custom_components.my_integration: - description: "Example on how to change the logging level for a Custom Integration. Possible values are debug, info, warn, warning, error, fatal, critical" + description: + "Example on how to change the logging level for a Custom Integration. + Possible values are debug, info, warn, warning, error, fatal, critical" example: debug aiohttp: - description: "Example on how to change the logging level for a Python module. Possible values are debug, info, warn, warning, error, fatal, critical" + description: + "Example on how to change the logging level for a Python module. + Possible values are debug, info, warn, warning, error, fatal, critical" example: error diff --git a/homeassistant/components/system_log/services.yaml b/homeassistant/components/system_log/services.yaml index 2545d47c825..e07aea9c2a1 100644 --- a/homeassistant/components/system_log/services.yaml +++ b/homeassistant/components/system_log/services.yaml @@ -1,15 +1,34 @@ clear: - description: Clear all log entries. + description: Clear all log entries write: - description: Write log entry. + description: Write log entry fields: message: - description: Message to log. [Required] + name: Message + description: Message to log. + required: true example: Something went wrong + selector: + text: level: - description: "Log level: debug, info, warning, error, critical. Defaults to 'error'." + name: Level + description: "Log level: debug, info, warning, error, critical." + default: error example: debug + selector: + select: + options: + - "debug" + - "info" + - "warning" + - "error" + - "critical" logger: - description: Logger name under which to log the message. Defaults to 'system_log.external'. + name: Logger + description: + Logger name under which to log the message. Defaults to + 'system_log.external'. example: mycomponent.myplatform + selector: + text: From cd6a83f00901eba78fa59cca66f96f7db5c9df1d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 16:37:15 +0100 Subject: [PATCH 521/796] Add selectors to MQTT service definitions (#46738) --- homeassistant/components/mqtt/services.yaml | 46 ++++++++++++++++++--- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/services.yaml b/homeassistant/components/mqtt/services.yaml index 992dd1b3545..c6c3014362f 100644 --- a/homeassistant/components/mqtt/services.yaml +++ b/homeassistant/components/mqtt/services.yaml @@ -1,40 +1,76 @@ # Describes the format for available MQTT services publish: - description: Publish a message to an MQTT topic. + description: Publish a message to an MQTT topic fields: topic: + name: Topic description: Topic to publish payload. + required: true example: /homeassistant/hello + selector: + text: payload: + name: Payload description: Payload to publish. example: This is great + selector: + text: payload_template: - description: Template to render as payload value. Ignored if payload given. + name: Payload Template + description: + Template to render as payload value. Ignored if payload given. + advanced: true example: "{{ states('sensor.temperature') }}" + selector: + object: qos: + name: QoS description: Quality of Service to use. + advanced: true example: 2 values: - 0 - 1 - 2 default: 0 + selector: + select: + options: + - "0" + - "1" + - "2" retain: + name: Retain description: If message should have the retain flag set. - example: true default: false + example: true + selector: + boolean: dump: - description: Dump messages on a topic selector to the 'mqtt_dump.txt' file in your config folder. + description: + Dump messages on a topic selector to the 'mqtt_dump.txt' file in your config + folder fields: topic: + name: Topic description: topic to listen to example: "OpenZWave/#" + selector: + text: duration: + name: Duration description: how long we should listen for messages in seconds example: 5 default: 5 + selector: + number: + min: 1 + max: 300 + step: 1 + unit_of_measurement: "seconds" + mode: slider reload: - description: Reload all MQTT entities from YAML. + description: Reload all MQTT entities from YAML From e57bfe05d5eb353b05dcdd5012b36e5c5e3d78bb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 16:43:34 +0100 Subject: [PATCH 522/796] Add selectors to Color Extractor service definitions (#46742) --- .../components/color_extractor/services.yaml | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/color_extractor/services.yaml b/homeassistant/components/color_extractor/services.yaml index 33055fd41b9..671a2a2ebb9 100644 --- a/homeassistant/components/color_extractor/services.yaml +++ b/homeassistant/components/color_extractor/services.yaml @@ -1,12 +1,22 @@ turn_on: - description: Set the light RGB to the predominant color found in the image provided by URL or file path. + description: + Set the light RGB to the predominant color found in the image provided by + URL or file path + target: fields: color_extract_url: - description: The URL of the image we want to extract RGB values from. Must be allowed in allowlist_external_urls. + name: URL + description: + The URL of the image we want to extract RGB values from. Must be allowed + in allowlist_external_urls. example: https://www.example.com/images/logo.png + selector: + text: color_extract_path: - description: The full system path to the image we want to extract RGB values from. Must be allowed in allowlist_external_dirs. + name: Path + description: + The full system path to the image we want to extract RGB values from. + Must be allowed in allowlist_external_dirs. example: /opt/images/logo.png - entity_id: - description: The entity we want to set our RGB color on. - example: "light.living_room_shelves" + selector: + text: From 4b6b03e33e6fc6314a4d437f86ac747ac898d0e0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 16:51:13 +0100 Subject: [PATCH 523/796] Add selectors to Lock service definitions (#46743) --- homeassistant/components/lock/services.yaml | 29 ++++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/lock/services.yaml b/homeassistant/components/lock/services.yaml index d1456f1e68e..22af0ab97cf 100644 --- a/homeassistant/components/lock/services.yaml +++ b/homeassistant/components/lock/services.yaml @@ -21,27 +21,29 @@ get_usercode: example: 1 lock: - description: Lock all or specified locks. + description: Lock all or specified locks + target: fields: - entity_id: - description: Name of lock to lock. - example: "lock.front_door" code: + name: Code description: An optional code to lock the lock with. example: 1234 + selector: + text: open: - description: Open all or specified locks. + description: Open all or specified locks + target: fields: - entity_id: - description: Name of lock to open. - example: "lock.front_door" code: + name: Code description: An optional code to open the lock with. example: 1234 + selector: + text: set_usercode: - description: Set a usercode to lock. + description: Set a usercode to lock fields: node_id: description: Node id of the lock. @@ -54,11 +56,12 @@ set_usercode: example: 1234 unlock: - description: Unlock all or specified locks. + description: Unlock all or specified locks + target: fields: - entity_id: - description: Name of lock to unlock. - example: "lock.front_door" code: + name: Code description: An optional code to unlock the lock with. example: 1234 + selector: + text: From a5ac338c7482285fdae57a33f0e51b54790c11e8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 16:54:20 +0100 Subject: [PATCH 524/796] Add selectors to Timer service definitions (#46744) --- homeassistant/components/timer/services.yaml | 34 +++++++------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/timer/services.yaml b/homeassistant/components/timer/services.yaml index cd810c21de5..fcde11cd47f 100644 --- a/homeassistant/components/timer/services.yaml +++ b/homeassistant/components/timer/services.yaml @@ -1,36 +1,24 @@ # Describes the format for available timer services start: - description: Start a timer. - + description: Start a timer + target: fields: - entity_id: - description: Entity id of the timer to start. [optional] - example: "timer.timer0" duration: description: Duration the timer requires to finish. [optional] + default: 0 example: "00:01:00 or 60" + selector: + text: pause: - description: Pause a timer. - - fields: - entity_id: - description: Entity id of the timer to pause. [optional] - example: "timer.timer0" + description: Pause a timer + target: cancel: - description: Cancel a timer. - - fields: - entity_id: - description: Entity id of the timer to cancel. [optional] - example: "timer.timer0" + description: Cancel a timer + target: finish: - description: Finish a timer. - - fields: - entity_id: - description: Entity id of the timer to finish. [optional] - example: "timer.timer0" + description: Finish a timer + target: From 3c7db7bd5b1db889f7e0473c6e1ee726dacf8817 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 19 Feb 2021 00:00:26 +0800 Subject: [PATCH 525/796] Skip repeated segment in stream recorder (#46701) * Skip repeated segment in stream recorder * Allow for multiple overlap --- homeassistant/components/stream/recorder.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index 0fc3d84b1b9..96531233771 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -42,7 +42,14 @@ def recorder_save_worker(file_out: str, segments: List[Segment], container_forma ) source.close() + last_sequence = float("-inf") for segment in segments: + # Because the stream_worker is in a different thread from the record service, + # the lookback segments may still have some overlap with the recorder segments + if segment.sequence <= last_sequence: + continue + last_sequence = segment.sequence + # Open segment source = av.open(segment.segment, "r", format=container_format) source_v = source.streams.video[0] From cdc4f634d18ac750b9185d24451c21427a3a1eea Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Thu, 18 Feb 2021 17:01:22 +0100 Subject: [PATCH 526/796] Fix typo in Tesla Powerwall strings (#46752) --- homeassistant/components/powerwall/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/powerwall/strings.json b/homeassistant/components/powerwall/strings.json index c576d931756..5deacd6a8f9 100644 --- a/homeassistant/components/powerwall/strings.json +++ b/homeassistant/components/powerwall/strings.json @@ -4,7 +4,7 @@ "step": { "user": { "title": "Connect to the powerwall", - "description": "The password is usually the last 5 characters of the serial number for Backup Gateway and can be found in the Telsa app; or the last 5 characters of the password found inside the door for Backup Gateway 2.", + "description": "The password is usually the last 5 characters of the serial number for Backup Gateway and can be found in the Tesla app or the last 5 characters of the password found inside the door for Backup Gateway 2.", "data": { "ip_address": "[%key:common::config_flow::data::ip%]", "password": "[%key:common::config_flow::data::password%]" From a164a6cf8020ba865f53988a32869c04a745f975 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 17:01:29 +0100 Subject: [PATCH 527/796] Add selectors to Hue service definitions (#46747) --- homeassistant/components/hue/services.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hue/services.yaml b/homeassistant/components/hue/services.yaml index 68eaf6ac377..80ca25007bc 100644 --- a/homeassistant/components/hue/services.yaml +++ b/homeassistant/components/hue/services.yaml @@ -1,11 +1,17 @@ # Describes the format for available hue services hue_activate_scene: - description: Activate a hue scene stored in the hue hub. + description: Activate a hue scene stored in the hue hub fields: group_name: + name: Group description: Name of hue group/room from the hue app. example: "Living Room" + selector: + text: scene_name: + name: Scene description: Name of hue scene from the hue app. example: "Energize" + selector: + text: From 81c7b3b9c9949b87cc32808960e15ceee6389c45 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 17:02:18 +0100 Subject: [PATCH 528/796] Add selectors to Media Player service definitions (#46739) --- .../components/media_player/services.yaml | 184 +++++++++--------- 1 file changed, 91 insertions(+), 93 deletions(-) diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 08637df0745..f85e0658426 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -1,168 +1,166 @@ # Describes the format for available media player services turn_on: - description: Turn a media player power on. - fields: - entity_id: - description: Name(s) of entities to turn on. - example: "media_player.living_room_chromecast" + description: Turn a media player power on + target: turn_off: - description: Turn a media player power off. - fields: - entity_id: - description: Name(s) of entities to turn off. - example: "media_player.living_room_chromecast" + description: Turn a media player power off + target: toggle: - description: Toggles a media player power state. - fields: - entity_id: - description: Name(s) of entities to toggle. - example: "media_player.living_room_chromecast" + description: Toggles a media player power state + target: volume_up: - description: Turn a media player volume up. - fields: - entity_id: - description: Name(s) of entities to turn volume up on. - example: "media_player.living_room_sonos" + description: Turn a media player volume up + target: volume_down: - description: Turn a media player volume down. - fields: - entity_id: - description: Name(s) of entities to turn volume down on. - example: "media_player.living_room_sonos" + description: Turn a media player volume down + target: volume_mute: - description: Mute a media player's volume. + description: Mute a media player's volume + target: fields: - entity_id: - description: Name(s) of entities to mute. - example: "media_player.living_room_sonos" is_volume_muted: + name: Muted description: True/false for mute/unmute. + required: true example: true + selector: + boolean: volume_set: - description: Set a media player's volume level. + description: Set a media player's volume level + target: fields: - entity_id: - description: Name(s) of entities to set volume level on. - example: "media_player.living_room_sonos" volume_level: + name: Level description: Volume level to set as float. + required: true example: 0.6 + selector: + number: + min: 0 + max: 1 + step: 0.01 + mode: slider media_play_pause: - description: Toggle media player play/pause state. - fields: - entity_id: - description: Name(s) of entities to toggle play/pause state on. - example: "media_player.living_room_sonos" + description: Toggle media player play/pause state + target: media_play: - description: Send the media player the command for play. - fields: - entity_id: - description: Name(s) of entities to play on. - example: "media_player.living_room_sonos" + description: Send the media player the command for play + target: media_pause: - description: Send the media player the command for pause. - fields: - entity_id: - description: Name(s) of entities to pause on. - example: "media_player.living_room_sonos" + description: Send the media player the command for pause + target: media_stop: - description: Send the media player the stop command. - fields: - entity_id: - description: Name(s) of entities to stop on. - example: "media_player.living_room_sonos" + description: Send the media player the stop command + target: media_next_track: - description: Send the media player the command for next track. - fields: - entity_id: - description: Name(s) of entities to send next track command to. - example: "media_player.living_room_sonos" + description: Send the media player the command for next track + target: media_previous_track: - description: Send the media player the command for previous track. - fields: - entity_id: - description: Name(s) of entities to send previous track command to. - example: "media_player.living_room_sonos" + description: Send the media player the command for previous track + target: media_seek: - description: Send the media player the command to seek in current playing media. + description: + Send the media player the command to seek in current playing media fields: - entity_id: - description: Name(s) of entities to seek media on. - example: "media_player.living_room_chromecast" seek_position: + name: Position description: Position to seek to. The format is platform dependent. + required: true example: 100 + selector: + number: + min: 0 + max: 9223372036854775807 + step: 0.01 + mode: box play_media: - description: Send the media player the command for playing media. + description: Send the media player the command for playing media + target: fields: - entity_id: - description: Name(s) of entities to seek media on - example: "media_player.living_room_chromecast" media_content_id: + name: Content ID description: The ID of the content to play. Platform dependent. + required: true example: "https://home-assistant.io/images/cast/splash.png" + selector: + text: + media_content_type: - description: The type of the content to play. Must be one of image, music, tvshow, video, episode, channel or playlist + name: Content type + description: + The type of the content to play. Must be one of image, music, tvshow, + video, episode, channel or playlist. + required: true example: "music" + selector: + text: select_source: - description: Send the media player the command to change input source. + description: Send the media player the command to change input source + target: fields: - entity_id: - description: Name(s) of entities to change source on. - example: "media_player.txnr535_0009b0d81f82" source: + name: Source description: Name of the source to switch to. Platform dependent. + required: true example: "video1" + selector: + text: select_sound_mode: - description: Send the media player the command to change sound mode. + description: Send the media player the command to change sound mode + target: fields: - entity_id: - description: Name(s) of entities to change sound mode on. - example: "media_player.marantz" sound_mode: + name: Sound mode description: Name of the sound mode to switch to. example: "Music" + selector: + text: clear_playlist: - description: Send the media player the command to clear players playlist. - fields: - entity_id: - description: Name(s) of entities to change source on. - example: "media_player.living_room_chromecast" + description: Send the media player the command to clear players playlist + target: shuffle_set: - description: Set shuffling state. + description: Set shuffling state + target: fields: - entity_id: - description: Name(s) of entities to set. - example: "media_player.spotify" shuffle: + name: Shuffle description: True/false for enabling/disabling shuffle. + required: true example: true + selector: + boolean: repeat_set: - description: Set repeat mode. + description: Set repeat mode + target: fields: - entity_id: - description: Name(s) of entities to set. - example: "media_player.sonos" repeat: + name: Repeat mode description: Repeat mode to set (off, all, one). + required: true example: "off" + selector: + select: + options: + - "off" + - "all" + - "one" From 208af0367a2138d3fb7a3bea2270bb90af6612ef Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 17:02:45 +0100 Subject: [PATCH 529/796] Add selectors to Toon service definitions (#46750) --- homeassistant/components/toon/services.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/toon/services.yaml b/homeassistant/components/toon/services.yaml index 7afedeb4bf6..3e06e6d3f9f 100644 --- a/homeassistant/components/toon/services.yaml +++ b/homeassistant/components/toon/services.yaml @@ -2,5 +2,9 @@ update: description: Update all entities with fresh data from Toon fields: display: + name: Display description: Toon display to update (optional) + advanced: true example: eneco-001-123456 + selector: + text: From 8b69608242774a4dbeeb6024974acae0e1e8e415 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 17:06:25 +0100 Subject: [PATCH 530/796] Add selectors to Browser, Recorder, Shopping List service definitions (#46749) --- .../components/browser/services.yaml | 7 ++++++- .../components/recorder/services.yaml | 20 ++++++++++++++++--- .../components/shopping_list/services.yaml | 15 +++++++++++--- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/browser/services.yaml b/homeassistant/components/browser/services.yaml index 460def22dc1..f6c5e7c90e1 100644 --- a/homeassistant/components/browser/services.yaml +++ b/homeassistant/components/browser/services.yaml @@ -1,6 +1,11 @@ browse_url: - description: Open a URL in the default browser on the host machine of Home Assistant. + description: + Open a URL in the default browser on the host machine of Home Assistant fields: url: + name: URL description: The URL to open. + required: true example: "https://www.home-assistant.io" + selector: + text: diff --git a/homeassistant/components/recorder/services.yaml b/homeassistant/components/recorder/services.yaml index 512807c9f69..cad1925080f 100644 --- a/homeassistant/components/recorder/services.yaml +++ b/homeassistant/components/recorder/services.yaml @@ -1,11 +1,25 @@ # Describes the format for available recorder services purge: - description: Start purge task - delete events and states older than x days, according to keep_days service data. + description: Start purge task - to clean up old data from your database fields: keep_days: - description: Number of history days to keep in database after purge. Value >= 0. + name: Days to keep + description: Number of history days to keep in database after purge. example: 2 + selector: + number: + min: 0 + max: 365 + step: 1 + unit_of_measurement: days + mode: slider + repack: - description: Attempt to save disk space by rewriting the entire database file. + name: Repack + description: + Attempt to save disk space by rewriting the entire database file. example: true + default: false + selector: + boolean: diff --git a/homeassistant/components/shopping_list/services.yaml b/homeassistant/components/shopping_list/services.yaml index 04457e2abec..961fb867aa7 100644 --- a/homeassistant/components/shopping_list/services.yaml +++ b/homeassistant/components/shopping_list/services.yaml @@ -1,12 +1,21 @@ add_item: - description: Adds an item to the shopping list. + description: Adds an item to the shopping list fields: name: + name: Name description: The name of the item to add. + required: true example: Beer + selector: + text: + complete_item: - description: Marks an item as completed in the shopping list. It does not remove the item. + description: Marks an item as completed in the shopping list. fields: name: - description: The name of the item to mark as completed. + name: Name + description: The name of the item to mark as completed (without removing). + required: true example: Beer + selector: + text: From b9d5b3c34e71adc8375eb88a56228ca42339d668 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 17:13:59 +0100 Subject: [PATCH 531/796] Add selectors to Conversation, Image Processing and Number service definitions (#46746) --- homeassistant/components/conversation/services.yaml | 5 ++++- homeassistant/components/image_processing/services.yaml | 7 ++----- homeassistant/components/number/services.yaml | 9 +++++---- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/conversation/services.yaml b/homeassistant/components/conversation/services.yaml index 032edba8db1..5c85de7c187 100644 --- a/homeassistant/components/conversation/services.yaml +++ b/homeassistant/components/conversation/services.yaml @@ -1,7 +1,10 @@ # Describes the format for available component services process: - description: Launch a conversation from a transcribed text. + description: Launch a conversation from a transcribed text fields: text: + name: Text description: Transcribed text example: Turn all lights on + selector: + text: diff --git a/homeassistant/components/image_processing/services.yaml b/homeassistant/components/image_processing/services.yaml index 69e455344b0..cd074acd9f4 100644 --- a/homeassistant/components/image_processing/services.yaml +++ b/homeassistant/components/image_processing/services.yaml @@ -1,8 +1,5 @@ # Describes the format for available image processing services scan: - description: Process an image immediately. - fields: - entity_id: - description: Name(s) of entities to scan immediately. - example: "image_processing.alpr_garage" + description: Process an image immediately + target: diff --git a/homeassistant/components/number/services.yaml b/homeassistant/components/number/services.yaml index d18416f9974..4cb0cf09829 100644 --- a/homeassistant/components/number/services.yaml +++ b/homeassistant/components/number/services.yaml @@ -1,11 +1,12 @@ # Describes the format for available Number entity services set_value: - description: Set the value of a Number entity. + description: Set the value of a Number entity + target: fields: - entity_id: - description: Entity ID of the Number to set the new value. - example: number.volume value: + name: Value description: The target value the entity should be set to. example: 42 + selector: + text: From a80921ab65208cb7c704318b4c2fb415c040649b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 17:14:36 +0100 Subject: [PATCH 532/796] Minor service definition tweaks (#46741) --- .../components/bayesian/services.yaml | 2 +- homeassistant/components/cloud/services.yaml | 4 ++-- .../components/cloudflare/services.yaml | 2 +- .../components/command_line/services.yaml | 2 +- .../components/debugpy/services.yaml | 2 +- homeassistant/components/filter/services.yaml | 2 +- .../components/keyboard/services.yaml | 24 ++++++++++++++----- .../components/lovelace/services.yaml | 2 +- homeassistant/components/rest/services.yaml | 2 +- .../components/universal/services.yaml | 2 +- 10 files changed, 28 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/bayesian/services.yaml b/homeassistant/components/bayesian/services.yaml index ec7313a8630..2fe3a4f7c9b 100644 --- a/homeassistant/components/bayesian/services.yaml +++ b/homeassistant/components/bayesian/services.yaml @@ -1,2 +1,2 @@ reload: - description: Reload all bayesian entities. + description: Reload all bayesian entities diff --git a/homeassistant/components/cloud/services.yaml b/homeassistant/components/cloud/services.yaml index 20c25225ce2..a7fb6b2f21b 100644 --- a/homeassistant/components/cloud/services.yaml +++ b/homeassistant/components/cloud/services.yaml @@ -1,7 +1,7 @@ # Describes the format for available cloud services remote_connect: - description: Make instance UI available outside over NabuCasa cloud. + description: Make instance UI available outside over NabuCasa cloud remote_disconnect: - description: Disconnect UI from NabuCasa cloud. + description: Disconnect UI from NabuCasa cloud diff --git a/homeassistant/components/cloudflare/services.yaml b/homeassistant/components/cloudflare/services.yaml index 23ffdd14d5f..80165700dbb 100644 --- a/homeassistant/components/cloudflare/services.yaml +++ b/homeassistant/components/cloudflare/services.yaml @@ -1,2 +1,2 @@ update_records: - description: Manually trigger update to Cloudflare records. + description: Manually trigger update to Cloudflare records diff --git a/homeassistant/components/command_line/services.yaml b/homeassistant/components/command_line/services.yaml index 8876e8dc925..de010ba8b85 100644 --- a/homeassistant/components/command_line/services.yaml +++ b/homeassistant/components/command_line/services.yaml @@ -1,2 +1,2 @@ reload: - description: Reload all command_line entities. + description: Reload all command_line entities diff --git a/homeassistant/components/debugpy/services.yaml b/homeassistant/components/debugpy/services.yaml index 4e3c19dd0d7..6bf9ad67288 100644 --- a/homeassistant/components/debugpy/services.yaml +++ b/homeassistant/components/debugpy/services.yaml @@ -1,3 +1,3 @@ # Describes the format for available Remote Python Debugger services start: - description: Start the Remote Python Debugger. + description: Start the Remote Python Debugger diff --git a/homeassistant/components/filter/services.yaml b/homeassistant/components/filter/services.yaml index 6f6ea1b04d6..7d64b34a4f7 100644 --- a/homeassistant/components/filter/services.yaml +++ b/homeassistant/components/filter/services.yaml @@ -1,2 +1,2 @@ reload: - description: Reload all filter entities. + description: Reload all filter entities diff --git a/homeassistant/components/keyboard/services.yaml b/homeassistant/components/keyboard/services.yaml index 8e49cdd6a12..d0919d59514 100644 --- a/homeassistant/components/keyboard/services.yaml +++ b/homeassistant/components/keyboard/services.yaml @@ -1,17 +1,29 @@ volume_up: - description: Simulates a key press of the "Volume Up" button on Home Assistant's host machine. + description: + Simulates a key press of the "Volume Up" button on Home Assistant's host + machine volume_down: - description: Simulates a key press of the "Volume Down" button on Home Assistant's host machine. + description: + Simulates a key press of the "Volume Down" button on Home Assistant's host + machine volume_mute: - description: Simulates a key press of the "Volume Mute" button on Home Assistant's host machine. + description: + Simulates a key press of the "Volume Mute" button on Home Assistant's host + machine media_play_pause: - description: Simulates a key press of the "Media Play/Pause" button on Home Assistant's host machine. + description: + Simulates a key press of the "Media Play/Pause" button on Home Assistant's + host machine media_next_track: - description: Simulates a key press of the "Media Next Track" button on Home Assistant's host machine. + description: + Simulates a key press of the "Media Next Track" button on Home Assistant's + host machine media_prev_track: - description: Simulates a key press of the "Media Previous Track" button on Home Assistant's host machine. + description: + Simulates a key press of the "Media Previous Track" button on Home + Assistant's host machine diff --git a/homeassistant/components/lovelace/services.yaml b/homeassistant/components/lovelace/services.yaml index 20450942dce..b324b551e94 100644 --- a/homeassistant/components/lovelace/services.yaml +++ b/homeassistant/components/lovelace/services.yaml @@ -1,4 +1,4 @@ # Describes the format for available lovelace services reload_resources: - description: Reload Lovelace resources from YAML configuration. + description: Reload Lovelace resources from YAML configuration diff --git a/homeassistant/components/rest/services.yaml b/homeassistant/components/rest/services.yaml index 06baa8734f2..7e324670134 100644 --- a/homeassistant/components/rest/services.yaml +++ b/homeassistant/components/rest/services.yaml @@ -1,2 +1,2 @@ reload: - description: Reload all rest entities and notify services. + description: Reload all rest entities and notify services diff --git a/homeassistant/components/universal/services.yaml b/homeassistant/components/universal/services.yaml index ed8f550275e..8b515151fd9 100644 --- a/homeassistant/components/universal/services.yaml +++ b/homeassistant/components/universal/services.yaml @@ -1,2 +1,2 @@ reload: - description: Reload all universal entities. + description: Reload all universal entities From e000b9c813c625e2ab9042c041fc8c03ac20547d Mon Sep 17 00:00:00 2001 From: AdmiralStipe <64564398+AdmiralStipe@users.noreply.github.com> Date: Thu, 18 Feb 2021 17:16:45 +0100 Subject: [PATCH 533/796] Added Slovenian language (sl-si) to Microsoft TTS (#46720) --- homeassistant/components/microsoft/tts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index bbfe9b0379e..1e1c088b351 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -54,6 +54,7 @@ SUPPORTED_LANGUAGES = [ "ro-ro", "ru-ru", "sk-sk", + "sl-si", "sv-se", "th-th", "tr-tr", From 113059c1c737d7d873c950be58c27a0517f88dc7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 17:32:48 +0100 Subject: [PATCH 534/796] Add selectors to HomeKit service definitions (#46745) --- homeassistant/components/homekit/services.yaml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homekit/services.yaml b/homeassistant/components/homekit/services.yaml index 96971f70300..6f9c005ed64 100644 --- a/homeassistant/components/homekit/services.yaml +++ b/homeassistant/components/homekit/services.yaml @@ -1,14 +1,11 @@ # Describes the format for available HomeKit services start: - description: Starts the HomeKit driver. + description: Starts the HomeKit driver reload: - description: Reload homekit and re-process YAML configuration. + description: Reload homekit and re-process YAML configuration reset_accessory: - description: Reset a HomeKit accessory. This can be useful when changing a media_player’s device class to tv, linking a battery, or whenever Home Assistant adds support for new HomeKit features to existing entities. - fields: - entity_id: - description: Name of the entity to reset. - example: "binary_sensor.grid_status" + description: Reset a HomeKit accessory + target: From b6f374b507679962344bea9952e6b2cb1bf8c9a1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 18:05:03 +0100 Subject: [PATCH 535/796] Add selectors to Twente Milieu service definitions (#46748) --- homeassistant/components/twentemilieu/services.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/twentemilieu/services.yaml b/homeassistant/components/twentemilieu/services.yaml index 7a5b1db301d..7a6a16f33ad 100644 --- a/homeassistant/components/twentemilieu/services.yaml +++ b/homeassistant/components/twentemilieu/services.yaml @@ -2,5 +2,9 @@ update: description: Update all entities with fresh data from Twente Milieu fields: id: + name: ID description: Specific unique address ID to update + advanced: true example: 1300012345 + selector: + text: From 782863ca8712a045f34f78375c7c34ad3c6977b7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 18:17:09 +0100 Subject: [PATCH 536/796] Ensure pre-commit's hassfest triggers on service file changes (#46753) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index efd4b86e8ac..6b650ebac79 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -90,4 +90,4 @@ repos: pass_filenames: false language: script types: [text] - files: ^(homeassistant/.+/(manifest|strings)\.json|\.coveragerc)$ + files: ^(homeassistant/.+/(manifest|strings)\.json|\.coveragerc|homeassistant/.+/services\.yaml)$ From 01ebb156a05d874c71c5596501a953fcf6278831 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 18:19:49 +0100 Subject: [PATCH 537/796] Add selectors to Home Assistant service definitions (#46733) Co-authored-by: Bram Kragten --- .../components/homeassistant/services.yaml | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/homeassistant/services.yaml b/homeassistant/components/homeassistant/services.yaml index cb3efb0d524..d23bdfdba72 100644 --- a/homeassistant/components/homeassistant/services.yaml +++ b/homeassistant/components/homeassistant/services.yaml @@ -1,49 +1,49 @@ check_config: - description: Check the Home Assistant configuration files for errors. Errors will be displayed in the Home Assistant log. + description: + Check the Home Assistant configuration files for errors. Errors will be + displayed in the Home Assistant log reload_core_config: - description: Reload the core configuration. + description: Reload the core configuration restart: - description: Restart the Home Assistant service. + description: Restart the Home Assistant service set_location: - description: Update the Home Assistant location. + description: Update the Home Assistant location fields: latitude: - description: Latitude of your location + name: Latitude + description: Latitude of your location. + required: true example: 32.87336 + selector: + text: longitude: - description: Longitude of your location + name: Longitude + description: Longitude of your location. + required: true example: 117.22743 + selector: + text: stop: description: Stop the Home Assistant service. toggle: - description: Generic service to toggle devices on/off under any domain. Same usage as the light.turn_on, switch.turn_on, etc. services. - fields: - entity_id: - description: The entity_id of the device to toggle on/off. - example: light.living_room + description: Generic service to toggle devices on/off under any domain + target: + entity: {} turn_on: - description: Generic service to turn devices on under any domain. Same usage as the light.turn_on, switch.turn_on, etc. services. - fields: - entity_id: - description: The entity_id of the device to turn on. - example: light.living_room + description: Generic service to turn devices on under any domain. + target: + entity: {} turn_off: - description: Generic service to turn devices off under any domain. Same usage as the light.turn_on, switch.turn_on, etc. services. - fields: - entity_id: - description: The entity_id of the device to turn off. - example: light.living_room + description: Generic service to turn devices off under any domain. update_entity: description: Force one or more entities to update its data - fields: - entity_id: - description: One or multiple entity_ids to update. Can be a list. - example: light.living_room + target: + entity: {} From 3353c63f8f9502a6a0d81741f1f8677a47f2f826 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 18:25:15 +0100 Subject: [PATCH 538/796] Upgrade sentry-sdk to 0.20.3 (#46754) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 4c823362aee..d0592493f15 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,6 +3,6 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==0.20.2"], + "requirements": ["sentry-sdk==0.20.3"], "codeowners": ["@dcramer", "@frenck"] } diff --git a/requirements_all.txt b/requirements_all.txt index 815484aa700..1e9b7295946 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2017,7 +2017,7 @@ sense-hat==2.2.0 sense_energy==0.9.0 # homeassistant.components.sentry -sentry-sdk==0.20.2 +sentry-sdk==0.20.3 # homeassistant.components.sharkiq sharkiqpy==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf1b25c9350..71bcca6fe2f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1033,7 +1033,7 @@ scapy==2.4.4 sense_energy==0.9.0 # homeassistant.components.sentry -sentry-sdk==0.20.2 +sentry-sdk==0.20.3 # homeassistant.components.sharkiq sharkiqpy==0.1.8 From 8b97f62a8e0fd309ad3807fff58c13e45a762375 Mon Sep 17 00:00:00 2001 From: Ellis Michael Date: Thu, 18 Feb 2021 09:40:16 -0800 Subject: [PATCH 539/796] Allow LIFX bulbs to fade color even when off (#46596) LIFX bulbs have the capability to fade their color attributes even while the bulb is off. When the bulb is later turned on, the fade will continue as if the bulb was on all along. --- homeassistant/components/lifx/light.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index e775b5623d3..f06a7720bb2 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -608,9 +608,13 @@ class LIFXLight(LightEntity): if not self.is_on: if power_off: await self.set_power(ack, False) - if hsbk: + # If fading on with color, set color immediately + if hsbk and power_on: await self.set_color(ack, hsbk, kwargs) - if power_on: + await self.set_power(ack, True, duration=fade) + elif hsbk: + await self.set_color(ack, hsbk, kwargs, duration=fade) + elif power_on: await self.set_power(ack, True, duration=fade) else: if power_on: From 76e5f86b76ffbe10ba22505270946b56933217f9 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Thu, 18 Feb 2021 10:08:48 -0800 Subject: [PATCH 540/796] Add @esev as codeowner for wemo (#46756) --- CODEOWNERS | 1 + homeassistant/components/wemo/manifest.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 75faf90eb75..ab10b4dfd60 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -521,6 +521,7 @@ homeassistant/components/watson_tts/* @rutkai homeassistant/components/weather/* @fabaff homeassistant/components/webostv/* @bendavid homeassistant/components/websocket_api/* @home-assistant/core +homeassistant/components/wemo/* @esev homeassistant/components/wiffi/* @mampfes homeassistant/components/wilight/* @leofig-rj homeassistant/components/withings/* @vangorra diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index fe5559b58d6..94bc0fa72aa 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -12,5 +12,5 @@ "homekit": { "models": ["Socket", "Wemo"] }, - "codeowners": [] + "codeowners": ["@esev"] } From 52a9a66d0f499a8ff402392187d59ad3e6682d0d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Feb 2021 21:30:54 +0100 Subject: [PATCH 541/796] Upgrade cryptography to 3.3.2 (#46759) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7db311b56d5..5f2aa859e26 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ awesomeversion==21.2.2 bcrypt==3.1.7 certifi>=2020.12.5 ciso8601==2.1.3 -cryptography==3.3.1 +cryptography==3.3.2 defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 diff --git a/requirements.txt b/requirements.txt index 42be3cfdf49..d0b894bcb58 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ ciso8601==2.1.3 httpx==0.16.1 jinja2>=2.11.3 PyJWT==1.7.1 -cryptography==3.3.1 +cryptography==3.3.2 pip>=8.0.3,<20.3 python-slugify==4.0.1 pytz>=2021.1 diff --git a/setup.py b/setup.py index 1b4b52ff26d..1dbc29f5537 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ REQUIRES = [ "jinja2>=2.11.3", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. - "cryptography==3.3.1", + "cryptography==3.3.2", "pip>=8.0.3,<20.3", "python-slugify==4.0.1", "pytz>=2021.1", From d9ce7db554d86790cf28b5b58a95b727c7864cf4 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 19 Feb 2021 00:03:06 +0000 Subject: [PATCH 542/796] [ci skip] Translation update --- .../components/aemet/translations/fr.json | 22 ++++++ .../components/asuswrt/translations/fr.json | 45 +++++++++++ .../fireservicerota/translations/fr.json | 3 +- .../components/foscam/translations/fr.json | 2 + .../components/fritzbox/translations/fr.json | 3 +- .../components/habitica/translations/fr.json | 20 +++++ .../components/habitica/translations/ru.json | 20 +++++ .../components/hyperion/translations/fr.json | 4 +- .../keenetic_ndms2/translations/fr.json | 36 +++++++++ .../components/kulersky/translations/fr.json | 5 ++ .../components/local_ip/translations/fr.json | 1 + .../lutron_caseta/translations/fr.json | 10 +++ .../components/lyric/translations/fr.json | 9 +++ .../components/mazda/translations/fr.json | 3 +- .../media_player/translations/fr.json | 7 ++ .../motion_blinds/translations/fr.json | 1 + .../components/mysensors/translations/fr.json | 79 +++++++++++++++++++ .../components/neato/translations/fr.json | 11 ++- .../components/nest/translations/fr.json | 4 +- .../components/nuki/translations/fr.json | 3 +- .../ondilo_ico/translations/fr.json | 14 ++++ .../components/ozw/translations/fr.json | 1 + .../philips_js/translations/fr.json | 24 ++++++ .../components/powerwall/translations/ca.json | 2 +- .../components/powerwall/translations/en.json | 2 +- .../components/powerwall/translations/fr.json | 8 +- .../components/roku/translations/fr.json | 1 + .../components/shelly/translations/fr.json | 5 +- .../components/smarttub/translations/fr.json | 22 ++++++ .../components/smarttub/translations/ru.json | 22 ++++++ .../components/tesla/translations/fr.json | 4 + .../components/tuya/translations/fr.json | 2 + .../components/unifi/translations/fr.json | 3 +- .../xiaomi_miio/translations/fr.json | 12 ++- .../components/zwave_js/translations/fr.json | 4 +- 35 files changed, 398 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/aemet/translations/fr.json create mode 100644 homeassistant/components/asuswrt/translations/fr.json create mode 100644 homeassistant/components/habitica/translations/fr.json create mode 100644 homeassistant/components/habitica/translations/ru.json create mode 100644 homeassistant/components/keenetic_ndms2/translations/fr.json create mode 100644 homeassistant/components/mysensors/translations/fr.json create mode 100644 homeassistant/components/philips_js/translations/fr.json create mode 100644 homeassistant/components/smarttub/translations/fr.json create mode 100644 homeassistant/components/smarttub/translations/ru.json diff --git a/homeassistant/components/aemet/translations/fr.json b/homeassistant/components/aemet/translations/fr.json new file mode 100644 index 00000000000..bb1e792aa5e --- /dev/null +++ b/homeassistant/components/aemet/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "invalid_api_key": "Cl\u00e9 API invalide" + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nom de l'int\u00e9gration" + }, + "description": "Configurez l'int\u00e9gration AEMET OpenData. Pour g\u00e9n\u00e9rer la cl\u00e9 API, acc\u00e9dez \u00e0 https://opendata.aemet.es/centrodedescargas/altaUsuario", + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/fr.json b/homeassistant/components/asuswrt/translations/fr.json new file mode 100644 index 00000000000..0d53f3f24cf --- /dev/null +++ b/homeassistant/components/asuswrt/translations/fr.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", + "pwd_and_ssh": "Fournissez uniquement le mot de passe ou le fichier de cl\u00e9 SSH", + "pwd_or_ssh": "Veuillez fournir un mot de passe ou un fichier de cl\u00e9 SSH", + "ssh_not_file": "Fichier cl\u00e9 SSH non trouv\u00e9", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "mode": "Mode", + "name": "Nom", + "password": "Mot de passe", + "port": "Port", + "protocol": "Protocole de communication \u00e0 utiliser", + "ssh_key": "Chemin d'acc\u00e8s \u00e0 votre fichier de cl\u00e9s SSH (au lieu du mot de passe)", + "username": "Nom d'utilisateur" + }, + "description": "D\u00e9finissez les param\u00e8tres n\u00e9cessaires pour vous connecter \u00e0 votre routeur", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Quelques secondes d'attente avant d'envisager l'abandon d'un appareil", + "dnsmasq": "L\u2019emplacement dans le routeur des fichiers dnsmasq.leases", + "interface": "L'interface \u00e0 partir de laquelle vous souhaitez obtenir des statistiques (e.g. eth0,eth1 etc)", + "require_ip": "Les appareils doivent avoir une IP (pour le mode point d'acc\u00e8s)", + "track_unknown": "Traquer les appareils inconnus / non identifi\u00e9s" + }, + "title": "Options AsusWRT" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/fr.json b/homeassistant/components/fireservicerota/translations/fr.json index d0ce81458e3..fdbf28e32e1 100644 --- a/homeassistant/components/fireservicerota/translations/fr.json +++ b/homeassistant/components/fireservicerota/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Le compte \u00e0 d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9" + "already_configured": "Le compte \u00e0 d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "create_entry": { "default": "Autentification r\u00e9ussie" diff --git a/homeassistant/components/foscam/translations/fr.json b/homeassistant/components/foscam/translations/fr.json index 9af8115c305..1424c22ad61 100644 --- a/homeassistant/components/foscam/translations/fr.json +++ b/homeassistant/components/foscam/translations/fr.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Echec de connection", "invalid_auth": "Authentification invalide", + "invalid_response": "R\u00e9ponse invalide de l\u2019appareil", "unknown": "Erreur inattendue" }, "step": { @@ -14,6 +15,7 @@ "host": "H\u00f4te", "password": "Mot de passe", "port": "Port", + "rtsp_port": "Port RTSP", "stream": "Flux", "username": "Nom d'utilisateur" } diff --git a/homeassistant/components/fritzbox/translations/fr.json b/homeassistant/components/fritzbox/translations/fr.json index 0cd425410e6..e6302964988 100644 --- a/homeassistant/components/fritzbox/translations/fr.json +++ b/homeassistant/components/fritzbox/translations/fr.json @@ -4,7 +4,8 @@ "already_configured": "Cette AVM FRITZ!Box est d\u00e9j\u00e0 configur\u00e9e.", "already_in_progress": "Une configuration d'AVM FRITZ!Box est d\u00e9j\u00e0 en cours.", "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", - "not_supported": "Connect\u00e9 \u00e0 AVM FRITZ! Box mais impossible de contr\u00f4ler les appareils Smart Home." + "not_supported": "Connect\u00e9 \u00e0 AVM FRITZ! Box mais impossible de contr\u00f4ler les appareils Smart Home.", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "invalid_auth": "Authentification invalide" diff --git a/homeassistant/components/habitica/translations/fr.json b/homeassistant/components/habitica/translations/fr.json new file mode 100644 index 00000000000..00fcd36a508 --- /dev/null +++ b/homeassistant/components/habitica/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "invalid_credentials": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 API", + "api_user": "ID utilisateur de l'API d'Habitica", + "name": "Remplacez le nom d\u2019utilisateur d\u2019Habitica. Sera utilis\u00e9 pour les appels de service", + "url": "URL" + }, + "description": "Connectez votre profil Habitica pour permettre la surveillance du profil et des t\u00e2ches de votre utilisateur. Notez que api_id et api_key doivent \u00eatre obtenus de https://habitica.com/user/settings/api" + } + } + }, + "title": "Habitica" +} \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/ru.json b/homeassistant/components/habitica/translations/ru.json new file mode 100644 index 00000000000..b3e81a34997 --- /dev/null +++ b/homeassistant/components/habitica/translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "api_user": "ID \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f API Habitica", + "name": "\u041f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u0438\u043c\u0435\u043d\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f Habitica. \u0411\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0432\u044b\u0437\u043e\u0432\u043e\u0432 \u0441\u043b\u0443\u0436\u0431", + "url": "URL-\u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 \u043f\u0440\u043e\u0444\u0438\u043b\u044c Habitica, \u0447\u0442\u043e\u0431\u044b \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0444\u0438\u043b\u044c \u0438 \u0437\u0430\u0434\u0430\u0447\u0438 \u0412\u0430\u0448\u0435\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f. \u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e api_id \u0438 api_key \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u044b \u0441 https://habitica.com/user/settings/api" + } + } + }, + "title": "Habitica" +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/fr.json b/homeassistant/components/hyperion/translations/fr.json index 4b374f097a4..f69fd6acdc6 100644 --- a/homeassistant/components/hyperion/translations/fr.json +++ b/homeassistant/components/hyperion/translations/fr.json @@ -2,11 +2,13 @@ "config": { "abort": { "already_configured": "Le service est d\u00e9ja configur\u00e9 ", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "auth_new_token_not_granted_error": "Le jeton nouvellement cr\u00e9\u00e9 n'a pas \u00e9t\u00e9 approuv\u00e9 sur l'interface utilisateur Hyperion", "auth_new_token_not_work_error": "\u00c9chec de l'authentification \u00e0 l'aide du jeton nouvellement cr\u00e9\u00e9", "auth_required_error": "Impossible de d\u00e9terminer si une autorisation est requise", "cannot_connect": "Echec de connection", - "no_id": "L'instance Hyperion Ambilight n'a pas signal\u00e9 son identifiant" + "no_id": "L'instance Hyperion Ambilight n'a pas signal\u00e9 son identifiant", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "cannot_connect": "Echec de la connexion ", diff --git a/homeassistant/components/keenetic_ndms2/translations/fr.json b/homeassistant/components/keenetic_ndms2/translations/fr.json new file mode 100644 index 00000000000..2ac19dcdc64 --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/fr.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "name": "Nom", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur" + }, + "title": "Configurer le routeur Keenetic NDMS2" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "consider_home": "Consid\u00e9rez l'intervalle de home assistant", + "include_arp": "Utiliser les donn\u00e9es ARP (ignor\u00e9es si les donn\u00e9es du hotspot sont utilis\u00e9es)", + "include_associated": "Utiliser les donn\u00e9es d'associations WiFi AP (ignor\u00e9es si les donn\u00e9es du hotspot sont utilis\u00e9es)", + "interfaces": "Choisissez les interfaces \u00e0 analyser", + "scan_interval": "Intervalle d\u2019analyse", + "try_hotspot": "Utiliser les donn\u00e9es 'ip hotspot' (plus pr\u00e9cis)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/fr.json b/homeassistant/components/kulersky/translations/fr.json index 649a3d387bd..42f356ac365 100644 --- a/homeassistant/components/kulersky/translations/fr.json +++ b/homeassistant/components/kulersky/translations/fr.json @@ -3,6 +3,11 @@ "abort": { "no_devices_found": "Aucun appareil n'a \u00e9t\u00e9 d\u00e9tect\u00e9 sur le r\u00e9seau", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Seulement une seule configuration est possible " + }, + "step": { + "confirm": { + "description": "Voulez-vous commencer la configuration ?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/local_ip/translations/fr.json b/homeassistant/components/local_ip/translations/fr.json index c1933032ed0..1c5a8fc9634 100644 --- a/homeassistant/components/local_ip/translations/fr.json +++ b/homeassistant/components/local_ip/translations/fr.json @@ -8,6 +8,7 @@ "data": { "name": "Nom du capteur" }, + "description": "Voulez-vous commencer la configuration ?", "title": "Adresse IP locale" } } diff --git a/homeassistant/components/lutron_caseta/translations/fr.json b/homeassistant/components/lutron_caseta/translations/fr.json index 4d7dccd0acf..ff561548b44 100644 --- a/homeassistant/components/lutron_caseta/translations/fr.json +++ b/homeassistant/components/lutron_caseta/translations/fr.json @@ -42,6 +42,11 @@ "group_1_button_2": "Premier groupe deuxi\u00e8me bouton", "group_2_button_1": "Premier bouton du deuxi\u00e8me groupe", "group_2_button_2": "Deuxi\u00e8me bouton du deuxi\u00e8me groupe", + "lower": "Bas", + "lower_1": "Bas 1", + "lower_2": "Bas 2", + "lower_3": "Bas 3", + "lower_4": "Bas 4", "lower_all": "Tout baisser", "off": "Eteint", "on": "Allumer", @@ -50,6 +55,11 @@ "open_3": "Ouvrir 3", "open_4": "Ouvrir 4", "open_all": "Ouvre tout", + "raise": "Haut", + "raise_1": "Haut 1", + "raise_2": "Haut 2", + "raise_3": "Haut 3", + "raise_4": "Haut 4", "raise_all": "Lever tout", "stop": "Stop (favori)", "stop_1": "Arr\u00eat 1", diff --git a/homeassistant/components/lyric/translations/fr.json b/homeassistant/components/lyric/translations/fr.json index 794e85b7fa6..540d3e1e6c2 100644 --- a/homeassistant/components/lyric/translations/fr.json +++ b/homeassistant/components/lyric/translations/fr.json @@ -1,7 +1,16 @@ { "config": { "abort": { + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation." + }, + "create_entry": { + "default": "Authentification r\u00e9ussie" + }, + "step": { + "pick_implementation": { + "title": "S\u00e9lectionner une m\u00e9thode d'authentification" + } } } } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/fr.json b/homeassistant/components/mazda/translations/fr.json index e9ccb013b5e..aa1ea252c0c 100644 --- a/homeassistant/components/mazda/translations/fr.json +++ b/homeassistant/components/mazda/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Le compte est d\u00e9ja configur\u00e9" + "already_configured": "Le compte est d\u00e9ja configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "account_locked": "Compte bloqu\u00e9. Veuillez r\u00e9essayer plus tard.", diff --git a/homeassistant/components/media_player/translations/fr.json b/homeassistant/components/media_player/translations/fr.json index f3992f74616..9ecdd19037f 100644 --- a/homeassistant/components/media_player/translations/fr.json +++ b/homeassistant/components/media_player/translations/fr.json @@ -6,6 +6,13 @@ "is_on": "{entity_name} est activ\u00e9", "is_paused": "{entity_name} est en pause", "is_playing": "{entity_name} joue" + }, + "trigger_type": { + "idle": "{entity_name} devient inactif", + "paused": "{entity_name} est mis en pause", + "playing": "{entity_name} commence \u00e0 jouer", + "turned_off": "{entity_name} d\u00e9sactiv\u00e9", + "turned_on": "{entity_name} activ\u00e9" } }, "state": { diff --git a/homeassistant/components/motion_blinds/translations/fr.json b/homeassistant/components/motion_blinds/translations/fr.json index da8abbcc564..b6715970e40 100644 --- a/homeassistant/components/motion_blinds/translations/fr.json +++ b/homeassistant/components/motion_blinds/translations/fr.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9ja configur\u00e9 ", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "connection_error": "\u00c9chec de la connexion " }, "error": { diff --git a/homeassistant/components/mysensors/translations/fr.json b/homeassistant/components/mysensors/translations/fr.json new file mode 100644 index 00000000000..00f9831c035 --- /dev/null +++ b/homeassistant/components/mysensors/translations/fr.json @@ -0,0 +1,79 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", + "duplicate_persistence_file": "Fichier de persistance d\u00e9j\u00e0 utilis\u00e9", + "duplicate_topic": "Sujet d\u00e9j\u00e0 utilis\u00e9", + "invalid_auth": "Authentification invalide", + "invalid_device": "Appareil non valide", + "invalid_ip": "Adresse IP non valide", + "invalid_persistence_file": "Fichier de persistance non valide", + "invalid_port": "Num\u00e9ro de port non valide", + "invalid_publish_topic": "Sujet de publication non valide", + "invalid_serial": "Port s\u00e9rie non valide", + "invalid_subscribe_topic": "Sujet d'abonnement non valide", + "invalid_version": "Version de MySensors non valide", + "not_a_number": "Veuillez saisir un nombre", + "port_out_of_range": "Le num\u00e9ro de port doit \u00eatre au moins 1 et au plus 65535", + "same_topic": "Les sujets de souscription et de publication sont identiques", + "unknown": "Erreur inattendue" + }, + "error": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", + "duplicate_persistence_file": "Fichier de persistance d\u00e9j\u00e0 utilis\u00e9", + "duplicate_topic": "Sujet d\u00e9j\u00e0 utilis\u00e9", + "invalid_auth": "Authentification invalide", + "invalid_device": "Appareil non valide", + "invalid_ip": "Adresse IP non valide", + "invalid_persistence_file": "Fichier de persistance non valide", + "invalid_port": "Num\u00e9ro de port non valide", + "invalid_publish_topic": "Sujet de publication non valide", + "invalid_serial": "Port s\u00e9rie non valide", + "invalid_subscribe_topic": "Sujet d'abonnement non valide", + "invalid_version": "Version de MySensors non valide", + "not_a_number": "Veuillez saisir un nombre", + "port_out_of_range": "Le num\u00e9ro de port doit \u00eatre au moins 1 et au plus 65535", + "same_topic": "Les sujets de souscription et de publication sont identiques", + "unknown": "Erreur inattendue" + }, + "step": { + "gw_mqtt": { + "data": { + "persistence_file": "fichier de persistance (laissez vide pour g\u00e9n\u00e9rer automatiquement)", + "retain": "mqtt conserver", + "topic_in_prefix": "pr\u00e9fixe pour les sujets d\u2019entr\u00e9e (topic_in_prefix)", + "topic_out_prefix": "pr\u00e9fixe pour les sujets de sortie (topic_out_prefix)", + "version": "Version de MySensors" + }, + "description": "Configuration de la passerelle MQTT" + }, + "gw_serial": { + "data": { + "baud_rate": "d\u00e9bit en bauds", + "device": "Port s\u00e9rie", + "persistence_file": "fichier de persistance (laissez vide pour g\u00e9n\u00e9rer automatiquement)", + "version": "Version de MySensors" + }, + "description": "Configuration de la passerelle s\u00e9rie" + }, + "gw_tcp": { + "data": { + "device": "Adresse IP de la passerelle", + "persistence_file": "fichier de persistance (laisser vide pour g\u00e9n\u00e9rer automatiquement)", + "tcp_port": "port", + "version": "Version de MySensors" + }, + "description": "Configuration de la passerelle Ethernet" + }, + "user": { + "data": { + "gateway_type": "Type de passerelle" + }, + "description": "Choisissez la m\u00e9thode de connexion \u00e0 la passerelle" + } + } + }, + "title": "MySensors" +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/fr.json b/homeassistant/components/neato/translations/fr.json index 4b71a93a783..26b97e83c0b 100644 --- a/homeassistant/components/neato/translations/fr.json +++ b/homeassistant/components/neato/translations/fr.json @@ -2,8 +2,11 @@ "config": { "abort": { "already_configured": "D\u00e9j\u00e0 configur\u00e9", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", "invalid_auth": "Authentification invalide", - "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation " + "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation ", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "create_entry": { "default": "Voir [Documentation Neato]({docs_url})." @@ -13,6 +16,12 @@ "unknown": "Erreur inattendue" }, "step": { + "pick_implementation": { + "title": "S\u00e9lectionner une m\u00e9thode d'authentification" + }, + "reauth_confirm": { + "title": "Voulez-vous commencer la configuration ?" + }, "user": { "data": { "password": "Mot de passe", diff --git a/homeassistant/components/nest/translations/fr.json b/homeassistant/components/nest/translations/fr.json index 0d1f5b761f5..2830bf5da87 100644 --- a/homeassistant/components/nest/translations/fr.json +++ b/homeassistant/components/nest/translations/fr.json @@ -5,6 +5,7 @@ "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "unknown_authorize_url_generation": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation." }, @@ -36,7 +37,8 @@ "title": "S\u00e9lectionner une m\u00e9thode d'authentification" }, "reauth_confirm": { - "description": "L'int\u00e9gration Nest doit r\u00e9-authentifier votre compte" + "description": "L'int\u00e9gration Nest doit r\u00e9-authentifier votre compte", + "title": "R\u00e9-authentifier l'int\u00e9gration" } } }, diff --git a/homeassistant/components/nuki/translations/fr.json b/homeassistant/components/nuki/translations/fr.json index 26a949038d5..035c0732576 100644 --- a/homeassistant/components/nuki/translations/fr.json +++ b/homeassistant/components/nuki/translations/fr.json @@ -2,7 +2,8 @@ "config": { "error": { "cannot_connect": "\u00c9chec de la connexion ", - "invalid_auth": "Authentification invalide " + "invalid_auth": "Authentification invalide ", + "unknown": "Erreur inattendue" }, "step": { "user": { diff --git a/homeassistant/components/ondilo_ico/translations/fr.json b/homeassistant/components/ondilo_ico/translations/fr.json index 33271e594a3..c05fc0caaa6 100644 --- a/homeassistant/components/ondilo_ico/translations/fr.json +++ b/homeassistant/components/ondilo_ico/translations/fr.json @@ -1,3 +1,17 @@ { + "config": { + "abort": { + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation." + }, + "create_entry": { + "default": "Authentification r\u00e9ussie" + }, + "step": { + "pick_implementation": { + "title": "S\u00e9lectionner une m\u00e9thode d'authentification" + } + } + }, "title": "Ondilo ICO" } \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/fr.json b/homeassistant/components/ozw/translations/fr.json index 5eed478549d..bf4ba5c6995 100644 --- a/homeassistant/components/ozw/translations/fr.json +++ b/homeassistant/components/ozw/translations/fr.json @@ -5,6 +5,7 @@ "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", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "mqtt_required": "L'int\u00e9gration MQTT n'est pas configur\u00e9e", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, diff --git a/homeassistant/components/philips_js/translations/fr.json b/homeassistant/components/philips_js/translations/fr.json new file mode 100644 index 00000000000..9ae65c18fa4 --- /dev/null +++ b/homeassistant/components/philips_js/translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "api_version": "Version de l'API", + "host": "H\u00f4te" + } + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "Il a \u00e9t\u00e9 demand\u00e9 \u00e0 l'appareil de s'allumer" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/ca.json b/homeassistant/components/powerwall/translations/ca.json index 38a86f05d11..8016cd12371 100644 --- a/homeassistant/components/powerwall/translations/ca.json +++ b/homeassistant/components/powerwall/translations/ca.json @@ -17,7 +17,7 @@ "ip_address": "Adre\u00e7a IP", "password": "Contrasenya" }, - "description": "La contrasenya normalment s\u00f3n els darrers cinc car\u00e0cters del n\u00famero de s\u00e8rie de la pasarel\u00b7la de control i es pot trobar a l'aplicaci\u00f3 de Tesla; tamb\u00e9 pot consistir en els darrers 5 car\u00e0cters de la contrasenya que es troba a l'interior de la tapa de la pasarel\u00b7la de control 2.", + "description": "La contrasenya normalment s\u00f3n els darrers cinc car\u00e0cters del n\u00famero de s\u00e8rie de la pasarel\u00b7la (backup gateway) i es pot trobar a l'aplicaci\u00f3 de Tesla. Tamb\u00e9 s\u00f3n els darrers 5 car\u00e0cters de la contrasenya que es troba a l'interior de la tapa de la pasarel\u00b7la vers\u00f3 2 (backup gateway 2).", "title": "Connexi\u00f3 amb el Powerwall" } } diff --git a/homeassistant/components/powerwall/translations/en.json b/homeassistant/components/powerwall/translations/en.json index ae8122589be..06fc09804d9 100644 --- a/homeassistant/components/powerwall/translations/en.json +++ b/homeassistant/components/powerwall/translations/en.json @@ -17,7 +17,7 @@ "ip_address": "IP Address", "password": "Password" }, - "description": "The password is usually the last 5 characters of the serial number for Backup Gateway and can be found in the Telsa app; or the last 5 characters of the password found inside the door for Backup Gateway 2.", + "description": "The password is usually the last 5 characters of the serial number for Backup Gateway and can be found in the Tesla app or the last 5 characters of the password found inside the door for Backup Gateway 2.", "title": "Connect to the powerwall" } } diff --git a/homeassistant/components/powerwall/translations/fr.json b/homeassistant/components/powerwall/translations/fr.json index 2086393dfef..3bfd70cd44c 100644 --- a/homeassistant/components/powerwall/translations/fr.json +++ b/homeassistant/components/powerwall/translations/fr.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Le Powerwall est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Le Powerwall est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", + "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue", "wrong_version": "Votre Powerwall utilise une version logicielle qui n'est pas prise en charge. Veuillez envisager de mettre \u00e0 niveau ou de signaler ce probl\u00e8me afin qu'il puisse \u00eatre r\u00e9solu." }, @@ -12,8 +14,10 @@ "step": { "user": { "data": { - "ip_address": "Adresse IP" + "ip_address": "Adresse IP", + "password": "Mot de passe" }, + "description": "Le mot de passe est g\u00e9n\u00e9ralement les 5 derniers caract\u00e8res du num\u00e9ro de s\u00e9rie de Backup Gateway et peut \u00eatre trouv\u00e9 dans l\u2019application Tesla ou les 5 derniers caract\u00e8res du mot de passe trouv\u00e9 \u00e0 l\u2019int\u00e9rieur de la porte pour la passerelle de Backup Gateway 2.", "title": "Connectez-vous au Powerwall" } } diff --git a/homeassistant/components/roku/translations/fr.json b/homeassistant/components/roku/translations/fr.json index 6d237992592..b3dc08a7dc8 100644 --- a/homeassistant/components/roku/translations/fr.json +++ b/homeassistant/components/roku/translations/fr.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "unknown": "Erreur inattendue" }, "error": { diff --git a/homeassistant/components/shelly/translations/fr.json b/homeassistant/components/shelly/translations/fr.json index 0dea9111c62..e4bdc99db1e 100644 --- a/homeassistant/components/shelly/translations/fr.json +++ b/homeassistant/components/shelly/translations/fr.json @@ -12,7 +12,7 @@ "flow_title": "Shelly: {name}", "step": { "confirm_discovery": { - "description": "Voulez-vous configurer le {model} \u00e0 {host}?" + "description": "Voulez-vous configurer le {model} \u00e0 {host}?\n\nLes appareils aliment\u00e9s par batterie prot\u00e9g\u00e9s par mot de passe doivent \u00eatre r\u00e9veill\u00e9s avant de continuer \u00e0 s\u2019installer.\nLes appareils aliment\u00e9s par batterie qui ne sont pas prot\u00e9g\u00e9s par mot de passe seront ajout\u00e9s lorsque l\u2019appareil se r\u00e9veillera, vous pouvez maintenant r\u00e9veiller manuellement l\u2019appareil \u00e0 l\u2019aide d\u2019un bouton dessus ou attendre la prochaine mise \u00e0 jour des donn\u00e9es de l\u2019appareil." }, "credentials": { "data": { @@ -24,7 +24,7 @@ "data": { "host": "H\u00f4te" }, - "description": "Avant la configuration, l'appareil aliment\u00e9 par batterie doit \u00eatre r\u00e9veill\u00e9 en appuyant sur le bouton de l'appareil." + "description": "Avant la configuration, les appareils aliment\u00e9s par batterie doivent \u00eatre r\u00e9veill\u00e9s, vous pouvez maintenant r\u00e9veiller l'appareil \u00e0 l'aide d'un bouton dessus." } } }, @@ -38,6 +38,7 @@ "trigger_type": { "double": "{subtype} double-cliqu\u00e9", "long": " {sous-type} long cliqu\u00e9", + "long_single": "{subtype} clic long et simple clic", "single": "{subtype} simple clic", "single_long": "{subtype} simple clic, puis un clic long", "triple": "{subtype} cliqu\u00e9 trois fois" diff --git a/homeassistant/components/smarttub/translations/fr.json b/homeassistant/components/smarttub/translations/fr.json new file mode 100644 index 00000000000..15dfa04fc78 --- /dev/null +++ b/homeassistant/components/smarttub/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a \u00e9t\u00e9 un succ\u00e8s" + }, + "error": { + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Mot de passe" + }, + "description": "Entrez votre adresse e-mail et votre mot de passe SmartTub pour vous connecter", + "title": "Connexion" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarttub/translations/ru.json b/homeassistant/components/smarttub/translations/ru.json new file mode 100644 index 00000000000..67e055a32c5 --- /dev/null +++ b/homeassistant/components/smarttub/translations/ru.json @@ -0,0 +1,22 @@ +{ + "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.", + "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": { + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0414\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c SmartTub.", + "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/translations/fr.json b/homeassistant/components/tesla/translations/fr.json index 6134ff25f6b..889c32a7d91 100644 --- a/homeassistant/components/tesla/translations/fr.json +++ b/homeassistant/components/tesla/translations/fr.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, "error": { "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", diff --git a/homeassistant/components/tuya/translations/fr.json b/homeassistant/components/tuya/translations/fr.json index 9ef1c325d1e..1681343f3b7 100644 --- a/homeassistant/components/tuya/translations/fr.json +++ b/homeassistant/components/tuya/translations/fr.json @@ -40,8 +40,10 @@ "max_temp": "Temp\u00e9rature cible maximale (utilisez min et max = 0 par d\u00e9faut)", "min_kelvin": "Temp\u00e9rature de couleur minimale prise en charge en kelvin", "min_temp": "Temp\u00e9rature cible minimale (utilisez min et max = 0 par d\u00e9faut)", + "set_temp_divided": "Utilisez la valeur de temp\u00e9rature divis\u00e9e pour la commande de temp\u00e9rature d\u00e9finie", "support_color": "Forcer la prise en charge des couleurs", "temp_divider": "Diviseur de valeurs de temp\u00e9rature (0 = utiliser la valeur par d\u00e9faut)", + "temp_step_override": "Pas de temp\u00e9rature cible", "tuya_max_coltemp": "Temp\u00e9rature de couleur maximale rapport\u00e9e par l'appareil", "unit_of_measurement": "Unit\u00e9 de temp\u00e9rature utilis\u00e9e par l'appareil" }, diff --git a/homeassistant/components/unifi/translations/fr.json b/homeassistant/components/unifi/translations/fr.json index 49d9c68c01c..d750fb0cdd9 100644 --- a/homeassistant/components/unifi/translations/fr.json +++ b/homeassistant/components/unifi/translations/fr.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Le contr\u00f4leur est d\u00e9j\u00e0 configur\u00e9", - "configuration_updated": "Configuration mise \u00e0 jour." + "configuration_updated": "Configuration mise \u00e0 jour.", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "faulty_credentials": "Authentification invalide", diff --git a/homeassistant/components/xiaomi_miio/translations/fr.json b/homeassistant/components/xiaomi_miio/translations/fr.json index 84849041c8c..10ce9972818 100644 --- a/homeassistant/components/xiaomi_miio/translations/fr.json +++ b/homeassistant/components/xiaomi_miio/translations/fr.json @@ -6,10 +6,20 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "no_device_selected": "Aucun appareil s\u00e9lectionn\u00e9, veuillez s\u00e9lectionner un appareil." + "no_device_selected": "Aucun appareil s\u00e9lectionn\u00e9, veuillez s\u00e9lectionner un appareil.", + "unknown_device": "Le mod\u00e8le d'appareil n'est pas connu, impossible de configurer l'appareil \u00e0 l'aide du flux de configuration." }, "flow_title": "Xiaomi Miio: {name}", "step": { + "device": { + "data": { + "host": "Adresse IP", + "name": "Nom de l'appareil", + "token": "Jeton d'API" + }, + "description": "Vous aurez besoin des 32 caract\u00e8res Jeton d'API , voir https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token pour les instructions. Veuillez noter que cette Jeton d'API est diff\u00e9rente de la cl\u00e9 utilis\u00e9e par l'int\u00e9gration Xiaomi Aqara.", + "title": "Connectez-vous \u00e0 un appareil Xiaomi Miio ou \u00e0 une passerelle Xiaomi" + }, "gateway": { "data": { "host": "Adresse IP", diff --git a/homeassistant/components/zwave_js/translations/fr.json b/homeassistant/components/zwave_js/translations/fr.json index e52552fa986..2196ed0259e 100644 --- a/homeassistant/components/zwave_js/translations/fr.json +++ b/homeassistant/components/zwave_js/translations/fr.json @@ -7,6 +7,7 @@ "addon_missing_discovery_info": "Informations manquantes sur la d\u00e9couverte du module compl\u00e9mentaire Z-Wave JS.", "addon_set_config_failed": "\u00c9chec de la d\u00e9finition de la configuration Z-Wave JS.", "already_configured": "Le p\u00e9riph\u00e9rique est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "cannot_connect": "\u00c9chec de la connexion " }, "error": { @@ -39,7 +40,8 @@ }, "start_addon": { "data": { - "network_key": "Cl\u00e9 r\u00e9seau" + "network_key": "Cl\u00e9 r\u00e9seau", + "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" }, "title": "Entrez la configuration du module compl\u00e9mentaire Z-Wave JS" }, From da51e2351405b02c36dcfeaca9056f775b870070 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Thu, 18 Feb 2021 16:18:02 -0800 Subject: [PATCH 543/796] Address late smarttub review (#46703) * _config -> config * remove unused string * remove entity tests * replace unit tests with integration tests using the core * refactor polling to use asyncio.gather * remove redundant component init * remove gather in favor of simple loop * use async_fire_time_changed instead of async_update_entity * use hass.config_entries.async_setup instead of calling smarttub.async_setup_entry directly * replace stray smarttub.async_setup_entry call * async_unload_entry -> hass.config_entries.async_unload * remove broken test --- homeassistant/components/smarttub/__init__.py | 2 +- .../components/smarttub/controller.py | 5 ++- .../components/smarttub/strings.json | 3 +- .../components/smarttub/translations/en.json | 3 +- tests/components/smarttub/conftest.py | 27 ++++-------- tests/components/smarttub/test_climate.py | 28 +++++++++++-- tests/components/smarttub/test_controller.py | 37 ---------------- tests/components/smarttub/test_entity.py | 18 -------- tests/components/smarttub/test_init.py | 42 +++++++++---------- 9 files changed, 58 insertions(+), 107 deletions(-) delete mode 100644 tests/components/smarttub/test_controller.py delete mode 100644 tests/components/smarttub/test_entity.py diff --git a/homeassistant/components/smarttub/__init__.py b/homeassistant/components/smarttub/__init__.py index 85298a75f65..4700b0df4de 100644 --- a/homeassistant/components/smarttub/__init__.py +++ b/homeassistant/components/smarttub/__init__.py @@ -10,7 +10,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["climate"] -async def async_setup(hass, _config): +async def async_setup(hass, config): """Set up smarttub component.""" hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/smarttub/controller.py b/homeassistant/components/smarttub/controller.py index 31fe71d14b6..ad40c94fbed 100644 --- a/homeassistant/components/smarttub/controller.py +++ b/homeassistant/components/smarttub/controller.py @@ -79,12 +79,15 @@ class SmartTubController: try: async with async_timeout.timeout(POLLING_TIMEOUT): for spa in self.spas: - data[spa.id] = {"status": await spa.get_status()} + data[spa.id] = await self._get_spa_data(spa) except APIError as err: raise UpdateFailed(err) from err return data + async def _get_spa_data(self, spa): + return {"status": await spa.get_status()} + async def async_register_devices(self, entry): """Register devices with the device registry for all spas.""" device_registry = await dr.async_get_registry(self._hass) diff --git a/homeassistant/components/smarttub/strings.json b/homeassistant/components/smarttub/strings.json index 0d52673a469..8ba888a9ffb 100644 --- a/homeassistant/components/smarttub/strings.json +++ b/homeassistant/components/smarttub/strings.json @@ -11,8 +11,7 @@ } }, "error": { - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/homeassistant/components/smarttub/translations/en.json b/homeassistant/components/smarttub/translations/en.json index 4cf93091887..b013f816559 100644 --- a/homeassistant/components/smarttub/translations/en.json +++ b/homeassistant/components/smarttub/translations/en.json @@ -5,8 +5,7 @@ "reauth_successful": "Re-authentication was successful" }, "error": { - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" + "invalid_auth": "Invalid authentication" }, "step": { "user": { diff --git a/tests/components/smarttub/conftest.py b/tests/components/smarttub/conftest.py index fec74a2b30a..3519d4f85ce 100644 --- a/tests/components/smarttub/conftest.py +++ b/tests/components/smarttub/conftest.py @@ -6,8 +6,8 @@ import pytest import smarttub from homeassistant.components.smarttub.const import DOMAIN -from homeassistant.components.smarttub.controller import SmartTubController from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -28,6 +28,12 @@ def config_entry(config_data): ) +@pytest.fixture +async def setup_component(hass): + """Set up the component.""" + assert await async_setup_component(hass, DOMAIN, {}) is True + + @pytest.fixture(name="spa") def mock_spa(): """Mock a SmartTub.Spa.""" @@ -65,22 +71,3 @@ def mock_api(account, spa): api_mock = api_class_mock.return_value api_mock.get_account.return_value = account yield api_mock - - -@pytest.fixture -async def controller(smarttub_api, hass, config_entry): - """Instantiate controller for testing.""" - - controller = SmartTubController(hass) - assert len(controller.spas) == 0 - assert await controller.async_setup_entry(config_entry) - - assert len(controller.spas) > 0 - - return controller - - -@pytest.fixture -async def coordinator(controller): - """Provide convenient access to the coordinator via the controller.""" - return controller.coordinator diff --git a/tests/components/smarttub/test_climate.py b/tests/components/smarttub/test_climate.py index f0e6ced4abd..ad47e3ede04 100644 --- a/tests/components/smarttub/test_climate.py +++ b/tests/components/smarttub/test_climate.py @@ -1,5 +1,9 @@ """Test the SmartTub climate platform.""" +from datetime import timedelta + +import smarttub + from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, ATTR_HVAC_ACTION, @@ -15,15 +19,22 @@ from homeassistant.components.climate.const import ( SERVICE_SET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.components.smarttub.const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP +from homeassistant.components.smarttub.const import ( + DEFAULT_MAX_TEMP, + DEFAULT_MIN_TEMP, + SCAN_INTERVAL, +) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, ) +from homeassistant.util import dt + +from tests.common import async_fire_time_changed -async def test_thermostat(coordinator, spa, hass, config_entry): +async def test_thermostat_update(spa, hass, config_entry, smarttub_api): """Test the thermostat entity.""" spa.get_status.return_value = { @@ -44,7 +55,7 @@ async def test_thermostat(coordinator, spa, hass, config_entry): assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT spa.get_status.return_value["heater"] = "OFF" - await hass.helpers.entity_component.async_update_entity(entity_id) + await trigger_update(hass) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE @@ -72,3 +83,14 @@ async def test_thermostat(coordinator, spa, hass, config_entry): blocking=True, ) # does nothing + + spa.get_status.side_effect = smarttub.APIError + await trigger_update(hass) + # should not fail + + +async def trigger_update(hass): + """Trigger a polling update by moving time forward.""" + new_time = dt.utcnow() + timedelta(seconds=SCAN_INTERVAL + 1) + async_fire_time_changed(hass, new_time) + await hass.async_block_till_done() diff --git a/tests/components/smarttub/test_controller.py b/tests/components/smarttub/test_controller.py deleted file mode 100644 index e59ad86c09e..00000000000 --- a/tests/components/smarttub/test_controller.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Test the SmartTub controller.""" - -import pytest -import smarttub - -from homeassistant.components.smarttub.controller import SmartTubController -from homeassistant.helpers.update_coordinator import UpdateFailed - - -async def test_invalid_credentials(hass, controller, smarttub_api, config_entry): - """Check that we return False if the configured credentials are invalid. - - This should mean that the user changed their SmartTub password. - """ - - smarttub_api.login.side_effect = smarttub.LoginFailed - controller = SmartTubController(hass) - ret = await controller.async_setup_entry(config_entry) - assert ret is False - - -async def test_update(controller, spa): - """Test data updates from API.""" - data = await controller.async_update_data() - assert data[spa.id] == {"status": spa.get_status.return_value} - - spa.get_status.side_effect = smarttub.APIError - with pytest.raises(UpdateFailed): - data = await controller.async_update_data() - - -async def test_login(controller, smarttub_api, account): - """Test SmartTubController.login.""" - smarttub_api.get_account.return_value.id = "account-id1" - account = await controller.login("test-email1", "test-password1") - smarttub_api.login.assert_called() - assert account == account diff --git a/tests/components/smarttub/test_entity.py b/tests/components/smarttub/test_entity.py deleted file mode 100644 index 4a19b265090..00000000000 --- a/tests/components/smarttub/test_entity.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Test SmartTubEntity.""" - -from homeassistant.components.smarttub.entity import SmartTubEntity - - -async def test_entity(coordinator, spa): - """Test SmartTubEntity.""" - - entity = SmartTubEntity(coordinator, spa, "entity1") - - assert entity.device_info - assert entity.name - - coordinator.data[spa.id] = {} - assert entity.get_spa_status("foo") is None - coordinator.data[spa.id]["status"] = {"foo": "foo1", "bar": {"baz": "barbaz1"}} - assert entity.get_spa_status("foo") == "foo1" - assert entity.get_spa_status("bar.baz") == "barbaz1" diff --git a/tests/components/smarttub/test_init.py b/tests/components/smarttub/test_init.py index aa22780e9b8..13a447529d4 100644 --- a/tests/components/smarttub/test_init.py +++ b/tests/components/smarttub/test_init.py @@ -1,48 +1,50 @@ """Test smarttub setup process.""" import asyncio -from unittest.mock import patch -import pytest from smarttub import LoginFailed from homeassistant.components import smarttub -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.config_entries import ( + ENTRY_STATE_SETUP_ERROR, + ENTRY_STATE_SETUP_RETRY, +) from homeassistant.setup import async_setup_component -async def test_setup_with_no_config(hass): +async def test_setup_with_no_config(setup_component, hass, smarttub_api): """Test that we do not discover anything.""" - assert await async_setup_component(hass, smarttub.DOMAIN, {}) is True # No flows started assert len(hass.config_entries.flow.async_progress()) == 0 - assert smarttub.const.SMARTTUB_CONTROLLER not in hass.data[smarttub.DOMAIN] + smarttub_api.login.assert_not_called() -async def test_setup_entry_not_ready(hass, config_entry, smarttub_api): +async def test_setup_entry_not_ready(setup_component, hass, config_entry, smarttub_api): """Test setup when the entry is not ready.""" - assert await async_setup_component(hass, smarttub.DOMAIN, {}) is True smarttub_api.login.side_effect = asyncio.TimeoutError - with pytest.raises(ConfigEntryNotReady): - await smarttub.async_setup_entry(hass, config_entry) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state == ENTRY_STATE_SETUP_RETRY -async def test_setup_auth_failed(hass, config_entry, smarttub_api): +async def test_setup_auth_failed(setup_component, hass, config_entry, smarttub_api): """Test setup when the credentials are invalid.""" - assert await async_setup_component(hass, smarttub.DOMAIN, {}) is True smarttub_api.login.side_effect = LoginFailed - assert await smarttub.async_setup_entry(hass, config_entry) is False + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state == ENTRY_STATE_SETUP_ERROR -async def test_config_passed_to_config_entry(hass, config_entry, config_data): +async def test_config_passed_to_config_entry( + hass, config_entry, config_data, smarttub_api +): """Test that configured options are loaded via config entry.""" config_entry.add_to_hass(hass) - ret = await async_setup_component(hass, smarttub.DOMAIN, config_data) - assert ret is True + assert await async_setup_component(hass, smarttub.DOMAIN, config_data) async def test_unload_entry(hass, config_entry, smarttub_api): @@ -51,10 +53,4 @@ async def test_unload_entry(hass, config_entry, smarttub_api): assert await async_setup_component(hass, smarttub.DOMAIN, {}) is True - assert await smarttub.async_unload_entry(hass, config_entry) - - # test failure of platform unload - assert await async_setup_component(hass, smarttub.DOMAIN, {}) is True - with patch.object(hass.config_entries, "async_forward_entry_unload") as mock: - mock.return_value = False - assert await smarttub.async_unload_entry(hass, config_entry) is False + assert await hass.config_entries.async_unload(config_entry.entry_id) From 5df46b60e82160abd103c4d4cd27353de8472536 Mon Sep 17 00:00:00 2001 From: shbatm Date: Thu, 18 Feb 2021 20:00:14 -0600 Subject: [PATCH 544/796] Fix flip-flopped substitutions in Custom Version Type Warning message. (#46768) --- homeassistant/loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 152a3d88b80..2ae279da79e 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -806,7 +806,7 @@ def custom_integration_warning(integration: Integration) -> None: if not validate_custom_integration_version(integration.manifest["version"]): _LOGGER.warning( CUSTOM_WARNING_VERSION_TYPE, - integration.domain, integration.manifest["version"], integration.domain, + integration.domain, ) From f2b303d5099f20db5b62a7954087df94e8b0c6e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Feb 2021 21:05:09 -1000 Subject: [PATCH 545/796] Implement percentage step sizes for fans (#46512) Co-authored-by: Paulus Schoutsen --- homeassistant/components/bond/fan.py | 6 + homeassistant/components/comfoconnect/fan.py | 6 + homeassistant/components/demo/fan.py | 10 ++ homeassistant/components/dyson/fan.py | 6 + homeassistant/components/esphome/fan.py | 5 + homeassistant/components/fan/__init__.py | 67 ++++++++++ homeassistant/components/fan/services.yaml | 38 ++++++ homeassistant/components/isy994/fan.py | 17 ++- homeassistant/components/knx/fan.py | 8 ++ homeassistant/components/lutron_caseta/fan.py | 5 + homeassistant/components/ozw/fan.py | 6 + homeassistant/components/smartthings/fan.py | 6 + homeassistant/components/template/fan.py | 13 ++ homeassistant/components/tuya/fan.py | 7 + homeassistant/components/vesync/fan.py | 6 + homeassistant/components/wemo/fan.py | 6 + homeassistant/components/wilight/fan.py | 5 + homeassistant/components/zwave/fan.py | 6 + homeassistant/components/zwave_js/fan.py | 6 + homeassistant/util/percentage.py | 14 +- tests/components/demo/test_fan.py | 123 ++++++++++++++++++ tests/components/fan/common.py | 35 +++++ tests/components/fan/test_init.py | 8 ++ tests/components/template/test_fan.py | 41 ++++++ 24 files changed, 447 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index 9b70195db5d..cef2efae690 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -16,6 +16,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.util.percentage import ( + int_states_in_range, percentage_to_ranged_value, ranged_value_to_percentage, ) @@ -85,6 +86,11 @@ class BondFan(BondEntity, FanEntity): return 0 return ranged_value_to_percentage(self._speed_range, self._speed) + @property + def speed_count(self) -> Optional[int]: + """Return the number of speeds the fan supports.""" + return int_states_in_range(self._speed_range) + @property def current_direction(self) -> Optional[str]: """Return fan rotation direction.""" diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index 7457d0ffad2..26abd85522a 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -13,6 +13,7 @@ from pycomfoconnect import ( from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.percentage import ( + int_states_in_range, percentage_to_ranged_value, ranged_value_to_percentage, ) @@ -101,6 +102,11 @@ class ComfoConnectFan(FanEntity): return None return ranged_value_to_percentage(SPEED_RANGE, speed) + @property + def speed_count(self) -> int: + """Return the number of speeds the fan supports.""" + return int_states_in_range(SPEED_RANGE) + def turn_on( self, speed: str = None, percentage=None, preset_mode=None, **kwargs ) -> None: diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index cb6036a8938..6bbd8b81f6d 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -215,6 +215,11 @@ class DemoPercentageFan(BaseDemoFan, FanEntity): """Return the current speed.""" return self._percentage + @property + def speed_count(self) -> Optional[float]: + """Return the number of speeds the fan supports.""" + return 3 + def set_percentage(self, percentage: int) -> None: """Set the speed of the fan, as a percentage.""" self._percentage = percentage @@ -270,6 +275,11 @@ class AsyncDemoPercentageFan(BaseDemoFan, FanEntity): """Return the current speed.""" return self._percentage + @property + def speed_count(self) -> Optional[float]: + """Return the number of speeds the fan supports.""" + return 3 + async def async_set_percentage(self, percentage: int) -> None: """Set the speed of the fan, as a percentage.""" self._percentage = percentage diff --git a/homeassistant/components/dyson/fan.py b/homeassistant/components/dyson/fan.py index 7a403902ee8..9e49badbc8e 100644 --- a/homeassistant/components/dyson/fan.py +++ b/homeassistant/components/dyson/fan.py @@ -13,6 +13,7 @@ import voluptuous as vol from homeassistant.components.fan import SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.util.percentage import ( + int_states_in_range, percentage_to_ranged_value, ranged_value_to_percentage, ) @@ -154,6 +155,11 @@ class DysonFanEntity(DysonEntity, FanEntity): return None return ranged_value_to_percentage(SPEED_RANGE, int(self._device.state.speed)) + @property + def speed_count(self) -> Optional[int]: + """Return the number of speeds the fan supports.""" + return int_states_in_range(SPEED_RANGE) + @property def preset_modes(self): """Return the available preset modes.""" diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 8da52b8d584..092c416acab 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -119,6 +119,11 @@ class EsphomeFan(EsphomeEntity, FanEntity): ORDERED_NAMED_FAN_SPEEDS, self._state.speed ) + @property + def speed_count(self) -> Optional[int]: + """Return the number of speeds the fan supports.""" + return len(ORDERED_NAMED_FAN_SPEEDS) + @esphome_state_property def oscillating(self) -> None: """Return the oscillation state.""" diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 8d6fcbea2c9..692588cff48 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import functools as ft import logging +import math from typing import List, Optional import voluptuous as vol @@ -23,6 +24,8 @@ from homeassistant.loader import bind_hass from homeassistant.util.percentage import ( ordered_list_item_to_percentage, percentage_to_ordered_list_item, + percentage_to_ranged_value, + ranged_value_to_percentage, ) _LOGGER = logging.getLogger(__name__) @@ -39,6 +42,8 @@ SUPPORT_DIRECTION = 4 SUPPORT_PRESET_MODE = 8 SERVICE_SET_SPEED = "set_speed" +SERVICE_INCREASE_SPEED = "increase_speed" +SERVICE_DECREASE_SPEED = "decrease_speed" SERVICE_OSCILLATE = "oscillate" SERVICE_SET_DIRECTION = "set_direction" SERVICE_SET_PERCENTAGE = "set_percentage" @@ -54,6 +59,7 @@ DIRECTION_REVERSE = "reverse" ATTR_SPEED = "speed" ATTR_PERCENTAGE = "percentage" +ATTR_PERCENTAGE_STEP = "percentage_step" ATTR_SPEED_LIST = "speed_list" ATTR_OSCILLATING = "oscillating" ATTR_DIRECTION = "direction" @@ -142,6 +148,26 @@ async def async_setup(hass, config: dict): "async_set_speed_deprecated", [SUPPORT_SET_SPEED], ) + component.async_register_entity_service( + SERVICE_INCREASE_SPEED, + { + vol.Optional(ATTR_PERCENTAGE_STEP): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ) + }, + "async_increase_speed", + [SUPPORT_SET_SPEED], + ) + component.async_register_entity_service( + SERVICE_DECREASE_SPEED, + { + vol.Optional(ATTR_PERCENTAGE_STEP): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ) + }, + "async_decrease_speed", + [SUPPORT_SET_SPEED], + ) component.async_register_entity_service( SERVICE_OSCILLATE, {vol.Required(ATTR_OSCILLATING): cv.boolean}, @@ -246,6 +272,33 @@ class FanEntity(ToggleEntity): else: await self.async_set_speed(self.percentage_to_speed(percentage)) + async def async_increase_speed(self, percentage_step=None) -> None: + """Increase the speed of the fan.""" + await self._async_adjust_speed(1, percentage_step) + + async def async_decrease_speed(self, percentage_step=None) -> None: + """Decrease the speed of the fan.""" + await self._async_adjust_speed(-1, percentage_step) + + async def _async_adjust_speed(self, modifier, percentage_step) -> None: + """Increase or decrease the speed of the fan.""" + current_percentage = self.percentage or 0 + + if percentage_step is not None: + new_percentage = current_percentage + (percentage_step * modifier) + else: + speed_range = (1, self.speed_count) + speed_index = math.ceil( + percentage_to_ranged_value(speed_range, current_percentage) + ) + new_percentage = ranged_value_to_percentage( + speed_range, speed_index + modifier + ) + + new_percentage = max(0, min(100, new_percentage)) + + await self.async_set_percentage(new_percentage) + @_fan_native def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" @@ -408,6 +461,19 @@ class FanEntity(ToggleEntity): return self.speed_to_percentage(self.speed) return 0 + @property + def speed_count(self) -> Optional[int]: + """Return the number of speeds the fan supports.""" + speed_list = speed_list_without_preset_modes(self.speed_list) + if speed_list: + return len(speed_list) + return 100 + + @property + def percentage_step(self) -> Optional[float]: + """Return the step size for percentage.""" + return 100 / self.speed_count + @property def speed_list(self) -> list: """Get the list of available speeds.""" @@ -531,6 +597,7 @@ class FanEntity(ToggleEntity): if supported_features & SUPPORT_SET_SPEED: data[ATTR_SPEED] = self.speed data[ATTR_PERCENTAGE] = self.percentage + data[ATTR_PERCENTAGE_STEP] = self.percentage_step if ( supported_features & SUPPORT_PRESET_MODE diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index 2f5802b69f7..ad513b84e8f 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -100,3 +100,41 @@ set_direction: options: - "forward" - "reverse" + +increase_speed: + description: Increase the speed of the fan by one speed or a percentage_step. + fields: + entity_id: + description: Name(s) of the entities to increase speed + example: "fan.living_room" + percentage_step: + advanced: true + required: false + description: Increase speed by a percentage. Should be between 0..100. [optional] + example: 50 + selector: + number: + min: 0 + max: 100 + step: 1 + unit_of_measurement: "%" + mode: slider + +decrease_speed: + description: Decrease the speed of the fan by one speed or a percentage_step. + fields: + entity_id: + description: Name(s) of the entities to decrease speed + example: "fan.living_room" + percentage_step: + advanced: true + required: false + description: Decrease speed by a percentage. Should be between 0..100. [optional] + example: 50 + selector: + number: + min: 0 + max: 100 + step: 1 + unit_of_measurement: "%" + mode: slider diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index 74ed477d3a7..f565383f007 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -2,12 +2,13 @@ import math from typing import Callable -from pyisy.constants import ISY_VALUE_UNKNOWN +from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_INSTEON from homeassistant.components.fan import DOMAIN as FAN, SUPPORT_SET_SPEED, FanEntity from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.percentage import ( + int_states_in_range, percentage_to_ranged_value, ranged_value_to_percentage, ) @@ -48,6 +49,13 @@ class ISYFanEntity(ISYNodeEntity, FanEntity): return None return ranged_value_to_percentage(SPEED_RANGE, self._node.status) + @property + def speed_count(self) -> int: + """Return the number of speeds the fan supports.""" + if self._node.protocol == PROTO_INSTEON: + return 3 + return int_states_in_range(SPEED_RANGE) + @property def is_on(self) -> bool: """Get if the fan is on.""" @@ -95,6 +103,13 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity): return None return ranged_value_to_percentage(SPEED_RANGE, self._node.status) + @property + def speed_count(self) -> int: + """Return the number of speeds the fan supports.""" + if self._node.protocol == PROTO_INSTEON: + return 3 + return int_states_in_range(SPEED_RANGE) + @property def is_on(self) -> bool: """Get if the fan is on.""" diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index de08b576edd..d0b7b4c5546 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -7,6 +7,7 @@ from xknx.devices.fan import FanSpeedMode from homeassistant.components.fan import SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity from homeassistant.util.percentage import ( + int_states_in_range, percentage_to_ranged_value, ranged_value_to_percentage, ) @@ -68,6 +69,13 @@ class KNXFan(KnxEntity, FanEntity): ) return self._device.current_speed + @property + def speed_count(self) -> Optional[int]: + """Return the number of speeds the fan supports.""" + if self._step_range is None: + return super().speed_count + return int_states_in_range(self._step_range) + async def async_turn_on( self, speed: Optional[str] = None, diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py index 935c8827c84..330ff81d1d2 100644 --- a/homeassistant/components/lutron_caseta/fan.py +++ b/homeassistant/components/lutron_caseta/fan.py @@ -48,6 +48,11 @@ class LutronCasetaFan(LutronCasetaDevice, FanEntity): ORDERED_NAMED_FAN_SPEEDS, self._device["fan_speed"] ) + @property + def speed_count(self) -> int: + """Return the number of speeds the fan supports.""" + return len(ORDERED_NAMED_FAN_SPEEDS) + @property def supported_features(self) -> int: """Flag supported features. Speed Only.""" diff --git a/homeassistant/components/ozw/fan.py b/homeassistant/components/ozw/fan.py index be0bd372b65..505959dd343 100644 --- a/homeassistant/components/ozw/fan.py +++ b/homeassistant/components/ozw/fan.py @@ -9,6 +9,7 @@ from homeassistant.components.fan import ( from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.percentage import ( + int_states_in_range, percentage_to_ranged_value, ranged_value_to_percentage, ) @@ -72,6 +73,11 @@ class ZwaveFan(ZWaveDeviceEntity, FanEntity): """ return ranged_value_to_percentage(SPEED_RANGE, self.values.primary.value) + @property + def speed_count(self) -> int: + """Return the number of speeds the fan supports.""" + return int_states_in_range(SPEED_RANGE) + @property def supported_features(self): """Flag supported features.""" diff --git a/homeassistant/components/smartthings/fan.py b/homeassistant/components/smartthings/fan.py index b09bfe0ad46..12edac36dfe 100644 --- a/homeassistant/components/smartthings/fan.py +++ b/homeassistant/components/smartthings/fan.py @@ -6,6 +6,7 @@ from pysmartthings import Capability from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity from homeassistant.util.percentage import ( + int_states_in_range, percentage_to_ranged_value, ranged_value_to_percentage, ) @@ -79,6 +80,11 @@ class SmartThingsFan(SmartThingsEntity, FanEntity): """Return the current speed percentage.""" return ranged_value_to_percentage(SPEED_RANGE, self._device.status.fan_speed) + @property + def speed_count(self) -> Optional[int]: + """Return the number of speeds the fan supports.""" + return int_states_in_range(SPEED_RANGE) + @property def supported_features(self) -> int: """Flag supported features.""" diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index ac77b3dc333..18a7d8262e0 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -46,6 +46,7 @@ _LOGGER = logging.getLogger(__name__) CONF_FANS = "fans" CONF_SPEED_LIST = "speeds" +CONF_SPEED_COUNT = "speed_count" CONF_PRESET_MODES = "preset_modes" CONF_SPEED_TEMPLATE = "speed_template" CONF_PERCENTAGE_TEMPLATE = "percentage_template" @@ -86,6 +87,7 @@ FAN_SCHEMA = vol.All( vol.Optional(CONF_SET_PRESET_MODE_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_DIRECTION_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_SPEED_COUNT): vol.Coerce(int), vol.Optional( CONF_SPEED_LIST, default=[SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH], @@ -126,6 +128,7 @@ async def _async_create_entities(hass, config): set_direction_action = device_config.get(CONF_SET_DIRECTION_ACTION) speed_list = device_config[CONF_SPEED_LIST] + speed_count = device_config.get(CONF_SPEED_COUNT) preset_modes = device_config.get(CONF_PRESET_MODES) unique_id = device_config.get(CONF_UNIQUE_ID) @@ -148,6 +151,7 @@ async def _async_create_entities(hass, config): set_preset_mode_action, set_oscillating_action, set_direction_action, + speed_count, speed_list, preset_modes, unique_id, @@ -185,6 +189,7 @@ class TemplateFan(TemplateEntity, FanEntity): set_preset_mode_action, set_oscillating_action, set_direction_action, + speed_count, speed_list, preset_modes, unique_id, @@ -260,6 +265,9 @@ class TemplateFan(TemplateEntity, FanEntity): self._unique_id = unique_id + # Number of valid speeds + self._speed_count = speed_count + # List of valid speeds self._speed_list = speed_list @@ -281,6 +289,11 @@ class TemplateFan(TemplateEntity, FanEntity): """Flag supported features.""" return self._supported_features + @property + def speed_count(self) -> int: + """Return the number of speeds the fan supports.""" + return self._speed_count or super().speed_count + @property def speed_list(self) -> list: """Get the list of available speeds.""" diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index 12e963f05d3..4c555bb942a 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -102,6 +102,13 @@ class TuyaFanDevice(TuyaDevice, FanEntity): """Oscillate the fan.""" self._tuya.oscillate(oscillating) + @property + def speed_count(self) -> Optional[int]: + """Return the number of speeds the fan supports.""" + if self.speeds is None: + return super().speed_count + return len(self.speeds) + @property def oscillating(self): """Return current oscillating status.""" diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index 10754007ce6..e9f421215fb 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -6,6 +6,7 @@ from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.percentage import ( + int_states_in_range, percentage_to_ranged_value, ranged_value_to_percentage, ) @@ -77,6 +78,11 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): return ranged_value_to_percentage(SPEED_RANGE, current_level) return None + @property + def speed_count(self) -> int: + """Return the number of speeds the fan supports.""" + return int_states_in_range(SPEED_RANGE) + @property def preset_modes(self): """Get the list of available preset modes.""" diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 94dab468a69..1f45194659d 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -10,6 +10,7 @@ from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.percentage import ( + int_states_in_range, percentage_to_ranged_value, ranged_value_to_percentage, ) @@ -130,6 +131,11 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity): """Return the current speed percentage.""" return ranged_value_to_percentage(SPEED_RANGE, self._fan_mode) + @property + def speed_count(self) -> int: + """Return the number of speeds the fan supports.""" + return int_states_in_range(SPEED_RANGE) + @property def supported_features(self) -> int: """Flag supported features.""" diff --git a/homeassistant/components/wilight/fan.py b/homeassistant/components/wilight/fan.py index ece79874ccf..d663dc39ded 100644 --- a/homeassistant/components/wilight/fan.py +++ b/homeassistant/components/wilight/fan.py @@ -87,6 +87,11 @@ class WiLightFan(WiLightDevice, FanEntity): return None return ordered_list_item_to_percentage(ORDERED_NAMED_FAN_SPEEDS, wl_speed) + @property + def speed_count(self) -> int: + """Return the number of speeds the fan supports.""" + return len(ORDERED_NAMED_FAN_SPEEDS) + @property def current_direction(self) -> str: """Return the current direction of the fan.""" diff --git a/homeassistant/components/zwave/fan.py b/homeassistant/components/zwave/fan.py index ea529ccd90b..7fb0fb8e8be 100644 --- a/homeassistant/components/zwave/fan.py +++ b/homeassistant/components/zwave/fan.py @@ -5,6 +5,7 @@ from homeassistant.components.fan import DOMAIN, SUPPORT_SET_SPEED, FanEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.percentage import ( + int_states_in_range, percentage_to_ranged_value, ranged_value_to_percentage, ) @@ -68,6 +69,11 @@ class ZwaveFan(ZWaveDeviceEntity, FanEntity): """Return the current speed percentage.""" return ranged_value_to_percentage(SPEED_RANGE, self._state) + @property + def speed_count(self) -> int: + """Return the number of speeds the fan supports.""" + return int_states_in_range(SPEED_RANGE) + @property def supported_features(self): """Flag supported features.""" diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index e957d774e56..ae903d9efaf 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -13,6 +13,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.percentage import ( + int_states_in_range, percentage_to_ranged_value, ranged_value_to_percentage, ) @@ -96,6 +97,11 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): return None return ranged_value_to_percentage(SPEED_RANGE, self.info.primary_value.value) + @property + def speed_count(self) -> Optional[int]: + """Return the number of speeds the fan supports.""" + return int_states_in_range(SPEED_RANGE) + @property def supported_features(self) -> int: """Flag supported features.""" diff --git a/homeassistant/util/percentage.py b/homeassistant/util/percentage.py index fa4c9dcc252..10a72a85dff 100644 --- a/homeassistant/util/percentage.py +++ b/homeassistant/util/percentage.py @@ -67,7 +67,7 @@ def ranged_value_to_percentage( (1,255), 127: 50 (1,255), 10: 4 """ - return int((value * 100) // (low_high_range[1] - low_high_range[0] + 1)) + return int((value * 100) // states_in_range(low_high_range)) def percentage_to_ranged_value( @@ -84,4 +84,14 @@ def percentage_to_ranged_value( (1,255), 50: 127.5 (1,255), 4: 10.2 """ - return (low_high_range[1] - low_high_range[0] + 1) * percentage / 100 + return states_in_range(low_high_range) * percentage / 100 + + +def states_in_range(low_high_range: Tuple[float, float]) -> float: + """Given a range of low and high values return how many states exist.""" + return low_high_range[1] - low_high_range[0] + 1 + + +def int_states_in_range(low_high_range: Tuple[float, float]) -> int: + """Given a range of low and high values return how many integer states exist.""" + return int(states_in_range(low_high_range)) diff --git a/tests/components/demo/test_fan.py b/tests/components/demo/test_fan.py index 2439d49685c..a788e69b0d3 100644 --- a/tests/components/demo/test_fan.py +++ b/tests/components/demo/test_fan.py @@ -27,6 +27,7 @@ LIMITED_AND_FULL_FAN_ENTITY_IDS = FULL_FAN_ENTITY_IDS + [ FANS_WITH_PRESET_MODES = FULL_FAN_ENTITY_IDS + [ "fan.percentage_limited_fan", ] +PERCENTAGE_MODEL_FANS = ["fan.percentage_full_fan", "fan.percentage_limited_fan"] @pytest.fixture(autouse=True) @@ -397,6 +398,128 @@ async def test_set_percentage(hass, fan_entity_id): assert state.attributes[fan.ATTR_PERCENTAGE] == 33 +@pytest.mark.parametrize("fan_entity_id", LIMITED_AND_FULL_FAN_ENTITY_IDS) +async def test_increase_decrease_speed(hass, fan_entity_id): + """Test increasing and decreasing the percentage speed of the device.""" + state = hass.states.get(fan_entity_id) + assert state.state == STATE_OFF + assert state.attributes[fan.ATTR_PERCENTAGE_STEP] == 100 / 3 + + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_INCREASE_SPEED, + {ATTR_ENTITY_ID: fan_entity_id}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW + assert state.attributes[fan.ATTR_PERCENTAGE] == 33 + + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_INCREASE_SPEED, + {ATTR_ENTITY_ID: fan_entity_id}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM + assert state.attributes[fan.ATTR_PERCENTAGE] == 66 + + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_INCREASE_SPEED, + {ATTR_ENTITY_ID: fan_entity_id}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH + assert state.attributes[fan.ATTR_PERCENTAGE] == 100 + + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_INCREASE_SPEED, + {ATTR_ENTITY_ID: fan_entity_id}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH + assert state.attributes[fan.ATTR_PERCENTAGE] == 100 + + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_DECREASE_SPEED, + {ATTR_ENTITY_ID: fan_entity_id}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.attributes[fan.ATTR_PERCENTAGE] == 66 + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM + + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_DECREASE_SPEED, + {ATTR_ENTITY_ID: fan_entity_id}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW + assert state.attributes[fan.ATTR_PERCENTAGE] == 33 + + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_DECREASE_SPEED, + {ATTR_ENTITY_ID: fan_entity_id}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF + assert state.attributes[fan.ATTR_PERCENTAGE] == 0 + + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_DECREASE_SPEED, + {ATTR_ENTITY_ID: fan_entity_id}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF + assert state.attributes[fan.ATTR_PERCENTAGE] == 0 + + +@pytest.mark.parametrize("fan_entity_id", PERCENTAGE_MODEL_FANS) +async def test_increase_decrease_speed_with_percentage_step(hass, fan_entity_id): + """Test increasing speed with a percentage step.""" + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_INCREASE_SPEED, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE_STEP: 25}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW + assert state.attributes[fan.ATTR_PERCENTAGE] == 25 + + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_INCREASE_SPEED, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE_STEP: 25}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM + assert state.attributes[fan.ATTR_PERCENTAGE] == 50 + + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_INCREASE_SPEED, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE_STEP: 25}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH + assert state.attributes[fan.ATTR_PERCENTAGE] == 75 + + @pytest.mark.parametrize("fan_entity_id", FULL_FAN_ENTITY_IDS) async def test_oscillate(hass, fan_entity_id): """Test oscillating the fan.""" diff --git a/tests/components/fan/common.py b/tests/components/fan/common.py index 215849e6aab..c32686b9311 100644 --- a/tests/components/fan/common.py +++ b/tests/components/fan/common.py @@ -7,9 +7,12 @@ from homeassistant.components.fan import ( ATTR_DIRECTION, ATTR_OSCILLATING, ATTR_PERCENTAGE, + ATTR_PERCENTAGE_STEP, ATTR_PRESET_MODE, ATTR_SPEED, DOMAIN, + SERVICE_DECREASE_SPEED, + SERVICE_INCREASE_SPEED, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SERVICE_SET_PERCENTAGE, @@ -106,6 +109,38 @@ async def async_set_percentage( await hass.services.async_call(DOMAIN, SERVICE_SET_PERCENTAGE, data, blocking=True) +async def async_increase_speed( + hass, entity_id=ENTITY_MATCH_ALL, percentage_step: int = None +) -> None: + """Increase speed for all or specified fan.""" + data = { + key: value + for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_PERCENTAGE_STEP, percentage_step), + ] + if value is not None + } + + await hass.services.async_call(DOMAIN, SERVICE_INCREASE_SPEED, data, blocking=True) + + +async def async_decrease_speed( + hass, entity_id=ENTITY_MATCH_ALL, percentage_step: int = None +) -> None: + """Decrease speed for all or specified fan.""" + data = { + key: value + for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_PERCENTAGE_STEP, percentage_step), + ] + if value is not None + } + + await hass.services.async_call(DOMAIN, SERVICE_DECREASE_SPEED, data, blocking=True) + + async def async_set_direction( hass, entity_id=ENTITY_MATCH_ALL, direction: str = None ) -> None: diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index f5c303bd416..05ced3b8be7 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -19,6 +19,8 @@ def test_fanentity(): assert len(fan.speed_list) == 0 assert len(fan.preset_modes) == 0 assert fan.supported_features == 0 + assert fan.percentage_step == 1 + assert fan.speed_count == 100 assert fan.capability_attributes == {} # Test set_speed not required with pytest.raises(NotImplementedError): @@ -43,6 +45,8 @@ async def test_async_fanentity(hass): assert len(fan.speed_list) == 0 assert len(fan.preset_modes) == 0 assert fan.supported_features == 0 + assert fan.percentage_step == 1 + assert fan.speed_count == 100 assert fan.capability_attributes == {} # Test set_speed not required with pytest.raises(NotImplementedError): @@ -57,3 +61,7 @@ async def test_async_fanentity(hass): await fan.async_turn_on() with pytest.raises(NotImplementedError): await fan.async_turn_off() + with pytest.raises(NotImplementedError): + await fan.async_increase_speed() + with pytest.raises(NotImplementedError): + await fan.async_decrease_speed() diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index b3927ad3118..2b9059017c6 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -203,6 +203,7 @@ async def test_templates_with_entities(hass, calls): "preset_mode_template": "{{ states('input_select.preset_mode') }}", "oscillating_template": "{{ states('input_select.osc') }}", "direction_template": "{{ states('input_select.direction') }}", + "speed_count": "3", "set_percentage": { "service": "script.fans_set_speed", "data_template": {"percentage": "{{ percentage }}"}, @@ -648,6 +649,46 @@ async def test_set_percentage(hass, calls): _verify(hass, STATE_ON, SPEED_MEDIUM, 50, None, None, None) +async def test_increase_decrease_speed(hass, calls): + """Test set valid increase and derease speed.""" + await _register_components(hass) + + # Turn on fan + await common.async_turn_on(hass, _TEST_FAN) + + # Set fan's percentage speed to 100 + await common.async_set_percentage(hass, _TEST_FAN, 100) + + # verify + assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 100 + + _verify(hass, STATE_ON, SPEED_HIGH, 100, None, None, None) + + # Set fan's percentage speed to 66 + await common.async_decrease_speed(hass, _TEST_FAN) + assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 66 + + _verify(hass, STATE_ON, SPEED_MEDIUM, 66, None, None, None) + + # Set fan's percentage speed to 33 + await common.async_decrease_speed(hass, _TEST_FAN) + assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 33 + + _verify(hass, STATE_ON, SPEED_LOW, 33, None, None, None) + + # Set fan's percentage speed to 0 + await common.async_decrease_speed(hass, _TEST_FAN) + assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 0 + + _verify(hass, STATE_OFF, SPEED_OFF, 0, None, None, None) + + # Set fan's percentage speed to 33 + await common.async_increase_speed(hass, _TEST_FAN) + assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 33 + + _verify(hass, STATE_ON, SPEED_LOW, 33, None, None, None) + + async def test_set_invalid_speed_from_initial_stage(hass, calls): """Test set invalid speed when fan is in initial state.""" await _register_components(hass) From 0ed0c7c02603e4917f1dcf52901cfcb022ac9e7d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Feb 2021 21:54:40 -1000 Subject: [PATCH 546/796] Fix backwards compatibility with vesync fans (#45950) --- homeassistant/components/vesync/fan.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index e9f421215fb..1d1320d8d78 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -91,8 +91,8 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): @property def preset_mode(self): """Get the current preset mode.""" - if self.smartfan.mode == FAN_MODE_AUTO: - return FAN_MODE_AUTO + if self.smartfan.mode in (FAN_MODE_AUTO, FAN_MODE_SLEEP): + return self.smartfan.mode return None @property @@ -136,7 +136,11 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): if not self.smartfan.is_on: self.smartfan.turn_on() - self.smartfan.auto_mode() + if preset_mode == FAN_MODE_AUTO: + self.smartfan.auto_mode() + elif preset_mode == FAN_MODE_SLEEP: + self.smartfan.sleep_mode() + self.schedule_update_ha_state() def turn_on( @@ -150,4 +154,6 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): if preset_mode: self.set_preset_mode(preset_mode) return + if percentage is None: + percentage = 50 self.set_percentage(percentage) From 40068c2f1bae65dd48e4be212ec03c0cfd3e1204 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Feb 2021 08:57:28 +0100 Subject: [PATCH 547/796] Bump actions/stale from v3.0.16 to v3.0.17 (#46777) --- .github/workflows/stale.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index fd8ca4eb477..a280d0ee89a 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -16,7 +16,7 @@ jobs: # - No PRs marked as no-stale # - No issues marked as no-stale or help-wanted - name: 90 days stale issues & PRs policy - uses: actions/stale@v3.0.16 + uses: actions/stale@v3.0.17 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 90 @@ -53,7 +53,7 @@ jobs: # - No PRs marked as no-stale or new-integrations # - No issues (-1) - name: 30 days stale PRs policy - uses: actions/stale@v3.0.16 + uses: actions/stale@v3.0.17 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 30 @@ -78,7 +78,7 @@ jobs: # - No Issues marked as no-stale or help-wanted # - No PRs (-1) - name: Needs more information stale issues policy - uses: actions/stale@v3.0.16 + uses: actions/stale@v3.0.17 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-labels: "needs-more-information" From e7e3e09063678e6154a1dc7cd83baae6c08bb576 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 19 Feb 2021 13:14:47 +0100 Subject: [PATCH 548/796] Raise ConditionError for zone errors (#46253) * Raise ConditionError for zone errors * Do not test missing state * Handle multiple entities/zones --- .../components/geo_location/trigger.py | 7 +- homeassistant/components/zone/trigger.py | 2 +- homeassistant/helpers/condition.py | 56 +++++++++---- tests/helpers/test_condition.py | 80 +++++++++++++++++++ 4 files changed, 127 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py index aad281da117..9ca8c86e150 100644 --- a/homeassistant/components/geo_location/trigger.py +++ b/homeassistant/components/geo_location/trigger.py @@ -48,8 +48,11 @@ async def async_attach_trigger(hass, config, action, automation_info): return zone_state = hass.states.get(zone_entity_id) - from_match = condition.zone(hass, zone_state, from_state) - to_match = condition.zone(hass, zone_state, to_state) + + from_match = ( + condition.zone(hass, zone_state, from_state) if from_state else False + ) + to_match = condition.zone(hass, zone_state, to_state) if to_state else False if ( trigger_event == EVENT_ENTER diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py index bc827c2ba0d..a5f89a7515d 100644 --- a/homeassistant/components/zone/trigger.py +++ b/homeassistant/components/zone/trigger.py @@ -58,7 +58,7 @@ async def async_attach_trigger( zone_state = hass.states.get(zone_entity_id) from_match = condition.zone(hass, zone_state, from_s) if from_s else False - to_match = condition.zone(hass, zone_state, to_s) + to_match = condition.zone(hass, zone_state, to_s) if to_s else False if ( event == EVENT_ENTER diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 2f63498c9cd..e09176dc098 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -588,23 +588,36 @@ def zone( Async friendly. """ + if zone_ent is None: + raise ConditionError("No zone specified") + if isinstance(zone_ent, str): + zone_ent_id = zone_ent zone_ent = hass.states.get(zone_ent) - if zone_ent is None: - return False - - if isinstance(entity, str): - entity = hass.states.get(entity) + if zone_ent is None: + raise ConditionError(f"Unknown zone {zone_ent_id}") if entity is None: - return False + raise ConditionError("No entity specified") + + if isinstance(entity, str): + entity_id = entity + entity = hass.states.get(entity) + + if entity is None: + raise ConditionError(f"Unknown entity {entity_id}") + else: + entity_id = entity.entity_id latitude = entity.attributes.get(ATTR_LATITUDE) longitude = entity.attributes.get(ATTR_LONGITUDE) - if latitude is None or longitude is None: - return False + if latitude is None: + raise ConditionError(f"Entity {entity_id} has no 'latitude' attribute") + + if longitude is None: + raise ConditionError(f"Entity {entity_id} has no 'longitude' attribute") return zone_cmp.in_zone( zone_ent, latitude, longitude, entity.attributes.get(ATTR_GPS_ACCURACY, 0) @@ -622,13 +635,26 @@ def zone_from_config( def if_in_zone(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: """Test if condition.""" - return all( - any( - zone(hass, zone_entity_id, entity_id) - for zone_entity_id in zone_entity_ids - ) - for entity_id in entity_ids - ) + errors = [] + + all_ok = True + for entity_id in entity_ids: + entity_ok = False + for zone_entity_id in zone_entity_ids: + try: + if zone(hass, zone_entity_id, entity_id): + entity_ok = True + except ConditionError as ex: + errors.append(str(ex)) + + if not entity_ok: + all_ok = False + + # Raise the errors only if no definitive result was found + if errors and not all_ok: + raise ConditionError("Error in 'zone' condition: " + ", ".join(errors)) + + return all_ok return if_in_zone diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 1fc1e07da5d..0db8220bc50 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -811,6 +811,86 @@ async def test_numeric_state_using_input_number(hass): ) +async def test_zone_raises(hass): + """Test that zone raises ConditionError on errors.""" + test = await condition.async_from_config( + hass, + { + "condition": "zone", + "entity_id": "device_tracker.cat", + "zone": "zone.home", + }, + ) + + with pytest.raises(ConditionError, match="Unknown zone"): + test(hass) + + hass.states.async_set( + "zone.home", + "zoning", + {"name": "home", "latitude": 2.1, "longitude": 1.1, "radius": 10}, + ) + + with pytest.raises(ConditionError, match="Unknown entity"): + test(hass) + + hass.states.async_set( + "device_tracker.cat", + "home", + {"friendly_name": "cat"}, + ) + + with pytest.raises(ConditionError, match="latitude"): + test(hass) + + hass.states.async_set( + "device_tracker.cat", + "home", + {"friendly_name": "cat", "latitude": 2.1}, + ) + + with pytest.raises(ConditionError, match="longitude"): + test(hass) + + hass.states.async_set( + "device_tracker.cat", + "home", + {"friendly_name": "cat", "latitude": 2.1, "longitude": 1.1}, + ) + + # All okay, now test multiple failed conditions + assert test(hass) + + test = await condition.async_from_config( + hass, + { + "condition": "zone", + "entity_id": ["device_tracker.cat", "device_tracker.dog"], + "zone": ["zone.home", "zone.work"], + }, + ) + + with pytest.raises(ConditionError, match="dog"): + test(hass) + + with pytest.raises(ConditionError, match="work"): + test(hass) + + hass.states.async_set( + "zone.work", + "zoning", + {"name": "work", "latitude": 20, "longitude": 10, "radius": 25000}, + ) + + hass.states.async_set( + "device_tracker.dog", + "work", + {"friendly_name": "dog", "latitude": 20.1, "longitude": 10.1}, + ) + + assert test(hass) + + async def test_zone_multiple_entities(hass): """Test with multiple entities in condition.""" test = await condition.async_from_config( From bfea7d0baa0c94e9f048972cf4645d11ff8c7380 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 19 Feb 2021 13:15:30 +0100 Subject: [PATCH 549/796] Raise ConditionError for and/or/not errors (#46767) --- .../components/automation/__init__.py | 17 +++++-- homeassistant/helpers/condition.py | 48 +++++++++++++------ tests/helpers/test_condition.py | 20 ++++++-- 3 files changed, 63 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index ae8c71b4fb8..3a48b3e3cc2 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -615,12 +615,21 @@ async def _async_process_if(hass, config, p_config): def if_action(variables=None): """AND all conditions.""" - try: - return all(check(hass, variables) for check in checks) - except ConditionError as ex: - LOGGER.warning("Error in 'condition' evaluation: %s", ex) + errors = [] + for check in checks: + try: + if not check(hass, variables): + return False + except ConditionError as ex: + errors.append(f"Error in 'condition' evaluation: {ex}") + + if errors: + for error in errors: + LOGGER.warning("%s", error) return False + return True + if_action.config = if_configs return if_action diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index e09176dc098..b66ee6c7976 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -108,13 +108,19 @@ async def async_and_from_config( hass: HomeAssistant, variables: TemplateVarsType = None ) -> bool: """Test and condition.""" - try: - for check in checks: + errors = [] + for check in checks: + try: if not check(hass, variables): return False - except Exception as ex: # pylint: disable=broad-except - _LOGGER.warning("Error during and-condition: %s", ex) - return False + except ConditionError as ex: + errors.append(str(ex)) + except Exception as ex: # pylint: disable=broad-except + errors.append(str(ex)) + + # Raise the errors if no check was false + if errors: + raise ConditionError("Error in 'and' condition: " + ", ".join(errors)) return True @@ -134,13 +140,20 @@ async def async_or_from_config( def if_or_condition( hass: HomeAssistant, variables: TemplateVarsType = None ) -> bool: - """Test and condition.""" - try: - for check in checks: + """Test or condition.""" + errors = [] + for check in checks: + try: if check(hass, variables): return True - except Exception as ex: # pylint: disable=broad-except - _LOGGER.warning("Error during or-condition: %s", ex) + except ConditionError as ex: + errors.append(str(ex)) + except Exception as ex: # pylint: disable=broad-except + errors.append(str(ex)) + + # Raise the errors if no check was true + if errors: + raise ConditionError("Error in 'or' condition: " + ", ".join(errors)) return False @@ -161,12 +174,19 @@ async def async_not_from_config( hass: HomeAssistant, variables: TemplateVarsType = None ) -> bool: """Test not condition.""" - try: - for check in checks: + errors = [] + for check in checks: + try: if check(hass, variables): return False - except Exception as ex: # pylint: disable=broad-except - _LOGGER.warning("Error during not-condition: %s", ex) + except ConditionError as ex: + errors.append(str(ex)) + except Exception as ex: # pylint: disable=broad-except + errors.append(str(ex)) + + # Raise the errors if no check was true + if errors: + raise ConditionError("Error in 'not' condition: " + ", ".join(errors)) return True diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 0db8220bc50..3e7833b24dd 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -50,6 +50,9 @@ async def test_and_condition(hass): }, ) + with pytest.raises(ConditionError): + test(hass) + hass.states.async_set("sensor.temperature", 120) assert not test(hass) @@ -111,6 +114,9 @@ async def test_or_condition(hass): }, ) + with pytest.raises(ConditionError): + test(hass) + hass.states.async_set("sensor.temperature", 120) assert not test(hass) @@ -169,6 +175,9 @@ async def test_not_condition(hass): }, ) + with pytest.raises(ConditionError): + test(hass) + hass.states.async_set("sensor.temperature", 101) assert test(hass) @@ -466,7 +475,8 @@ async def test_state_attribute(hass): ) hass.states.async_set("sensor.temperature", 100, {"unkown_attr": 200}) - assert not test(hass) + with pytest.raises(ConditionError): + test(hass) hass.states.async_set("sensor.temperature", 100, {"attribute1": 200}) assert test(hass) @@ -720,7 +730,7 @@ async def test_numeric_state_multiple_entities(hass): assert not test(hass) -async def test_numberic_state_attribute(hass): +async def test_numeric_state_attribute(hass): """Test with numeric state attribute in condition.""" test = await condition.async_from_config( hass, @@ -738,7 +748,8 @@ async def test_numberic_state_attribute(hass): ) hass.states.async_set("sensor.temperature", 100, {"unkown_attr": 10}) - assert not test(hass) + with pytest.raises(ConditionError): + assert test(hass) hass.states.async_set("sensor.temperature", 100, {"attribute1": 49}) assert test(hass) @@ -750,7 +761,8 @@ async def test_numberic_state_attribute(hass): assert not test(hass) hass.states.async_set("sensor.temperature", 100, {"attribute1": None}) - assert not test(hass) + with pytest.raises(ConditionError): + assert test(hass) async def test_numeric_state_using_input_number(hass): From d2a187a57bc52763e466b82a737e7dd584dcb381 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 19 Feb 2021 13:28:00 +0100 Subject: [PATCH 550/796] Bump RMVtransport to 0.3.1 (#46780) --- homeassistant/components/rmvtransport/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rmvtransport/manifest.json b/homeassistant/components/rmvtransport/manifest.json index 30aef1a0616..68f895cb2b8 100644 --- a/homeassistant/components/rmvtransport/manifest.json +++ b/homeassistant/components/rmvtransport/manifest.json @@ -3,7 +3,7 @@ "name": "RMV", "documentation": "https://www.home-assistant.io/integrations/rmvtransport", "requirements": [ - "PyRMVtransport==0.3.0" + "PyRMVtransport==0.3.1" ], "codeowners": [ "@cgtobi" diff --git a/requirements_all.txt b/requirements_all.txt index 1e9b7295946..082b6db5dd2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -49,7 +49,7 @@ PyNaCl==1.3.0 PyQRCode==1.2.1 # homeassistant.components.rmvtransport -PyRMVtransport==0.3.0 +PyRMVtransport==0.3.1 # homeassistant.components.telegram_bot PySocks==1.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 71bcca6fe2f..74080a3945e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -21,7 +21,7 @@ PyNaCl==1.3.0 PyQRCode==1.2.1 # homeassistant.components.rmvtransport -PyRMVtransport==0.3.0 +PyRMVtransport==0.3.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 6ad7020f99f82e86698e4ae1d457d4d2a7ac8dd8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 19 Feb 2021 13:30:27 +0100 Subject: [PATCH 551/796] Add Home Assistant color (#46751) --- homeassistant/components/light/services.yaml | 2 ++ homeassistant/util/color.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index f777cd3d348..2161ed3f81d 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -30,6 +30,7 @@ turn_on: selector: select: options: + - "homeassistant" - "aliceblue" - "antiquewhite" - "aqua" @@ -368,6 +369,7 @@ toggle: selector: select: options: + - "homeassistant" - "aliceblue" - "antiquewhite" - "aqua" diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 1e782f0c0f9..9a5fbdb180f 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -160,6 +160,8 @@ COLORS = { "whitesmoke": (245, 245, 245), "yellow": (255, 255, 0), "yellowgreen": (154, 205, 50), + # And... + "homeassistant": (3, 169, 244), } From 3e82509263098fd8940ba086444e04e16404163e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 19 Feb 2021 15:23:59 +0100 Subject: [PATCH 552/796] Use string instead of slug for add-on slug validation (#46784) --- homeassistant/components/hassio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 1c246ae753b..4f6f0ed8348 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -76,7 +76,7 @@ SERVICE_RESTORE_PARTIAL = "restore_partial" SCHEMA_NO_DATA = vol.Schema({}) -SCHEMA_ADDON = vol.Schema({vol.Required(ATTR_ADDON): cv.slug}) +SCHEMA_ADDON = vol.Schema({vol.Required(ATTR_ADDON): cv.string}) SCHEMA_ADDON_STDIN = SCHEMA_ADDON.extend( {vol.Required(ATTR_INPUT): vol.Any(dict, cv.string)} From 250327fac45e5bfb177fe7d5d6d60d674d16e02a Mon Sep 17 00:00:00 2001 From: Andreas Oetken Date: Fri, 19 Feb 2021 15:56:20 +0100 Subject: [PATCH 553/796] Allow multiple recipients for XMPP (#45328) * recipient is a string list instead of a single string now * this does NOT break existing automations/etc using this component --- homeassistant/components/xmpp/notify.py | 41 +++++++++++++------------ 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 78dc3c43032..68a041a2887 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -55,7 +55,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_SENDER): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_RECIPIENT): cv.string, + vol.Required(CONF_RECIPIENT): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_RESOURCE, default=DEFAULT_RESOURCE): cv.string, vol.Optional(CONF_ROOM, default=""): cv.string, vol.Optional(CONF_TLS, default=True): cv.boolean, @@ -87,7 +87,7 @@ class XmppNotificationService(BaseNotificationService): self._sender = sender self._resource = resource self._password = password - self._recipient = recipient + self._recipients = recipient self._tls = tls self._verify = verify self._room = room @@ -102,7 +102,7 @@ class XmppNotificationService(BaseNotificationService): await async_send_message( f"{self._sender}/{self._resource}", self._password, - self._recipient, + self._recipients, self._tls, self._verify, self._room, @@ -116,7 +116,7 @@ class XmppNotificationService(BaseNotificationService): async def async_send_message( sender, password, - recipient, + recipients, use_tls, verify_certificate, room, @@ -182,19 +182,21 @@ async def async_send_message( url = await self.upload_file(timeout=timeout) _LOGGER.info("Upload success") - if room: - _LOGGER.info("Sending file to %s", room) - message = self.Message(sto=room, stype="groupchat") - else: - _LOGGER.info("Sending file to %s", recipient) - message = self.Message(sto=recipient, stype="chat") - - message["body"] = url - message["oob"]["url"] = url - try: - message.send() - except (IqError, IqTimeout, XMPPError) as ex: - _LOGGER.error("Could not send image message %s", ex) + for recipient in recipients: + if room: + _LOGGER.info("Sending file to %s", room) + message = self.Message(sto=room, stype="groupchat") + else: + _LOGGER.info("Sending file to %s", recipient) + message = self.Message(sto=recipient, stype="chat") + message["body"] = url + message["oob"]["url"] = url + try: + message.send() + except (IqError, IqTimeout, XMPPError) as ex: + _LOGGER.error("Could not send image message %s", ex) + if room: + break except (IqError, IqTimeout, XMPPError) as ex: _LOGGER.error("Upload error, could not send message %s", ex) except NotConnectedError as ex: @@ -336,8 +338,9 @@ async def async_send_message( self.plugin["xep_0045"].join_muc(room, sender, wait=True) self.send_message(mto=room, mbody=message, mtype="groupchat") else: - _LOGGER.debug("Sending message to %s", recipient) - self.send_message(mto=recipient, mbody=message, mtype="chat") + for recipient in recipients: + _LOGGER.debug("Sending message to %s", recipient) + self.send_message(mto=recipient, mbody=message, mtype="chat") except (IqError, IqTimeout, XMPPError) as ex: _LOGGER.error("Could not send text message %s", ex) except NotConnectedError as ex: From 32bec5ea6320e0950e55fff457d1dc1b2e0cea3a Mon Sep 17 00:00:00 2001 From: Denise Yu Date: Fri, 19 Feb 2021 15:23:34 -0500 Subject: [PATCH 554/796] Update GitHub Issue Form template (#46791) This PR fixes the bug report template! Details: https://gh-community.github.io/issue-template-feedback/changes/#what-do-i-need-to-do --- .github/ISSUE_TEMPLATE/bug_report.yml | 28 +++++++++++++-------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 88e8afed67d..9a46dd82215 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -2,8 +2,8 @@ name: Report an issue with Home Assistant Core about: Report an issue with Home Assistant Core. title: "" issue_body: true -inputs: - - type: description +body: + - type: markdown attributes: value: | This issue form is for reporting bugs only! @@ -12,39 +12,41 @@ inputs: [fr]: https://community.home-assistant.io/c/feature-requests - type: textarea + validations: + required: true attributes: label: The problem - required: true description: >- Describe the issue you are experiencing here to communicate to the maintainers. Tell us what you were trying to do and what happened. Provide a clear and concise description of what the problem is. - - type: description + - type: markdown attributes: value: | ## Environment - type: input + validations: + required: true attributes: label: What is version of Home Assistant Core has the issue? - required: true placeholder: core- description: > Can be found in the Configuration panel -> Info. - type: input attributes: label: What was the last working version of Home Assistant Core? - required: false placeholder: core- description: > If known, otherwise leave blank. - type: dropdown + validations: + required: true attributes: label: What type of installation are you running? - required: true description: > If you don't know, you can find it in: Configuration panel -> Info. - choices: + options: - Home Assistant OS - Home Assistant Container - Home Assistant Supervised @@ -52,13 +54,11 @@ inputs: - type: input attributes: label: Integration causing the issue - required: false description: > The name of the integration, for example, Automation or Philips Hue. - type: input attributes: label: Link to integration documentation on our website - required: false placeholder: "https://www.home-assistant.io/integrations/..." description: | Providing a link [to the documentation][docs] help us categorizing the @@ -66,14 +66,13 @@ inputs: [docs]: https://www.home-assistant.io/integrations - - type: description + - type: markdown attributes: value: | # Details - type: textarea attributes: label: Example YAML snippet - required: false description: | If this issue has an example piece of YAML that can help reproducing this problem, please provide. This can be an piece of YAML from, e.g., an automation, script, scene or configuration. @@ -86,17 +85,16 @@ inputs: attributes: label: Anything in the logs that might be useful for us? description: For example, error message, or stack traces. - required: false value: | ```txt # Put your logs below this line ``` - - type: description + - type: markdown attributes: value: | ## Additional information - - type: description + - type: markdown attributes: value: > If you have any additional information for us, use the field below. From 4d23ffacd1a2b1e1841500715ada00a872b68961 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 19 Feb 2021 18:20:10 -0500 Subject: [PATCH 555/796] Add zwave_js thermostat fan mode and fan state support (#46793) * add thermostat fan mode and fan state support * return when fan mode is not supported * use get just in case * validate state key is in states so we dont have to use get * pylint --- homeassistant/components/zwave_js/climate.py | 69 ++++++++++++++++++- tests/components/zwave_js/test_climate.py | 56 +++++++++++++++ ...ate_radio_thermostat_ct100_plus_state.json | 46 +++++++++++++ 3 files changed, 170 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 689187f0b34..341b8f99fd6 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -1,5 +1,5 @@ """Representation of Z-Wave thermostats.""" -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable, Dict, List, Optional, cast from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import ( @@ -33,6 +33,7 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, PRESET_NONE, + SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, @@ -81,6 +82,8 @@ HVAC_CURRENT_MAP: Dict[int, str] = { ThermostatOperatingState.THIRD_STAGE_AUX_HEAT: CURRENT_HVAC_HEAT, } +ATTR_FAN_STATE = "fan_state" + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable @@ -148,6 +151,16 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): add_to_watched_value_ids=True, check_all_endpoints=True, ) + self._fan_mode = self.get_zwave_value( + THERMOSTAT_MODE_PROPERTY, + CommandClass.THERMOSTAT_FAN_MODE, + add_to_watched_value_ids=True, + ) + self._fan_state = self.get_zwave_value( + THERMOSTAT_OPERATING_STATE_PROPERTY, + CommandClass.THERMOSTAT_FAN_STATE, + add_to_watched_value_ids=True, + ) self._set_modes_and_presets() def _setpoint_value(self, setpoint_type: ThermostatSetpointType) -> ZwaveValue: @@ -275,6 +288,40 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): """Return a list of available preset modes.""" return list(self._hvac_presets) + @property + def fan_mode(self) -> Optional[str]: + """Return the fan setting.""" + if ( + self._fan_mode + and self._fan_mode.value is not None + and str(self._fan_mode.value) in self._fan_mode.metadata.states + ): + return cast(str, self._fan_mode.metadata.states[str(self._fan_mode.value)]) + return None + + @property + def fan_modes(self) -> Optional[List[str]]: + """Return the list of available fan modes.""" + if self._fan_mode and self._fan_mode.metadata.states: + return list(self._fan_mode.metadata.states.values()) + return None + + @property + def device_state_attributes(self) -> Optional[Dict[str, str]]: + """Return the optional state attributes.""" + if ( + self._fan_state + and self._fan_state.value is not None + and str(self._fan_state.value) in self._fan_state.metadata.states + ): + return { + ATTR_FAN_STATE: self._fan_state.metadata.states[ + str(self._fan_state.value) + ] + } + + return None + @property def supported_features(self) -> int: """Return the list of supported features.""" @@ -283,8 +330,28 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): support |= SUPPORT_TARGET_TEMPERATURE if len(self._current_mode_setpoint_enums) > 1: support |= SUPPORT_TARGET_TEMPERATURE_RANGE + if self._fan_mode: + support |= SUPPORT_FAN_MODE return support + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + if not self._fan_mode: + return + + try: + new_state = int( + next( + state + for state, label in self._fan_mode.metadata.states.items() + if label == fan_mode + ) + ) + except StopIteration: + raise ValueError(f"Received an invalid fan mode: {fan_mode}") from None + + await self.info.node.async_set_value(self._fan_mode, new_state) + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" assert self.hass diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index e11d3b75c47..7336acd82eb 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -5,6 +5,7 @@ from zwave_js_server.event import Event from homeassistant.components.climate.const import ( ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, + ATTR_FAN_MODE, ATTR_HVAC_ACTION, ATTR_HVAC_MODE, ATTR_HVAC_MODES, @@ -19,10 +20,12 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, PRESET_NONE, + SERVICE_SET_FAN_MODE, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, SERVICE_SET_TEMPERATURE, ) +from homeassistant.components.zwave_js.climate import ATTR_FAN_STATE from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE CLIMATE_RADIO_THERMOSTAT_ENTITY = "climate.z_wave_thermostat" @@ -50,6 +53,8 @@ async def test_thermostat_v2( assert state.attributes[ATTR_TEMPERATURE] == 22.2 assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + assert state.attributes[ATTR_FAN_MODE] == "Auto low" + assert state.attributes[ATTR_FAN_STATE] == "Idle / off" # Test setting preset mode await hass.services.async_call( @@ -329,6 +334,57 @@ async def test_thermostat_v2( blocking=True, ) + client.async_send_command.reset_mock() + + # Test setting fan mode + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + { + ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY, + ATTR_FAN_MODE: "Low", + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 13 + assert args["valueId"] == { + "endpoint": 1, + "commandClass": 68, + "commandClassName": "Thermostat Fan Mode", + "property": "mode", + "propertyName": "mode", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "min": 0, + "max": 255, + "states": {"0": "Auto low", "1": "Low"}, + "label": "Thermostat fan mode", + }, + "value": 0, + } + assert args["value"] == 1 + + client.async_send_command.reset_mock() + + # Test setting invalid fan mode + with pytest.raises(ValueError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + { + ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY, + ATTR_FAN_MODE: "fake value", + }, + blocking=True, + ) + async def test_thermostat_different_endpoints( hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, integration diff --git a/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_state.json b/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_state.json index 77a68aafde1..caad22aac36 100644 --- a/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_state.json +++ b/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_state.json @@ -594,6 +594,52 @@ "propertyName": "manufacturerData", "metadata": { "type": "any", "readable": true, "writeable": true } }, + { + "endpoint": 1, + "commandClass": 68, + "commandClassName": "Thermostat Fan Mode", + "property": "mode", + "propertyName": "mode", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "states": { "0": "Auto low", "1": "Low" }, + "label": "Thermostat fan mode" + }, + "value": 0 + }, + { + "endpoint": 1, + "commandClass": 69, + "commandClassName": "Thermostat Fan State", + "property": "state", + "propertyName": "state", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "states": { + "0": "Idle / off", + "1": "Running / running low", + "2": "Running high", + "3": "Running medium", + "4": "Circulation mode", + "5": "Humidity circulation mode", + "6": "Right - left circulation mode", + "7": "Up - down circulation mode", + "8": "Quiet circulation mode" + }, + "label": "Thermostat fan state" + }, + "value": 0 + }, { "commandClassName": "Thermostat Operating State", "commandClass": 66, From 47dcd2bf32ad36cd4734b439856fb6e0d417ad3e Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 19 Feb 2021 18:44:15 -0500 Subject: [PATCH 556/796] Format zwave_js dump as json (#46792) --- homeassistant/components/zwave_js/api.py | 6 +++--- tests/components/zwave_js/test_api.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 91c316d307e..bed2166a4da 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -281,9 +281,9 @@ class DumpView(HomeAssistantView): msgs = await dump.dump_msgs(entry.data[CONF_URL], async_get_clientsession(hass)) return web.Response( - body="\n".join(json.dumps(msg) for msg in msgs) + "\n", + body=json.dumps(msgs, indent=2) + "\n", headers={ - hdrs.CONTENT_TYPE: "application/jsonl", - hdrs.CONTENT_DISPOSITION: 'attachment; filename="zwave_js_dump.jsonl"', + hdrs.CONTENT_TYPE: "application/json", + hdrs.CONTENT_DISPOSITION: 'attachment; filename="zwave_js_dump.json"', }, ) diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 88e8acc5771..a36743421c9 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS Websocket API.""" +import json from unittest.mock import patch from zwave_js_server.event import Event @@ -164,7 +165,7 @@ async def test_dump_view(integration, hass_client): ): resp = await client.get(f"/api/zwave_js/dump/{integration.entry_id}") assert resp.status == 200 - assert await resp.text() == '{"hello": "world"}\n{"second": "msg"}\n' + assert json.loads(await resp.text()) == [{"hello": "world"}, {"second": "msg"}] async def test_dump_view_invalid_entry_id(integration, hass_client): From fd60d4273b554a1b94391e1fc7de70ccd89ec0c1 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 20 Feb 2021 00:08:25 +0000 Subject: [PATCH 557/796] [ci skip] Translation update --- .../components/airvisual/translations/es.json | 8 +++- .../components/asuswrt/translations/es.json | 32 ++++++++++++- .../components/econet/translations/es.json | 3 +- .../components/fritzbox/translations/es.json | 3 +- .../fritzbox_callmonitor/translations/es.json | 12 +++++ .../components/habitica/translations/es.json | 15 ++++++ .../components/habitica/translations/tr.json | 3 ++ .../components/homekit/translations/es.json | 5 +- .../keenetic_ndms2/translations/es.json | 16 +++++++ .../lutron_caseta/translations/es.json | 19 ++++++++ .../components/mazda/translations/es.json | 26 ++++++++++ .../media_player/translations/es.json | 7 +++ .../components/mysensors/translations/es.json | 47 ++++++++++++++++++- .../components/number/translations/es.json | 5 ++ .../philips_js/translations/es.json | 3 +- .../components/plaato/translations/es.json | 10 ++++ .../components/powerwall/translations/no.json | 2 +- .../components/smarttub/translations/en.json | 3 +- .../components/smarttub/translations/es.json | 10 ++++ .../components/tuya/translations/es.json | 2 + .../components/unifi/translations/es.json | 1 + .../xiaomi_miio/translations/es.json | 10 +++- .../xiaomi_miio/translations/tr.json | 5 ++ .../components/zwave_js/translations/es.json | 5 +- 24 files changed, 238 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/habitica/translations/es.json create mode 100644 homeassistant/components/habitica/translations/tr.json create mode 100644 homeassistant/components/mazda/translations/es.json create mode 100644 homeassistant/components/smarttub/translations/es.json diff --git a/homeassistant/components/airvisual/translations/es.json b/homeassistant/components/airvisual/translations/es.json index 9b3ac467b84..53768f679be 100644 --- a/homeassistant/components/airvisual/translations/es.json +++ b/homeassistant/components/airvisual/translations/es.json @@ -25,7 +25,9 @@ "api_key": "Clave API", "latitude": "Latitud", "longitude": "Longitud" - } + }, + "description": "Utilice la API de la nube de AirVisual para supervisar una latitud/longitud.", + "title": "Configurar una geograf\u00eda" }, "geography_by_name": { "data": { @@ -33,7 +35,9 @@ "city": "Ciudad", "country": "Pa\u00eds", "state": "estado" - } + }, + "description": "Utilice la API de la nube de AirVisual para supervisar una ciudad/estado/pa\u00eds.", + "title": "Configurar una geograf\u00eda" }, "node_pro": { "data": { diff --git a/homeassistant/components/asuswrt/translations/es.json b/homeassistant/components/asuswrt/translations/es.json index 3531e0a3cca..c5792babf00 100644 --- a/homeassistant/components/asuswrt/translations/es.json +++ b/homeassistant/components/asuswrt/translations/es.json @@ -1,16 +1,44 @@ { "config": { + "abort": { + "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, "error": { + "cannot_connect": "No se pudo conectar", + "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos", + "pwd_and_ssh": "S\u00f3lo proporcionar la contrase\u00f1a o el archivo de clave SSH", + "pwd_or_ssh": "Por favor, proporcione la contrase\u00f1a o el archivo de clave SSH", + "ssh_not_file": "Archivo de clave SSH no encontrado", "unknown": "Error inesperado" }, "step": { "user": { "data": { + "host": "Host", "mode": "Modo", "name": "Nombre", "password": "Contrase\u00f1a", - "port": "Puerto" - } + "port": "Puerto", + "protocol": "Protocolo de comunicaci\u00f3n a utilizar", + "ssh_key": "Ruta de acceso a su archivo de clave SSH (en lugar de la contrase\u00f1a)", + "username": "Nombre de usuario" + }, + "description": "Establezca los par\u00e1metros necesarios para conectarse a su router", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Segundos de espera antes de considerar un dispositivo ausente", + "dnsmasq": "La ubicaci\u00f3n en el router de los archivos dnsmasq.leases", + "interface": "La interfaz de la que desea obtener estad\u00edsticas (por ejemplo, eth0, eth1, etc.)", + "require_ip": "Los dispositivos deben tener IP (para el modo de punto de acceso)", + "track_unknown": "Seguimiento de dispositivos desconocidos / sin nombre" + }, + "title": "Opciones de AsusWRT" } } } diff --git a/homeassistant/components/econet/translations/es.json b/homeassistant/components/econet/translations/es.json index ac69f8f7be1..8634be9413f 100644 --- a/homeassistant/components/econet/translations/es.json +++ b/homeassistant/components/econet/translations/es.json @@ -14,7 +14,8 @@ "data": { "email": "Correo electr\u00f3nico", "password": "Contrase\u00f1a" - } + }, + "title": "Configurar la cuenta Rheem EcoNet" } } } diff --git a/homeassistant/components/fritzbox/translations/es.json b/homeassistant/components/fritzbox/translations/es.json index c9b67ca59f1..fcb240deb77 100644 --- a/homeassistant/components/fritzbox/translations/es.json +++ b/homeassistant/components/fritzbox/translations/es.json @@ -23,7 +23,8 @@ "data": { "password": "Contrase\u00f1a", "username": "Usuario" - } + }, + "description": "Actualice la informaci\u00f3n de inicio de sesi\u00f3n para {name}." }, "user": { "data": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/es.json b/homeassistant/components/fritzbox_callmonitor/translations/es.json index 899a5050755..4d4aa4cd86b 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/es.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/es.json @@ -1,9 +1,18 @@ { "config": { + "abort": { + "insufficient_permissions": "El usuario no tiene permisos suficientes para acceder a la configuraci\u00f3n de AVM FRITZ! Box y sus agendas telef\u00f3nicas." + }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, + "flow_title": "Monitor de llamadas de AVM FRITZ! Box: {name}", "step": { + "phonebook": { + "data": { + "phonebook": "Directorio telef\u00f3nico" + } + }, "user": { "data": { "host": "Host", @@ -20,6 +29,9 @@ }, "step": { "init": { + "data": { + "prefixes": "Prefijos (lista separada por comas)" + }, "title": "Configurar prefijos" } } diff --git a/homeassistant/components/habitica/translations/es.json b/homeassistant/components/habitica/translations/es.json new file mode 100644 index 00000000000..afdbb6666ad --- /dev/null +++ b/homeassistant/components/habitica/translations/es.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_user": "ID de usuario de la API de Habitica", + "name": "Anular el nombre de usuario de Habitica. Se utilizar\u00e1 para llamadas de servicio.", + "url": "URL" + }, + "description": "Conecta tu perfil de Habitica para permitir la supervisi\u00f3n del perfil y las tareas de tu usuario. Ten en cuenta que api_id y api_key deben obtenerse de https://habitica.com/user/settings/api" + } + } + }, + "title": "Habitica" +} \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/tr.json b/homeassistant/components/habitica/translations/tr.json new file mode 100644 index 00000000000..f77cc77798c --- /dev/null +++ b/homeassistant/components/habitica/translations/tr.json @@ -0,0 +1,3 @@ +{ + "title": "Habitica" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json index 8ceee3f3016..694b7dcdb6c 100644 --- a/homeassistant/components/homekit/translations/es.json +++ b/homeassistant/components/homekit/translations/es.json @@ -7,12 +7,15 @@ "accessory_mode": { "data": { "entity_id": "Entidad" - } + }, + "description": "Elija la entidad que desea incluir. En el modo accesorio, s\u00f3lo se incluye una \u00fanica entidad.", + "title": "Seleccione la entidad a incluir" }, "bridge_mode": { "data": { "include_domains": "Dominios a incluir" }, + "description": "Elija los dominios que se van a incluir. Se incluir\u00e1n todas las entidades admitidas en el dominio.", "title": "Selecciona los dominios a incluir" }, "pairing": { diff --git a/homeassistant/components/keenetic_ndms2/translations/es.json b/homeassistant/components/keenetic_ndms2/translations/es.json index 6b8af4f98af..6846cfbef42 100644 --- a/homeassistant/components/keenetic_ndms2/translations/es.json +++ b/homeassistant/components/keenetic_ndms2/translations/es.json @@ -9,10 +9,26 @@ "step": { "user": { "data": { + "host": "Host", "name": "Nombre", "password": "Contrase\u00f1a", "port": "Puerto", "username": "Usuario" + }, + "title": "Configurar el router Keenetic NDMS2" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "consider_home": "Considerar el intervalo en casa", + "include_arp": "Usar datos ARP (ignorado si se usan datos de hotspot)", + "include_associated": "Utilizar los datos de las asociaciones WiFi AP (se ignora si se utilizan los datos del hotspot)", + "interfaces": "Elija las interfaces para escanear", + "scan_interval": "Intervalo de escaneo", + "try_hotspot": "Utilizar datos de 'punto de acceso ip' (m\u00e1s precisos)" } } } diff --git a/homeassistant/components/lutron_caseta/translations/es.json b/homeassistant/components/lutron_caseta/translations/es.json index 460d4fc5e69..9dbedba1457 100644 --- a/homeassistant/components/lutron_caseta/translations/es.json +++ b/homeassistant/components/lutron_caseta/translations/es.json @@ -30,9 +30,24 @@ "device_automation": { "trigger_subtype": { "button_1": "Primer bot\u00f3n", + "button_2": "Segundo bot\u00f3n", "button_3": "Tercer bot\u00f3n", + "button_4": "Cuarto bot\u00f3n", "close_1": "Cerrar 1", "close_2": "Cerrar 2", + "close_3": "Cerrar 3", + "close_4": "Cerrar 4", + "close_all": "Cerrar todo", + "group_1_button_1": "Primer bot\u00f3n de primer grupo", + "group_1_button_2": "Segundo bot\u00f3n del primer grupo", + "group_2_button_1": "Primer bot\u00f3n del segundo grupo", + "group_2_button_2": "Segundo bot\u00f3n del segundo grupo", + "lower": "Inferior", + "lower_1": "Inferior 1", + "lower_2": "Inferior 2", + "lower_3": "Inferior 3", + "lower_4": "Inferior 4", + "lower_all": "Bajar todo", "off": "Apagado", "on": "Encendido", "open_1": "Abrir 1", @@ -52,6 +67,10 @@ "stop_3": "Detener 3", "stop_4": "Detener 4", "stop_all": "Detener todo" + }, + "trigger_type": { + "press": "\"{subtipo}\" presionado", + "release": "\"{subtipo}\" liberado" } } } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/es.json b/homeassistant/components/mazda/translations/es.json new file mode 100644 index 00000000000..72fc9ce7389 --- /dev/null +++ b/homeassistant/components/mazda/translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "account_locked": "Cuenta bloqueada. Por favor, int\u00e9ntelo de nuevo m\u00e1s tarde." + }, + "step": { + "reauth": { + "data": { + "region": "Regi\u00f3n" + }, + "description": "Ha fallado la autenticaci\u00f3n para los Servicios Conectados de Mazda. Por favor, introduce tus credenciales actuales.", + "title": "Servicios Conectados de Mazda - Fallo de autenticaci\u00f3n" + }, + "user": { + "data": { + "email": "Correo electronico", + "password": "Contrase\u00f1a", + "region": "Regi\u00f3n" + }, + "description": "Introduce la direcci\u00f3n de correo electr\u00f3nico y la contrase\u00f1a que utilizas para iniciar sesi\u00f3n en la aplicaci\u00f3n m\u00f3vil MyMazda.", + "title": "Servicios Conectados de Mazda - A\u00f1adir cuenta" + } + } + }, + "title": "Servicios Conectados de Mazda" +} \ No newline at end of file diff --git a/homeassistant/components/media_player/translations/es.json b/homeassistant/components/media_player/translations/es.json index fffaedc1d97..f1ffc44957e 100644 --- a/homeassistant/components/media_player/translations/es.json +++ b/homeassistant/components/media_player/translations/es.json @@ -6,6 +6,13 @@ "is_on": "{entity_name} est\u00e1 activado", "is_paused": "{entity_name} est\u00e1 en pausa", "is_playing": "{entity_name} est\u00e1 reproduciendo" + }, + "trigger_type": { + "idle": "{entity_name} est\u00e1 inactivo", + "paused": "{entity_name} est\u00e1 en pausa", + "playing": "{entity_name} comienza a reproducirse", + "turned_off": "{entity_name} desactivado", + "turned_on": "{entity_name} activado" } }, "state": { diff --git a/homeassistant/components/mysensors/translations/es.json b/homeassistant/components/mysensors/translations/es.json index f43762b9701..2a4b30910d1 100644 --- a/homeassistant/components/mysensors/translations/es.json +++ b/homeassistant/components/mysensors/translations/es.json @@ -1,21 +1,46 @@ { "config": { + "abort": { + "duplicate_persistence_file": "Archivo de persistencia ya en uso", + "duplicate_topic": "Tema ya en uso", + "invalid_device": "Dispositivo no v\u00e1lido", + "invalid_ip": "Direcci\u00f3n IP no v\u00e1lida", + "invalid_persistence_file": "Archivo de persistencia no v\u00e1lido", + "invalid_port": "N\u00famero de puerto no v\u00e1lido", + "invalid_publish_topic": "Tema de publicaci\u00f3n no v\u00e1lido", + "invalid_serial": "Puerto serie no v\u00e1lido", + "invalid_subscribe_topic": "Tema de suscripci\u00f3n no v\u00e1lido", + "invalid_version": "Versi\u00f3n inv\u00e1lida de MySensors", + "not_a_number": "Por favor, introduzca un n\u00famero", + "port_out_of_range": "El n\u00famero de puerto debe ser como m\u00ednimo 1 y como m\u00e1ximo 65535", + "same_topic": "Los temas de suscripci\u00f3n y publicaci\u00f3n son los mismos" + }, "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", + "duplicate_persistence_file": "Archivo de persistencia ya en uso", + "duplicate_topic": "Tema ya en uso", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_device": "Dispositivo no v\u00e1lido", "invalid_ip": "Direcci\u00f3n IP no v\u00e1lida", + "invalid_persistence_file": "Archivo de persistencia no v\u00e1lido", "invalid_port": "N\u00famero de puerto no v\u00e1lido", + "invalid_publish_topic": "Tema de publicaci\u00f3n no v\u00e1lido", "invalid_serial": "Puerto serie no v\u00e1lido", + "invalid_subscribe_topic": "Tema de suscripci\u00f3n no v\u00e1lido", "invalid_version": "Versi\u00f3n no v\u00e1lida de MySensors", "not_a_number": "Por favor, introduce un n\u00famero", "port_out_of_range": "El n\u00famero de puerto debe ser como m\u00ednimo 1 y como m\u00e1ximo 65535", + "same_topic": "Los temas de suscripci\u00f3n y publicaci\u00f3n son los mismos", "unknown": "Error inesperado" }, "step": { "gw_mqtt": { "data": { + "persistence_file": "archivo de persistencia (d\u00e9jelo vac\u00edo para que se genere autom\u00e1ticamente)", + "retain": "retener mqtt", + "topic_in_prefix": "prefijo para los temas de entrada (topic_in_prefix)", + "topic_out_prefix": "prefijo para los temas de salida (topic_out_prefix)", "version": "Versi\u00f3n de MySensors" }, "description": "Configuraci\u00f3n del gateway MQTT" @@ -24,9 +49,27 @@ "data": { "baud_rate": "tasa de baudios", "device": "Puerto serie", + "persistence_file": "archivo de persistencia (d\u00e9jelo vac\u00edo para que se genere autom\u00e1ticamente)", "version": "Versi\u00f3n de MySensors" - } + }, + "description": "Configuraci\u00f3n de la pasarela en serie" + }, + "gw_tcp": { + "data": { + "device": "Direcci\u00f3n IP de la pasarela", + "persistence_file": "archivo de persistencia (d\u00e9jelo vac\u00edo para que se genere autom\u00e1ticamente)", + "tcp_port": "Puerto", + "version": "Versi\u00f3n de MySensores" + }, + "description": "Configuraci\u00f3n de la pasarela Ethernet" + }, + "user": { + "data": { + "gateway_type": "Tipo de pasarela" + }, + "description": "Elija el m\u00e9todo de conexi\u00f3n con la pasarela" } } - } + }, + "title": "MySensors" } \ No newline at end of file diff --git a/homeassistant/components/number/translations/es.json b/homeassistant/components/number/translations/es.json index e77258e777d..e709346849e 100644 --- a/homeassistant/components/number/translations/es.json +++ b/homeassistant/components/number/translations/es.json @@ -1,3 +1,8 @@ { + "device_automation": { + "action_type": { + "set_value": "Establecer valor para {entity_name}" + } + }, "title": "N\u00famero" } \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/es.json b/homeassistant/components/philips_js/translations/es.json index 3d4beaa8752..d4476f29981 100644 --- a/homeassistant/components/philips_js/translations/es.json +++ b/homeassistant/components/philips_js/translations/es.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "api_version": "Versi\u00f3n del API" + "api_version": "Versi\u00f3n del API", + "host": "Host" } } } diff --git a/homeassistant/components/plaato/translations/es.json b/homeassistant/components/plaato/translations/es.json index 6ff49fc707c..e0b6c767043 100644 --- a/homeassistant/components/plaato/translations/es.json +++ b/homeassistant/components/plaato/translations/es.json @@ -19,13 +19,19 @@ "token": "Pega el token de autenticaci\u00f3n aqu\u00ed", "use_webhook": "Usar webhook" }, + "description": "Para poder consultar la API se necesita un `auth_token` que puede obtenerse siguiendo [estas](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instrucciones\n\n Dispositivo seleccionado: **{device_type}** \n\nSi prefiere utilizar el m\u00e9todo de webhook incorporado (s\u00f3lo Airlock), marque la casilla siguiente y deje en blanco el Auth Token", "title": "Selecciona el m\u00e9todo API" }, "user": { + "data": { + "device_name": "Nombre de su dispositivo", + "device_type": "Tipo de dispositivo Plaato" + }, "description": "\u00bfEst\u00e1s seguro de que quieres configurar el Airlock de Plaato?", "title": "Configurar el webhook de Plaato" }, "webhook": { + "description": "Para enviar eventos a Home Assistant, deber\u00e1 configurar la funci\u00f3n de webhook en Plaato Airlock. \n\n Complete la siguiente informaci\u00f3n: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n\n Consulte [la documentaci\u00f3n]({docs_url}) para obtener m\u00e1s detalles.", "title": "Webhook a utilizar" } } @@ -38,6 +44,10 @@ }, "description": "Intervalo de actualizaci\u00f3n (minutos)", "title": "Opciones de Plaato" + }, + "webhook": { + "description": "Informaci\u00f3n de webhook: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n\n", + "title": "Opciones para Plaato Airlock" } } } diff --git a/homeassistant/components/powerwall/translations/no.json b/homeassistant/components/powerwall/translations/no.json index 13609f911a4..00b77e14566 100644 --- a/homeassistant/components/powerwall/translations/no.json +++ b/homeassistant/components/powerwall/translations/no.json @@ -17,7 +17,7 @@ "ip_address": "IP adresse", "password": "Passord" }, - "description": "Passordet er vanligvis de siste 5 tegnene i serienummeret for Backup Gateway, og finnes i Telsa-appen. eller de siste 5 tegnene i passordet som er funnet inne i d\u00f8ren til Backup Gateway 2.", + "description": "Passordet er vanligvis de siste 5 tegnene i serienummeret for Backup Gateway, og kan bli funnet i Tesla-appen eller de siste 5 tegnene i passordet som er funnet inne i d\u00f8ren til Backup Gateway 2.", "title": "Koble til powerwall" } } diff --git a/homeassistant/components/smarttub/translations/en.json b/homeassistant/components/smarttub/translations/en.json index b013f816559..4cf93091887 100644 --- a/homeassistant/components/smarttub/translations/en.json +++ b/homeassistant/components/smarttub/translations/en.json @@ -5,7 +5,8 @@ "reauth_successful": "Re-authentication was successful" }, "error": { - "invalid_auth": "Invalid authentication" + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" }, "step": { "user": { diff --git a/homeassistant/components/smarttub/translations/es.json b/homeassistant/components/smarttub/translations/es.json new file mode 100644 index 00000000000..df5b4122bc4 --- /dev/null +++ b/homeassistant/components/smarttub/translations/es.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "Introduzca su direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a de SmartTub para iniciar sesi\u00f3n", + "title": "Inicio de sesi\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/es.json b/homeassistant/components/tuya/translations/es.json index cd8da781870..9c57a216888 100644 --- a/homeassistant/components/tuya/translations/es.json +++ b/homeassistant/components/tuya/translations/es.json @@ -40,8 +40,10 @@ "max_temp": "Temperatura objetivo m\u00e1xima (usa m\u00edn. y m\u00e1x. = 0 por defecto)", "min_kelvin": "Temperatura de color m\u00ednima soportada en kelvin", "min_temp": "Temperatura objetivo m\u00ednima (usa m\u00edn. y m\u00e1x. = 0 por defecto)", + "set_temp_divided": "Use el valor de temperatura dividido para el comando de temperatura establecida", "support_color": "Forzar soporte de color", "temp_divider": "Divisor de los valores de temperatura (0 = usar valor por defecto)", + "temp_step_override": "Temperatura deseada", "tuya_max_coltemp": "Temperatura de color m\u00e1xima notificada por dispositivo", "unit_of_measurement": "Unidad de temperatura utilizada por el dispositivo" }, diff --git a/homeassistant/components/unifi/translations/es.json b/homeassistant/components/unifi/translations/es.json index a676d70e88c..a5963d7019e 100644 --- a/homeassistant/components/unifi/translations/es.json +++ b/homeassistant/components/unifi/translations/es.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "El sitio del controlador ya est\u00e1 configurado", + "configuration_updated": "Configuraci\u00f3n actualizada.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { diff --git a/homeassistant/components/xiaomi_miio/translations/es.json b/homeassistant/components/xiaomi_miio/translations/es.json index 46fb93012ad..fd4b8c36a8b 100644 --- a/homeassistant/components/xiaomi_miio/translations/es.json +++ b/homeassistant/components/xiaomi_miio/translations/es.json @@ -6,10 +6,18 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "no_device_selected": "No se ha seleccionado ning\u00fan dispositivo, por favor, seleccione un dispositivo." + "no_device_selected": "No se ha seleccionado ning\u00fan dispositivo, por favor, seleccione un dispositivo.", + "unknown_device": "No se conoce el modelo del dispositivo, no se puede configurar el dispositivo mediante el flujo de configuraci\u00f3n." }, "flow_title": "Xiaomi Miio: {name}", "step": { + "device": { + "data": { + "name": "Nombre del dispositivo" + }, + "description": "Necesitar\u00e1 la clave de 32 caracteres Token API, consulte https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token para obtener instrucciones. Tenga en cuenta que esta Token API es diferente de la clave utilizada por la integraci\u00f3n de Xiaomi Aqara.", + "title": "Con\u00e9ctese a un dispositivo Xiaomi Miio o Xiaomi Gateway" + }, "gateway": { "data": { "host": "Direcci\u00f3n IP", diff --git a/homeassistant/components/xiaomi_miio/translations/tr.json b/homeassistant/components/xiaomi_miio/translations/tr.json index 46a6493ab3a..3dbf08bd6f1 100644 --- a/homeassistant/components/xiaomi_miio/translations/tr.json +++ b/homeassistant/components/xiaomi_miio/translations/tr.json @@ -9,6 +9,11 @@ "no_device_selected": "Cihaz se\u00e7ilmedi, l\u00fctfen bir cihaz se\u00e7in." }, "step": { + "device": { + "data": { + "name": "Cihaz\u0131n ad\u0131" + } + }, "gateway": { "data": { "host": "\u0130p Adresi", diff --git a/homeassistant/components/zwave_js/translations/es.json b/homeassistant/components/zwave_js/translations/es.json index 257d26eefd6..69a638e1f40 100644 --- a/homeassistant/components/zwave_js/translations/es.json +++ b/homeassistant/components/zwave_js/translations/es.json @@ -1,8 +1,10 @@ { "config": { "abort": { + "addon_get_discovery_info_failed": "Fallo en la obtenci\u00f3n de la informaci\u00f3n de descubrimiento del complemento Z-Wave JS.", "addon_info_failed": "No se pudo obtener la informaci\u00f3n del complemento Z-Wave JS.", "addon_install_failed": "No se ha podido instalar el complemento Z-Wave JS.", + "addon_missing_discovery_info": "Falta informaci\u00f3n de descubrimiento del complemento Z-Wave JS.", "addon_set_config_failed": "Fallo en la configuraci\u00f3n de Z-Wave JS.", "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", @@ -40,7 +42,8 @@ "data": { "network_key": "Clave de red", "usb_path": "Ruta del dispositivo USB" - } + }, + "title": "Introduzca la configuraci\u00f3n del complemento Z-Wave JS" }, "user": { "data": { From e6125a1e4ea4ecbd2578dfaa84733746835190c1 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Fri, 19 Feb 2021 18:49:39 -0800 Subject: [PATCH 558/796] Bump pywemo to 0.6.2 (#46797) --- homeassistant/components/wemo/__init__.py | 7 +++---- homeassistant/components/wemo/entity.py | 2 +- homeassistant/components/wemo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 0075b5dc851..db380ae11ca 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -3,7 +3,6 @@ import asyncio import logging import pywemo -import requests import voluptuous as vol from homeassistant import config_entries @@ -229,10 +228,10 @@ def validate_static_config(host, port): return None try: - device = pywemo.discovery.device_from_description(url, None) + device = pywemo.discovery.device_from_description(url) except ( - requests.exceptions.ConnectionError, - requests.exceptions.Timeout, + pywemo.exceptions.ActionException, + pywemo.exceptions.HTTPException, ) as err: _LOGGER.error("Unable to access WeMo at %s (%s)", url, err) return None diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index d9d90c5508b..65183a6f7a4 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -6,7 +6,7 @@ from typing import Any, Dict, Generator, Optional import async_timeout from pywemo import WeMoDevice -from pywemo.ouimeaux_device.api.service import ActionException +from pywemo.exceptions import ActionException from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index 94bc0fa72aa..23911b31be2 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.6.1"], + "requirements": ["pywemo==0.6.2"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/requirements_all.txt b/requirements_all.txt index 082b6db5dd2..0e3c6117780 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1902,7 +1902,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.6.1 +pywemo==0.6.2 # homeassistant.components.wilight pywilight==0.0.68 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 74080a3945e..317063ffffc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -981,7 +981,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.6.1 +pywemo==0.6.2 # homeassistant.components.wilight pywilight==0.0.68 From bb7e4d7daa9f369fc39f043f9fdb761f7f510436 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Feb 2021 19:34:33 -1000 Subject: [PATCH 559/796] Implement suggested_area in the device registry (#45940) Co-authored-by: Paulus Schoutsen --- homeassistant/helpers/area_registry.py | 70 ++++++++--- homeassistant/helpers/device_registry.py | 17 +++ homeassistant/helpers/entity_platform.py | 1 + tests/components/config/test_area_registry.py | 4 +- tests/helpers/test_area_registry.py | 77 +++++++++++- tests/helpers/test_device_registry.py | 113 +++++++++++++++++- tests/helpers/test_entity_platform.py | 2 + 7 files changed, 258 insertions(+), 26 deletions(-) diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index 562e832cc19..164207a8b2a 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -25,6 +25,7 @@ class AreaEntry: """Area Registry Entry.""" name: str = attr.ib() + normalized_name: str = attr.ib() id: Optional[str] = attr.ib(default=None) def generate_id(self, existing_ids: Container[str]) -> None: @@ -45,27 +46,47 @@ class AreaRegistry: self.hass = hass self.areas: MutableMapping[str, AreaEntry] = {} self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._normalized_name_area_idx: Dict[str, str] = {} @callback def async_get_area(self, area_id: str) -> Optional[AreaEntry]: - """Get all areas.""" + """Get area by id.""" return self.areas.get(area_id) + @callback + def async_get_area_by_name(self, name: str) -> Optional[AreaEntry]: + """Get area by name.""" + normalized_name = normalize_area_name(name) + if normalized_name not in self._normalized_name_area_idx: + return None + return self.areas[self._normalized_name_area_idx[normalized_name]] + @callback def async_list_areas(self) -> Iterable[AreaEntry]: """Get all areas.""" return self.areas.values() + @callback + def async_get_or_create(self, name: str) -> AreaEntry: + """Get or create an area.""" + area = self.async_get_area_by_name(name) + if area: + return area + return self.async_create(name) + @callback def async_create(self, name: str) -> AreaEntry: """Create a new area.""" - if self._async_is_registered(name): - raise ValueError("Name is already in use") + normalized_name = normalize_area_name(name) - area = AreaEntry(name=name) + if self.async_get_area_by_name(name): + raise ValueError(f"The name {name} ({normalized_name}) is already in use") + + area = AreaEntry(name=name, normalized_name=normalized_name) area.generate_id(self.areas) assert area.id is not None self.areas[area.id] = area + self._normalized_name_area_idx[normalized_name] = area.id self.async_schedule_save() self.hass.bus.async_fire( EVENT_AREA_REGISTRY_UPDATED, {"action": "create", "area_id": area.id} @@ -75,12 +96,14 @@ class AreaRegistry: @callback def async_delete(self, area_id: str) -> None: """Delete area.""" + area = self.areas[area_id] device_registry = dr.async_get(self.hass) entity_registry = er.async_get(self.hass) device_registry.async_clear_area_id(area_id) entity_registry.async_clear_area_id(area_id) del self.areas[area_id] + del self._normalized_name_area_idx[area.normalized_name] self.hass.bus.async_fire( EVENT_AREA_REGISTRY_UPDATED, {"action": "remove", "area_id": area_id} @@ -107,23 +130,25 @@ class AreaRegistry: if name == old.name: return old - if self._async_is_registered(name): - raise ValueError("Name is already in use") + normalized_name = normalize_area_name(name) + + if normalized_name != old.normalized_name: + if self.async_get_area_by_name(name): + raise ValueError( + f"The name {name} ({normalized_name}) is already in use" + ) changes["name"] = name + changes["normalized_name"] = normalized_name new = self.areas[area_id] = attr.evolve(old, **changes) + self._normalized_name_area_idx[ + normalized_name + ] = self._normalized_name_area_idx.pop(old.normalized_name) + self.async_schedule_save() return new - @callback - def _async_is_registered(self, name: str) -> Optional[AreaEntry]: - """Check if a name is currently registered.""" - for area in self.areas.values(): - if name == area.name: - return area - return None - async def async_load(self) -> None: """Load the area registry.""" data = await self._store.async_load() @@ -132,7 +157,11 @@ class AreaRegistry: if data is not None: for area in data["areas"]: - areas[area["id"]] = AreaEntry(name=area["name"], id=area["id"]) + normalized_name = normalize_area_name(area["name"]) + areas[area["id"]] = AreaEntry( + name=area["name"], id=area["id"], normalized_name=normalized_name + ) + self._normalized_name_area_idx[normalized_name] = area["id"] self.areas = areas @@ -147,7 +176,11 @@ class AreaRegistry: data = {} data["areas"] = [ - {"name": entry.name, "id": entry.id} for entry in self.areas.values() + { + "name": entry.name, + "id": entry.id, + } + for entry in self.areas.values() ] return data @@ -173,3 +206,8 @@ async def async_get_registry(hass: HomeAssistantType) -> AreaRegistry: This is deprecated and will be removed in the future. Use async_get instead. """ return async_get(hass) + + +def normalize_area_name(area_name: str) -> str: + """Normalize an area name by removing whitespace and case folding.""" + return area_name.casefold().replace(" ", "") diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 77dc2cdf609..3dd44364604 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -71,6 +71,7 @@ class DeviceEntry: ) ), ) + suggested_area: Optional[str] = attr.ib(default=None) @property def disabled(self) -> bool: @@ -251,6 +252,7 @@ class DeviceRegistry: via_device: Optional[Tuple[str, str]] = None, # To disable a device if it gets created disabled_by: Union[str, None, UndefinedType] = UNDEFINED, + suggested_area: Union[str, None, UndefinedType] = UNDEFINED, ) -> Optional[DeviceEntry]: """Get device. Create if it doesn't exist.""" if not identifiers and not connections: @@ -304,6 +306,7 @@ class DeviceRegistry: sw_version=sw_version, entry_type=entry_type, disabled_by=disabled_by, + suggested_area=suggested_area, ) @callback @@ -321,6 +324,7 @@ class DeviceRegistry: via_device_id: Union[str, None, UndefinedType] = UNDEFINED, remove_config_entry_id: Union[str, UndefinedType] = UNDEFINED, disabled_by: Union[str, None, UndefinedType] = UNDEFINED, + suggested_area: Union[str, None, UndefinedType] = UNDEFINED, ) -> Optional[DeviceEntry]: """Update properties of a device.""" return self._async_update_device( @@ -335,6 +339,7 @@ class DeviceRegistry: via_device_id=via_device_id, remove_config_entry_id=remove_config_entry_id, disabled_by=disabled_by, + suggested_area=suggested_area, ) @callback @@ -356,6 +361,7 @@ class DeviceRegistry: area_id: Union[str, None, UndefinedType] = UNDEFINED, name_by_user: Union[str, None, UndefinedType] = UNDEFINED, disabled_by: Union[str, None, UndefinedType] = UNDEFINED, + suggested_area: Union[str, None, UndefinedType] = UNDEFINED, ) -> Optional[DeviceEntry]: """Update device attributes.""" old = self.devices[device_id] @@ -364,6 +370,16 @@ class DeviceRegistry: config_entries = old.config_entries + if ( + suggested_area not in (UNDEFINED, None, "") + and area_id is UNDEFINED + and old.area_id is None + ): + area = self.hass.helpers.area_registry.async_get( + self.hass + ).async_get_or_create(suggested_area) + area_id = area.id + if ( add_config_entry_id is not UNDEFINED and add_config_entry_id not in old.config_entries @@ -403,6 +419,7 @@ class DeviceRegistry: ("entry_type", entry_type), ("via_device_id", via_device_id), ("disabled_by", disabled_by), + ("suggested_area", suggested_area), ): if value is not UNDEFINED and value != getattr(old, attr_name): changes[attr_name] = value diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 509508405a4..2caf7fe46ab 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -399,6 +399,7 @@ class EntityPlatform: "sw_version", "entry_type", "via_device", + "suggested_area", ): if key in device_info: processed_dev_info[key] = device_info[key] diff --git a/tests/components/config/test_area_registry.py b/tests/components/config/test_area_registry.py index f66e16e606f..35176cc79f9 100644 --- a/tests/components/config/test_area_registry.py +++ b/tests/components/config/test_area_registry.py @@ -55,7 +55,7 @@ async def test_create_area_with_name_already_in_use(hass, client, registry): assert not msg["success"] assert msg["error"]["code"] == "invalid_info" - assert msg["error"]["message"] == "Name is already in use" + assert msg["error"]["message"] == "The name mock (mock) is already in use" assert len(registry.areas) == 1 @@ -147,5 +147,5 @@ async def test_update_area_with_name_already_in_use(hass, client, registry): assert not msg["success"] assert msg["error"]["code"] == "invalid_info" - assert msg["error"]["message"] == "Name is already in use" + assert msg["error"]["message"] == "The name mock 2 (mock2) is already in use" assert len(registry.areas) == 2 diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py index 0bfa5e597d2..7dca029987e 100644 --- a/tests/helpers/test_area_registry.py +++ b/tests/helpers/test_area_registry.py @@ -58,7 +58,7 @@ async def test_create_area_with_name_already_in_use(hass, registry, update_event with pytest.raises(ValueError) as e_info: area2 = registry.async_create("mock") assert area1 != area2 - assert e_info == "Name is already in use" + assert e_info == "The name mock 2 (mock2) is already in use" await hass.async_block_till_done() @@ -133,6 +133,18 @@ async def test_update_area_with_same_name(registry): assert len(registry.areas) == 1 +async def test_update_area_with_same_name_change_case(registry): + """Make sure that we can reapply the same name with a different case to the area.""" + area = registry.async_create("mock") + + updated_area = registry.async_update(area.id, name="Mock") + + assert updated_area.name == "Mock" + assert updated_area.id == area.id + assert updated_area.normalized_name == area.normalized_name + assert len(registry.areas) == 1 + + async def test_update_area_with_name_already_in_use(registry): """Make sure that we can't update an area with a name already in use.""" area1 = registry.async_create("mock1") @@ -140,17 +152,31 @@ async def test_update_area_with_name_already_in_use(registry): with pytest.raises(ValueError) as e_info: registry.async_update(area1.id, name="mock2") - assert e_info == "Name is already in use" + assert e_info == "The name mock 2 (mock2) is already in use" assert area1.name == "mock1" assert area2.name == "mock2" assert len(registry.areas) == 2 +async def test_update_area_with_normalized_name_already_in_use(registry): + """Make sure that we can't update an area with a normalized name already in use.""" + area1 = registry.async_create("mock1") + area2 = registry.async_create("Moc k2") + + with pytest.raises(ValueError) as e_info: + registry.async_update(area1.id, name="mock2") + assert e_info == "The name mock 2 (mock2) is already in use" + + assert area1.name == "mock1" + assert area2.name == "Moc k2" + assert len(registry.areas) == 2 + + async def test_load_area(hass, registry): """Make sure that we can load/save data correctly.""" - registry.async_create("mock1") - registry.async_create("mock2") + area1 = registry.async_create("mock1") + area2 = registry.async_create("mock2") assert len(registry.areas) == 2 @@ -160,6 +186,11 @@ async def test_load_area(hass, registry): assert list(registry.areas) == list(registry2.areas) + area1_registry2 = registry2.async_get_or_create("mock1") + assert area1_registry2.id == area1.id + area2_registry2 = registry2.async_get_or_create("mock2") + assert area2_registry2.id == area2.id + @pytest.mark.parametrize("load_registries", [False]) async def test_loading_area_from_storage(hass, hass_storage): @@ -173,3 +204,41 @@ async def test_loading_area_from_storage(hass, hass_storage): registry = area_registry.async_get(hass) assert len(registry.areas) == 1 + + +async def test_async_get_or_create(hass, registry): + """Make sure we can get the area by name.""" + area = registry.async_get_or_create("Mock1") + area2 = registry.async_get_or_create("mock1") + area3 = registry.async_get_or_create("mock 1") + + assert area == area2 + assert area == area3 + assert area2 == area3 + + +async def test_async_get_area_by_name(hass, registry): + """Make sure we can get the area by name.""" + registry.async_create("Mock1") + + assert len(registry.areas) == 1 + + assert registry.async_get_area_by_name("M o c k 1").normalized_name == "mock1" + + +async def test_async_get_area_by_name_not_found(hass, registry): + """Make sure we return None for non-existent areas.""" + registry.async_create("Mock1") + + assert len(registry.areas) == 1 + + assert registry.async_get_area_by_name("non_exist") is None + + +async def test_async_get_area(hass, registry): + """Make sure we can get the area by id.""" + area = registry.async_create("Mock1") + + assert len(registry.areas) == 1 + + assert registry.async_get_area(area.id).normalized_name == "mock1" diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index a128f8aa390..bc0e1c3bec9 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -8,7 +8,12 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import CoreState, callback from homeassistant.helpers import device_registry, entity_registry -from tests.common import MockConfigEntry, flush_store, mock_device_registry +from tests.common import ( + MockConfigEntry, + flush_store, + mock_area_registry, + mock_device_registry, +) @pytest.fixture @@ -17,6 +22,12 @@ def registry(hass): return mock_device_registry(hass) +@pytest.fixture +def area_registry(hass): + """Return an empty, loaded, registry.""" + return mock_area_registry(hass) + + @pytest.fixture def update_events(hass): """Capture update events.""" @@ -31,7 +42,9 @@ def update_events(hass): return events -async def test_get_or_create_returns_same_entry(hass, registry, update_events): +async def test_get_or_create_returns_same_entry( + hass, registry, area_registry, update_events +): """Make sure we do not duplicate entries.""" entry = registry.async_get_or_create( config_entry_id="1234", @@ -41,6 +54,7 @@ async def test_get_or_create_returns_same_entry(hass, registry, update_events): name="name", manufacturer="manufacturer", model="model", + suggested_area="Game Room", ) entry2 = registry.async_get_or_create( config_entry_id="1234", @@ -48,21 +62,31 @@ async def test_get_or_create_returns_same_entry(hass, registry, update_events): identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", model="model", + suggested_area="Game Room", ) entry3 = registry.async_get_or_create( config_entry_id="1234", connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) + game_room_area = area_registry.async_get_area_by_name("Game Room") + assert game_room_area is not None + assert len(area_registry.areas) == 1 + assert len(registry.devices) == 1 + assert entry.area_id == game_room_area.id assert entry.id == entry2.id assert entry.id == entry3.id assert entry.identifiers == {("bridgeid", "0123")} + assert entry2.area_id == game_room_area.id + assert entry3.manufacturer == "manufacturer" assert entry3.model == "model" assert entry3.name == "name" assert entry3.sw_version == "sw-version" + assert entry3.suggested_area == "Game Room" + assert entry3.area_id == game_room_area.id await hass.async_block_till_done() @@ -154,6 +178,7 @@ async def test_loading_from_storage(hass, hass_storage): "area_id": "12345A", "name_by_user": "Test Friendly Name", "disabled_by": "user", + "suggested_area": "Kitchen", } ], "deleted_devices": [ @@ -444,7 +469,7 @@ async def test_specifying_via_device_update(registry): assert light.via_device_id == via.id -async def test_loading_saving_data(hass, registry): +async def test_loading_saving_data(hass, registry, area_registry): """Test that we load/save data correctly.""" orig_via = registry.async_get_or_create( config_entry_id="123", @@ -506,7 +531,18 @@ async def test_loading_saving_data(hass, registry): assert orig_light4.id == orig_light3.id - assert len(registry.devices) == 3 + orig_kitchen_light = registry.async_get_or_create( + config_entry_id="999", + connections=set(), + identifiers={("hue", "999")}, + manufacturer="manufacturer", + model="light", + via_device=("hue", "0123"), + disabled_by="user", + suggested_area="Kitchen", + ) + + assert len(registry.devices) == 4 assert len(registry.deleted_devices) == 1 orig_via = registry.async_update_device( @@ -530,6 +566,16 @@ async def test_loading_saving_data(hass, registry): assert orig_light == new_light assert orig_light4 == new_light4 + # Ensure a save/load cycle does not keep suggested area + new_kitchen_light = registry2.async_get_device({("hue", "999")}) + assert orig_kitchen_light.suggested_area == "Kitchen" + + orig_kitchen_light_witout_suggested_area = registry.async_update_device( + orig_kitchen_light.id, suggested_area=None + ) + orig_kitchen_light_witout_suggested_area.suggested_area is None + assert orig_kitchen_light_witout_suggested_area == new_kitchen_light + async def test_no_unnecessary_changes(registry): """Make sure we do not consider devices changes.""" @@ -706,6 +752,33 @@ async def test_update_sw_version(registry): assert updated_entry.sw_version == sw_version +async def test_update_suggested_area(registry, area_registry): + """Verify that we can update the suggested area version of a device.""" + entry = registry.async_get_or_create( + config_entry_id="1234", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bla", "123")}, + ) + assert not entry.suggested_area + assert entry.area_id is None + + suggested_area = "Pool" + + with patch.object(registry, "async_schedule_save") as mock_save: + updated_entry = registry.async_update_device( + entry.id, suggested_area=suggested_area + ) + + assert mock_save.call_count == 1 + assert updated_entry != entry + assert updated_entry.suggested_area == suggested_area + + pool_area = area_registry.async_get_area_by_name("Pool") + assert pool_area is not None + assert updated_entry.area_id == pool_area.id + assert len(area_registry.areas) == 1 + + async def test_cleanup_device_registry(hass, registry): """Test cleanup works.""" config_entry = MockConfigEntry(domain="hue") @@ -1104,3 +1177,35 @@ async def test_get_or_create_sets_default_values(hass, registry): assert entry.name == "default name 1" assert entry.model == "default model 1" assert entry.manufacturer == "default manufacturer 1" + + +async def test_verify_suggested_area_does_not_overwrite_area_id( + hass, registry, area_registry +): + """Make sure suggested area does not override a set area id.""" + game_room_area = area_registry.async_create("Game Room") + + original_entry = registry.async_get_or_create( + config_entry_id="1234", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + sw_version="sw-version", + name="name", + manufacturer="manufacturer", + model="model", + ) + entry = registry.async_update_device(original_entry.id, area_id=game_room_area.id) + + assert entry.area_id == game_room_area.id + + entry2 = registry.async_get_or_create( + config_entry_id="1234", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + sw_version="sw-version", + name="name", + manufacturer="manufacturer", + model="model", + suggested_area="New Game Room", + ) + assert entry2.area_id == game_room_area.id diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 0a939ba2825..ab3e04843f9 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -728,6 +728,7 @@ async def test_device_info_called(hass): "model": "test-model", "name": "test-name", "sw_version": "test-sw", + "suggested_area": "Heliport", "entry_type": "service", "via_device": ("hue", "via-id"), }, @@ -755,6 +756,7 @@ async def test_device_info_called(hass): assert device.model == "test-model" assert device.name == "test-name" assert device.sw_version == "test-sw" + assert device.suggested_area == "Heliport" assert device.entry_type == "service" assert device.via_device_id == via.id From 71586b766138f1440ce3e15a897870986e409a99 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Feb 2021 19:42:14 -1000 Subject: [PATCH 560/796] Cleanup inconsistencies in zha fan and make it a bit more dry (#46714) Co-authored-by: Martin Hjelmare --- homeassistant/components/zha/fan.py | 42 ++++++++++++++--------------- tests/components/zha/test_fan.py | 12 +++++++++ 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 1cd66f94686..ed041f8c6c0 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -13,11 +13,13 @@ from homeassistant.components.fan import ( DOMAIN, SUPPORT_SET_SPEED, FanEntity, + NotValidPresetModeError, ) from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import State, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.percentage import ( + int_states_in_range, percentage_to_ranged_value, ranged_value_to_percentage, ) @@ -75,7 +77,7 @@ class BaseFan(FanEntity): """Base representation of a ZHA fan.""" @property - def preset_modes(self) -> str: + def preset_modes(self) -> List[str]: """Return the available preset modes.""" return PRESET_MODES @@ -84,10 +86,17 @@ class BaseFan(FanEntity): """Flag supported features.""" return SUPPORT_SET_SPEED + @property + def speed_count(self) -> int: + """Return the number of speeds the fan supports.""" + return int_states_in_range(SPEED_RANGE) + async def async_turn_on( self, speed=None, percentage=None, preset_mode=None, **kwargs ) -> None: """Turn the entity on.""" + if percentage is None: + percentage = DEFAULT_ON_PERCENTAGE await self.async_set_percentage(percentage) async def async_turn_off(self, **kwargs) -> None: @@ -96,15 +105,16 @@ class BaseFan(FanEntity): async def async_set_percentage(self, percentage: Optional[int]) -> None: """Set the speed percenage of the fan.""" - if percentage is None: - percentage = DEFAULT_ON_PERCENTAGE fan_mode = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) await self._async_set_fan_mode(fan_mode) async def async_set_preset_mode(self, preset_mode: str) -> None: - """Set the speed percenage of the fan.""" - fan_mode = NAME_TO_PRESET_MODE.get(preset_mode) - await self._async_set_fan_mode(fan_mode) + """Set the preset mode for the fan.""" + if preset_mode not in self.preset_modes: + raise NotValidPresetModeError( + f"The preset_mode {preset_mode} is not a valid preset_mode: {self.preset_modes}" + ) + await self._async_set_fan_mode(NAME_TO_PRESET_MODE[preset_mode]) @abstractmethod async def _async_set_fan_mode(self, fan_mode: int) -> None: @@ -132,7 +142,7 @@ class ZhaFan(BaseFan, ZhaEntity): ) @property - def percentage(self) -> str: + def percentage(self) -> Optional[int]: """Return the current speed percentage.""" if ( self._fan_channel.fan_mode is None @@ -144,7 +154,7 @@ class ZhaFan(BaseFan, ZhaEntity): return ranged_value_to_percentage(SPEED_RANGE, self._fan_channel.fan_mode) @property - def preset_mode(self) -> str: + def preset_mode(self) -> Optional[str]: """Return the current preset mode.""" return PRESET_MODES_TO_NAME.get(self._fan_channel.fan_mode) @@ -175,27 +185,15 @@ class FanGroup(BaseFan, ZhaGroupEntity): self._preset_mode = None @property - def percentage(self) -> str: + def percentage(self) -> Optional[int]: """Return the current speed percentage.""" return self._percentage @property - def preset_mode(self) -> str: + def preset_mode(self) -> Optional[str]: """Return the current preset mode.""" return self._preset_mode - async def async_set_percentage(self, percentage: Optional[int]) -> None: - """Set the speed percenage of the fan.""" - if percentage is None: - percentage = DEFAULT_ON_PERCENTAGE - fan_mode = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) - await self._async_set_fan_mode(fan_mode) - - async def async_set_preset_mode(self, preset_mode: str) -> None: - """Set the speed percenage of the fan.""" - fan_mode = NAME_TO_PRESET_MODE.get(preset_mode) - await self._async_set_fan_mode(fan_mode) - async def _async_set_fan_mode(self, fan_mode: int) -> None: """Set the fan mode for the group.""" try: diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index b6347ac6568..81a441a4101 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -11,6 +11,7 @@ import zigpy.zcl.foundation as zcl_f from homeassistant.components import fan from homeassistant.components.fan import ( ATTR_PERCENTAGE, + ATTR_PERCENTAGE_STEP, ATTR_PRESET_MODE, ATTR_SPEED, DOMAIN, @@ -20,6 +21,7 @@ from homeassistant.components.fan import ( SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, + NotValidPresetModeError, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.zha.core.discovery import GROUP_PROBE @@ -188,6 +190,14 @@ async def test_fan(hass, zha_device_joined_restored, zigpy_device): assert len(cluster.write_attributes.mock_calls) == 1 assert cluster.write_attributes.call_args == call({"fan_mode": 4}) + # set invalid preset_mode from HA + cluster.write_attributes.reset_mock() + with pytest.raises(NotValidPresetModeError): + await async_set_preset_mode( + hass, entity_id, preset_mode="invalid does not exist" + ) + assert len(cluster.write_attributes.mock_calls) == 0 + # test adding new fan to the network and HA await async_test_rejoin(hass, zigpy_device, [cluster], (1,)) @@ -450,6 +460,7 @@ async def test_fan_update_entity( assert hass.states.get(entity_id).attributes[ATTR_SPEED] == SPEED_OFF assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == 0 assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None + assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE_STEP] == 100 / 3 assert cluster.read_attributes.await_count == 1 await async_setup_component(hass, "homeassistant", {}) @@ -470,4 +481,5 @@ async def test_fan_update_entity( assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == 33 assert hass.states.get(entity_id).attributes[ATTR_SPEED] == SPEED_LOW assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None + assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE_STEP] == 100 / 3 assert cluster.read_attributes.await_count == 3 From 2f3c2f5f4d713ba954237e426340f41eb3e782c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Feb 2021 19:44:15 -1000 Subject: [PATCH 561/796] Add support for using a single endpoint for rest data (#46711) --- homeassistant/components/rest/__init__.py | 172 ++++++++- .../components/rest/binary_sensor.py | 155 ++------ homeassistant/components/rest/const.py | 20 ++ homeassistant/components/rest/entity.py | 89 +++++ homeassistant/components/rest/notify.py | 5 - homeassistant/components/rest/schema.py | 99 +++++ homeassistant/components/rest/sensor.py | 172 ++------- homeassistant/components/rest/switch.py | 7 - tests/components/rest/test_init.py | 340 ++++++++++++++++++ tests/components/rest/test_notify.py | 4 + tests/components/rest/test_sensor.py | 32 ++ tests/components/rest/test_switch.py | 22 +- tests/fixtures/rest/configuration_empty.yaml | 0 .../rest/configuration_invalid.notyaml | 2 + .../rest/configuration_top_level.yaml | 12 + 15 files changed, 858 insertions(+), 273 deletions(-) create mode 100644 homeassistant/components/rest/const.py create mode 100644 homeassistant/components/rest/entity.py create mode 100644 homeassistant/components/rest/schema.py create mode 100644 tests/components/rest/test_init.py create mode 100644 tests/fixtures/rest/configuration_empty.yaml create mode 100644 tests/fixtures/rest/configuration_invalid.notyaml create mode 100644 tests/fixtures/rest/configuration_top_level.yaml diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index 69bc6172341..ebeddcfd7c7 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -1,4 +1,174 @@ """The rest component.""" -DOMAIN = "rest" +import asyncio +import logging + +import httpx +import voluptuous as vol + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ( + CONF_AUTHENTICATION, + CONF_HEADERS, + CONF_METHOD, + CONF_PARAMS, + CONF_PASSWORD, + CONF_PAYLOAD, + CONF_RESOURCE, + CONF_RESOURCE_TEMPLATE, + CONF_SCAN_INTERVAL, + CONF_TIMEOUT, + CONF_USERNAME, + CONF_VERIFY_SSL, + HTTP_DIGEST_AUTHENTICATION, + SERVICE_RELOAD, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import discovery +from homeassistant.helpers.entity_component import ( + DEFAULT_SCAN_INTERVAL, + EntityComponent, +) +from homeassistant.helpers.reload import async_reload_integration_platforms +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import COORDINATOR, DOMAIN, PLATFORM_IDX, REST, REST_IDX +from .data import RestData +from .schema import CONFIG_SCHEMA # noqa:F401 pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + PLATFORMS = ["binary_sensor", "notify", "sensor", "switch"] +COORDINATOR_AWARE_PLATFORMS = [SENSOR_DOMAIN, BINARY_SENSOR_DOMAIN] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the rest platforms.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + _async_setup_shared_data(hass) + + async def reload_service_handler(service): + """Remove all user-defined groups and load new ones from config.""" + conf = await component.async_prepare_reload() + if conf is None: + return + await async_reload_integration_platforms(hass, DOMAIN, PLATFORMS) + _async_setup_shared_data(hass) + await _async_process_config(hass, conf) + + hass.services.async_register( + DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=vol.Schema({}) + ) + + return await _async_process_config(hass, config) + + +@callback +def _async_setup_shared_data(hass: HomeAssistant): + """Create shared data for platform config and rest coordinators.""" + hass.data[DOMAIN] = {platform: {} for platform in COORDINATOR_AWARE_PLATFORMS} + + +async def _async_process_config(hass, config) -> bool: + """Process rest configuration.""" + if DOMAIN not in config: + return True + + refresh_tasks = [] + load_tasks = [] + for rest_idx, conf in enumerate(config[DOMAIN]): + scan_interval = conf.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + resource_template = conf.get(CONF_RESOURCE_TEMPLATE) + rest = create_rest_data_from_config(hass, conf) + coordinator = _wrap_rest_in_coordinator( + hass, rest, resource_template, scan_interval + ) + refresh_tasks.append(coordinator.async_refresh()) + hass.data[DOMAIN][rest_idx] = {REST: rest, COORDINATOR: coordinator} + + for platform_domain in COORDINATOR_AWARE_PLATFORMS: + if platform_domain not in conf: + continue + + for platform_idx, platform_conf in enumerate(conf[platform_domain]): + hass.data[DOMAIN][platform_domain][platform_idx] = platform_conf + + load = discovery.async_load_platform( + hass, + platform_domain, + DOMAIN, + {REST_IDX: rest_idx, PLATFORM_IDX: platform_idx}, + config, + ) + load_tasks.append(load) + + if refresh_tasks: + await asyncio.gather(*refresh_tasks) + + if load_tasks: + await asyncio.gather(*load_tasks) + + return True + + +async def async_get_config_and_coordinator(hass, platform_domain, discovery_info): + """Get the config and coordinator for the platform from discovery.""" + shared_data = hass.data[DOMAIN][discovery_info[REST_IDX]] + conf = hass.data[DOMAIN][platform_domain][discovery_info[PLATFORM_IDX]] + coordinator = shared_data[COORDINATOR] + rest = shared_data[REST] + if rest.data is None: + await coordinator.async_request_refresh() + return conf, coordinator, rest + + +def _wrap_rest_in_coordinator(hass, rest, resource_template, update_interval): + """Wrap a DataUpdateCoordinator around the rest object.""" + if resource_template: + + async def _async_refresh_with_resource_template(): + rest.set_url(resource_template.async_render(parse_result=False)) + await rest.async_update() + + update_method = _async_refresh_with_resource_template + else: + update_method = rest.async_update + + return DataUpdateCoordinator( + hass, + _LOGGER, + name="rest data", + update_method=update_method, + update_interval=update_interval, + ) + + +def create_rest_data_from_config(hass, config): + """Create RestData from config.""" + resource = config.get(CONF_RESOURCE) + resource_template = config.get(CONF_RESOURCE_TEMPLATE) + method = config.get(CONF_METHOD) + payload = config.get(CONF_PAYLOAD) + verify_ssl = config.get(CONF_VERIFY_SSL) + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + headers = config.get(CONF_HEADERS) + params = config.get(CONF_PARAMS) + timeout = config.get(CONF_TIMEOUT) + + if resource_template is not None: + resource_template.hass = hass + resource = resource_template.async_render(parse_result=False) + + if username and password: + if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: + auth = httpx.DigestAuth(username, password) + else: + auth = (username, password) + else: + auth = None + + return RestData( + hass, method, resource, auth, headers, params, payload, verify_ssl, timeout + ) diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py index 49c10354c51..9692f5b9339 100644 --- a/homeassistant/components/rest/binary_sensor.py +++ b/homeassistant/components/rest/binary_sensor.py @@ -1,64 +1,27 @@ """Support for RESTful binary sensors.""" -import httpx import voluptuous as vol from homeassistant.components.binary_sensor import ( - DEVICE_CLASSES_SCHEMA, + DOMAIN as BINARY_SENSOR_DOMAIN, PLATFORM_SCHEMA, BinarySensorEntity, ) from homeassistant.const import ( - CONF_AUTHENTICATION, CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, - CONF_HEADERS, - CONF_METHOD, CONF_NAME, - CONF_PARAMS, - CONF_PASSWORD, - CONF_PAYLOAD, CONF_RESOURCE, CONF_RESOURCE_TEMPLATE, - CONF_TIMEOUT, - CONF_USERNAME, CONF_VALUE_TEMPLATE, - CONF_VERIFY_SSL, - HTTP_BASIC_AUTHENTICATION, - HTTP_DIGEST_AUTHENTICATION, ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.reload import async_setup_reload_service -from . import DOMAIN, PLATFORMS -from .data import DEFAULT_TIMEOUT, RestData +from . import async_get_config_and_coordinator, create_rest_data_from_config +from .entity import RestEntity +from .schema import BINARY_SENSOR_SCHEMA, RESOURCE_SCHEMA -DEFAULT_METHOD = "GET" -DEFAULT_NAME = "REST Binary Sensor" -DEFAULT_VERIFY_SSL = True -DEFAULT_FORCE_UPDATE = False - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Exclusive(CONF_RESOURCE, CONF_RESOURCE): cv.url, - vol.Exclusive(CONF_RESOURCE_TEMPLATE, CONF_RESOURCE): cv.template, - vol.Optional(CONF_AUTHENTICATION): vol.In( - [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] - ), - vol.Optional(CONF_HEADERS): {cv.string: cv.string}, - vol.Optional(CONF_PARAMS): {cv.string: cv.string}, - vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(["POST", "GET"]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PAYLOAD): cv.string, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - } -) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({**RESOURCE_SCHEMA, **BINARY_SENSOR_SCHEMA}) PLATFORM_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_RESOURCE, CONF_RESOURCE_TEMPLATE), PLATFORM_SCHEMA @@ -67,51 +30,34 @@ PLATFORM_SCHEMA = vol.All( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the REST binary sensor.""" - - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - - name = config.get(CONF_NAME) - resource = config.get(CONF_RESOURCE) - resource_template = config.get(CONF_RESOURCE_TEMPLATE) - method = config.get(CONF_METHOD) - payload = config.get(CONF_PAYLOAD) - verify_ssl = config.get(CONF_VERIFY_SSL) - timeout = config.get(CONF_TIMEOUT) - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - headers = config.get(CONF_HEADERS) - params = config.get(CONF_PARAMS) - device_class = config.get(CONF_DEVICE_CLASS) - value_template = config.get(CONF_VALUE_TEMPLATE) - force_update = config.get(CONF_FORCE_UPDATE) - - if resource_template is not None: - resource_template.hass = hass - resource = resource_template.async_render(parse_result=False) - - if value_template is not None: - value_template.hass = hass - - if username and password: - if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: - auth = httpx.DigestAuth(username, password) - else: - auth = (username, password) + # Must update the sensor now (including fetching the rest resource) to + # ensure it's updating its state. + if discovery_info is not None: + conf, coordinator, rest = await async_get_config_and_coordinator( + hass, BINARY_SENSOR_DOMAIN, discovery_info + ) else: - auth = None - - rest = RestData( - hass, method, resource, auth, headers, params, payload, verify_ssl, timeout - ) - await rest.async_update() + conf = config + coordinator = None + rest = create_rest_data_from_config(hass, conf) + await rest.async_update() if rest.data is None: raise PlatformNotReady + name = conf.get(CONF_NAME) + device_class = conf.get(CONF_DEVICE_CLASS) + value_template = conf.get(CONF_VALUE_TEMPLATE) + force_update = conf.get(CONF_FORCE_UPDATE) + resource_template = conf.get(CONF_RESOURCE_TEMPLATE) + + if value_template is not None: + value_template.hass = hass + async_add_entities( [ RestBinarySensor( - hass, + coordinator, rest, name, device_class, @@ -123,12 +69,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class RestBinarySensor(BinarySensorEntity): +class RestBinarySensor(RestEntity, BinarySensorEntity): """Representation of a REST binary sensor.""" def __init__( self, - hass, + coordinator, rest, name, device_class, @@ -137,36 +83,23 @@ class RestBinarySensor(BinarySensorEntity): resource_template, ): """Initialize a REST binary sensor.""" - self._hass = hass - self.rest = rest - self._name = name - self._device_class = device_class + super().__init__( + coordinator, rest, name, device_class, resource_template, force_update + ) self._state = False self._previous_data = None self._value_template = value_template - self._force_update = force_update - self._resource_template = resource_template - - @property - def name(self): - """Return the name of the binary sensor.""" - return self._name - - @property - def device_class(self): - """Return the class of this sensor.""" - return self._device_class - - @property - def available(self): - """Return the availability of this sensor.""" - return self.rest.data is not None + self._is_on = None @property def is_on(self): """Return true if the binary sensor is on.""" + return self._is_on + + def _update_from_rest_data(self): + """Update state from the rest data.""" if self.rest.data is None: - return False + self._is_on = False response = self.rest.data @@ -176,20 +109,8 @@ class RestBinarySensor(BinarySensorEntity): ) try: - return bool(int(response)) + self._is_on = bool(int(response)) except ValueError: - return {"true": True, "on": True, "open": True, "yes": True}.get( + self._is_on = {"true": True, "on": True, "open": True, "yes": True}.get( response.lower(), False ) - - @property - def force_update(self): - """Force update.""" - return self._force_update - - async def async_update(self): - """Get the latest data from REST API and updates the state.""" - if self._resource_template is not None: - self.rest.set_url(self._resource_template.async_render(parse_result=False)) - - await self.rest.async_update() diff --git a/homeassistant/components/rest/const.py b/homeassistant/components/rest/const.py new file mode 100644 index 00000000000..31216b65968 --- /dev/null +++ b/homeassistant/components/rest/const.py @@ -0,0 +1,20 @@ +"""The rest component constants.""" + +DOMAIN = "rest" + +DEFAULT_METHOD = "GET" +DEFAULT_VERIFY_SSL = True +DEFAULT_FORCE_UPDATE = False + +DEFAULT_BINARY_SENSOR_NAME = "REST Binary Sensor" +DEFAULT_SENSOR_NAME = "REST Sensor" +CONF_JSON_ATTRS = "json_attributes" +CONF_JSON_ATTRS_PATH = "json_attributes_path" + +REST_IDX = "rest_idx" +PLATFORM_IDX = "platform_idx" + +COORDINATOR = "coordinator" +REST = "rest" + +METHODS = ["POST", "GET"] diff --git a/homeassistant/components/rest/entity.py b/homeassistant/components/rest/entity.py new file mode 100644 index 00000000000..acfe5a2dfc5 --- /dev/null +++ b/homeassistant/components/rest/entity.py @@ -0,0 +1,89 @@ +"""The base entity for the rest component.""" + +from abc import abstractmethod +from typing import Any + +from homeassistant.core import callback +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .data import RestData + + +class RestEntity(Entity): + """A class for entities using DataUpdateCoordinator or rest data directly.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator[Any], + rest: RestData, + name, + device_class, + resource_template, + force_update, + ) -> None: + """Create the entity that may have a coordinator.""" + self.coordinator = coordinator + self.rest = rest + self._name = name + self._device_class = device_class + self._resource_template = resource_template + self._force_update = force_update + super().__init__() + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._device_class + + @property + def force_update(self): + """Force update.""" + return self._force_update + + @property + def should_poll(self) -> bool: + """Poll only if we do noty have a coordinator.""" + return not self.coordinator + + @property + def available(self): + """Return the availability of this sensor.""" + if self.coordinator and not self.coordinator.last_update_success: + return False + return self.rest.data is not None + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._update_from_rest_data() + if self.coordinator: + self.async_on_remove( + self.coordinator.async_add_listener(self._handle_coordinator_update) + ) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_from_rest_data() + self.async_write_ha_state() + + async def async_update(self): + """Get the latest data from REST API and update the state.""" + if self.coordinator: + await self.coordinator.async_request_refresh() + return + + if self._resource_template is not None: + self.rest.set_url(self._resource_template.async_render(parse_result=False)) + await self.rest.async_update() + self._update_from_rest_data() + + @abstractmethod + def _update_from_rest_data(self): + """Update state from the rest data.""" diff --git a/homeassistant/components/rest/notify.py b/homeassistant/components/rest/notify.py index f15df428640..198e5b06c52 100644 --- a/homeassistant/components/rest/notify.py +++ b/homeassistant/components/rest/notify.py @@ -29,11 +29,8 @@ from homeassistant.const import ( HTTP_OK, ) 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 - CONF_DATA = "data" CONF_DATA_TEMPLATE = "data_template" CONF_MESSAGE_PARAMETER_NAME = "message_param_name" @@ -73,8 +70,6 @@ _LOGGER = logging.getLogger(__name__) def get_service(hass, config, discovery_info=None): """Get the RESTful notification service.""" - setup_reload_service(hass, DOMAIN, PLATFORMS) - resource = config.get(CONF_RESOURCE) method = config.get(CONF_METHOD) headers = config.get(CONF_HEADERS) diff --git a/homeassistant/components/rest/schema.py b/homeassistant/components/rest/schema.py new file mode 100644 index 00000000000..bedd02d272a --- /dev/null +++ b/homeassistant/components/rest/schema.py @@ -0,0 +1,99 @@ +"""The rest component schemas.""" + +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, + DOMAIN as BINARY_SENSOR_DOMAIN, +) +from homeassistant.components.sensor import ( + DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA, + DOMAIN as SENSOR_DOMAIN, +) +from homeassistant.const import ( + CONF_AUTHENTICATION, + CONF_DEVICE_CLASS, + CONF_FORCE_UPDATE, + CONF_HEADERS, + CONF_METHOD, + CONF_NAME, + CONF_PARAMS, + CONF_PASSWORD, + CONF_PAYLOAD, + CONF_RESOURCE, + CONF_RESOURCE_TEMPLATE, + CONF_SCAN_INTERVAL, + CONF_TIMEOUT, + CONF_UNIT_OF_MEASUREMENT, + CONF_USERNAME, + CONF_VALUE_TEMPLATE, + CONF_VERIFY_SSL, + HTTP_BASIC_AUTHENTICATION, + HTTP_DIGEST_AUTHENTICATION, +) +import homeassistant.helpers.config_validation as cv + +from .const import ( + CONF_JSON_ATTRS, + CONF_JSON_ATTRS_PATH, + DEFAULT_BINARY_SENSOR_NAME, + DEFAULT_FORCE_UPDATE, + DEFAULT_METHOD, + DEFAULT_SENSOR_NAME, + DEFAULT_VERIFY_SSL, + DOMAIN, + METHODS, +) +from .data import DEFAULT_TIMEOUT + +RESOURCE_SCHEMA = { + vol.Exclusive(CONF_RESOURCE, CONF_RESOURCE): cv.url, + vol.Exclusive(CONF_RESOURCE_TEMPLATE, CONF_RESOURCE): cv.template, + vol.Optional(CONF_AUTHENTICATION): vol.In( + [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] + ), + vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}), + vol.Optional(CONF_PARAMS): vol.Schema({cv.string: cv.string}), + vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(METHODS), + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PAYLOAD): cv.string, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, +} + +SENSOR_SCHEMA = { + vol.Optional(CONF_NAME, default=DEFAULT_SENSOR_NAME): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv, + vol.Optional(CONF_JSON_ATTRS_PATH): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, +} + +BINARY_SENSOR_SCHEMA = { + vol.Optional(CONF_NAME, default=DEFAULT_BINARY_SENSOR_NAME): cv.string, + vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, +} + + +COMBINED_SCHEMA = vol.Schema( + { + vol.Optional(CONF_SCAN_INTERVAL): cv.time_period, + **RESOURCE_SCHEMA, + vol.Optional(SENSOR_DOMAIN): vol.All( + cv.ensure_list, [vol.Schema(SENSOR_SCHEMA)] + ), + vol.Optional(BINARY_SENSOR_DOMAIN): vol.All( + cv.ensure_list, [vol.Schema(BINARY_SENSOR_SCHEMA)] + ), + } +) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.All(cv.ensure_list, [COMBINED_SCHEMA])}, + extra=vol.ALLOW_EXTRA, +) diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 85d79b6b331..0699d9dc07c 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -3,76 +3,31 @@ import json import logging from xml.parsers.expat import ExpatError -import httpx from jsonpath import jsonpath import voluptuous as vol import xmltodict -from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA from homeassistant.const import ( - CONF_AUTHENTICATION, CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, - CONF_HEADERS, - CONF_METHOD, CONF_NAME, - CONF_PARAMS, - CONF_PASSWORD, - CONF_PAYLOAD, CONF_RESOURCE, CONF_RESOURCE_TEMPLATE, - CONF_TIMEOUT, CONF_UNIT_OF_MEASUREMENT, - CONF_USERNAME, CONF_VALUE_TEMPLATE, - CONF_VERIFY_SSL, - HTTP_BASIC_AUTHENTICATION, - HTTP_DIGEST_AUTHENTICATION, ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.reload import async_setup_reload_service -from . import DOMAIN, PLATFORMS -from .data import DEFAULT_TIMEOUT, RestData +from . import async_get_config_and_coordinator, create_rest_data_from_config +from .const import CONF_JSON_ATTRS, CONF_JSON_ATTRS_PATH +from .entity import RestEntity +from .schema import RESOURCE_SCHEMA, SENSOR_SCHEMA _LOGGER = logging.getLogger(__name__) -DEFAULT_METHOD = "GET" -DEFAULT_NAME = "REST Sensor" -DEFAULT_VERIFY_SSL = True -DEFAULT_FORCE_UPDATE = False - - -CONF_JSON_ATTRS = "json_attributes" -CONF_JSON_ATTRS_PATH = "json_attributes_path" -METHODS = ["POST", "GET"] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Exclusive(CONF_RESOURCE, CONF_RESOURCE): cv.url, - vol.Exclusive(CONF_RESOURCE_TEMPLATE, CONF_RESOURCE): cv.template, - vol.Optional(CONF_AUTHENTICATION): vol.In( - [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] - ), - vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}), - vol.Optional(CONF_PARAMS): vol.Schema({cv.string: cv.string}), - vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv, - vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(METHODS), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PAYLOAD): cv.string, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_JSON_ATTRS_PATH): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - } -) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({**RESOURCE_SCHEMA, **SENSOR_SCHEMA}) PLATFORM_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_RESOURCE, CONF_RESOURCE_TEMPLATE), PLATFORM_SCHEMA @@ -81,55 +36,37 @@ PLATFORM_SCHEMA = vol.All( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the RESTful sensor.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - - name = config.get(CONF_NAME) - resource = config.get(CONF_RESOURCE) - resource_template = config.get(CONF_RESOURCE_TEMPLATE) - method = config.get(CONF_METHOD) - payload = config.get(CONF_PAYLOAD) - verify_ssl = config.get(CONF_VERIFY_SSL) - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - headers = config.get(CONF_HEADERS) - params = config.get(CONF_PARAMS) - unit = config.get(CONF_UNIT_OF_MEASUREMENT) - device_class = config.get(CONF_DEVICE_CLASS) - value_template = config.get(CONF_VALUE_TEMPLATE) - json_attrs = config.get(CONF_JSON_ATTRS) - json_attrs_path = config.get(CONF_JSON_ATTRS_PATH) - force_update = config.get(CONF_FORCE_UPDATE) - timeout = config.get(CONF_TIMEOUT) - - if value_template is not None: - value_template.hass = hass - - if resource_template is not None: - resource_template.hass = hass - resource = resource_template.async_render(parse_result=False) - - if username and password: - if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: - auth = httpx.DigestAuth(username, password) - else: - auth = (username, password) + # Must update the sensor now (including fetching the rest resource) to + # ensure it's updating its state. + if discovery_info is not None: + conf, coordinator, rest = await async_get_config_and_coordinator( + hass, SENSOR_DOMAIN, discovery_info + ) else: - auth = None - rest = RestData( - hass, method, resource, auth, headers, params, payload, verify_ssl, timeout - ) - - await rest.async_update() + conf = config + coordinator = None + rest = create_rest_data_from_config(hass, conf) + await rest.async_update() if rest.data is None: raise PlatformNotReady - # Must update the sensor now (including fetching the rest resource) to - # ensure it's updating its state. + name = conf.get(CONF_NAME) + unit = conf.get(CONF_UNIT_OF_MEASUREMENT) + device_class = conf.get(CONF_DEVICE_CLASS) + json_attrs = conf.get(CONF_JSON_ATTRS) + json_attrs_path = conf.get(CONF_JSON_ATTRS_PATH) + value_template = conf.get(CONF_VALUE_TEMPLATE) + force_update = conf.get(CONF_FORCE_UPDATE) + resource_template = conf.get(CONF_RESOURCE_TEMPLATE) + + if value_template is not None: + value_template.hass = hass + async_add_entities( [ RestSensor( - hass, + coordinator, rest, name, unit, @@ -144,12 +81,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class RestSensor(Entity): +class RestSensor(RestEntity): """Implementation of a REST sensor.""" def __init__( self, - hass, + coordinator, rest, name, unit_of_measurement, @@ -161,60 +98,30 @@ class RestSensor(Entity): json_attrs_path, ): """Initialize the REST sensor.""" - self._hass = hass - self.rest = rest - self._name = name + super().__init__( + coordinator, rest, name, device_class, resource_template, force_update + ) self._state = None self._unit_of_measurement = unit_of_measurement - self._device_class = device_class self._value_template = value_template self._json_attrs = json_attrs self._attributes = None - self._force_update = force_update - self._resource_template = resource_template self._json_attrs_path = json_attrs_path - @property - def name(self): - """Return the name of the sensor.""" - return self._name - @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement - @property - def device_class(self): - """Return the class of this sensor.""" - return self._device_class - - @property - def available(self): - """Return if the sensor data are available.""" - return self.rest.data is not None - @property def state(self): """Return the state of the device.""" return self._state @property - def force_update(self): - """Force update.""" - return self._force_update - - async def async_update(self): - """Get the latest data from REST API and update the state.""" - if self._resource_template is not None: - self.rest.set_url(self._resource_template.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 device_state_attributes(self): + """Return the state attributes.""" + return self._attributes def _update_from_rest_data(self): """Update state from the rest data.""" @@ -273,8 +180,3 @@ class RestSensor(Entity): ) self._state = value - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._attributes diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index ea480d549f3..e8ae1dee015 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -22,12 +22,8 @@ from homeassistant.const import ( ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.reload import async_setup_reload_service - -from . import DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) - CONF_BODY_OFF = "body_off" CONF_BODY_ON = "body_on" CONF_IS_ON_TEMPLATE = "is_on_template" @@ -65,9 +61,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the RESTful switch.""" - - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - body_off = config.get(CONF_BODY_OFF) body_on = config.get(CONF_BODY_ON) is_on_template = config.get(CONF_IS_ON_TEMPLATE) diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py new file mode 100644 index 00000000000..19a5651e989 --- /dev/null +++ b/tests/components/rest/test_init.py @@ -0,0 +1,340 @@ +"""Tests for rest component.""" + +import asyncio +from datetime import timedelta +from os import path +from unittest.mock import patch + +import respx + +from homeassistant import config as hass_config +from homeassistant.components.rest.const import DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + DATA_MEGABYTES, + SERVICE_RELOAD, + STATE_UNAVAILABLE, +) +from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed + + +@respx.mock +async def test_setup_with_endpoint_timeout_with_recovery(hass): + """Test setup with an endpoint that times out that recovers.""" + await async_setup_component(hass, "homeassistant", {}) + + respx.get("http://localhost").mock(side_effect=asyncio.TimeoutError()) + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: [ + { + "resource": "http://localhost", + "method": "GET", + "verify_ssl": "false", + "timeout": 30, + "sensor": [ + { + "unit_of_measurement": DATA_MEGABYTES, + "name": "sensor1", + "value_template": "{{ value_json.sensor1 }}", + }, + { + "unit_of_measurement": DATA_MEGABYTES, + "name": "sensor2", + "value_template": "{{ value_json.sensor2 }}", + }, + ], + "binary_sensor": [ + { + "name": "binary_sensor1", + "value_template": "{{ value_json.binary_sensor1 }}", + }, + { + "name": "binary_sensor2", + "value_template": "{{ value_json.binary_sensor2 }}", + }, + ], + } + ] + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 + + respx.get("http://localhost").respond( + status_code=200, + json={ + "sensor1": "1", + "sensor2": "2", + "binary_sensor1": "on", + "binary_sensor2": "off", + }, + ) + + # Refresh the coordinator + async_fire_time_changed(hass, utcnow() + timedelta(seconds=31)) + await hass.async_block_till_done() + + # Wait for platform setup retry + async_fire_time_changed(hass, utcnow() + timedelta(seconds=61)) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 4 + + assert hass.states.get("sensor.sensor1").state == "1" + assert hass.states.get("sensor.sensor2").state == "2" + assert hass.states.get("binary_sensor.binary_sensor1").state == "on" + assert hass.states.get("binary_sensor.binary_sensor2").state == "off" + + # Now the end point flakes out again + respx.get("http://localhost").mock(side_effect=asyncio.TimeoutError()) + + # Refresh the coordinator + async_fire_time_changed(hass, utcnow() + timedelta(seconds=31)) + await hass.async_block_till_done() + + assert hass.states.get("sensor.sensor1").state == STATE_UNAVAILABLE + assert hass.states.get("sensor.sensor2").state == STATE_UNAVAILABLE + assert hass.states.get("binary_sensor.binary_sensor1").state == STATE_UNAVAILABLE + assert hass.states.get("binary_sensor.binary_sensor2").state == STATE_UNAVAILABLE + + # We request a manual refresh when the + # endpoint is working again + + respx.get("http://localhost").respond( + status_code=200, + json={ + "sensor1": "1", + "sensor2": "2", + "binary_sensor1": "on", + "binary_sensor2": "off", + }, + ) + + await hass.services.async_call( + "homeassistant", + "update_entity", + {ATTR_ENTITY_ID: ["sensor.sensor1"]}, + blocking=True, + ) + assert hass.states.get("sensor.sensor1").state == "1" + assert hass.states.get("sensor.sensor2").state == "2" + assert hass.states.get("binary_sensor.binary_sensor1").state == "on" + assert hass.states.get("binary_sensor.binary_sensor2").state == "off" + + +@respx.mock +async def test_setup_minimum_resource_template(hass): + """Test setup with minimum configuration (resource_template).""" + + respx.get("http://localhost").respond( + status_code=200, + json={ + "sensor1": "1", + "sensor2": "2", + "binary_sensor1": "on", + "binary_sensor2": "off", + }, + ) + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: [ + { + "resource_template": "{% set url = 'http://localhost' %}{{ url }}", + "method": "GET", + "verify_ssl": "false", + "timeout": 30, + "sensor": [ + { + "unit_of_measurement": DATA_MEGABYTES, + "name": "sensor1", + "value_template": "{{ value_json.sensor1 }}", + }, + { + "unit_of_measurement": DATA_MEGABYTES, + "name": "sensor2", + "value_template": "{{ value_json.sensor2 }}", + }, + ], + "binary_sensor": [ + { + "name": "binary_sensor1", + "value_template": "{{ value_json.binary_sensor1 }}", + }, + { + "name": "binary_sensor2", + "value_template": "{{ value_json.binary_sensor2 }}", + }, + ], + } + ] + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 4 + + assert hass.states.get("sensor.sensor1").state == "1" + assert hass.states.get("sensor.sensor2").state == "2" + assert hass.states.get("binary_sensor.binary_sensor1").state == "on" + assert hass.states.get("binary_sensor.binary_sensor2").state == "off" + + +@respx.mock +async def test_reload(hass): + """Verify we can reload.""" + + respx.get("http://localhost") % 200 + + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: [ + { + "resource": "http://localhost", + "method": "GET", + "verify_ssl": "false", + "timeout": 30, + "sensor": [ + { + "name": "mockrest", + }, + ], + } + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + assert hass.states.get("sensor.mockrest") + + yaml_path = path.join( + _get_fixtures_base_path(), + "fixtures", + "rest/configuration_top_level.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + "rest", + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get("sensor.mockreset") is None + assert hass.states.get("sensor.rollout") + assert hass.states.get("sensor.fallover") + + +@respx.mock +async def test_reload_and_remove_all(hass): + """Verify we can reload and remove all.""" + + respx.get("http://localhost") % 200 + + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: [ + { + "resource": "http://localhost", + "method": "GET", + "verify_ssl": "false", + "timeout": 30, + "sensor": [ + { + "name": "mockrest", + }, + ], + } + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + assert hass.states.get("sensor.mockrest") + + yaml_path = path.join( + _get_fixtures_base_path(), + "fixtures", + "rest/configuration_empty.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + "rest", + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get("sensor.mockreset") is None + + +@respx.mock +async def test_reload_fails_to_read_configuration(hass): + """Verify reload when configuration is missing or broken.""" + + respx.get("http://localhost") % 200 + + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: [ + { + "resource": "http://localhost", + "method": "GET", + "verify_ssl": "false", + "timeout": 30, + "sensor": [ + { + "name": "mockrest", + }, + ], + } + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + yaml_path = path.join( + _get_fixtures_base_path(), + "fixtures", + "rest/configuration_invalid.notyaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + "rest", + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/components/rest/test_notify.py b/tests/components/rest/test_notify.py index aa3e40c2dd4..fb7b8a31238 100644 --- a/tests/components/rest/test_notify.py +++ b/tests/components/rest/test_notify.py @@ -2,6 +2,8 @@ from os import path from unittest.mock import patch +import respx + from homeassistant import config as hass_config import homeassistant.components.notify as notify from homeassistant.components.rest import DOMAIN @@ -9,8 +11,10 @@ from homeassistant.const import SERVICE_RELOAD from homeassistant.setup import async_setup_component +@respx.mock async def test_reload_notify(hass): """Verify we can reload the notify service.""" + respx.get("http://localhost") % 200 assert await async_setup_component( hass, diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index 58309cd7532..2e308f69384 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -91,6 +91,38 @@ async def test_setup_minimum(hass): assert len(hass.states.async_all()) == 1 +@respx.mock +async def test_manual_update(hass): + """Test setup with minimum configuration.""" + await async_setup_component(hass, "homeassistant", {}) + respx.get("http://localhost").respond(status_code=200, json={"data": "first"}) + assert await async_setup_component( + hass, + sensor.DOMAIN, + { + "sensor": { + "name": "mysensor", + "value_template": "{{ value_json.data }}", + "platform": "rest", + "resource_template": "{% set url = 'http://localhost' %}{{ url }}", + "method": "GET", + } + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + assert hass.states.get("sensor.mysensor").state == "first" + + respx.get("http://localhost").respond(status_code=200, json={"data": "second"}) + await hass.services.async_call( + "homeassistant", + "update_entity", + {ATTR_ENTITY_ID: ["sensor.mysensor"]}, + blocking=True, + ) + assert hass.states.get("sensor.mysensor").state == "second" + + @respx.mock async def test_setup_minimum_resource_template(hass): """Test setup with minimum configuration (resource_template).""" diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index 5e0c9fbeab3..7141a34203a 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -3,6 +3,7 @@ import asyncio import aiohttp +from homeassistant.components.rest import DOMAIN import homeassistant.components.rest.switch as rest from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( @@ -34,14 +35,14 @@ PARAMS = None async def test_setup_missing_config(hass): """Test setup with configuration missing required entries.""" - assert not await rest.async_setup_platform(hass, {CONF_PLATFORM: rest.DOMAIN}, None) + assert not await rest.async_setup_platform(hass, {CONF_PLATFORM: DOMAIN}, None) async def test_setup_missing_schema(hass): """Test setup with resource missing schema.""" assert not await rest.async_setup_platform( hass, - {CONF_PLATFORM: rest.DOMAIN, CONF_RESOURCE: "localhost"}, + {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "localhost"}, None, ) @@ -51,7 +52,7 @@ async def test_setup_failed_connect(hass, aioclient_mock): aioclient_mock.get("http://localhost", exc=aiohttp.ClientError) assert not await rest.async_setup_platform( hass, - {CONF_PLATFORM: rest.DOMAIN, CONF_RESOURCE: "http://localhost"}, + {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "http://localhost"}, None, ) @@ -61,7 +62,7 @@ async def test_setup_timeout(hass, aioclient_mock): aioclient_mock.get("http://localhost", exc=asyncio.TimeoutError()) assert not await rest.async_setup_platform( hass, - {CONF_PLATFORM: rest.DOMAIN, CONF_RESOURCE: "http://localhost"}, + {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "http://localhost"}, None, ) @@ -75,11 +76,12 @@ async def test_setup_minimum(hass, aioclient_mock): SWITCH_DOMAIN, { SWITCH_DOMAIN: { - CONF_PLATFORM: rest.DOMAIN, + CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "http://localhost", } }, ) + await hass.async_block_till_done() assert aioclient_mock.call_count == 1 @@ -92,12 +94,14 @@ async def test_setup_query_params(hass, aioclient_mock): SWITCH_DOMAIN, { SWITCH_DOMAIN: { - CONF_PLATFORM: rest.DOMAIN, + CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "http://localhost", CONF_PARAMS: {"search": "something"}, } }, ) + await hass.async_block_till_done() + print(aioclient_mock) assert aioclient_mock.call_count == 1 @@ -110,7 +114,7 @@ async def test_setup(hass, aioclient_mock): SWITCH_DOMAIN, { SWITCH_DOMAIN: { - CONF_PLATFORM: rest.DOMAIN, + CONF_PLATFORM: DOMAIN, CONF_NAME: "foo", CONF_RESOURCE: "http://localhost", CONF_HEADERS: {"Content-type": CONTENT_TYPE_JSON}, @@ -119,6 +123,7 @@ async def test_setup(hass, aioclient_mock): } }, ) + await hass.async_block_till_done() assert aioclient_mock.call_count == 1 assert_setup_component(1, SWITCH_DOMAIN) @@ -132,7 +137,7 @@ async def test_setup_with_state_resource(hass, aioclient_mock): SWITCH_DOMAIN, { SWITCH_DOMAIN: { - CONF_PLATFORM: rest.DOMAIN, + CONF_PLATFORM: DOMAIN, CONF_NAME: "foo", CONF_RESOURCE: "http://localhost", rest.CONF_STATE_RESOURCE: "http://localhost/state", @@ -142,6 +147,7 @@ async def test_setup_with_state_resource(hass, aioclient_mock): } }, ) + await hass.async_block_till_done() assert aioclient_mock.call_count == 1 assert_setup_component(1, SWITCH_DOMAIN) diff --git a/tests/fixtures/rest/configuration_empty.yaml b/tests/fixtures/rest/configuration_empty.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/rest/configuration_invalid.notyaml b/tests/fixtures/rest/configuration_invalid.notyaml new file mode 100644 index 00000000000..548d8bcf5a0 --- /dev/null +++ b/tests/fixtures/rest/configuration_invalid.notyaml @@ -0,0 +1,2 @@ +*!* NOT YAML + diff --git a/tests/fixtures/rest/configuration_top_level.yaml b/tests/fixtures/rest/configuration_top_level.yaml new file mode 100644 index 00000000000..df27e160117 --- /dev/null +++ b/tests/fixtures/rest/configuration_top_level.yaml @@ -0,0 +1,12 @@ +rest: + - method: GET + resource: "http://localhost" + sensor: + name: fallover + +sensor: + - platform: rest + resource: "http://localhost" + method: GET + name: rollout + From 54cf9543531619d4d3963f635c3c20963ece4831 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 20 Feb 2021 06:50:59 +0100 Subject: [PATCH 562/796] Add device_entities template function/filter (#46406) --- homeassistant/helpers/template.py | 52 +++++++++++++-------- tests/helpers/test_template.py | 75 +++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 18 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 200d678719a..7377120af40 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -33,7 +33,7 @@ from homeassistant.const import ( ) from homeassistant.core import State, callback, split_entity_id, valid_entity_id from homeassistant.exceptions import TemplateError -from homeassistant.helpers import location as loc_helper +from homeassistant.helpers import entity_registry, location as loc_helper from homeassistant.helpers.typing import HomeAssistantType, TemplateVarsType from homeassistant.loader import bind_hass from homeassistant.util import convert, dt as dt_util, location as loc_util @@ -48,6 +48,7 @@ DATE_STR_FORMAT = "%Y-%m-%d %H:%M:%S" _RENDER_INFO = "template.render_info" _ENVIRONMENT = "template.environment" +_ENVIRONMENT_LIMITED = "template.environment_limited" _RE_JINJA_DELIMITERS = re.compile(r"\{%|\{\{|\{#") # Match "simple" ints and floats. -1.0, 1, +5, 5.0 @@ -300,11 +301,12 @@ class Template: @property def _env(self) -> TemplateEnvironment: - if self.hass is None or self._limited: + if self.hass is None: return _NO_HASS_ENV - ret: Optional[TemplateEnvironment] = self.hass.data.get(_ENVIRONMENT) + wanted_env = _ENVIRONMENT_LIMITED if self._limited else _ENVIRONMENT + ret: Optional[TemplateEnvironment] = self.hass.data.get(wanted_env) if ret is None: - ret = self.hass.data[_ENVIRONMENT] = TemplateEnvironment(self.hass) # type: ignore[no-untyped-call] + ret = self.hass.data[wanted_env] = TemplateEnvironment(self.hass, self._limited) # type: ignore[no-untyped-call] return ret def ensure_valid(self) -> None: @@ -867,6 +869,13 @@ def expand(hass: HomeAssistantType, *args: Any) -> Iterable[State]: return sorted(found.values(), key=lambda a: a.entity_id) +def device_entities(hass: HomeAssistantType, device_id: str) -> Iterable[str]: + """Get entity ids for entities tied to a device.""" + entity_reg = entity_registry.async_get(hass) + entries = entity_registry.async_entries_for_device(entity_reg, device_id) + return [entry.entity_id for entry in entries] + + def closest(hass, *args): """Find closest entity. @@ -1311,7 +1320,7 @@ def urlencode(value): class TemplateEnvironment(ImmutableSandboxedEnvironment): """The Home Assistant template environment.""" - def __init__(self, hass): + def __init__(self, hass, limited=False): """Initialise template environment.""" super().__init__() self.hass = hass @@ -1368,7 +1377,27 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["strptime"] = strptime self.globals["urlencode"] = urlencode if hass is None: + return + # We mark these as a context functions to ensure they get + # evaluated fresh with every execution, rather than executed + # at compile time and the value stored. The context itself + # can be discarded, we only need to get at the hass object. + def hassfunction(func): + """Wrap function that depend on hass.""" + + @wraps(func) + def wrapper(*args, **kwargs): + return func(hass, *args[1:], **kwargs) + + return contextfunction(wrapper) + + self.globals["device_entities"] = hassfunction(device_entities) + self.filters["device_entities"] = contextfilter(self.globals["device_entities"]) + + if limited: + # Only device_entities is available to limited templates, mark other + # functions and filters as unsupported. def unsupported(name): def warn_unsupported(*args, **kwargs): raise TemplateError( @@ -1395,19 +1424,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters[filt] = unsupported(filt) return - # We mark these as a context functions to ensure they get - # evaluated fresh with every execution, rather than executed - # at compile time and the value stored. The context itself - # can be discarded, we only need to get at the hass object. - def hassfunction(func): - """Wrap function that depend on hass.""" - - @wraps(func) - def wrapper(*args, **kwargs): - return func(hass, *args[1:], **kwargs) - - return contextfunction(wrapper) - self.globals["expand"] = hassfunction(expand) self.filters["expand"] = contextfilter(self.globals["expand"]) self.globals["closest"] = hassfunction(closest) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 174d61ea470..4259e7302ed 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -24,6 +24,8 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import UnitSystem +from tests.common import MockConfigEntry, mock_device_registry, mock_registry + def _set_up_units(hass): """Set up the tests.""" @@ -1470,6 +1472,79 @@ async def test_expand(hass): assert info.rate_limit is None +async def test_device_entities(hass): + """Test expand function.""" + config_entry = MockConfigEntry(domain="light") + device_registry = mock_device_registry(hass) + entity_registry = mock_registry(hass) + + # Test non existing device ids + info = render_to_info(hass, "{{ device_entities('abc123') }}") + assert_result_info(info, []) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ device_entities(56) }}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Test device without entities + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={("mac", "12:34:56:AB:CD:EF")}, + ) + info = render_to_info(hass, f"{{{{ device_entities('{device_entry.id}') }}}}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Test device with single entity, which has no state + entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry, + device_id=device_entry.id, + ) + info = render_to_info(hass, f"{{{{ device_entities('{device_entry.id}') }}}}") + assert_result_info(info, ["light.hue_5678"], []) + assert info.rate_limit is None + info = render_to_info( + hass, + f"{{{{ device_entities('{device_entry.id}') | expand | map(attribute='entity_id') | join(', ') }}}}", + ) + assert_result_info(info, "", ["light.hue_5678"]) + assert info.rate_limit is None + + # Test device with single entity, with state + hass.states.async_set("light.hue_5678", "happy") + info = render_to_info( + hass, + f"{{{{ device_entities('{device_entry.id}') | expand | map(attribute='entity_id') | join(', ') }}}}", + ) + assert_result_info(info, "light.hue_5678", ["light.hue_5678"]) + assert info.rate_limit is None + + # Test device with multiple entities, which have a state + entity_registry.async_get_or_create( + "light", + "hue", + "ABCD", + config_entry=config_entry, + device_id=device_entry.id, + ) + hass.states.async_set("light.hue_abcd", "camper") + info = render_to_info(hass, f"{{{{ device_entities('{device_entry.id}') }}}}") + assert_result_info(info, ["light.hue_5678", "light.hue_abcd"], []) + assert info.rate_limit is None + info = render_to_info( + hass, + f"{{{{ device_entities('{device_entry.id}') | expand | map(attribute='entity_id') | join(', ') }}}}", + ) + assert_result_info( + info, "light.hue_5678, light.hue_abcd", ["light.hue_5678", "light.hue_abcd"] + ) + assert info.rate_limit is None + + def test_closest_function_to_coord(hass): """Test closest function to coord.""" hass.states.async_set( From 0e9148e239c01f2ca29e1ef2e0d69058aae24d51 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Feb 2021 19:57:02 -1000 Subject: [PATCH 563/796] Add suggested area support to nuheat (#46801) --- homeassistant/components/nuheat/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index e8f21fc89c2..35000dd21fa 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -291,4 +291,5 @@ class NuHeatThermostat(CoordinatorEntity, ClimateEntity): "name": self._thermostat.room, "model": "nVent Signature", "manufacturer": MANUFACTURER, + "suggested_area": self._thermostat.room, } From 3e334a4950ae8620167a2c470b42ebdb8dd88dfb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Feb 2021 19:57:21 -1000 Subject: [PATCH 564/796] Fix typing of fan speed count and steps (#46790) --- homeassistant/components/bond/fan.py | 2 +- homeassistant/components/demo/fan.py | 4 ++-- homeassistant/components/dyson/fan.py | 2 +- homeassistant/components/esphome/fan.py | 2 +- homeassistant/components/fan/__init__.py | 12 +++++++----- homeassistant/components/knx/fan.py | 2 +- homeassistant/components/smartthings/fan.py | 2 +- homeassistant/components/tuya/fan.py | 2 +- homeassistant/components/zwave_js/fan.py | 2 +- 9 files changed, 16 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index cef2efae690..5ff7e0c7065 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -87,7 +87,7 @@ class BondFan(BondEntity, FanEntity): return ranged_value_to_percentage(self._speed_range, self._speed) @property - def speed_count(self) -> Optional[int]: + def speed_count(self) -> int: """Return the number of speeds the fan supports.""" return int_states_in_range(self._speed_range) diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index 6bbd8b81f6d..c79b53c0918 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -216,7 +216,7 @@ class DemoPercentageFan(BaseDemoFan, FanEntity): return self._percentage @property - def speed_count(self) -> Optional[float]: + def speed_count(self) -> int: """Return the number of speeds the fan supports.""" return 3 @@ -276,7 +276,7 @@ class AsyncDemoPercentageFan(BaseDemoFan, FanEntity): return self._percentage @property - def speed_count(self) -> Optional[float]: + def speed_count(self) -> int: """Return the number of speeds the fan supports.""" return 3 diff --git a/homeassistant/components/dyson/fan.py b/homeassistant/components/dyson/fan.py index 9e49badbc8e..a8e737bb48b 100644 --- a/homeassistant/components/dyson/fan.py +++ b/homeassistant/components/dyson/fan.py @@ -156,7 +156,7 @@ class DysonFanEntity(DysonEntity, FanEntity): return ranged_value_to_percentage(SPEED_RANGE, int(self._device.state.speed)) @property - def speed_count(self) -> Optional[int]: + def speed_count(self) -> int: """Return the number of speeds the fan supports.""" return int_states_in_range(SPEED_RANGE) diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 092c416acab..df23f37cb63 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -120,7 +120,7 @@ class EsphomeFan(EsphomeEntity, FanEntity): ) @property - def speed_count(self) -> Optional[int]: + def speed_count(self) -> int: """Return the number of speeds the fan supports.""" return len(ORDERED_NAMED_FAN_SPEEDS) diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 692588cff48..18f46b3d619 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -272,15 +272,17 @@ class FanEntity(ToggleEntity): else: await self.async_set_speed(self.percentage_to_speed(percentage)) - async def async_increase_speed(self, percentage_step=None) -> None: + async def async_increase_speed(self, percentage_step: Optional[int] = None) -> None: """Increase the speed of the fan.""" await self._async_adjust_speed(1, percentage_step) - async def async_decrease_speed(self, percentage_step=None) -> None: + async def async_decrease_speed(self, percentage_step: Optional[int] = None) -> None: """Decrease the speed of the fan.""" await self._async_adjust_speed(-1, percentage_step) - async def _async_adjust_speed(self, modifier, percentage_step) -> None: + async def _async_adjust_speed( + self, modifier: int, percentage_step: Optional[int] + ) -> None: """Increase or decrease the speed of the fan.""" current_percentage = self.percentage or 0 @@ -462,7 +464,7 @@ class FanEntity(ToggleEntity): return 0 @property - def speed_count(self) -> Optional[int]: + def speed_count(self) -> int: """Return the number of speeds the fan supports.""" speed_list = speed_list_without_preset_modes(self.speed_list) if speed_list: @@ -470,7 +472,7 @@ class FanEntity(ToggleEntity): return 100 @property - def percentage_step(self) -> Optional[float]: + def percentage_step(self) -> float: """Return the step size for percentage.""" return 100 / self.speed_count diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index d0b7b4c5546..6aa4f722892 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -70,7 +70,7 @@ class KNXFan(KnxEntity, FanEntity): return self._device.current_speed @property - def speed_count(self) -> Optional[int]: + def speed_count(self) -> int: """Return the number of speeds the fan supports.""" if self._step_range is None: return super().speed_count diff --git a/homeassistant/components/smartthings/fan.py b/homeassistant/components/smartthings/fan.py index 12edac36dfe..4cd451e2416 100644 --- a/homeassistant/components/smartthings/fan.py +++ b/homeassistant/components/smartthings/fan.py @@ -81,7 +81,7 @@ class SmartThingsFan(SmartThingsEntity, FanEntity): return ranged_value_to_percentage(SPEED_RANGE, self._device.status.fan_speed) @property - def speed_count(self) -> Optional[int]: + def speed_count(self) -> int: """Return the number of speeds the fan supports.""" return int_states_in_range(SPEED_RANGE) diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index 4c555bb942a..cb6f96358c9 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -103,7 +103,7 @@ class TuyaFanDevice(TuyaDevice, FanEntity): self._tuya.oscillate(oscillating) @property - def speed_count(self) -> Optional[int]: + def speed_count(self) -> int: """Return the number of speeds the fan supports.""" if self.speeds is None: return super().speed_count diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index ae903d9efaf..ea17fbe4cff 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -98,7 +98,7 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): return ranged_value_to_percentage(SPEED_RANGE, self.info.primary_value.value) @property - def speed_count(self) -> Optional[int]: + def speed_count(self) -> int: """Return the number of speeds the fan supports.""" return int_states_in_range(SPEED_RANGE) From 2807cb1de783939251f29611534c532c8f6807d3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Feb 2021 19:57:37 -1000 Subject: [PATCH 565/796] Implement suggested area for netatmo (#46802) --- homeassistant/components/netatmo/climate.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index dee8d3b668d..34a94df008a 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -567,6 +567,11 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): schedule_id, ) + @property + def device_info(self): + """Return the device info for the thermostat.""" + return {**super().device_info, "suggested_area": self._room_data["name"]} + def interpolate(batterylevel, module_type): """Interpolate battery level depending on device type.""" From 11277faa93da7bc18447e1ac5973a4c8c92c8de0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Feb 2021 20:01:22 -1000 Subject: [PATCH 566/796] Add suggested area to nexia (#46776) --- homeassistant/components/nexia/entity.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nexia/entity.py b/homeassistant/components/nexia/entity.py index 7820ebb6216..62f6e8275c4 100644 --- a/homeassistant/components/nexia/entity.py +++ b/homeassistant/components/nexia/entity.py @@ -83,10 +83,12 @@ class NexiaThermostatZoneEntity(NexiaThermostatEntity): def device_info(self): """Return the device_info of the device.""" data = super().device_info + zone_name = self._zone.get_name() data.update( { "identifiers": {(DOMAIN, self._zone.zone_id)}, - "name": self._zone.get_name(), + "name": zone_name, + "suggested_area": zone_name, "via_device": (DOMAIN, self._zone.thermostat.thermostat_id), } ) From 4078a8782edcf4ec60a5e193fdd499dae5e05efe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Feb 2021 20:17:00 -1000 Subject: [PATCH 567/796] Add suggested area to hunterdouglas_powerview (#46774) --- .../components/hunterdouglas_powerview/cover.py | 11 +++++------ .../components/hunterdouglas_powerview/entity.py | 9 ++++++--- .../components/hunterdouglas_powerview/scene.py | 16 +++++++--------- .../components/hunterdouglas_powerview/sensor.py | 8 +++++++- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index d96beec53ae..e90b315fd16 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -77,9 +77,11 @@ async def async_setup_entry(hass, entry, async_add_entities): name_before_refresh, ) continue + room_id = shade.raw_data.get(ROOM_ID_IN_SHADE) + room_name = room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "") entities.append( PowerViewShade( - shade, name_before_refresh, room_data, coordinator, device_info + coordinator, device_info, room_name, shade, name_before_refresh ) ) async_add_entities(entities) @@ -98,17 +100,14 @@ def hass_position_to_hd(hass_positon): class PowerViewShade(ShadeEntity, CoverEntity): """Representation of a powerview shade.""" - def __init__(self, shade, name, room_data, coordinator, device_info): + def __init__(self, coordinator, device_info, room_name, shade, name): """Initialize the shade.""" - room_id = shade.raw_data.get(ROOM_ID_IN_SHADE) - super().__init__(coordinator, device_info, shade, name) + super().__init__(coordinator, device_info, room_name, shade, name) self._shade = shade - self._device_info = device_info self._is_opening = False self._is_closing = False self._last_action_timestamp = 0 self._scheduled_transition_update = None - self._room_name = room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "") self._current_cover_position = MIN_POSITION @property diff --git a/homeassistant/components/hunterdouglas_powerview/entity.py b/homeassistant/components/hunterdouglas_powerview/entity.py index 4ed68fc3557..679e55e806c 100644 --- a/homeassistant/components/hunterdouglas_powerview/entity.py +++ b/homeassistant/components/hunterdouglas_powerview/entity.py @@ -23,9 +23,10 @@ from .const import ( class HDEntity(CoordinatorEntity): """Base class for hunter douglas entities.""" - def __init__(self, coordinator, device_info, unique_id): + def __init__(self, coordinator, device_info, room_name, unique_id): """Initialize the entity.""" super().__init__(coordinator) + self._room_name = room_name self._unique_id = unique_id self._device_info = device_info @@ -45,6 +46,7 @@ class HDEntity(CoordinatorEntity): (dr.CONNECTION_NETWORK_MAC, self._device_info[DEVICE_MAC_ADDRESS]) }, "name": self._device_info[DEVICE_NAME], + "suggested_area": self._room_name, "model": self._device_info[DEVICE_MODEL], "sw_version": sw_version, "manufacturer": MANUFACTURER, @@ -54,9 +56,9 @@ class HDEntity(CoordinatorEntity): class ShadeEntity(HDEntity): """Base class for hunter douglas shade entities.""" - def __init__(self, coordinator, device_info, shade, shade_name): + def __init__(self, coordinator, device_info, room_name, shade, shade_name): """Initialize the shade.""" - super().__init__(coordinator, device_info, shade.id) + super().__init__(coordinator, device_info, room_name, shade.id) self._shade_name = shade_name self._shade = shade @@ -67,6 +69,7 @@ class ShadeEntity(HDEntity): device_info = { "identifiers": {(DOMAIN, self._shade.id)}, "name": self._shade_name, + "suggested_area": self._room_name, "manufacturer": MANUFACTURER, "via_device": (DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER]), } diff --git a/homeassistant/components/hunterdouglas_powerview/scene.py b/homeassistant/components/hunterdouglas_powerview/scene.py index 61c93078aa1..33c7e7129fc 100644 --- a/homeassistant/components/hunterdouglas_powerview/scene.py +++ b/homeassistant/components/hunterdouglas_powerview/scene.py @@ -49,23 +49,21 @@ async def async_setup_entry(hass, entry, async_add_entities): coordinator = pv_data[COORDINATOR] device_info = pv_data[DEVICE_INFO] - pvscenes = ( - PowerViewScene( - PvScene(raw_scene, pv_request), room_data, coordinator, device_info - ) - for scene_id, raw_scene in scene_data.items() - ) + pvscenes = [] + for raw_scene in scene_data.values(): + scene = PvScene(raw_scene, pv_request) + room_name = room_data.get(scene.room_id, {}).get(ROOM_NAME_UNICODE, "") + pvscenes.append(PowerViewScene(coordinator, device_info, room_name, scene)) async_add_entities(pvscenes) class PowerViewScene(HDEntity, Scene): """Representation of a Powerview scene.""" - def __init__(self, scene, room_data, coordinator, device_info): + def __init__(self, coordinator, device_info, room_name, scene): """Initialize the scene.""" - super().__init__(coordinator, device_info, scene.id) + super().__init__(coordinator, device_info, room_name, scene.id) self._scene = scene - self._room_name = room_data.get(scene.room_id, {}).get(ROOM_NAME_UNICODE, "") @property def name(self): diff --git a/homeassistant/components/hunterdouglas_powerview/sensor.py b/homeassistant/components/hunterdouglas_powerview/sensor.py index 6241ddd4d62..130e8dd507a 100644 --- a/homeassistant/components/hunterdouglas_powerview/sensor.py +++ b/homeassistant/components/hunterdouglas_powerview/sensor.py @@ -9,7 +9,10 @@ from .const import ( DEVICE_INFO, DOMAIN, PV_API, + PV_ROOM_DATA, PV_SHADE_DATA, + ROOM_ID_IN_SHADE, + ROOM_NAME_UNICODE, SHADE_BATTERY_LEVEL, SHADE_BATTERY_LEVEL_MAX, ) @@ -20,6 +23,7 @@ async def async_setup_entry(hass, entry, async_add_entities): """Set up the hunter douglas shades sensors.""" pv_data = hass.data[DOMAIN][entry.entry_id] + room_data = pv_data[PV_ROOM_DATA] shade_data = pv_data[PV_SHADE_DATA] pv_request = pv_data[PV_API] coordinator = pv_data[COORDINATOR] @@ -31,9 +35,11 @@ async def async_setup_entry(hass, entry, async_add_entities): if SHADE_BATTERY_LEVEL not in shade.raw_data: continue name_before_refresh = shade.name + room_id = shade.raw_data.get(ROOM_ID_IN_SHADE) + room_name = room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "") entities.append( PowerViewShadeBatterySensor( - coordinator, device_info, shade, name_before_refresh + coordinator, device_info, room_name, shade, name_before_refresh ) ) async_add_entities(entities) From 22dbac259bc0761d99558a7daf6c6aa487ed6fc6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Feb 2021 20:18:21 -1000 Subject: [PATCH 568/796] Ensure recorder shuts down cleanly on restart before startup is finished (#46604) --- homeassistant/components/recorder/__init__.py | 9 ++- homeassistant/components/recorder/util.py | 19 ++++++- tests/components/recorder/test_init.py | 37 +++++++++++- tests/components/recorder/test_util.py | 56 +++++++++++++++---- 4 files changed, 104 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 16232bcaa16..ceec7ee9eed 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -346,8 +346,15 @@ class Recorder(threading.Thread): self.hass.add_job(register) result = hass_started.result() + self.event_session = self.get_session() + self.event_session.expire_on_commit = False + # If shutdown happened before Home Assistant finished starting if result is shutdown_task: + # Make sure we cleanly close the run if + # we restart before startup finishes + self._close_run() + self._close_connection() return # Start periodic purge @@ -363,8 +370,6 @@ class Recorder(threading.Thread): async_purge, hour=4, minute=12, second=0 ) - self.event_session = self.get_session() - self.event_session.expire_on_commit = False # Use a session for the event read loop # with a commit every time the event time # has changed. This reduces the disk io. diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index abf14268687..41bca335a56 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -171,7 +171,10 @@ def validate_sqlite_database(dbpath: str, db_integrity_check: bool) -> bool: def run_checks_on_open_db(dbpath, cursor, db_integrity_check): """Run checks that will generate a sqlite3 exception if there is corruption.""" - if basic_sanity_check(cursor) and last_run_was_recently_clean(cursor): + sanity_check_passed = basic_sanity_check(cursor) + last_run_was_clean = last_run_was_recently_clean(cursor) + + if sanity_check_passed and last_run_was_clean: _LOGGER.debug( "The quick_check will be skipped as the system was restarted cleanly and passed the basic sanity check" ) @@ -187,7 +190,19 @@ def run_checks_on_open_db(dbpath, cursor, db_integrity_check): ) return - _LOGGER.debug( + if not sanity_check_passed: + _LOGGER.warning( + "The database sanity check failed to validate the sqlite3 database at %s", + dbpath, + ) + + if not last_run_was_clean: + _LOGGER.warning( + "The system could not validate that the sqlite3 database at %s was shutdown cleanly.", + dbpath, + ) + + _LOGGER.info( "A quick_check is being performed on the sqlite3 database at %s", dbpath ) cursor.execute("PRAGMA QUICK_CHECK") diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index d4092d709c0..3b71648166e 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -16,14 +16,45 @@ from homeassistant.components.recorder import ( from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.models import Events, RecorderRuns, States from homeassistant.components.recorder.util import session_scope -from homeassistant.const import MATCH_ALL, STATE_LOCKED, STATE_UNLOCKED -from homeassistant.core import Context, callback +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, + MATCH_ALL, + STATE_LOCKED, + STATE_UNLOCKED, +) +from homeassistant.core import Context, CoreState, callback from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from .common import wait_recording_done -from tests.common import fire_time_changed, get_test_home_assistant +from tests.common import ( + async_init_recorder_component, + fire_time_changed, + get_test_home_assistant, +) + + +async def test_shutdown_before_startup_finishes(hass): + """Test shutdown before recorder starts is clean.""" + + hass.state = CoreState.not_running + + await async_init_recorder_component(hass) + await hass.async_block_till_done() + + session = await hass.async_add_executor_job(hass.data[DATA_INSTANCE].get_session) + + with patch.object(hass.data[DATA_INSTANCE], "engine"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + hass.stop() + + run_info = await hass.async_add_executor_job(run_information_with_session, session) + + assert run_info.run_id == 1 + assert run_info.start is not None + assert run_info.end is not None def test_saving_state(hass, hass_recorder): diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index a4109648d2f..38df1285008 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -178,36 +178,72 @@ def test_basic_sanity_check(hass_recorder): util.basic_sanity_check(cursor) -def test_combined_checks(hass_recorder): +def test_combined_checks(hass_recorder, caplog): """Run Checks on the open database.""" hass = hass_recorder() - db_integrity_check = False - cursor = hass.data[DATA_INSTANCE].engine.raw_connection().cursor() - assert ( - util.run_checks_on_open_db("fake_db_path", cursor, db_integrity_check) is None - ) + assert util.run_checks_on_open_db("fake_db_path", cursor, False) is None + assert "skipped because db_integrity_check was disabled" in caplog.text + + caplog.clear() + assert util.run_checks_on_open_db("fake_db_path", cursor, True) is None + assert "could not validate that the sqlite3 database" in caplog.text + + # We are patching recorder.util here in order + # to avoid creating the full database on disk + with patch( + "homeassistant.components.recorder.util.basic_sanity_check", return_value=False + ): + caplog.clear() + assert util.run_checks_on_open_db("fake_db_path", cursor, False) is None + assert "skipped because db_integrity_check was disabled" in caplog.text + + caplog.clear() + assert util.run_checks_on_open_db("fake_db_path", cursor, True) is None + assert "could not validate that the sqlite3 database" in caplog.text # We are patching recorder.util here in order # to avoid creating the full database on disk with patch("homeassistant.components.recorder.util.last_run_was_recently_clean"): + caplog.clear() + assert util.run_checks_on_open_db("fake_db_path", cursor, False) is None assert ( - util.run_checks_on_open_db("fake_db_path", cursor, db_integrity_check) - is None + "system was restarted cleanly and passed the basic sanity check" + in caplog.text ) + caplog.clear() + assert util.run_checks_on_open_db("fake_db_path", cursor, True) is None + assert ( + "system was restarted cleanly and passed the basic sanity check" + in caplog.text + ) + + caplog.clear() with patch( "homeassistant.components.recorder.util.last_run_was_recently_clean", side_effect=sqlite3.DatabaseError, ), pytest.raises(sqlite3.DatabaseError): - util.run_checks_on_open_db("fake_db_path", cursor, db_integrity_check) + util.run_checks_on_open_db("fake_db_path", cursor, False) + + caplog.clear() + with patch( + "homeassistant.components.recorder.util.last_run_was_recently_clean", + side_effect=sqlite3.DatabaseError, + ), pytest.raises(sqlite3.DatabaseError): + util.run_checks_on_open_db("fake_db_path", cursor, True) cursor.execute("DROP TABLE events;") + caplog.clear() with pytest.raises(sqlite3.DatabaseError): - util.run_checks_on_open_db("fake_db_path", cursor, db_integrity_check) + util.run_checks_on_open_db("fake_db_path", cursor, False) + + caplog.clear() + with pytest.raises(sqlite3.DatabaseError): + util.run_checks_on_open_db("fake_db_path", cursor, True) def _corrupt_db_file(test_db_file): From 500cb172981b743ea8463183758337427efb94ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Feb 2021 20:22:48 -1000 Subject: [PATCH 569/796] Ensure HomeAssistant can still restart when a library file is missing (#46664) --- homeassistant/config.py | 13 ++++++++++--- tests/test_config.py | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 73d0273d1c0..90df365c349 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -76,6 +76,13 @@ AUTOMATION_CONFIG_PATH = "automations.yaml" SCRIPT_CONFIG_PATH = "scripts.yaml" SCENE_CONFIG_PATH = "scenes.yaml" +LOAD_EXCEPTIONS = (ImportError, FileNotFoundError) +INTEGRATION_LOAD_EXCEPTIONS = ( + IntegrationNotFound, + RequirementsNotFound, + *LOAD_EXCEPTIONS, +) + DEFAULT_CONFIG = f""" # Configure a default setup of Home Assistant (frontend, api, etc) default_config: @@ -689,7 +696,7 @@ async def merge_packages_config( hass, domain ) component = integration.get_component() - except (IntegrationNotFound, RequirementsNotFound, ImportError) as ex: + except INTEGRATION_LOAD_EXCEPTIONS as ex: _log_pkg_error(pack_name, comp_name, config, str(ex)) continue @@ -746,7 +753,7 @@ async def async_process_component_config( domain = integration.domain try: component = integration.get_component() - except ImportError as ex: + except LOAD_EXCEPTIONS as ex: _LOGGER.error("Unable to import %s: %s", domain, ex) return None @@ -825,7 +832,7 @@ async def async_process_component_config( try: platform = p_integration.get_platform(domain) - except ImportError: + except LOAD_EXCEPTIONS: _LOGGER.exception("Platform error: %s", domain) continue diff --git a/tests/test_config.py b/tests/test_config.py index 7dd7d61e8ef..299cf9caa73 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1088,6 +1088,26 @@ async def test_component_config_exceptions(hass, caplog): in caplog.text ) + # get_component raising + caplog.clear() + assert ( + await config_util.async_process_component_config( + hass, + {"test_domain": {}}, + integration=Mock( + pkg_path="homeassistant.components.test_domain", + domain="test_domain", + get_component=Mock( + side_effect=FileNotFoundError( + "No such file or directory: b'liblibc.a'" + ) + ), + ), + ) + is None + ) + assert "Unable to import test_domain: No such file or directory" in caplog.text + @pytest.mark.parametrize( "domain, schema, expected", From 749883dc62278f1e1de33c2c9a03ff20d87f9f73 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Feb 2021 20:24:49 -1000 Subject: [PATCH 570/796] Implement suggested area in lutron_caseta (#45941) --- homeassistant/components/lutron_caseta/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 220096fe0bf..56cc7a78c96 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -229,6 +229,7 @@ async def _async_register_button_devices( dr_device = device_registry.async_get_or_create( name=device["leap_name"], + suggested_area=device["leap_name"].split("_")[0], manufacturer=MANUFACTURER, config_entry_id=config_entry_id, identifiers={(DOMAIN, device["serial"])}, @@ -344,6 +345,7 @@ class LutronCasetaDevice(Entity): return { "identifiers": {(DOMAIN, self.serial)}, "name": self.name, + "suggested_area": self._device["name"].split("_")[0], "manufacturer": MANUFACTURER, "model": f"{self._device['model']} ({self._device['type']})", "via_device": (DOMAIN, self._bridge_device["serial"]), From 773a2027771d72d62fee7484d247f08d1f3032f0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Feb 2021 20:37:12 -1000 Subject: [PATCH 571/796] Implement percentage step sizes in HomeKit (#46722) --- homeassistant/components/homekit/type_fans.py | 9 ++++++++- tests/components/homekit/test_type_fans.py | 5 ++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index 7ed7256d48c..d7215be9508 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -7,6 +7,7 @@ from homeassistant.components.fan import ( ATTR_DIRECTION, ATTR_OSCILLATING, ATTR_PERCENTAGE, + ATTR_PERCENTAGE_STEP, DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, @@ -33,6 +34,7 @@ from .const import ( CHAR_ROTATION_DIRECTION, CHAR_ROTATION_SPEED, CHAR_SWING_MODE, + PROP_MIN_STEP, SERV_FANV2, ) @@ -53,6 +55,7 @@ class Fan(HomeAccessory): state = self.hass.states.get(self.entity_id) features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + percentage_step = state.attributes.get(ATTR_PERCENTAGE_STEP, 1) if features & SUPPORT_DIRECTION: chars.append(CHAR_ROTATION_DIRECTION) @@ -77,7 +80,11 @@ class Fan(HomeAccessory): # Initial value is set to 100 because 0 is a special value (off). 100 is # an arbitrary non-zero value. It is updated immediately by async_update_state # to set to the correct initial value. - self.char_speed = serv_fan.configure_char(CHAR_ROTATION_SPEED, value=100) + self.char_speed = serv_fan.configure_char( + CHAR_ROTATION_SPEED, + value=100, + properties={PROP_MIN_STEP: percentage_step}, + ) if CHAR_SWING_MODE in chars: self.char_swing = serv_fan.configure_char(CHAR_SWING_MODE, value=0) diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index 8111d256594..e99f9d0b95a 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -6,6 +6,7 @@ from homeassistant.components.fan import ( ATTR_DIRECTION, ATTR_OSCILLATING, ATTR_PERCENTAGE, + ATTR_PERCENTAGE_STEP, DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, @@ -13,7 +14,7 @@ from homeassistant.components.fan import ( SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, ) -from homeassistant.components.homekit.const import ATTR_VALUE +from homeassistant.components.homekit.const import ATTR_VALUE, PROP_MIN_STEP from homeassistant.components.homekit.type_fans import Fan from homeassistant.const import ( ATTR_ENTITY_ID, @@ -254,6 +255,7 @@ async def test_fan_speed(hass, hk_driver, events): { ATTR_SUPPORTED_FEATURES: SUPPORT_SET_SPEED, ATTR_PERCENTAGE: 0, + ATTR_PERCENTAGE_STEP: 25, }, ) await hass.async_block_till_done() @@ -263,6 +265,7 @@ async def test_fan_speed(hass, hk_driver, events): # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the # speed to 100 when turning on a fan on a freshly booted up server. assert acc.char_speed.value != 0 + assert acc.char_speed.properties[PROP_MIN_STEP] == 25 await acc.run_handler() await hass.async_block_till_done() From 5b95f61fd36cca19bfcc24e3df1cdb098c0d109b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Feb 2021 20:38:45 -1000 Subject: [PATCH 572/796] Update smarty to use new fan entity model (#45879) --- homeassistant/components/smarty/fan.py | 110 +++++++++++-------------- 1 file changed, 48 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/smarty/fan.py b/homeassistant/components/smarty/fan.py index 40c244944ce..20376e1d44e 100644 --- a/homeassistant/components/smarty/fan.py +++ b/homeassistant/components/smarty/fan.py @@ -1,26 +1,25 @@ """Platform to control a Salda Smarty XP/XV ventilation unit.""" import logging +import math +from typing import Optional -from homeassistant.components.fan import ( - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, - SUPPORT_SET_SPEED, - FanEntity, -) +from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.percentage import ( + int_states_in_range, + percentage_to_ranged_value, + ranged_value_to_percentage, +) from . import DOMAIN, SIGNAL_UPDATE_SMARTY _LOGGER = logging.getLogger(__name__) -SPEED_LIST = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - -SPEED_MAPPING = {1: SPEED_LOW, 2: SPEED_MEDIUM, 3: SPEED_HIGH} -SPEED_TO_MODE = {v: k for k, v in SPEED_MAPPING.items()} +DEFAULT_ON_PERCENTAGE = 66 +SPEED_RANGE = (1, 3) # off is not included async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -37,8 +36,7 @@ class SmartyFan(FanEntity): def __init__(self, name, smarty): """Initialize the entity.""" self._name = name - self._speed = SPEED_OFF - self._state = None + self._smarty_fan_speed = 0 self._smarty = smarty @property @@ -61,76 +59,64 @@ class SmartyFan(FanEntity): """Return the list of supported features.""" return SUPPORT_SET_SPEED - @property - def speed_list(self): - """List of available fan modes.""" - return SPEED_LIST - @property def is_on(self): """Return state of the fan.""" - return self._state + return bool(self._smarty_fan_speed) @property - def speed(self) -> str: - """Return speed of the fan.""" - return self._speed + def speed_count(self) -> Optional[int]: + """Return the number of speeds the fan supports.""" + return int_states_in_range(SPEED_RANGE) - def set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - _LOGGER.debug("Set the fan speed to %s", speed) - if speed == SPEED_OFF: + @property + def percentage(self) -> str: + """Return speed percentage of the fan.""" + if self._smarty_fan_speed == 0: + return 0 + return ranged_value_to_percentage(SPEED_RANGE, self._smarty_fan_speed) + + def set_percentage(self, percentage: int) -> None: + """Set the speed percentage of the fan.""" + _LOGGER.debug("Set the fan percentage to %s", percentage) + if percentage == 0: self.turn_off() - else: - self._smarty.set_fan_speed(SPEED_TO_MODE.get(speed)) - self._speed = speed - self._state = True + return + + fan_speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) + if not self._smarty.set_fan_speed(fan_speed): + raise HomeAssistantError( + f"Failed to set the fan speed percentage to {percentage}" + ) + + self._smarty_fan_speed = fan_speed + self.schedule_update_ha_state() - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # def turn_on(self, speed=None, percentage=None, preset_mode=None, **kwargs): """Turn on the fan.""" _LOGGER.debug("Turning on fan. Speed is %s", speed) - if speed is None: - if self._smarty.turn_on(SPEED_TO_MODE.get(self._speed)): - self._state = True - self._speed = SPEED_MEDIUM - else: - if self._smarty.set_fan_speed(SPEED_TO_MODE.get(speed)): - self._speed = speed - self._state = True - - self.schedule_update_ha_state() + self.set_percentage(percentage or DEFAULT_ON_PERCENTAGE) def turn_off(self, **kwargs): """Turn off the fan.""" _LOGGER.debug("Turning off fan") - if self._smarty.turn_off(): - self._state = False + if not self._smarty.turn_off(): + raise HomeAssistantError("Failed to turn off the fan") + self._smarty_fan_speed = 0 self.schedule_update_ha_state() async def async_added_to_hass(self): """Call to update fan.""" - async_dispatcher_connect(self.hass, SIGNAL_UPDATE_SMARTY, self._update_callback) + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_SMARTY, self._update_callback + ) + ) @callback def _update_callback(self): """Call update method.""" - self.async_schedule_update_ha_state(True) - - def update(self): - """Update state.""" _LOGGER.debug("Updating state") - result = self._smarty.fan_speed - if result: - self._speed = SPEED_MAPPING[result] - _LOGGER.debug("Speed is %s, Mode is %s", self._speed, result) - self._state = True - else: - self._state = False + self._smarty_fan_speed = self._smarty.fan_speed + self.async_write_ha_state() From 6707496c5d73f04d0e5ae4929663bd73a12ab7ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Feb 2021 20:45:14 -1000 Subject: [PATCH 573/796] Update alexa for new fan model (#45836) --- .../components/alexa/capabilities.py | 9 +-- homeassistant/components/alexa/const.py | 8 --- homeassistant/components/alexa/handlers.py | 62 ++++--------------- tests/components/alexa/test_capabilities.py | 4 ++ tests/components/alexa/test_smart_home.py | 37 +++++------ 5 files changed, 37 insertions(+), 83 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 008870c8dd9..acfba91a933 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -46,7 +46,6 @@ from .const import ( API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, DATE_FORMAT, - PERCENTAGE_FAN_MAP, Inputs, ) from .errors import UnsupportedProperty @@ -668,9 +667,7 @@ class AlexaPercentageController(AlexaCapability): raise UnsupportedProperty(name) if self.entity.domain == fan.DOMAIN: - speed = self.entity.attributes.get(fan.ATTR_SPEED) - - return PERCENTAGE_FAN_MAP.get(speed, 0) + return self.entity.attributes.get(fan.ATTR_PERCENTAGE) or 0 if self.entity.domain == cover.DOMAIN: return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION, 0) @@ -1155,9 +1152,7 @@ class AlexaPowerLevelController(AlexaCapability): raise UnsupportedProperty(name) if self.entity.domain == fan.DOMAIN: - speed = self.entity.attributes.get(fan.ATTR_SPEED) - - return PERCENTAGE_FAN_MAP.get(speed) + return self.entity.attributes.get(fan.ATTR_PERCENTAGE) or 0 return None diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index ca0d8435e02..a076fdcad9e 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -1,7 +1,6 @@ """Constants for the Alexa integration.""" from collections import OrderedDict -from homeassistant.components import fan from homeassistant.components.climate import const as climate from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT @@ -80,13 +79,6 @@ API_THERMOSTAT_MODES = OrderedDict( API_THERMOSTAT_MODES_CUSTOM = {climate.HVAC_MODE_DRY: "DEHUMIDIFY"} API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"} -PERCENTAGE_FAN_MAP = { - fan.SPEED_OFF: 0, - fan.SPEED_LOW: 33, - fan.SPEED_MEDIUM: 66, - fan.SPEED_HIGH: 100, -} - class Cause: """Possible causes for property changes. diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 8837210b6ad..dce4f9f2210 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -54,7 +54,6 @@ from .const import ( API_THERMOSTAT_MODES, API_THERMOSTAT_MODES_CUSTOM, API_THERMOSTAT_PRESETS, - PERCENTAGE_FAN_MAP, Cause, Inputs, ) @@ -360,17 +359,9 @@ async def async_api_set_percentage(hass, config, directive, context): data = {ATTR_ENTITY_ID: entity.entity_id} if entity.domain == fan.DOMAIN: - service = fan.SERVICE_SET_SPEED - speed = "off" - + service = fan.SERVICE_SET_PERCENTAGE percentage = int(directive.payload["percentage"]) - if percentage <= 33: - speed = "low" - elif percentage <= 66: - speed = "medium" - elif percentage <= 100: - speed = "high" - data[fan.ATTR_SPEED] = speed + data[fan.ATTR_PERCENTAGE] = percentage await hass.services.async_call( entity.domain, service, data, blocking=False, context=context @@ -388,22 +379,12 @@ async def async_api_adjust_percentage(hass, config, directive, context): data = {ATTR_ENTITY_ID: entity.entity_id} if entity.domain == fan.DOMAIN: - service = fan.SERVICE_SET_SPEED - speed = entity.attributes.get(fan.ATTR_SPEED) - current = PERCENTAGE_FAN_MAP.get(speed, 100) + service = fan.SERVICE_SET_PERCENTAGE + current = entity.attributes.get(fan.ATTR_PERCENTAGE) or 0 # set percentage - percentage = max(0, percentage_delta + current) - speed = "off" - - if percentage <= 33: - speed = "low" - elif percentage <= 66: - speed = "medium" - elif percentage <= 100: - speed = "high" - - data[fan.ATTR_SPEED] = speed + percentage = min(100, max(0, percentage_delta + current)) + data[fan.ATTR_PERCENTAGE] = percentage await hass.services.async_call( entity.domain, service, data, blocking=False, context=context @@ -854,18 +835,9 @@ async def async_api_set_power_level(hass, config, directive, context): data = {ATTR_ENTITY_ID: entity.entity_id} if entity.domain == fan.DOMAIN: - service = fan.SERVICE_SET_SPEED - speed = "off" - + service = fan.SERVICE_SET_PERCENTAGE percentage = int(directive.payload["powerLevel"]) - if percentage <= 33: - speed = "low" - elif percentage <= 66: - speed = "medium" - else: - speed = "high" - - data[fan.ATTR_SPEED] = speed + data[fan.ATTR_PERCENTAGE] = percentage await hass.services.async_call( entity.domain, service, data, blocking=False, context=context @@ -883,22 +855,12 @@ async def async_api_adjust_power_level(hass, config, directive, context): data = {ATTR_ENTITY_ID: entity.entity_id} if entity.domain == fan.DOMAIN: - service = fan.SERVICE_SET_SPEED - speed = entity.attributes.get(fan.ATTR_SPEED) - current = PERCENTAGE_FAN_MAP.get(speed, 100) + service = fan.SERVICE_SET_PERCENTAGE + current = entity.attributes.get(fan.ATTR_PERCENTAGE) or 0 # set percentage - percentage = max(0, percentage_delta + current) - speed = "off" - - if percentage <= 33: - speed = "low" - elif percentage <= 66: - speed = "medium" - else: - speed = "high" - - data[fan.ATTR_SPEED] = speed + percentage = min(100, max(0, percentage_delta + current)) + data[fan.ATTR_PERCENTAGE] = percentage await hass.services.async_call( entity.domain, service, data, blocking=False, context=context diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 0bdbac70d7d..cd013ca70d9 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -323,6 +323,7 @@ async def test_report_fan_speed_state(hass): "friendly_name": "Off fan", "speed": "off", "supported_features": 1, + "percentage": 0, "speed_list": ["off", "low", "medium", "high"], }, ) @@ -333,6 +334,7 @@ async def test_report_fan_speed_state(hass): "friendly_name": "Low speed fan", "speed": "low", "supported_features": 1, + "percentage": 33, "speed_list": ["off", "low", "medium", "high"], }, ) @@ -343,6 +345,7 @@ async def test_report_fan_speed_state(hass): "friendly_name": "Medium speed fan", "speed": "medium", "supported_features": 1, + "percentage": 66, "speed_list": ["off", "low", "medium", "high"], }, ) @@ -353,6 +356,7 @@ async def test_report_fan_speed_state(hass): "friendly_name": "High speed fan", "speed": "high", "supported_features": 1, + "percentage": 100, "speed_list": ["off", "low", "medium", "high"], }, ) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 05a60c86ae0..657bc407fb0 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -383,6 +383,7 @@ async def test_variable_fan(hass): "supported_features": 1, "speed_list": ["low", "medium", "high"], "speed": "high", + "percentage": 100, }, ) appliance = await discovery_test(device, hass) @@ -423,82 +424,82 @@ async def test_variable_fan(hass): "Alexa.PercentageController", "SetPercentage", "fan#test_2", - "fan.set_speed", + "fan.set_percentage", hass, payload={"percentage": "50"}, ) - assert call.data["speed"] == "medium" + assert call.data["percentage"] == 50 call, _ = await assert_request_calls_service( "Alexa.PercentageController", "SetPercentage", "fan#test_2", - "fan.set_speed", + "fan.set_percentage", hass, payload={"percentage": "33"}, ) - assert call.data["speed"] == "low" + assert call.data["percentage"] == 33 call, _ = await assert_request_calls_service( "Alexa.PercentageController", "SetPercentage", "fan#test_2", - "fan.set_speed", + "fan.set_percentage", hass, payload={"percentage": "100"}, ) - assert call.data["speed"] == "high" + assert call.data["percentage"] == 100 await assert_percentage_changes( hass, - [("high", "-5"), ("off", "5"), ("low", "-80"), ("medium", "-34")], + [(95, "-5"), (100, "5"), (20, "-80"), (66, "-34")], "Alexa.PercentageController", "AdjustPercentage", "fan#test_2", "percentageDelta", - "fan.set_speed", - "speed", + "fan.set_percentage", + "percentage", ) call, _ = await assert_request_calls_service( "Alexa.PowerLevelController", "SetPowerLevel", "fan#test_2", - "fan.set_speed", + "fan.set_percentage", hass, payload={"powerLevel": "20"}, ) - assert call.data["speed"] == "low" + assert call.data["percentage"] == 20 call, _ = await assert_request_calls_service( "Alexa.PowerLevelController", "SetPowerLevel", "fan#test_2", - "fan.set_speed", + "fan.set_percentage", hass, payload={"powerLevel": "50"}, ) - assert call.data["speed"] == "medium" + assert call.data["percentage"] == 50 call, _ = await assert_request_calls_service( "Alexa.PowerLevelController", "SetPowerLevel", "fan#test_2", - "fan.set_speed", + "fan.set_percentage", hass, payload={"powerLevel": "99"}, ) - assert call.data["speed"] == "high" + assert call.data["percentage"] == 99 await assert_percentage_changes( hass, - [("high", "-5"), ("medium", "-50"), ("low", "-80")], + [(95, "-5"), (50, "-50"), (20, "-80")], "Alexa.PowerLevelController", "AdjustPowerLevel", "fan#test_2", "powerLevelDelta", - "fan.set_speed", - "speed", + "fan.set_percentage", + "percentage", ) From 5b0b01d727f8364229dafc443b7744abe7e2f32e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Feb 2021 21:06:43 -1000 Subject: [PATCH 574/796] Implement suggested areas in bond (#45942) Co-authored-by: Paulus Schoutsen --- homeassistant/components/bond/__init__.py | 4 +- homeassistant/components/bond/config_flow.py | 22 ++-- homeassistant/components/bond/entity.py | 3 +- homeassistant/components/bond/manifest.json | 2 +- homeassistant/components/bond/utils.py | 52 +++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bond/common.py | 35 +++++- tests/components/bond/test_config_flow.py | 62 ++++++++-- tests/components/bond/test_init.py | 124 ++++++++++++++++--- tests/components/bond/test_light.py | 2 +- 11 files changed, 249 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 4e6705cbe09..9d0a613000a 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -50,14 +50,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if not entry.unique_id: hass.config_entries.async_update_entry(entry, unique_id=hub.bond_id) + hub_name = hub.name or hub.bond_id device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( config_entry_id=config_entry_id, identifiers={(DOMAIN, hub.bond_id)}, manufacturer=BRIDGE_MAKE, - name=hub.bond_id, + name=hub_name, model=hub.target, sw_version=hub.fw_ver, + suggested_area=hub.location, ) _async_remove_old_device_identifiers(config_entry_id, device_registry, hub) diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 9298961269e..0132df486d3 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -1,6 +1,6 @@ """Config flow for Bond integration.""" import logging -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Tuple from aiohttp import ClientConnectionError, ClientResponseError from bond_api import Bond @@ -16,6 +16,7 @@ from homeassistant.const import ( from .const import CONF_BOND_ID from .const import DOMAIN # pylint:disable=unused-import +from .utils import BondHub _LOGGER = logging.getLogger(__name__) @@ -25,14 +26,13 @@ DATA_SCHEMA_USER = vol.Schema( DATA_SCHEMA_DISCOVERY = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}) -async def _validate_input(data: Dict[str, Any]) -> str: +async def _validate_input(data: Dict[str, Any]) -> Tuple[str, Optional[str]]: """Validate the user input allows us to connect.""" + bond = Bond(data[CONF_HOST], data[CONF_ACCESS_TOKEN]) try: - bond = Bond(data[CONF_HOST], data[CONF_ACCESS_TOKEN]) - version = await bond.version() - # call to non-version API is needed to validate authentication - await bond.devices() + hub = BondHub(bond) + await hub.setup(max_devices=1) except ClientConnectionError as error: raise InputValidationError("cannot_connect") from error except ClientResponseError as error: @@ -44,11 +44,10 @@ async def _validate_input(data: Dict[str, Any]) -> str: raise InputValidationError("unknown") from error # Return unique ID from the hub to be stored in the config entry. - bond_id = version.get("bondid") - if not bond_id: + if not hub.bond_id: raise InputValidationError("old_firmware") - return bond_id + return hub.bond_id, hub.name class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -113,10 +112,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) async def _try_create_entry(self, data: Dict[str, Any]) -> Dict[str, Any]: - bond_id = await _validate_input(data) + bond_id, name = await _validate_input(data) await self.async_set_unique_id(bond_id) self._abort_if_unique_id_configured() - return self.async_create_entry(title=bond_id, data=data) + hub_name = name or bond_id + return self.async_create_entry(title=hub_name, data=data) class InputValidationError(exceptions.HomeAssistantError): diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index 769794a31e8..f6165eb7890 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -65,7 +65,8 @@ class BondEntity(Entity): device_info = { ATTR_NAME: self.name, "manufacturer": self._hub.make, - "identifiers": {(DOMAIN, self._hub.bond_id, self._device_id)}, + "identifiers": {(DOMAIN, self._hub.bond_id, self._device.device_id)}, + "suggested_area": self._device.location, "via_device": (DOMAIN, self._hub.bond_id), } if not self._hub.is_bridge: diff --git a/homeassistant/components/bond/manifest.json b/homeassistant/components/bond/manifest.json index e1ec5e5dd46..cf009c11caa 100644 --- a/homeassistant/components/bond/manifest.json +++ b/homeassistant/components/bond/manifest.json @@ -3,7 +3,7 @@ "name": "Bond", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bond", - "requirements": ["bond-api==0.1.9"], + "requirements": ["bond-api==0.1.10"], "zeroconf": ["_bond._tcp.local."], "codeowners": ["@prystupa"], "quality_scale": "platinum" diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py index df3373ed7a1..6d3aacf5e42 100644 --- a/homeassistant/components/bond/utils.py +++ b/homeassistant/components/bond/utils.py @@ -1,7 +1,9 @@ """Reusable utilities for the Bond component.""" +import asyncio import logging from typing import List, Optional +from aiohttp import ClientResponseError from bond_api import Action, Bond from .const import BRIDGE_MAKE @@ -39,7 +41,7 @@ class BondDevice: @property def location(self) -> str: """Get the location of this device.""" - return self._attrs["location"] + return self._attrs.get("location") @property def template(self) -> str: @@ -89,31 +91,40 @@ class BondHub: def __init__(self, bond: Bond): """Initialize Bond Hub.""" self.bond: Bond = bond + self._bridge: Optional[dict] = None self._version: Optional[dict] = None self._devices: Optional[List[BondDevice]] = None - async def setup(self): + async def setup(self, max_devices=None): """Read hub version information.""" self._version = await self.bond.version() _LOGGER.debug("Bond reported the following version info: %s", self._version) - # Fetch all available devices using Bond API. device_ids = await self.bond.devices() - self._devices = [ - BondDevice( - device_id, - await self.bond.device(device_id), - await self.bond.device_properties(device_id), + self._devices = [] + for idx, device_id in enumerate(device_ids): + if max_devices is not None and idx >= max_devices: + break + + device, props = await asyncio.gather( + self.bond.device(device_id), self.bond.device_properties(device_id) ) - for device_id in device_ids - ] + + self._devices.append(BondDevice(device_id, device, props)) _LOGGER.debug("Discovered Bond devices: %s", self._devices) + try: + # Smart by bond devices do not have a bridge api call + self._bridge = await self.bond.bridge() + except ClientResponseError: + self._bridge = {} + _LOGGER.debug("Bond reported the following bridge info: %s", self._bridge) @property - def bond_id(self) -> str: + def bond_id(self) -> Optional[str]: """Return unique Bond ID for this hub.""" - return self._version["bondid"] + # Old firmwares are missing the bondid + return self._version.get("bondid") @property def target(self) -> str: @@ -130,6 +141,20 @@ class BondHub: """Return this hub make.""" return self._version.get("make", BRIDGE_MAKE) + @property + def name(self) -> Optional[str]: + """Get the name of this bridge.""" + if not self.is_bridge and self._devices: + return self._devices[0].name + return self._bridge.get("name") + + @property + def location(self) -> Optional[str]: + """Get the location of this bridge.""" + if not self.is_bridge and self._devices: + return self._devices[0].location + return self._bridge.get("location") + @property def fw_ver(self) -> str: """Return this hub firmware version.""" @@ -143,5 +168,4 @@ class BondHub: @property def is_bridge(self) -> bool: """Return if the Bond is a Bond Bridge.""" - # If False, it means that it is a Smart by Bond product. Assumes that it is if the model is not available. - return self._version.get("model", "BD-").startswith("BD-") + return bool(self._bridge) diff --git a/requirements_all.txt b/requirements_all.txt index 0e3c6117780..971b8ac9c92 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -371,7 +371,7 @@ blockchain==1.4.4 # bme680==1.0.5 # homeassistant.components.bond -bond-api==0.1.9 +bond-api==0.1.10 # homeassistant.components.amazon_polly # homeassistant.components.route53 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 317063ffffc..c552f8452c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -205,7 +205,7 @@ blebox_uniapi==1.3.2 blinkpy==0.17.0 # homeassistant.components.bond -bond-api==0.1.9 +bond-api==0.1.10 # homeassistant.components.braviatv bravia-tv==1.0.8 diff --git a/tests/components/bond/common.py b/tests/components/bond/common.py index ba4d10c8892..54d127832b5 100644 --- a/tests/components/bond/common.py +++ b/tests/components/bond/common.py @@ -29,13 +29,16 @@ async def setup_bond_entity( patch_version=False, patch_device_ids=False, patch_platforms=False, + patch_bridge=False, ): """Set up Bond entity.""" config_entry.add_to_hass(hass) - with patch_start_bpup(), patch_bond_version( - enabled=patch_version - ), patch_bond_device_ids(enabled=patch_device_ids), patch_setup_entry( + with patch_start_bpup(), patch_bond_bridge( + enabled=patch_bridge + ), patch_bond_version(enabled=patch_version), patch_bond_device_ids( + enabled=patch_device_ids + ), patch_setup_entry( "cover", enabled=patch_platforms ), patch_setup_entry( "fan", enabled=patch_platforms @@ -56,6 +59,7 @@ async def setup_platform( bond_version: Dict[str, Any] = None, props: Dict[str, Any] = None, state: Dict[str, Any] = None, + bridge: Dict[str, Any] = None, ): """Set up the specified Bond platform.""" mock_entry = MockConfigEntry( @@ -65,7 +69,9 @@ async def setup_platform( mock_entry.add_to_hass(hass) with patch("homeassistant.components.bond.PLATFORMS", [platform]): - with patch_bond_version(return_value=bond_version), patch_bond_device_ids( + with patch_bond_version(return_value=bond_version), patch_bond_bridge( + return_value=bridge + ), patch_bond_device_ids( return_value=[bond_device_id] ), patch_start_bpup(), patch_bond_device( return_value=discovered_device @@ -97,6 +103,27 @@ def patch_bond_version( ) +def patch_bond_bridge( + enabled: bool = True, return_value: Optional[dict] = None, side_effect=None +): + """Patch Bond API bridge endpoint.""" + if not enabled: + return nullcontext() + + if return_value is None: + return_value = { + "name": "bond-name", + "location": "bond-location", + "bluelight": 127, + } + + return patch( + "homeassistant.components.bond.Bond.bridge", + return_value=return_value, + side_effect=side_effect, + ) + + def patch_bond_device_ids(enabled: bool = True, return_value=None, side_effect=None): """Patch Bond API devices endpoint.""" if not enabled: diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index dba6c590641..2a76e1fa6a0 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -8,7 +8,13 @@ from homeassistant import config_entries, core, setup from homeassistant.components.bond.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST -from .common import patch_bond_device_ids, patch_bond_version +from .common import ( + patch_bond_bridge, + patch_bond_device, + patch_bond_device_ids, + patch_bond_device_properties, + patch_bond_version, +) from tests.common import MockConfigEntry @@ -24,7 +30,9 @@ async def test_user_form(hass: core.HomeAssistant): with patch_bond_version( return_value={"bondid": "test-bond-id"} - ), patch_bond_device_ids(), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: + ), patch_bond_device_ids( + return_value=["f6776c11", "f6776c12"] + ), patch_bond_bridge(), patch_bond_device_properties(), patch_bond_device(), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, @@ -32,7 +40,43 @@ async def test_user_form(hass: core.HomeAssistant): await hass.async_block_till_done() assert result2["type"] == "create_entry" - assert result2["title"] == "test-bond-id" + assert result2["title"] == "bond-name" + assert result2["data"] == { + CONF_HOST: "some host", + CONF_ACCESS_TOKEN: "test-token", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_form_with_non_bridge(hass: core.HomeAssistant): + """Test setup a smart by bond fan.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch_bond_version( + return_value={"bondid": "test-bond-id"} + ), patch_bond_device_ids( + return_value=["f6776c11"] + ), patch_bond_device_properties(), patch_bond_device( + return_value={ + "name": "New Fan", + } + ), patch_bond_bridge( + return_value={} + ), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "New Fan" assert result2["data"] == { CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token", @@ -49,7 +93,7 @@ async def test_user_form_invalid_auth(hass: core.HomeAssistant): with patch_bond_version( return_value={"bond_id": "test-bond-id"} - ), patch_bond_device_ids( + ), patch_bond_bridge(), patch_bond_device_ids( side_effect=ClientResponseError(Mock(), Mock(), status=401), ): result2 = await hass.config_entries.flow.async_configure( @@ -69,7 +113,7 @@ async def test_user_form_cannot_connect(hass: core.HomeAssistant): with patch_bond_version( side_effect=ClientConnectionError() - ), patch_bond_device_ids(): + ), patch_bond_bridge(), patch_bond_device_ids(): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, @@ -87,7 +131,7 @@ async def test_user_form_old_firmware(hass: core.HomeAssistant): with patch_bond_version( return_value={"no_bond_id": "present"} - ), patch_bond_device_ids(): + ), patch_bond_bridge(), patch_bond_device_ids(): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, @@ -133,7 +177,7 @@ async def test_user_form_one_entry_per_device_allowed(hass: core.HomeAssistant): with patch_bond_version( return_value={"bondid": "already-registered-bond-id"} - ), patch_bond_device_ids(), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: + ), patch_bond_bridge(), patch_bond_device_ids(), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, @@ -160,7 +204,7 @@ async def test_zeroconf_form(hass: core.HomeAssistant): with patch_bond_version( return_value={"bondid": "test-bond-id"} - ), patch_bond_device_ids(), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: + ), patch_bond_bridge(), patch_bond_device_ids(), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: "test-token"}, @@ -168,7 +212,7 @@ async def test_zeroconf_form(hass: core.HomeAssistant): await hass.async_block_till_done() assert result2["type"] == "create_entry" - assert result2["title"] == "test-bond-id" + assert result2["title"] == "bond-name" assert result2["data"] == { CONF_HOST: "test-host", CONF_ACCESS_TOKEN: "test-token", diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index 4dc7ae5c8d4..7346acc5276 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -1,5 +1,7 @@ """Tests for the Bond module.""" -from aiohttp import ClientConnectionError +from unittest.mock import Mock + +from aiohttp import ClientConnectionError, ClientResponseError from bond_api import DeviceType from homeassistant.components.bond.const import DOMAIN @@ -14,6 +16,7 @@ from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from .common import ( + patch_bond_bridge, patch_bond_device, patch_bond_device_ids, patch_bond_device_properties, @@ -54,25 +57,22 @@ async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAss data={CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, ) - with patch_bond_version( + with patch_bond_bridge(), patch_bond_version( return_value={ "bondid": "test-bond-id", "target": "test-model", "fw_ver": "test-version", } - ): - with patch_setup_entry( - "cover" - ) as mock_cover_async_setup_entry, patch_setup_entry( - "fan" - ) as mock_fan_async_setup_entry, patch_setup_entry( - "light" - ) as mock_light_async_setup_entry, patch_setup_entry( - "switch" - ) as mock_switch_async_setup_entry: - result = await setup_bond_entity(hass, config_entry, patch_device_ids=True) - assert result is True - await hass.async_block_till_done() + ), patch_setup_entry("cover") as mock_cover_async_setup_entry, patch_setup_entry( + "fan" + ) as mock_fan_async_setup_entry, patch_setup_entry( + "light" + ) as mock_light_async_setup_entry, patch_setup_entry( + "switch" + ) as mock_switch_async_setup_entry: + result = await setup_bond_entity(hass, config_entry, patch_device_ids=True) + assert result is True + await hass.async_block_till_done() assert config_entry.entry_id in hass.data[DOMAIN] assert config_entry.state == ENTRY_STATE_LOADED @@ -81,7 +81,7 @@ async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAss # verify hub device is registered correctly device_registry = await dr.async_get_registry(hass) hub = device_registry.async_get_device(identifiers={(DOMAIN, "test-bond-id")}) - assert hub.name == "test-bond-id" + assert hub.name == "bond-name" assert hub.manufacturer == "Olibra" assert hub.model == "test-model" assert hub.sw_version == "test-version" @@ -106,6 +106,7 @@ async def test_unload_config_entry(hass: HomeAssistant): patch_version=True, patch_device_ids=True, patch_platforms=True, + patch_bridge=True, ) assert result is True await hass.async_block_till_done() @@ -136,7 +137,7 @@ async def test_old_identifiers_are_removed(hass: HomeAssistant): config_entry.add_to_hass(hass) - with patch_bond_version( + with patch_bond_bridge(), patch_bond_version( return_value={ "bondid": "test-bond-id", "target": "test-model", @@ -164,3 +165,92 @@ async def test_old_identifiers_are_removed(hass: HomeAssistant): # verify the device info is cleaned up assert device_registry.async_get_device(identifiers={old_identifers}) is None assert device_registry.async_get_device(identifiers={new_identifiers}) is not None + + +async def test_smart_by_bond_device_suggested_area(hass: HomeAssistant): + """Test we can setup a smart by bond device and get the suggested area.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, + ) + + config_entry.add_to_hass(hass) + + with patch_bond_bridge( + side_effect=ClientResponseError(Mock(), Mock(), status=404) + ), patch_bond_version( + return_value={ + "bondid": "test-bond-id", + "target": "test-model", + "fw_ver": "test-version", + } + ), patch_start_bpup(), patch_bond_device_ids( + return_value=["bond-device-id", "device_id"] + ), patch_bond_device( + return_value={ + "name": "test1", + "type": DeviceType.GENERIC_DEVICE, + "location": "Den", + } + ), patch_bond_device_properties( + return_value={} + ), patch_bond_device_state( + return_value={} + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) is True + await hass.async_block_till_done() + + assert config_entry.entry_id in hass.data[DOMAIN] + assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.unique_id == "test-bond-id" + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get_device(identifiers={(DOMAIN, "test-bond-id")}) + assert device is not None + assert device.suggested_area == "Den" + + +async def test_bridge_device_suggested_area(hass: HomeAssistant): + """Test we can setup a bridge bond device and get the suggested area.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, + ) + + config_entry.add_to_hass(hass) + + with patch_bond_bridge( + return_value={ + "name": "Office Bridge", + "location": "Office", + } + ), patch_bond_version( + return_value={ + "bondid": "test-bond-id", + "target": "test-model", + "fw_ver": "test-version", + } + ), patch_start_bpup(), patch_bond_device_ids( + return_value=["bond-device-id", "device_id"] + ), patch_bond_device( + return_value={ + "name": "test1", + "type": DeviceType.GENERIC_DEVICE, + "location": "Bathroom", + } + ), patch_bond_device_properties( + return_value={} + ), patch_bond_device_state( + return_value={} + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) is True + await hass.async_block_till_done() + + assert config_entry.entry_id in hass.data[DOMAIN] + assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.unique_id == "test-bond-id" + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get_device(identifiers={(DOMAIN, "test-bond-id")}) + assert device is not None + assert device.suggested_area == "Office" diff --git a/tests/components/bond/test_light.py b/tests/components/bond/test_light.py index 3f7a3ef62f9..e4cd4e4e3e9 100644 --- a/tests/components/bond/test_light.py +++ b/tests/components/bond/test_light.py @@ -148,7 +148,7 @@ async def test_sbb_trust_state(hass: core.HomeAssistant): "bondid": "test-bond-id", } await setup_platform( - hass, LIGHT_DOMAIN, ceiling_fan("name-1"), bond_version=version + hass, LIGHT_DOMAIN, ceiling_fan("name-1"), bond_version=version, bridge={} ) device = hass.states.get("light.name_1") From 9b69549f734d225227e9f1d3829f387ab7f724e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Feb 2021 21:26:24 -1000 Subject: [PATCH 575/796] Recover and restart the recorder if the sqlite database encounters corruption while running (#46612) --- homeassistant/components/recorder/__init__.py | 393 ++++++++++-------- homeassistant/components/recorder/util.py | 11 +- tests/components/recorder/common.py | 13 + tests/components/recorder/test_init.py | 54 ++- tests/components/recorder/test_util.py | 15 +- 5 files changed, 288 insertions(+), 198 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index ceec7ee9eed..915e6b45181 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -5,6 +5,7 @@ import concurrent.futures from datetime import datetime import logging import queue +import sqlite3 import threading import time from typing import Any, Callable, List, Optional @@ -37,7 +38,12 @@ import homeassistant.util.dt as dt_util from . import migration, purge from .const import CONF_DB_INTEGRITY_CHECK, DATA_INSTANCE, DOMAIN, SQLITE_URL_PREFIX from .models import Base, Events, RecorderRuns, States -from .util import session_scope, validate_or_move_away_sqlite_database +from .util import ( + dburl_to_path, + move_away_broken_database, + session_scope, + validate_or_move_away_sqlite_database, +) _LOGGER = logging.getLogger(__name__) @@ -247,7 +253,7 @@ class Recorder(threading.Thread): self._pending_expunge = [] self.event_session = None self.get_session = None - self._completed_database_setup = False + self._completed_database_setup = None @callback def async_initialize(self): @@ -278,39 +284,8 @@ class Recorder(threading.Thread): def run(self): """Start processing events to save.""" - tries = 1 - connected = False - while not connected and tries <= self.db_max_retries: - if tries != 1: - time.sleep(self.db_retry_wait) - try: - self._setup_connection() - migration.migrate_schema(self) - self._setup_run() - connected = True - _LOGGER.debug("Connected to recorder database") - except Exception as err: # pylint: disable=broad-except - _LOGGER.error( - "Error during connection setup: %s (retrying in %s seconds)", - err, - self.db_retry_wait, - ) - tries += 1 - - if not connected: - - @callback - def connection_failed(): - """Connect failed tasks.""" - self.async_db_ready.set_result(False) - persistent_notification.async_create( - self.hass, - "The recorder could not start, please check the log", - "Recorder", - ) - - self.hass.add_job(connection_failed) + if not self._setup_recorder(): return shutdown_task = object() @@ -346,15 +321,11 @@ class Recorder(threading.Thread): self.hass.add_job(register) result = hass_started.result() - self.event_session = self.get_session() - self.event_session.expire_on_commit = False - # If shutdown happened before Home Assistant finished starting if result is shutdown_task: # Make sure we cleanly close the run if # we restart before startup finishes - self._close_run() - self._close_connection() + self._shutdown() return # Start periodic purge @@ -370,175 +341,180 @@ class Recorder(threading.Thread): async_purge, hour=4, minute=12, second=0 ) + _LOGGER.debug("Recorder processing the queue") # Use a session for the event read loop # with a commit every time the event time # has changed. This reduces the disk io. while True: event = self.queue.get() + if event is None: - self._close_run() - self._close_connection() + self._shutdown() return - if isinstance(event, PurgeTask): - # Schedule a new purge task if this one didn't finish - if not purge.purge_old_data(self, event.keep_days, event.repack): - self.queue.put(PurgeTask(event.keep_days, event.repack)) - continue - if isinstance(event, WaitTask): - self._queue_watch.set() - continue - if event.event_type == EVENT_TIME_CHANGED: - self._keepalive_count += 1 - if self._keepalive_count >= KEEPALIVE_TIME: - self._keepalive_count = 0 - self._send_keep_alive() - if self.commit_interval: - self._timechanges_seen += 1 - if self._timechanges_seen >= self.commit_interval: - self._timechanges_seen = 0 - self._commit_event_session_or_retry() - continue + self._process_one_event(event) + + def _setup_recorder(self) -> bool: + """Create schema and connect to the database.""" + tries = 1 + + while tries <= self.db_max_retries: try: - if event.event_type == EVENT_STATE_CHANGED: - dbevent = Events.from_event(event, event_data="{}") - else: - dbevent = Events.from_event(event) - dbevent.created = event.time_fired - self.event_session.add(dbevent) - except (TypeError, ValueError): - _LOGGER.warning("Event is not JSON serializable: %s", event) + self._setup_connection() + migration.migrate_schema(self) + self._setup_run() except Exception as err: # pylint: disable=broad-except - # Must catch the exception to prevent the loop from collapsing - _LOGGER.exception("Error adding event: %s", err) + _LOGGER.error( + "Error during connection setup to %s: %s (retrying in %s seconds)", + self.db_url, + err, + self.db_retry_wait, + ) + else: + _LOGGER.debug("Connected to recorder database") + self._open_event_session() + return True - if dbevent and event.event_type == EVENT_STATE_CHANGED: - try: - dbstate = States.from_event(event) - has_new_state = event.data.get("new_state") - if dbstate.entity_id in self._old_states: - old_state = self._old_states.pop(dbstate.entity_id) - if old_state.state_id: - dbstate.old_state_id = old_state.state_id - else: - dbstate.old_state = old_state - if not has_new_state: - dbstate.state = None - dbstate.event = dbevent - dbstate.created = event.time_fired - self.event_session.add(dbstate) - if has_new_state: - self._old_states[dbstate.entity_id] = dbstate - self._pending_expunge.append(dbstate) - except (TypeError, ValueError): - _LOGGER.warning( - "State is not JSON serializable: %s", - event.data.get("new_state"), - ) - except Exception as err: # pylint: disable=broad-except - # Must catch the exception to prevent the loop from collapsing - _LOGGER.exception("Error adding state change: %s", err) + tries += 1 + time.sleep(self.db_retry_wait) - # If they do not have a commit interval - # than we commit right away - if not self.commit_interval: - self._commit_event_session_or_retry() + @callback + def connection_failed(): + """Connect failed tasks.""" + self.async_db_ready.set_result(False) + persistent_notification.async_create( + self.hass, + "The recorder could not start, please check the log", + "Recorder", + ) + + self.hass.add_job(connection_failed) + return False + + def _process_one_event(self, event): + """Process one event.""" + if isinstance(event, PurgeTask): + # Schedule a new purge task if this one didn't finish + if not purge.purge_old_data(self, event.keep_days, event.repack): + self.queue.put(PurgeTask(event.keep_days, event.repack)) + return + if isinstance(event, WaitTask): + self._queue_watch.set() + return + if event.event_type == EVENT_TIME_CHANGED: + self._keepalive_count += 1 + if self._keepalive_count >= KEEPALIVE_TIME: + self._keepalive_count = 0 + self._send_keep_alive() + if self.commit_interval: + self._timechanges_seen += 1 + if self._timechanges_seen >= self.commit_interval: + self._timechanges_seen = 0 + self._commit_event_session_or_recover() + return - def _send_keep_alive(self): try: - _LOGGER.debug("Sending keepalive") - self.event_session.connection().scalar(select([1])) + if event.event_type == EVENT_STATE_CHANGED: + dbevent = Events.from_event(event, event_data="{}") + else: + dbevent = Events.from_event(event) + dbevent.created = event.time_fired + self.event_session.add(dbevent) + except (TypeError, ValueError): + _LOGGER.warning("Event is not JSON serializable: %s", event) return except Exception as err: # pylint: disable=broad-except # Must catch the exception to prevent the loop from collapsing - _LOGGER.error( - "Error in database connectivity during keepalive: %s", - err, - ) - self._reopen_event_session() + _LOGGER.exception("Error adding event: %s", err) + return + + if event.event_type == EVENT_STATE_CHANGED: + try: + dbstate = States.from_event(event) + has_new_state = event.data.get("new_state") + if dbstate.entity_id in self._old_states: + old_state = self._old_states.pop(dbstate.entity_id) + if old_state.state_id: + dbstate.old_state_id = old_state.state_id + else: + dbstate.old_state = old_state + if not has_new_state: + dbstate.state = None + dbstate.event = dbevent + dbstate.created = event.time_fired + self.event_session.add(dbstate) + if has_new_state: + self._old_states[dbstate.entity_id] = dbstate + self._pending_expunge.append(dbstate) + except (TypeError, ValueError): + _LOGGER.warning( + "State is not JSON serializable: %s", + event.data.get("new_state"), + ) + except Exception as err: # pylint: disable=broad-except + # Must catch the exception to prevent the loop from collapsing + _LOGGER.exception("Error adding state change: %s", err) + + # If they do not have a commit interval + # than we commit right away + if not self.commit_interval: + self._commit_event_session_or_recover() + + def _commit_event_session_or_recover(self): + """Commit changes to the database and recover if the database fails when possible.""" + try: + self._commit_event_session_or_retry() + return + except exc.DatabaseError as err: + if isinstance(err.__cause__, sqlite3.DatabaseError): + _LOGGER.exception( + "Unrecoverable sqlite3 database corruption detected: %s", err + ) + self._handle_sqlite_corruption() + return + _LOGGER.exception("Unexpected error saving events: %s", err) + except Exception as err: # pylint: disable=broad-except + # Must catch the exception to prevent the loop from collapsing + _LOGGER.exception("Unexpected error saving events: %s", err) + + self._reopen_event_session() + return def _commit_event_session_or_retry(self): tries = 1 while tries <= self.db_max_retries: - if tries != 1: - time.sleep(self.db_retry_wait) - try: self._commit_event_session() return except (exc.InternalError, exc.OperationalError) as err: if err.connection_invalidated: - _LOGGER.error( - "Database connection invalidated: %s. " - "(retrying in %s seconds)", - err, - self.db_retry_wait, - ) + message = "Database connection invalidated" else: - _LOGGER.error( - "Error in database connectivity during commit: %s. " - "(retrying in %s seconds)", - err, - self.db_retry_wait, - ) + message = "Error in database connectivity during commit" + _LOGGER.error( + "%s: Error executing query: %s. (retrying in %s seconds)", + message, + err, + self.db_retry_wait, + ) + if tries == self.db_max_retries: + raise + tries += 1 - - except Exception as err: # pylint: disable=broad-except - # Must catch the exception to prevent the loop from collapsing - _LOGGER.exception("Error saving events: %s", err) - return - - _LOGGER.error( - "Error in database update. Could not save " "after %d tries. Giving up", - tries, - ) - self._reopen_event_session() - - def _reopen_event_session(self): - try: - self.event_session.rollback() - except Exception as err: # pylint: disable=broad-except - # Must catch the exception to prevent the loop from collapsing - _LOGGER.exception("Error while rolling back event session: %s", err) - - try: - self.event_session.close() - except Exception as err: # pylint: disable=broad-except - # Must catch the exception to prevent the loop from collapsing - _LOGGER.exception("Error while closing event session: %s", err) - - try: - self.event_session = self.get_session() - self.event_session.expire_on_commit = False - except Exception as err: # pylint: disable=broad-except - # Must catch the exception to prevent the loop from collapsing - _LOGGER.exception("Error while creating new event session: %s", err) + time.sleep(self.db_retry_wait) def _commit_event_session(self): self._commits_without_expire += 1 - try: - if self._pending_expunge: - self.event_session.flush() - for dbstate in self._pending_expunge: - # Expunge the state so its not expired - # until we use it later for dbstate.old_state - if dbstate in self.event_session: - self.event_session.expunge(dbstate) - self._pending_expunge = [] - self.event_session.commit() - except exc.IntegrityError as err: - _LOGGER.error( - "Integrity error executing query (database likely deleted out from under us): %s", - err, - ) - self.event_session.rollback() - self._old_states = {} - raise - except Exception as err: - _LOGGER.error("Error executing query: %s", err) - self.event_session.rollback() - raise + if self._pending_expunge: + self.event_session.flush() + for dbstate in self._pending_expunge: + # Expunge the state so its not expired + # until we use it later for dbstate.old_state + if dbstate in self.event_session: + self.event_session.expunge(dbstate) + self._pending_expunge = [] + self.event_session.commit() # Expire is an expensive operation (frequently more expensive # than the flush and commit itself) so we only @@ -547,6 +523,47 @@ class Recorder(threading.Thread): self._commits_without_expire = 0 self.event_session.expire_all() + def _handle_sqlite_corruption(self): + """Handle the sqlite3 database being corrupt.""" + self._close_connection() + move_away_broken_database(dburl_to_path(self.db_url)) + self._setup_recorder() + + def _reopen_event_session(self): + """Rollback the event session and reopen it after a failure.""" + self._old_states = {} + + try: + self.event_session.rollback() + self.event_session.close() + except Exception as err: # pylint: disable=broad-except + # Must catch the exception to prevent the loop from collapsing + _LOGGER.exception( + "Error while rolling back and closing the event session: %s", err + ) + + self._open_event_session() + + def _open_event_session(self): + """Open the event session.""" + try: + self.event_session = self.get_session() + self.event_session.expire_on_commit = False + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Error while creating new event session: %s", err) + + def _send_keep_alive(self): + try: + _LOGGER.debug("Sending keepalive") + self.event_session.connection().scalar(select([1])) + return + except Exception as err: # pylint: disable=broad-except + _LOGGER.error( + "Error in database connectivity during keepalive: %s", + err, + ) + self._reopen_event_session() + @callback def event_listener(self, event): """Listen for new events and put them in the process queue.""" @@ -571,6 +588,7 @@ class Recorder(threading.Thread): def _setup_connection(self): """Ensure database is ready to fly.""" kwargs = {} + self._completed_database_setup = False def setup_recorder_connection(dbapi_connection, connection_record): """Dbapi specific connection settings.""" @@ -603,9 +621,7 @@ class Recorder(threading.Thread): else: kwargs["echo"] = False - if self.db_url != SQLITE_URL_PREFIX and self.db_url.startswith( - SQLITE_URL_PREFIX - ): + if self._using_file_sqlite: with self.hass.timeout.freeze(DOMAIN): # # Here we run an sqlite3 quick_check. In the majority @@ -628,6 +644,13 @@ class Recorder(threading.Thread): Base.metadata.create_all(self.engine) self.get_session = scoped_session(sessionmaker(bind=self.engine)) + @property + def _using_file_sqlite(self): + """Short version to check if we are using sqlite3 as a file.""" + return self.db_url != SQLITE_URL_PREFIX and self.db_url.startswith( + SQLITE_URL_PREFIX + ) + def _close_connection(self): """Close the connection.""" self.engine.dispose() @@ -652,12 +675,18 @@ class Recorder(threading.Thread): session.flush() session.expunge(self.run_info) - def _close_run(self): + def _shutdown(self): """Save end time for current run.""" if self.event_session is not None: self.run_info.end = dt_util.utcnow() self.event_session.add(self.run_info) - self._commit_event_session_or_retry() - self.event_session.close() + try: + self._commit_event_session_or_retry() + self.event_session.close() + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception( + "Error saving the event session during shutdown: %s", err + ) self.run_info = None + self._close_connection() diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 41bca335a56..b945386de82 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -112,19 +112,24 @@ def execute(qry, to_native=False, validate_entity_ids=True): def validate_or_move_away_sqlite_database(dburl: str, db_integrity_check: bool) -> bool: """Ensure that the database is valid or move it away.""" - dbpath = dburl[len(SQLITE_URL_PREFIX) :] + dbpath = dburl_to_path(dburl) if not os.path.exists(dbpath): # Database does not exist yet, this is OK return True if not validate_sqlite_database(dbpath, db_integrity_check): - _move_away_broken_database(dbpath) + move_away_broken_database(dbpath) return False return True +def dburl_to_path(dburl): + """Convert the db url into a filesystem path.""" + return dburl[len(SQLITE_URL_PREFIX) :] + + def last_run_was_recently_clean(cursor): """Verify the last recorder run was recently clean.""" @@ -208,7 +213,7 @@ def run_checks_on_open_db(dbpath, cursor, db_integrity_check): cursor.execute("PRAGMA QUICK_CHECK") -def _move_away_broken_database(dbfile: str) -> None: +def move_away_broken_database(dbfile: str) -> None: """Move away a broken sqlite3 database.""" isotime = dt_util.utcnow().isoformat() diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index 1d0e6dbbfa0..d2b731777e2 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -10,14 +10,27 @@ from tests.common import fire_time_changed def wait_recording_done(hass): """Block till recording is done.""" + hass.block_till_done() trigger_db_commit(hass) hass.block_till_done() hass.data[recorder.DATA_INSTANCE].block_till_done() hass.block_till_done() +async def async_wait_recording_done(hass): + """Block till recording is done.""" + await hass.loop.run_in_executor(None, wait_recording_done, hass) + + def trigger_db_commit(hass): """Force the recorder to commit.""" for _ in range(recorder.DEFAULT_COMMIT_INTERVAL): # We only commit on time change fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1)) + + +def corrupt_db_file(test_db_file): + """Corrupt an sqlite3 database file.""" + with open(test_db_file, "w+") as fhandle: + fhandle.seek(200) + fhandle.write("I am a corrupt db" * 100) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 3b71648166e..ca25fe10284 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -6,6 +6,7 @@ from unittest.mock import patch from sqlalchemy.exc import OperationalError from homeassistant.components.recorder import ( + CONF_DB_URL, CONFIG_SCHEMA, DOMAIN, Recorder, @@ -13,7 +14,7 @@ from homeassistant.components.recorder import ( run_information_from_instance, run_information_with_session, ) -from homeassistant.components.recorder.const import DATA_INSTANCE +from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX from homeassistant.components.recorder.models import Events, RecorderRuns, States from homeassistant.components.recorder.util import session_scope from homeassistant.const import ( @@ -26,7 +27,7 @@ from homeassistant.core import Context, CoreState, callback from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from .common import wait_recording_done +from .common import async_wait_recording_done, corrupt_db_file, wait_recording_done from tests.common import ( async_init_recorder_component, @@ -519,3 +520,52 @@ def test_run_information(hass_recorder): class CannotSerializeMe: """A class that the JSONEncoder cannot serialize.""" + + +async def test_database_corruption_while_running(hass, tmpdir, caplog): + """Test we can recover from sqlite3 db corruption.""" + + def _create_tmpdir_for_test_db(): + return tmpdir.mkdir("sqlite").join("test.db") + + test_db_file = await hass.async_add_executor_job(_create_tmpdir_for_test_db) + dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) + await hass.async_block_till_done() + caplog.clear() + + hass.states.async_set("test.lost", "on", {}) + + await async_wait_recording_done(hass) + await hass.async_add_executor_job(corrupt_db_file, test_db_file) + await async_wait_recording_done(hass) + + # This state will not be recorded because + # the database corruption will be discovered + # and we will have to rollback to recover + hass.states.async_set("test.one", "off", {}) + await async_wait_recording_done(hass) + + assert "Unrecoverable sqlite3 database corruption detected" in caplog.text + assert "The system will rename the corrupt database file" in caplog.text + assert "Connected to recorder database" in caplog.text + + # This state should go into the new database + hass.states.async_set("test.two", "on", {}) + await async_wait_recording_done(hass) + + def _get_last_state(): + with session_scope(hass=hass) as session: + db_states = list(session.query(States)) + assert len(db_states) == 1 + assert db_states[0].event_id > 0 + return db_states[0].to_native() + + state = await hass.async_add_executor_job(_get_last_state) + assert state.entity_id == "test.two" + assert state.state == "on" + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + hass.stop() diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 38df1285008..f1d55999ae4 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -10,7 +10,7 @@ from homeassistant.components.recorder import util from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX from homeassistant.util import dt as dt_util -from .common import wait_recording_done +from .common import corrupt_db_file, wait_recording_done from tests.common import get_test_home_assistant, init_recorder_component @@ -90,7 +90,7 @@ def test_validate_or_move_away_sqlite_database_with_integrity_check( util.validate_or_move_away_sqlite_database(dburl, db_integrity_check) is False ) - _corrupt_db_file(test_db_file) + corrupt_db_file(test_db_file) assert util.validate_sqlite_database(dburl, db_integrity_check) is False @@ -127,7 +127,7 @@ def test_validate_or_move_away_sqlite_database_without_integrity_check( util.validate_or_move_away_sqlite_database(dburl, db_integrity_check) is False ) - _corrupt_db_file(test_db_file) + corrupt_db_file(test_db_file) assert util.validate_sqlite_database(dburl, db_integrity_check) is False @@ -150,7 +150,7 @@ def test_last_run_was_recently_clean(hass_recorder): assert util.last_run_was_recently_clean(cursor) is False - hass.data[DATA_INSTANCE]._close_run() + hass.data[DATA_INSTANCE]._shutdown() wait_recording_done(hass) assert util.last_run_was_recently_clean(cursor) is True @@ -244,10 +244,3 @@ def test_combined_checks(hass_recorder, caplog): caplog.clear() with pytest.raises(sqlite3.DatabaseError): util.run_checks_on_open_db("fake_db_path", cursor, True) - - -def _corrupt_db_file(test_db_file): - """Corrupt an sqlite3 database file.""" - f = open(test_db_file, "a") - f.write("I am a corrupt db") - f.close() From 41332493b52fd77ba1580309e4a697910b61a681 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Feb 2021 21:28:52 -1000 Subject: [PATCH 576/796] Add suggested area support to Sonos (#46794) --- homeassistant/components/sonos/media_player.py | 1 + tests/components/sonos/test_media_player.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 2c69730211b..363e499292e 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -589,6 +589,7 @@ class SonosEntity(MediaPlayerEntity): "sw_version": self._sw_version, "connections": {(dr.CONNECTION_NETWORK_MAC, self._mac_address)}, "manufacturer": "Sonos", + "suggested_area": self._name, } @property diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 6a401ee0c16..ba9ba1c6db6 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -56,4 +56,5 @@ async def test_device_registry(hass, config_entry, config, soco): assert reg_device.sw_version == "49.2-64250" assert reg_device.connections == {("mac", "00:11:22:33:44:55")} assert reg_device.manufacturer == "Sonos" + assert reg_device.suggested_area == "Zone A" assert reg_device.name == "Zone A" From b775a0d796974608b2e5edfe3e772d7689470f0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Feb 2021 21:34:52 -1000 Subject: [PATCH 577/796] Run homekit service calls in async since the server is now async (#45859) * Simplify homekit runs and service calls Now that the homekit server is async, call_service and run are running in the Home Assistant event loop * remove comment * remove another comment --- .../components/homekit/accessories.py | 28 ++++--------- .../components/homekit/type_cameras.py | 4 +- .../components/homekit/type_covers.py | 18 +++++---- homeassistant/components/homekit/type_fans.py | 8 ++-- .../components/homekit/type_humidifiers.py | 8 ++-- .../components/homekit/type_lights.py | 2 +- .../components/homekit/type_locks.py | 2 +- .../components/homekit/type_media_players.py | 20 +++++----- .../homekit/type_security_systems.py | 2 +- .../components/homekit/type_switches.py | 10 +++-- .../components/homekit/type_thermostats.py | 6 +-- tests/components/homekit/test_accessories.py | 34 ++++++++-------- tests/components/homekit/test_type_cameras.py | 36 ++++++++--------- tests/components/homekit/test_type_covers.py | 16 ++++---- tests/components/homekit/test_type_fans.py | 10 ++--- .../homekit/test_type_humidifiers.py | 14 +++---- tests/components/homekit/test_type_lights.py | 16 ++++---- tests/components/homekit/test_type_locks.py | 2 +- .../homekit/test_type_media_players.py | 8 ++-- .../homekit/test_type_security_systems.py | 4 +- tests/components/homekit/test_type_sensors.py | 16 ++++---- .../components/homekit/test_type_switches.py | 20 +++++----- .../homekit/test_type_thermostats.py | 40 +++++++++---------- 23 files changed, 157 insertions(+), 167 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 6a33b63e89a..e31b9ec842e 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -300,17 +300,7 @@ class HomeAccessory(Accessory): return state is not None and state.state != STATE_UNAVAILABLE async def run(self): - """Handle accessory driver started event. - - Run inside the HAP-python event loop. - """ - self.hass.add_job(self.run_handler) - - async def run_handler(self): - """Handle accessory driver started event. - - Run inside the Home Assistant event loop. - """ + """Handle accessory driver started event.""" state = self.hass.states.get(self.entity_id) self.async_update_state_callback(state) self._subscriptions.append( @@ -441,15 +431,9 @@ class HomeAccessory(Accessory): """ raise NotImplementedError() - def call_service(self, domain, service, service_data, value=None): + @ha_callback + def async_call_service(self, domain, service, service_data, value=None): """Fire event and call service for changes from HomeKit.""" - self.hass.add_job(self.async_call_service, domain, service, service_data, value) - - async def async_call_service(self, domain, service, service_data, value=None): - """Fire event and call service for changes from HomeKit. - - This method must be run in the event loop. - """ event_data = { ATTR_ENTITY_ID: self.entity_id, ATTR_DISPLAY_NAME: self.display_name, @@ -459,8 +443,10 @@ class HomeAccessory(Accessory): context = Context() self.hass.bus.async_fire(EVENT_HOMEKIT_CHANGED, event_data, context=context) - await self.hass.services.async_call( - domain, service, service_data, context=context + self.hass.async_create_task( + self.hass.services.async_call( + domain, service, service_data, context=context + ) ) @ha_callback diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index 0a499bf5d24..48f7ad9b064 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -240,7 +240,7 @@ class Camera(HomeAccessory, PyhapCamera): self._async_update_doorbell_state(state) - async def run_handler(self): + async def run(self): """Handle accessory driver started event. Run inside the Home Assistant event loop. @@ -259,7 +259,7 @@ class Camera(HomeAccessory, PyhapCamera): self._async_update_doorbell_state_event, ) - await super().run_handler() + await super().run() @callback def _async_update_motion_state_event(self, event): diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index daa782b8d67..ca375bb6f37 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -113,7 +113,7 @@ class GarageDoorOpener(HomeAccessory): self.async_update_state(state) - async def run_handler(self): + async def run(self): """Handle accessory driver started event. Run inside the Home Assistant event loop. @@ -125,7 +125,7 @@ class GarageDoorOpener(HomeAccessory): self._async_update_obstruction_event, ) - await super().run_handler() + await super().run() @callback def _async_update_obstruction_event(self, event): @@ -158,11 +158,11 @@ class GarageDoorOpener(HomeAccessory): if value == HK_DOOR_OPEN: if self.char_current_state.value != value: self.char_current_state.set_value(HK_DOOR_OPENING) - self.call_service(DOMAIN, SERVICE_OPEN_COVER, params) + self.async_call_service(DOMAIN, SERVICE_OPEN_COVER, params) elif value == HK_DOOR_CLOSED: if self.char_current_state.value != value: self.char_current_state.set_value(HK_DOOR_CLOSING) - self.call_service(DOMAIN, SERVICE_CLOSE_COVER, params) + self.async_call_service(DOMAIN, SERVICE_CLOSE_COVER, params) @callback def async_update_state(self, new_state): @@ -231,7 +231,9 @@ class OpeningDeviceBase(HomeAccessory): """Stop the cover motion from HomeKit.""" if value != 1: return - self.call_service(DOMAIN, SERVICE_STOP_COVER, {ATTR_ENTITY_ID: self.entity_id}) + self.async_call_service( + DOMAIN, SERVICE_STOP_COVER, {ATTR_ENTITY_ID: self.entity_id} + ) def set_tilt(self, value): """Set tilt to value if call came from HomeKit.""" @@ -243,7 +245,7 @@ class OpeningDeviceBase(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id, ATTR_TILT_POSITION: value} - self.call_service(DOMAIN, SERVICE_SET_COVER_TILT_POSITION, params, value) + self.async_call_service(DOMAIN, SERVICE_SET_COVER_TILT_POSITION, params, value) @callback def async_update_state(self, new_state): @@ -287,7 +289,7 @@ class OpeningDevice(OpeningDeviceBase, HomeAccessory): """Move cover to value if call came from HomeKit.""" _LOGGER.debug("%s: Set position to %d", self.entity_id, value) params = {ATTR_ENTITY_ID: self.entity_id, ATTR_POSITION: value} - self.call_service(DOMAIN, SERVICE_SET_COVER_POSITION, params, value) + self.async_call_service(DOMAIN, SERVICE_SET_COVER_POSITION, params, value) @callback def async_update_state(self, new_state): @@ -376,7 +378,7 @@ class WindowCoveringBasic(OpeningDeviceBase, HomeAccessory): service, position = (SERVICE_CLOSE_COVER, 0) params = {ATTR_ENTITY_ID: self.entity_id} - self.call_service(DOMAIN, service, params) + self.async_call_service(DOMAIN, service, params) # Snap the current/target position to the expected final position. self.char_current_position.set_value(position) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index d7215be9508..306beb89c48 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -125,27 +125,27 @@ class Fan(HomeAccessory): _LOGGER.debug("%s: Set state to %d", self.entity_id, value) service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF params = {ATTR_ENTITY_ID: self.entity_id} - self.call_service(DOMAIN, service, params) + self.async_call_service(DOMAIN, service, params) def set_direction(self, value): """Set state if call came from HomeKit.""" _LOGGER.debug("%s: Set direction to %d", self.entity_id, value) direction = DIRECTION_REVERSE if value == 1 else DIRECTION_FORWARD params = {ATTR_ENTITY_ID: self.entity_id, ATTR_DIRECTION: direction} - self.call_service(DOMAIN, SERVICE_SET_DIRECTION, params, direction) + self.async_call_service(DOMAIN, SERVICE_SET_DIRECTION, params, direction) def set_oscillating(self, value): """Set state if call came from HomeKit.""" _LOGGER.debug("%s: Set oscillating to %d", self.entity_id, value) oscillating = value == 1 params = {ATTR_ENTITY_ID: self.entity_id, ATTR_OSCILLATING: oscillating} - self.call_service(DOMAIN, SERVICE_OSCILLATE, params, oscillating) + self.async_call_service(DOMAIN, SERVICE_OSCILLATE, params, oscillating) def set_percentage(self, value): """Set state if call came from HomeKit.""" _LOGGER.debug("%s: Set speed to %d", self.entity_id, value) params = {ATTR_ENTITY_ID: self.entity_id, ATTR_PERCENTAGE: value} - self.call_service(DOMAIN, SERVICE_SET_PERCENTAGE, params, value) + self.async_call_service(DOMAIN, SERVICE_SET_PERCENTAGE, params, value) @callback def async_update_state(self, new_state): diff --git a/homeassistant/components/homekit/type_humidifiers.py b/homeassistant/components/homekit/type_humidifiers.py index dd829206b0c..6e1978d9499 100644 --- a/homeassistant/components/homekit/type_humidifiers.py +++ b/homeassistant/components/homekit/type_humidifiers.py @@ -143,7 +143,7 @@ class HumidifierDehumidifier(HomeAccessory): if humidity_state: self._async_update_current_humidity(humidity_state) - async def run_handler(self): + async def run(self): """Handle accessory driver started event. Run inside the Home Assistant event loop. @@ -155,7 +155,7 @@ class HumidifierDehumidifier(HomeAccessory): self.async_update_current_humidity_event, ) - await super().run_handler() + await super().run() @callback def async_update_current_humidity_event(self, event): @@ -201,7 +201,7 @@ class HumidifierDehumidifier(HomeAccessory): ) if CHAR_ACTIVE in char_values: - self.call_service( + self.async_call_service( DOMAIN, SERVICE_TURN_ON if char_values[CHAR_ACTIVE] else SERVICE_TURN_OFF, {ATTR_ENTITY_ID: self.entity_id}, @@ -210,7 +210,7 @@ class HumidifierDehumidifier(HomeAccessory): if self._target_humidity_char_name in char_values: humidity = round(char_values[self._target_humidity_char_name]) - self.call_service( + self.async_call_service( DOMAIN, SERVICE_SET_HUMIDITY, {ATTR_ENTITY_ID: self.entity_id, ATTR_HUMIDITY: humidity}, diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 086934ea6f7..8be1580537d 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -139,7 +139,7 @@ class Light(HomeAccessory): params[ATTR_HS_COLOR] = color events.append(f"set color at {color}") - self.call_service(DOMAIN, service, params, ", ".join(events)) + self.async_call_service(DOMAIN, service, params, ", ".join(events)) @callback def async_update_state(self, new_state): diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index af5b24c50e1..140940dda47 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -61,7 +61,7 @@ class Lock(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id} if self._code: params[ATTR_CODE] = self._code - self.call_service(DOMAIN, service, params) + self.async_call_service(DOMAIN, service, params) @callback def async_update_state(self, new_state): diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index 901ac2173f4..b54b62372f9 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -177,7 +177,7 @@ class MediaPlayer(HomeAccessory): _LOGGER.debug('%s: Set switch state for "on_off" to %s', self.entity_id, value) service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF params = {ATTR_ENTITY_ID: self.entity_id} - self.call_service(DOMAIN, service, params) + self.async_call_service(DOMAIN, service, params) def set_play_pause(self, value): """Move switch state to value if call came from HomeKit.""" @@ -186,7 +186,7 @@ class MediaPlayer(HomeAccessory): ) service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_PAUSE params = {ATTR_ENTITY_ID: self.entity_id} - self.call_service(DOMAIN, service, params) + self.async_call_service(DOMAIN, service, params) def set_play_stop(self, value): """Move switch state to value if call came from HomeKit.""" @@ -195,7 +195,7 @@ class MediaPlayer(HomeAccessory): ) service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_STOP params = {ATTR_ENTITY_ID: self.entity_id} - self.call_service(DOMAIN, service, params) + self.async_call_service(DOMAIN, service, params) def set_toggle_mute(self, value): """Move switch state to value if call came from HomeKit.""" @@ -203,7 +203,7 @@ class MediaPlayer(HomeAccessory): '%s: Set switch state for "toggle_mute" to %s', self.entity_id, value ) params = {ATTR_ENTITY_ID: self.entity_id, ATTR_MEDIA_VOLUME_MUTED: value} - self.call_service(DOMAIN, SERVICE_VOLUME_MUTE, params) + self.async_call_service(DOMAIN, SERVICE_VOLUME_MUTE, params) @callback def async_update_state(self, new_state): @@ -344,7 +344,7 @@ class TelevisionMediaPlayer(HomeAccessory): _LOGGER.debug('%s: Set switch state for "on_off" to %s', self.entity_id, value) service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF params = {ATTR_ENTITY_ID: self.entity_id} - self.call_service(DOMAIN, service, params) + self.async_call_service(DOMAIN, service, params) def set_mute(self, value): """Move switch state to value if call came from HomeKit.""" @@ -352,27 +352,27 @@ class TelevisionMediaPlayer(HomeAccessory): '%s: Set switch state for "toggle_mute" to %s', self.entity_id, value ) params = {ATTR_ENTITY_ID: self.entity_id, ATTR_MEDIA_VOLUME_MUTED: value} - self.call_service(DOMAIN, SERVICE_VOLUME_MUTE, params) + self.async_call_service(DOMAIN, SERVICE_VOLUME_MUTE, params) def set_volume(self, value): """Send volume step value if call came from HomeKit.""" _LOGGER.debug("%s: Set volume to %s", self.entity_id, value) params = {ATTR_ENTITY_ID: self.entity_id, ATTR_MEDIA_VOLUME_LEVEL: value} - self.call_service(DOMAIN, SERVICE_VOLUME_SET, params) + self.async_call_service(DOMAIN, SERVICE_VOLUME_SET, params) def set_volume_step(self, value): """Send volume step value if call came from HomeKit.""" _LOGGER.debug("%s: Step volume by %s", self.entity_id, value) service = SERVICE_VOLUME_DOWN if value else SERVICE_VOLUME_UP params = {ATTR_ENTITY_ID: self.entity_id} - self.call_service(DOMAIN, service, params) + self.async_call_service(DOMAIN, service, params) def set_input_source(self, value): """Send input set value if call came from HomeKit.""" _LOGGER.debug("%s: Set current input to %s", self.entity_id, value) source = self.sources[value] params = {ATTR_ENTITY_ID: self.entity_id, ATTR_INPUT_SOURCE: source} - self.call_service(DOMAIN, SERVICE_SELECT_SOURCE, params) + self.async_call_service(DOMAIN, SERVICE_SELECT_SOURCE, params) def set_remote_key(self, value): """Send remote key value if call came from HomeKit.""" @@ -392,7 +392,7 @@ class TelevisionMediaPlayer(HomeAccessory): else: service = SERVICE_MEDIA_PLAY_PAUSE params = {ATTR_ENTITY_ID: self.entity_id} - self.call_service(DOMAIN, service, params) + self.async_call_service(DOMAIN, service, params) else: # Unhandled keys can be handled by listening to the event bus self.hass.bus.fire( diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index feae1b5cd06..acbf636c1c3 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -150,7 +150,7 @@ class SecuritySystem(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id} if self._alarm_code: params[ATTR_CODE] = self._alarm_code - self.call_service(DOMAIN, service, params) + self.async_call_service(DOMAIN, service, params) @callback def async_update_state(self, new_state): diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index b3ee8a06497..1ce6c364896 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -80,7 +80,7 @@ class Outlet(HomeAccessory): _LOGGER.debug("%s: Set switch state to %s", self.entity_id, value) params = {ATTR_ENTITY_ID: self.entity_id} service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF - self.call_service(DOMAIN, service, params) + self.async_call_service(DOMAIN, service, params) @callback def async_update_state(self, new_state): @@ -131,7 +131,7 @@ class Switch(HomeAccessory): return params = {ATTR_ENTITY_ID: self.entity_id} service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF - self.call_service(self._domain, service, params) + self.async_call_service(self._domain, service, params) if self.activate_only: call_later(self.hass, 1, self.reset_switch) @@ -169,7 +169,9 @@ class Vacuum(Switch): sup_return_home = features & SUPPORT_RETURN_HOME service = SERVICE_RETURN_TO_BASE if sup_return_home else SERVICE_TURN_OFF - self.call_service(VACUUM_DOMAIN, service, {ATTR_ENTITY_ID: self.entity_id}) + self.async_call_service( + VACUUM_DOMAIN, service, {ATTR_ENTITY_ID: self.entity_id} + ) @callback def async_update_state(self, new_state): @@ -209,7 +211,7 @@ class Valve(HomeAccessory): self.char_in_use.set_value(value) params = {ATTR_ENTITY_ID: self.entity_id} service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF - self.call_service(DOMAIN, service, params) + self.async_call_service(DOMAIN, service, params) @callback def async_update_state(self, new_state): diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index a1c13432614..2eb63f4c840 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -356,7 +356,7 @@ class Thermostat(HomeAccessory): if service: params[ATTR_ENTITY_ID] = self.entity_id - self.call_service( + self.async_call_service( DOMAIN_CLIMATE, service, params, @@ -407,7 +407,7 @@ class Thermostat(HomeAccessory): """Set target humidity to value if call came from HomeKit.""" _LOGGER.debug("%s: Set target humidity to %d", self.entity_id, value) params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HUMIDITY: value} - self.call_service( + self.async_call_service( DOMAIN_CLIMATE, SERVICE_SET_HUMIDITY, params, f"{value}{PERCENTAGE}" ) @@ -584,7 +584,7 @@ class WaterHeater(HomeAccessory): _LOGGER.debug("%s: Set target temperature to %.1f°C", self.entity_id, value) temperature = temperature_to_states(value, self._unit) params = {ATTR_ENTITY_ID: self.entity_id, ATTR_TEMPERATURE: temperature} - self.call_service( + self.async_call_service( DOMAIN_WATER_HEATER, SERVICE_SET_TEMPERATURE_WATER_HEATER, params, diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 8ba2d9fb0b2..e308ed21537 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -57,7 +57,7 @@ async def test_accessory_cancels_track_state_change_on_stop(hass, hk_driver): with patch( "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" ): - await acc.run_handler() + await acc.run() assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS][entity_id]) == 1 acc.async_stop() assert entity_id not in hass.data[TRACK_STATE_CHANGE_CALLBACKS] @@ -121,7 +121,7 @@ async def test_home_accessory(hass, hk_driver): with patch( "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" ) as mock_async_update_state: - await acc.run_handler() + await acc.run() await hass.async_block_till_done() state = hass.states.get(entity_id) mock_async_update_state.assert_called_with(state) @@ -156,7 +156,7 @@ async def test_battery_service(hass, hk_driver, caplog): with patch( "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" ) as mock_async_update_state: - await acc.run_handler() + await acc.run() await hass.async_block_till_done() state = hass.states.get(entity_id) mock_async_update_state.assert_called_with(state) @@ -212,7 +212,7 @@ async def test_battery_service(hass, hk_driver, caplog): with patch( "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" ) as mock_async_update_state: - await acc.run_handler() + await acc.run() await hass.async_block_till_done() state = hass.states.get(entity_id) mock_async_update_state.assert_called_with(state) @@ -253,7 +253,7 @@ async def test_linked_battery_sensor(hass, hk_driver, caplog): with patch( "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" ) as mock_async_update_state: - await acc.run_handler() + await acc.run() await hass.async_block_till_done() state = hass.states.get(entity_id) mock_async_update_state.assert_called_with(state) @@ -298,7 +298,7 @@ async def test_linked_battery_sensor(hass, hk_driver, caplog): with patch( "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" ) as mock_async_update_state: - await acc.run_handler() + await acc.run() await hass.async_block_till_done() state = hass.states.get(entity_id) mock_async_update_state.assert_called_with(state) @@ -340,7 +340,7 @@ async def test_linked_battery_charging_sensor(hass, hk_driver, caplog): with patch( "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" ) as mock_async_update_state: - await acc.run_handler() + await acc.run() await hass.async_block_till_done() state = hass.states.get(entity_id) mock_async_update_state.assert_called_with(state) @@ -352,7 +352,7 @@ async def test_linked_battery_charging_sensor(hass, hk_driver, caplog): "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" ) as mock_async_update_state: hass.states.async_set(linked_battery_charging_sensor, STATE_OFF, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() state = hass.states.get(entity_id) mock_async_update_state.assert_called_with(state) @@ -362,7 +362,7 @@ async def test_linked_battery_charging_sensor(hass, hk_driver, caplog): "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" ) as mock_async_update_state: hass.states.async_set(linked_battery_charging_sensor, STATE_ON, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() state = hass.states.get(entity_id) mock_async_update_state.assert_called_with(state) @@ -372,7 +372,7 @@ async def test_linked_battery_charging_sensor(hass, hk_driver, caplog): "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" ) as mock_async_update_state: hass.states.async_remove(linked_battery_charging_sensor) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc._char_charging.value == 1 @@ -405,7 +405,7 @@ async def test_linked_battery_sensor_and_linked_battery_charging_sensor( with patch( "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" ) as mock_async_update_state: - await acc.run_handler() + await acc.run() await hass.async_block_till_done() state = hass.states.get(entity_id) mock_async_update_state.assert_called_with(state) @@ -449,7 +449,7 @@ async def test_missing_linked_battery_charging_sensor(hass, hk_driver, caplog): with patch( "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" ): - await acc.run_handler() + await acc.run() await hass.async_block_till_done() # Make sure we don't throw if the entity_id @@ -458,7 +458,7 @@ async def test_missing_linked_battery_charging_sensor(hass, hk_driver, caplog): with patch( "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" ): - await acc.run_handler() + await acc.run() await hass.async_block_till_done() @@ -482,7 +482,7 @@ async def test_missing_linked_battery_sensor(hass, hk_driver, caplog): with patch( "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" ) as mock_async_update_state: - await acc.run_handler() + await acc.run() await hass.async_block_till_done() state = hass.states.get(entity_id) mock_async_update_state.assert_called_with(state) @@ -496,7 +496,7 @@ async def test_missing_linked_battery_sensor(hass, hk_driver, caplog): "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" ) as mock_async_update_state: hass.states.async_remove(entity_id) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert not acc.linked_battery_sensor @@ -517,7 +517,7 @@ async def test_battery_appears_after_startup(hass, hk_driver, caplog): with patch( "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" ) as mock_async_update_state: - await acc.run_handler() + await acc.run() await hass.async_block_till_done() state = hass.states.get(entity_id) mock_async_update_state.assert_called_with(state) @@ -551,7 +551,7 @@ async def test_call_service(hass, hk_driver, events): test_service = "open_cover" test_value = "value" - await acc.async_call_service( + acc.async_call_service( test_domain, test_service, {ATTR_ENTITY_ID: entity_id}, test_value ) await hass.async_block_till_done() diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index c9b2ebc422c..ba08ea3caaf 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -45,35 +45,35 @@ PID_THAT_WILL_NEVER_BE_ALIVE = 2147483647 async def _async_start_streaming(hass, acc): """Start streaming a camera.""" acc.set_selected_stream_configuration(MOCK_START_STREAM_TLV) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() async def _async_setup_endpoints(hass, acc): """Set camera endpoints.""" acc.set_endpoints(MOCK_END_POINTS_TLV) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() async def _async_reconfigure_stream(hass, acc, session_info, stream_config): """Reconfigure the stream.""" await acc.reconfigure_stream(session_info, stream_config) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() async def _async_stop_all_streams(hass, acc): """Stop all camera streams.""" await acc.stop() - await acc.run_handler() + await acc.run() await hass.async_block_till_done() async def _async_stop_stream(hass, acc, session_info): """Stop a camera stream.""" await acc.stop_stream(session_info) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() @@ -156,7 +156,7 @@ async def test_camera_stream_source_configured(hass, run_driver, events): bridge.add_accessory(acc) bridge.add_accessory(not_camera_acc) - await acc.run_handler() + await acc.run() assert acc.aid == 2 assert acc.category == 17 # Camera @@ -271,7 +271,7 @@ async def test_camera_stream_source_configured_with_failing_ffmpeg( bridge.add_accessory(acc) bridge.add_accessory(not_camera_acc) - await acc.run_handler() + await acc.run() assert acc.aid == 2 assert acc.category == 17 # Camera @@ -311,7 +311,7 @@ async def test_camera_stream_source_found(hass, run_driver, events): 2, {}, ) - await acc.run_handler() + await acc.run() assert acc.aid == 2 assert acc.category == 17 # Camera @@ -361,7 +361,7 @@ async def test_camera_stream_source_fails(hass, run_driver, events): 2, {}, ) - await acc.run_handler() + await acc.run() assert acc.aid == 2 assert acc.category == 17 # Camera @@ -396,7 +396,7 @@ async def test_camera_with_no_stream(hass, run_driver, events): 2, {}, ) - await acc.run_handler() + await acc.run() assert acc.aid == 2 assert acc.category == 17 # Camera @@ -439,7 +439,7 @@ async def test_camera_stream_source_configured_and_copy_codec(hass, run_driver, bridge = HomeBridge("hass", run_driver, "Test Bridge") bridge.add_accessory(acc) - await acc.run_handler() + await acc.run() assert acc.aid == 2 assert acc.category == 17 # Camera @@ -510,7 +510,7 @@ async def test_camera_streaming_fails_after_starting_ffmpeg(hass, run_driver, ev bridge = HomeBridge("hass", run_driver, "Test Bridge") bridge.add_accessory(acc) - await acc.run_handler() + await acc.run() assert acc.aid == 2 assert acc.category == 17 # Camera @@ -588,7 +588,7 @@ async def test_camera_with_linked_motion_sensor(hass, run_driver, events): bridge = HomeBridge("hass", run_driver, "Test Bridge") bridge.add_accessory(acc) - await acc.run_handler() + await acc.run() assert acc.aid == 2 assert acc.category == 17 # Camera @@ -617,7 +617,7 @@ async def test_camera_with_linked_motion_sensor(hass, run_driver, events): # motion sensor is removed hass.states.async_remove(motion_entity_id) await hass.async_block_till_done() - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert char.value is True @@ -644,7 +644,7 @@ async def test_camera_with_a_missing_linked_motion_sensor(hass, run_driver, even bridge = HomeBridge("hass", run_driver, "Test Bridge") bridge.add_accessory(acc) - await acc.run_handler() + await acc.run() assert acc.aid == 2 assert acc.category == 17 # Camera @@ -686,7 +686,7 @@ async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events): bridge = HomeBridge("hass", run_driver, "Test Bridge") bridge.add_accessory(acc) - await acc.run_handler() + await acc.run() assert acc.aid == 2 assert acc.category == 17 # Camera @@ -725,7 +725,7 @@ async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events): # doorbell sensor is removed hass.states.async_remove(doorbell_entity_id) await hass.async_block_till_done() - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert char.value == 0 assert char2.value == 0 @@ -753,7 +753,7 @@ async def test_camera_with_a_missing_linked_doorbell_sensor(hass, run_driver, ev bridge = HomeBridge("hass", run_driver, "Test Bridge") bridge.add_accessory(acc) - await acc.run_handler() + await acc.run() assert acc.aid == 2 assert acc.category == 17 # Camera diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index d39e9cda7d0..1c0de6c3af2 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -52,7 +52,7 @@ async def test_garage_door_open_close(hass, hk_driver, events): hass.states.async_set(entity_id, None) await hass.async_block_till_done() acc = GarageDoorOpener(hass, hk_driver, "Garage Door", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 2 @@ -136,7 +136,7 @@ async def test_windowcovering_set_cover_position(hass, hk_driver, events): hass.states.async_set(entity_id, None) await hass.async_block_till_done() acc = WindowCovering(hass, hk_driver, "Cover", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 2 @@ -206,7 +206,7 @@ async def test_window_instantiate(hass, hk_driver, events): hass.states.async_set(entity_id, None) await hass.async_block_till_done() acc = Window(hass, hk_driver, "Window", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 2 @@ -225,7 +225,7 @@ async def test_windowcovering_cover_set_tilt(hass, hk_driver, events): ) await hass.async_block_till_done() acc = WindowCovering(hass, hk_driver, "Cover", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 2 @@ -289,7 +289,7 @@ async def test_windowcovering_open_close(hass, hk_driver, events): hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: 0}) acc = WindowCoveringBasic(hass, hk_driver, "Cover", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 2 @@ -372,7 +372,7 @@ async def test_windowcovering_open_close_stop(hass, hk_driver, events): entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP} ) acc = WindowCoveringBasic(hass, hk_driver, "Cover", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() # Set from HomeKit @@ -423,7 +423,7 @@ async def test_windowcovering_open_close_with_position_and_stop( {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP | SUPPORT_SET_POSITION}, ) acc = WindowCovering(hass, hk_driver, "Cover", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() # Set from HomeKit @@ -534,7 +534,7 @@ async def test_garage_door_with_linked_obstruction_sensor(hass, hk_driver, event 2, {CONF_LINKED_OBSTRUCTION_SENSOR: linked_obstruction_sensor_entity_id}, ) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 2 diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index e99f9d0b95a..baa47462cdc 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -46,7 +46,7 @@ async def test_fan_basic(hass, hk_driver, events): # If there are no speed_list values, then HomeKit speed is unsupported assert acc.char_speed is None - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_active.value == 1 @@ -123,7 +123,7 @@ async def test_fan_direction(hass, hk_driver, events): assert acc.char_direction.value == 0 - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_direction.value == 0 @@ -191,7 +191,7 @@ async def test_fan_oscillate(hass, hk_driver, events): assert acc.char_swing.value == 0 - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_swing.value == 0 @@ -267,7 +267,7 @@ async def test_fan_speed(hass, hk_driver, events): assert acc.char_speed.value != 0 assert acc.char_speed.properties[PROP_MIN_STEP] == 25 - await acc.run_handler() + await acc.run() await hass.async_block_till_done() hass.states.async_set(entity_id, STATE_ON, {ATTR_PERCENTAGE: 100}) @@ -349,7 +349,7 @@ async def test_fan_set_all_one_shot(hass, hk_driver, events): # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the # speed to 100 when turning on a fan on a freshly booted up server. assert acc.char_speed.value != 0 - await acc.run_handler() + await acc.run() await hass.async_block_till_done() hass.states.async_set( diff --git a/tests/components/homekit/test_type_humidifiers.py b/tests/components/homekit/test_type_humidifiers.py index 51f9621d15a..1a301e340b3 100644 --- a/tests/components/homekit/test_type_humidifiers.py +++ b/tests/components/homekit/test_type_humidifiers.py @@ -54,7 +54,7 @@ async def test_humidifier(hass, hk_driver, events): ) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 1 @@ -135,7 +135,7 @@ async def test_dehumidifier(hass, hk_driver, events): ) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 1 @@ -220,7 +220,7 @@ async def test_hygrostat_power_state(hass, hk_driver, events): ) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_current_humidifier_dehumidifier.value == 2 @@ -298,7 +298,7 @@ async def test_hygrostat_get_humidity_range(hass, hk_driver): ) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_target_humidity.properties[PROP_MAX_VALUE] == 45 @@ -332,7 +332,7 @@ async def test_humidifier_with_linked_humidity_sensor(hass, hk_driver): ) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_current_humidity.value == 42.0 @@ -384,7 +384,7 @@ async def test_humidifier_with_a_missing_linked_humidity_sensor(hass, hk_driver) ) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_current_humidity.value == 0 @@ -401,7 +401,7 @@ async def test_humidifier_as_dehumidifier(hass, hk_driver, events, caplog): ) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_target_humidifier_dehumidifier.value == 1 diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 42ef18f3505..0ab3ef8e45d 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -42,7 +42,7 @@ async def test_light_basic(hass, hk_driver, events): assert acc.category == 5 # Lightbulb assert acc.char_on.value - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_on.value == 1 @@ -117,7 +117,7 @@ async def test_light_brightness(hass, hk_driver, events): char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_brightness.value == 100 @@ -231,7 +231,7 @@ async def test_light_color_temperature(hass, hk_driver, events): assert acc.char_color_temperature.value == 190 - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_color_temperature.value == 190 @@ -285,14 +285,14 @@ async def test_light_color_temperature_and_rgb_color(hass, hk_driver, events): hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: 224}) await hass.async_block_till_done() - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_hue.value == 27 assert acc.char_saturation.value == 27 hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: 352}) await hass.async_block_till_done() - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_hue.value == 28 assert acc.char_saturation.value == 61 @@ -314,7 +314,7 @@ async def test_light_rgb_color(hass, hk_driver, events): assert acc.char_hue.value == 260 assert acc.char_saturation.value == 90 - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_hue.value == 260 assert acc.char_saturation.value == 90 @@ -407,7 +407,7 @@ async def test_light_set_brightness_and_color(hass, hk_driver, events): char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID] char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID] - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_brightness.value == 100 @@ -482,7 +482,7 @@ async def test_light_set_brightness_and_color_temp(hass, hk_driver, events): char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] char_color_temperature_iid = acc.char_color_temperature.to_HAP()[HAP_REPR_IID] - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_brightness.value == 100 diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index 7899af36995..b2bb9b4736e 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -24,7 +24,7 @@ async def test_lock_unlock(hass, hk_driver, events): hass.states.async_set(entity_id, None) await hass.async_block_till_done() acc = Lock(hass, hk_driver, "Lock", entity_id, 2, config) - await acc.run_handler() + await acc.run() assert acc.aid == 2 assert acc.category == 6 # DoorLock diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index 9516963a982..0b9d25ce3ec 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -61,7 +61,7 @@ async def test_media_player_set_state(hass, hk_driver, events): ) await hass.async_block_till_done() acc = MediaPlayer(hass, hk_driver, "MediaPlayer", entity_id, 2, config) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 2 @@ -203,7 +203,7 @@ async def test_media_player_television(hass, hk_driver, events, caplog): ) await hass.async_block_till_done() acc = TelevisionMediaPlayer(hass, hk_driver, "MediaPlayer", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 2 @@ -375,7 +375,7 @@ async def test_media_player_television_basic(hass, hk_driver, events, caplog): ) await hass.async_block_till_done() acc = TelevisionMediaPlayer(hass, hk_driver, "MediaPlayer", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.chars_tv == [CHAR_REMOTE_KEY] @@ -411,7 +411,7 @@ async def test_media_player_television_supports_source_select_no_sources( ) await hass.async_block_till_done() acc = TelevisionMediaPlayer(hass, hk_driver, "MediaPlayer", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.support_select_source is False diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index d6bf74bb7cf..19b8b5720e2 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -34,7 +34,7 @@ async def test_switch_set_state(hass, hk_driver, events): hass.states.async_set(entity_id, None) await hass.async_block_till_done() acc = SecuritySystem(hass, hk_driver, "SecuritySystem", entity_id, 2, config) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 2 @@ -238,7 +238,7 @@ async def test_supported_states(hass, hk_driver, events): await hass.async_block_till_done() acc = SecuritySystem(hass, hk_driver, "SecuritySystem", entity_id, 2, config) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() valid_current_values = acc.char_current_state.properties.get("ValidValues") diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 7ee79352d7b..fe2ae7566d5 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -40,7 +40,7 @@ async def test_temperature(hass, hk_driver): hass.states.async_set(entity_id, None) await hass.async_block_till_done() acc = TemperatureSensor(hass, hk_driver, "Temperature", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 2 @@ -74,7 +74,7 @@ async def test_humidity(hass, hk_driver): hass.states.async_set(entity_id, None) await hass.async_block_till_done() acc = HumiditySensor(hass, hk_driver, "Humidity", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 2 @@ -98,7 +98,7 @@ async def test_air_quality(hass, hk_driver): hass.states.async_set(entity_id, None) await hass.async_block_till_done() acc = AirQualitySensor(hass, hk_driver, "Air Quality", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 2 @@ -130,7 +130,7 @@ async def test_co(hass, hk_driver): hass.states.async_set(entity_id, None) await hass.async_block_till_done() acc = CarbonMonoxideSensor(hass, hk_driver, "CO", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 2 @@ -170,7 +170,7 @@ async def test_co2(hass, hk_driver): hass.states.async_set(entity_id, None) await hass.async_block_till_done() acc = CarbonDioxideSensor(hass, hk_driver, "CO2", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 2 @@ -210,7 +210,7 @@ async def test_light(hass, hk_driver): hass.states.async_set(entity_id, None) await hass.async_block_till_done() acc = LightSensor(hass, hk_driver, "Light", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 2 @@ -235,7 +235,7 @@ async def test_binary(hass, hk_driver): await hass.async_block_till_done() acc = BinarySensor(hass, hk_driver, "Window Opening", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 2 @@ -274,7 +274,7 @@ async def test_motion_uses_bool(hass, hk_driver): await hass.async_block_till_done() acc = BinarySensor(hass, hk_driver, "Motion Sensor", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 2 diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 5d218a6ef8a..2ce0acfc8bc 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -42,7 +42,7 @@ async def test_outlet_set_state(hass, hk_driver, events): hass.states.async_set(entity_id, None) await hass.async_block_till_done() acc = Outlet(hass, hk_driver, "Outlet", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 2 @@ -95,7 +95,7 @@ async def test_switch_set_state(hass, hk_driver, entity_id, attrs, events): hass.states.async_set(entity_id, None, attrs) await hass.async_block_till_done() acc = Switch(hass, hk_driver, "Switch", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 2 @@ -139,25 +139,25 @@ async def test_valve_set_state(hass, hk_driver, events): await hass.async_block_till_done() acc = Valve(hass, hk_driver, "Valve", entity_id, 2, {CONF_TYPE: TYPE_FAUCET}) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.category == 29 # Faucet assert acc.char_valve_type.value == 3 # Water faucet acc = Valve(hass, hk_driver, "Valve", entity_id, 2, {CONF_TYPE: TYPE_SHOWER}) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.category == 30 # Shower assert acc.char_valve_type.value == 2 # Shower head acc = Valve(hass, hk_driver, "Valve", entity_id, 2, {CONF_TYPE: TYPE_SPRINKLER}) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.category == 28 # Sprinkler assert acc.char_valve_type.value == 1 # Irrigation acc = Valve(hass, hk_driver, "Valve", entity_id, 2, {CONF_TYPE: TYPE_VALVE}) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 2 @@ -210,7 +210,7 @@ async def test_vacuum_set_state_with_returnhome_and_start_support( await hass.async_block_till_done() acc = Vacuum(hass, hk_driver, "Vacuum", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 2 assert acc.category == 8 # Switch @@ -266,7 +266,7 @@ async def test_vacuum_set_state_without_returnhome_and_start_support( await hass.async_block_till_done() acc = Vacuum(hass, hk_driver, "Vacuum", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 2 assert acc.category == 8 # Switch @@ -310,7 +310,7 @@ async def test_reset_switch(hass, hk_driver, events): hass.states.async_set(entity_id, None) await hass.async_block_till_done() acc = Switch(hass, hk_driver, "Switch", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.activate_only is True @@ -347,7 +347,7 @@ async def test_reset_switch_reload(hass, hk_driver, events): hass.states.async_set(entity_id, None) await hass.async_block_till_done() acc = Switch(hass, hk_driver, "Switch", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.activate_only is False diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 79b5ca21097..7d3d0d14c2f 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -89,7 +89,7 @@ async def test_thermostat(hass, hk_driver, events): acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 1 @@ -431,7 +431,7 @@ async def test_thermostat_auto(hass, hk_driver, events): acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_cooling_thresh_temp.value == 23.0 @@ -570,7 +570,7 @@ async def test_thermostat_humidity(hass, hk_driver, events): acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_target_humidity.value == 50 @@ -645,7 +645,7 @@ async def test_thermostat_power_state(hass, hk_driver, events): acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_current_heat_cool.value == 1 @@ -756,7 +756,7 @@ async def test_thermostat_fahrenheit(hass, hk_driver, events): with patch.object(hass.config.units, CONF_TEMPERATURE_UNIT, new=TEMP_FAHRENHEIT): acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() hass.states.async_set( @@ -879,7 +879,7 @@ async def test_thermostat_temperature_step_whole(hass, hk_driver): acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_target_temp.properties[PROP_MIN_STEP] == 0.1 @@ -942,7 +942,7 @@ async def test_thermostat_hvac_modes(hass, hk_driver): acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() hap = acc.char_target_heat_cool.to_HAP() assert hap["valid-values"] == [0, 1] @@ -985,7 +985,7 @@ async def test_thermostat_hvac_modes_with_auto_heat_cool(hass, hk_driver): acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() hap = acc.char_target_heat_cool.to_HAP() assert hap["valid-values"] == [0, 1, 3] @@ -1041,7 +1041,7 @@ async def test_thermostat_hvac_modes_with_auto_no_heat_cool(hass, hk_driver): acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() hap = acc.char_target_heat_cool.to_HAP() assert hap["valid-values"] == [0, 1, 3] @@ -1095,7 +1095,7 @@ async def test_thermostat_hvac_modes_with_auto_only(hass, hk_driver): acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() hap = acc.char_target_heat_cool.to_HAP() assert hap["valid-values"] == [0, 3] @@ -1149,7 +1149,7 @@ async def test_thermostat_hvac_modes_with_heat_only(hass, hk_driver): acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() 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] @@ -1209,7 +1209,7 @@ async def test_thermostat_hvac_modes_with_cool_only(hass, hk_driver): acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() 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] @@ -1273,7 +1273,7 @@ async def test_thermostat_hvac_modes_with_heat_cool_only(hass, hk_driver): acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() hap = acc.char_target_heat_cool.to_HAP() assert hap["valid-values"] == [ @@ -1362,7 +1362,7 @@ async def test_thermostat_hvac_modes_without_off(hass, hk_driver): acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() hap = acc.char_target_heat_cool.to_HAP() assert hap["valid-values"] == [1, 3] @@ -1401,7 +1401,7 @@ async def test_thermostat_without_target_temp_only_range(hass, hk_driver, events acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_cooling_thresh_temp.value == 23.0 @@ -1576,7 +1576,7 @@ async def test_water_heater(hass, hk_driver, events): hass.states.async_set(entity_id, HVAC_MODE_HEAT) await hass.async_block_till_done() acc = WaterHeater(hass, hk_driver, "WaterHeater", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.aid == 2 @@ -1655,7 +1655,7 @@ async def test_water_heater_fahrenheit(hass, hk_driver, events): await hass.async_block_till_done() with patch.object(hass.config.units, CONF_TEMPERATURE_UNIT, new=TEMP_FAHRENHEIT): acc = WaterHeater(hass, hk_driver, "WaterHeater", entity_id, 2, None) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() hass.states.async_set(entity_id, HVAC_MODE_HEAT, {ATTR_TEMPERATURE: 131}) @@ -1762,7 +1762,7 @@ async def test_thermostat_with_no_modes_when_we_first_see(hass, hk_driver, event acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_cooling_thresh_temp.value == 23.0 @@ -1815,7 +1815,7 @@ async def test_thermostat_with_no_off_after_recheck(hass, hk_driver, events): acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_cooling_thresh_temp.value == 23.0 @@ -1869,7 +1869,7 @@ async def test_thermostat_with_temp_clamps(hass, hk_driver, events): acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) - await acc.run_handler() + await acc.run() await hass.async_block_till_done() assert acc.char_cooling_thresh_temp.value == 100 From 26ce316c18f3f7e753589bdac835aa5a278491df Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 20 Feb 2021 09:11:36 +0100 Subject: [PATCH 578/796] Do not trigger when numeric_state is true at startup (#46424) --- .../homeassistant/triggers/numeric_state.py | 54 +++++--- tests/components/cover/test_device_trigger.py | 22 ++- .../triggers/test_numeric_state.py | 129 +++++++++++++++++- .../components/sensor/test_device_trigger.py | 3 +- 4 files changed, 175 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index 4f406de23ca..16b3fb97475 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -73,7 +73,7 @@ async def async_attach_trigger( template.attach(hass, time_delta) value_template = config.get(CONF_VALUE_TEMPLATE) unsub_track_same = {} - entities_triggered = set() + armed_entities = set() period: dict = {} attribute = config.get(CONF_ATTRIBUTE) job = HassJob(action) @@ -100,20 +100,22 @@ async def async_attach_trigger( @callback def check_numeric_state(entity_id, from_s, to_s): - """Return True if criteria are now met.""" + """Return whether the criteria are met, raise ConditionError if unknown.""" + return condition.async_numeric_state( + hass, to_s, below, above, value_template, variables(entity_id), attribute + ) + + # Each entity that starts outside the range is already armed (ready to fire). + for entity_id in entity_ids: try: - return condition.async_numeric_state( - hass, - to_s, - below, - above, - value_template, - variables(entity_id), - attribute, + if not check_numeric_state(entity_id, None, entity_id): + armed_entities.add(entity_id) + except exceptions.ConditionError as ex: + _LOGGER.warning( + "Error initializing 'numeric_state' trigger for '%s': %s", + automation_info["name"], + ex, ) - except exceptions.ConditionError as err: - _LOGGER.warning("%s", err) - return False @callback def state_automation_listener(event): @@ -142,12 +144,27 @@ async def async_attach_trigger( to_s.context, ) - matching = check_numeric_state(entity_id, from_s, to_s) + @callback + def check_numeric_state_no_raise(entity_id, from_s, to_s): + """Return True if the criteria are now met, False otherwise.""" + try: + return check_numeric_state(entity_id, from_s, to_s) + except exceptions.ConditionError: + # This is an internal same-state listener so we just drop the + # error. The same error will be reached and logged by the + # primary async_track_state_change_event() listener. + return False + + try: + matching = check_numeric_state(entity_id, from_s, to_s) + except exceptions.ConditionError as ex: + _LOGGER.warning("Error in '%s' trigger: %s", automation_info["name"], ex) + return if not matching: - entities_triggered.discard(entity_id) - elif entity_id not in entities_triggered: - entities_triggered.add(entity_id) + armed_entities.add(entity_id) + elif entity_id in armed_entities: + armed_entities.discard(entity_id) if time_delta: try: @@ -160,7 +177,6 @@ async def async_attach_trigger( automation_info["name"], ex, ) - entities_triggered.discard(entity_id) return unsub_track_same[entity_id] = async_track_same_state( @@ -168,7 +184,7 @@ async def async_attach_trigger( period[entity_id], call_action, entity_ids=entity_id, - async_check_same_func=check_numeric_state, + async_check_same_func=check_numeric_state_no_raise, ) else: call_action() diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index ab054ad8223..e8bb3cdc8df 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -542,8 +542,12 @@ async def test_if_fires_on_position(hass, calls): ] }, ) + hass.states.async_set(ent.entity_id, STATE_OPEN, attributes={"current_position": 1}) hass.states.async_set( - ent.entity_id, STATE_CLOSED, attributes={"current_position": 50} + ent.entity_id, STATE_CLOSED, attributes={"current_position": 95} + ) + hass.states.async_set( + ent.entity_id, STATE_OPEN, attributes={"current_position": 50} ) await hass.async_block_till_done() assert len(calls) == 3 @@ -551,8 +555,8 @@ async def test_if_fires_on_position(hass, calls): [calls[0].data["some"], calls[1].data["some"], calls[2].data["some"]] ) == sorted( [ - "is_pos_gt_45_lt_90 - device - cover.set_position_cover - open - closed - None", - "is_pos_lt_90 - device - cover.set_position_cover - open - closed - None", + "is_pos_gt_45_lt_90 - device - cover.set_position_cover - closed - open - None", + "is_pos_lt_90 - device - cover.set_position_cover - closed - open - None", "is_pos_gt_45 - device - cover.set_position_cover - open - closed - None", ] ) @@ -666,7 +670,13 @@ async def test_if_fires_on_tilt_position(hass, calls): }, ) hass.states.async_set( - ent.entity_id, STATE_CLOSED, attributes={"current_tilt_position": 50} + ent.entity_id, STATE_OPEN, attributes={"current_tilt_position": 1} + ) + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_tilt_position": 95} + ) + hass.states.async_set( + ent.entity_id, STATE_OPEN, attributes={"current_tilt_position": 50} ) await hass.async_block_till_done() assert len(calls) == 3 @@ -674,8 +684,8 @@ async def test_if_fires_on_tilt_position(hass, calls): [calls[0].data["some"], calls[1].data["some"], calls[2].data["some"]] ) == sorted( [ - "is_pos_gt_45_lt_90 - device - cover.set_position_cover - open - closed - None", - "is_pos_lt_90 - device - cover.set_position_cover - open - closed - None", + "is_pos_gt_45_lt_90 - device - cover.set_position_cover - closed - open - None", + "is_pos_lt_90 - device - cover.set_position_cover - closed - open - None", "is_pos_gt_45 - device - cover.set_position_cover - open - closed - None", ] ) diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index 85dc68c770d..831e20b78a1 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -10,7 +10,12 @@ import homeassistant.components.automation as automation from homeassistant.components.homeassistant.triggers import ( numeric_state as numeric_state_trigger, ) -from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF +from homeassistant.const import ( + ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, + SERVICE_TURN_OFF, + STATE_UNAVAILABLE, +) from homeassistant.core import Context from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -241,7 +246,7 @@ async def test_if_not_below_fires_on_entity_change_to_equal(hass, calls, below): @pytest.mark.parametrize("below", (10, "input_number.value_10")) -async def test_if_fires_on_initial_entity_below(hass, calls, below): +async def test_if_not_fires_on_initial_entity_below(hass, calls, below): """Test the firing when starting with a match.""" hass.states.async_set("test.entity", 9) await hass.async_block_till_done() @@ -261,14 +266,14 @@ async def test_if_fires_on_initial_entity_below(hass, calls, below): }, ) - # Fire on first update even if initial state was already below + # Do not fire on first update when initial state was already below hass.states.async_set("test.entity", 8) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(calls) == 0 @pytest.mark.parametrize("above", (10, "input_number.value_10")) -async def test_if_fires_on_initial_entity_above(hass, calls, above): +async def test_if_not_fires_on_initial_entity_above(hass, calls, above): """Test the firing when starting with a match.""" hass.states.async_set("test.entity", 11) await hass.async_block_till_done() @@ -288,10 +293,10 @@ async def test_if_fires_on_initial_entity_above(hass, calls, above): }, ) - # Fire on first update even if initial state was already above + # Do not fire on first update when initial state was already above hass.states.async_set("test.entity", 12) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(calls) == 0 @pytest.mark.parametrize("above", (10, "input_number.value_10")) @@ -320,6 +325,74 @@ async def test_if_fires_on_entity_change_above(hass, calls, above): assert len(calls) == 1 +async def test_if_fires_on_entity_unavailable_at_startup(hass, calls): + """Test the firing with changed entity at startup.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "numeric_state", + "entity_id": "test.entity", + "above": 10, + }, + "action": {"service": "test.automation"}, + } + }, + ) + # 11 is above 10 + hass.states.async_set("test.entity", 11) + await hass.async_block_till_done() + assert len(calls) == 0 + + +async def test_if_not_fires_on_entity_unavailable(hass, calls): + """Test the firing with entity changing to unavailable.""" + # set initial state + hass.states.async_set("test.entity", 9) + await hass.async_block_till_done() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "numeric_state", + "entity_id": "test.entity", + "above": 10, + }, + "action": {"service": "test.automation"}, + } + }, + ) + + # 11 is above 10 + hass.states.async_set("test.entity", 11) + await hass.async_block_till_done() + assert len(calls) == 1 + + # Going to unavailable and back should not fire + hass.states.async_set("test.entity", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert len(calls) == 1 + hass.states.async_set("test.entity", 11) + await hass.async_block_till_done() + assert len(calls) == 1 + + # Crossing threshold via unavailable should fire + hass.states.async_set("test.entity", 9) + await hass.async_block_till_done() + assert len(calls) == 1 + hass.states.async_set("test.entity", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert len(calls) == 1 + hass.states.async_set("test.entity", 11) + await hass.async_block_till_done() + assert len(calls) == 2 + + @pytest.mark.parametrize("above", (10, "input_number.value_10")) async def test_if_fires_on_entity_change_below_to_above(hass, calls, above): """Test the firing with changed entity.""" @@ -1449,6 +1522,48 @@ async def test_if_fires_on_change_with_for_template_3(hass, calls, above, below) assert len(calls) == 1 +async def test_if_not_fires_on_error_with_for_template(hass, caplog, calls): + """Test for not firing on error with for template.""" + hass.states.async_set("test.entity", 0) + await hass.async_block_till_done() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "numeric_state", + "entity_id": "test.entity", + "above": 100, + "for": "00:00:05", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + hass.states.async_set("test.entity", 101) + await hass.async_block_till_done() + assert len(calls) == 0 + + caplog.clear() + caplog.set_level(logging.WARNING) + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3)) + hass.states.async_set("test.entity", "unavailable") + await hass.async_block_till_done() + assert len(calls) == 0 + + assert len(caplog.record_tuples) == 1 + assert caplog.record_tuples[0][1] == logging.WARNING + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3)) + hass.states.async_set("test.entity", 101) + await hass.async_block_till_done() + assert len(calls) == 0 + + @pytest.mark.parametrize( "above, below", ( diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index c39b4597632..d5755ac3288 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -428,6 +428,7 @@ async def test_if_fires_on_state_change_with_for(hass, calls): assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN assert len(calls) == 0 + hass.states.async_set(sensor1.entity_id, 10) hass.states.async_set(sensor1.entity_id, 11) await hass.async_block_till_done() assert len(calls) == 0 @@ -437,5 +438,5 @@ async def test_if_fires_on_state_change_with_for(hass, calls): await hass.async_block_till_done() assert ( calls[0].data["some"] - == f"turn_off device - {sensor1.entity_id} - unknown - 11 - 0:00:05" + == f"turn_off device - {sensor1.entity_id} - 10 - 11 - 0:00:05" ) From 788134cbc408cec45e1ff2014d112a8cdaf1683c Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 20 Feb 2021 03:50:00 -0500 Subject: [PATCH 579/796] Bump zwave-js-server-python to 0.18.0 (#46787) * updates to support changes in zwave-js-server-python * bump lib version * use named arguments for optional args * re-add lost commits --- homeassistant/components/zwave_js/climate.py | 10 +++++- homeassistant/components/zwave_js/entity.py | 21 ++++++----- homeassistant/components/zwave_js/light.py | 36 ++++++++++++++----- .../components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/test_climate.py | 1 - tests/components/zwave_js/test_light.py | 16 ++++++--- 8 files changed, 63 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 341b8f99fd6..f864efe91ff 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -125,10 +125,18 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): ) self._setpoint_values: Dict[ThermostatSetpointType, ZwaveValue] = {} for enum in ThermostatSetpointType: + # Some devices don't include a property key so we need to check for value + # ID's, both with and without the property key self._setpoint_values[enum] = self.get_zwave_value( THERMOSTAT_SETPOINT_PROPERTY, command_class=CommandClass.THERMOSTAT_SETPOINT, - value_property_key_name=enum.value, + value_property_key=enum.value.key, + value_property_key_name=enum.value.name, + add_to_watched_value_ids=True, + ) or self.get_zwave_value( + THERMOSTAT_SETPOINT_PROPERTY, + command_class=CommandClass.THERMOSTAT_SETPOINT, + value_property_key_name=enum.value.name, add_to_watched_value_ids=True, ) # Use the first found setpoint value to always determine the temperature unit diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 911b0b7b2f8..3e81bfaeadf 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -134,6 +134,7 @@ class ZWaveBaseEntity(Entity): value_property: Union[str, int], command_class: Optional[int] = None, endpoint: Optional[int] = None, + value_property_key: Optional[int] = None, value_property_key_name: Optional[str] = None, add_to_watched_value_ids: bool = True, check_all_endpoints: bool = False, @@ -146,16 +147,14 @@ class ZWaveBaseEntity(Entity): if endpoint is None: endpoint = self.info.primary_value.endpoint - # Build partial event data dictionary so we can change the endpoint later - partial_evt_data = { - "commandClass": command_class, - "property": value_property, - "propertyKeyName": value_property_key_name, - } - # lookup value by value_id value_id = get_value_id( - self.info.node, {**partial_evt_data, "endpoint": endpoint} + self.info.node, + command_class, + value_property, + endpoint=endpoint, + property_key=value_property_key, + property_key_name=value_property_key_name, ) return_value = self.info.node.values.get(value_id) @@ -166,7 +165,11 @@ class ZWaveBaseEntity(Entity): if endpoint_.index != self.info.primary_value.endpoint: value_id = get_value_id( self.info.node, - {**partial_evt_data, "endpoint": endpoint_.index}, + command_class, + value_property, + endpoint=endpoint_.index, + property_key=value_property_key, + property_key_name=value_property_key_name, ) return_value = self.info.node.values.get(value_id) if return_value: diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index dd444fdb40d..6ed0286e184 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -3,7 +3,7 @@ import logging from typing import Any, Callable, Optional, Tuple from zwave_js_server.client import Client as ZwaveClient -from zwave_js_server.const import CommandClass +from zwave_js_server.const import ColorComponent, CommandClass from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -200,10 +200,18 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): async def _async_set_color(self, color_name: str, new_value: int) -> None: """Set defined color to given value.""" + try: + property_key = ColorComponent[color_name.upper().replace(" ", "_")].value + except KeyError: + raise ValueError( + "Illegal color name specified, color must be one of " + f"{','.join([color.name for color in ColorComponent])}" + ) from None cur_zwave_value = self.get_zwave_value( "currentColor", CommandClass.SWITCH_COLOR, - value_property_key_name=color_name, + value_property_key=property_key.key, + value_property_key_name=property_key.name, ) # guard for unsupported command if cur_zwave_value is None: @@ -212,7 +220,8 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): target_zwave_value = self.get_zwave_value( "targetColor", CommandClass.SWITCH_COLOR, - value_property_key_name=color_name, + value_property_key=property_key.key, + value_property_key_name=property_key.name, ) if target_zwave_value is None: return @@ -276,13 +285,22 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): # RGB support red_val = self.get_zwave_value( - "currentColor", CommandClass.SWITCH_COLOR, value_property_key_name="Red" + "currentColor", + CommandClass.SWITCH_COLOR, + value_property_key=ColorComponent.RED.value.key, + value_property_key_name=ColorComponent.RED.value.name, ) green_val = self.get_zwave_value( - "currentColor", CommandClass.SWITCH_COLOR, value_property_key_name="Green" + "currentColor", + CommandClass.SWITCH_COLOR, + value_property_key=ColorComponent.GREEN.value.key, + value_property_key_name=ColorComponent.GREEN.value.name, ) blue_val = self.get_zwave_value( - "currentColor", CommandClass.SWITCH_COLOR, value_property_key_name="Blue" + "currentColor", + CommandClass.SWITCH_COLOR, + value_property_key=ColorComponent.BLUE.value.key, + value_property_key_name=ColorComponent.BLUE.value.name, ) if red_val and green_val and blue_val: self._supports_color = True @@ -300,12 +318,14 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): ww_val = self.get_zwave_value( "currentColor", CommandClass.SWITCH_COLOR, - value_property_key_name="Warm White", + value_property_key=ColorComponent.WARM_WHITE.value.key, + value_property_key_name=ColorComponent.WARM_WHITE.value.name, ) cw_val = self.get_zwave_value( "currentColor", CommandClass.SWITCH_COLOR, - value_property_key_name="Cold White", + value_property_key=ColorComponent.COLD_WHITE.value.key, + value_property_key_name=ColorComponent.COLD_WHITE.value.name, ) if ww_val and cw_val: # Color temperature (CW + WW) Support diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 4bd12baa685..56e60fc322c 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.17.2"], + "requirements": ["zwave-js-server-python==0.18.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"] } diff --git a/requirements_all.txt b/requirements_all.txt index 971b8ac9c92..d8d91f2213c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2385,4 +2385,4 @@ zigpy==0.32.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.17.2 +zwave-js-server-python==0.18.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c552f8452c0..65c9a788f31 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1222,4 +1222,4 @@ zigpy-znp==0.3.0 zigpy==0.32.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.17.2 +zwave-js-server-python==0.18.0 diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index 7336acd82eb..17f4dd38144 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -456,7 +456,6 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat "commandClass": 67, "endpoint": 0, "property": "setpoint", - "propertyKey": 1, "propertyKeyName": "Heating", "propertyName": "setpoint", "newValue": 23, diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py index bcd9a2edd9f..40950362ad7 100644 --- a/tests/components/zwave_js/test_light.py +++ b/tests/components/zwave_js/test_light.py @@ -203,17 +203,21 @@ async def test_light(hass, client, bulb_6_multi_color, integration): "property": "currentColor", "newValue": 255, "prevValue": 0, + "propertyKey": 2, "propertyKeyName": "Red", }, }, ) green_event = deepcopy(red_event) - green_event.data["args"].update({"newValue": 76, "propertyKeyName": "Green"}) + green_event.data["args"].update( + {"newValue": 76, "propertyKey": 3, "propertyKeyName": "Green"} + ) blue_event = deepcopy(red_event) + blue_event.data["args"]["propertyKey"] = 4 blue_event.data["args"]["propertyKeyName"] = "Blue" warm_white_event = deepcopy(red_event) warm_white_event.data["args"].update( - {"newValue": 0, "propertyKeyName": "Warm White"} + {"newValue": 0, "propertyKey": 0, "propertyKeyName": "Warm White"} ) node.receive_event(warm_white_event) node.receive_event(red_event) @@ -316,23 +320,25 @@ async def test_light(hass, client, bulb_6_multi_color, integration): "property": "currentColor", "newValue": 0, "prevValue": 255, + "propertyKey": 2, "propertyKeyName": "Red", }, }, ) green_event = deepcopy(red_event) green_event.data["args"].update( - {"newValue": 0, "prevValue": 76, "propertyKeyName": "Green"} + {"newValue": 0, "prevValue": 76, "propertyKey": 3, "propertyKeyName": "Green"} ) blue_event = deepcopy(red_event) + blue_event.data["args"]["propertyKey"] = 4 blue_event.data["args"]["propertyKeyName"] = "Blue" warm_white_event = deepcopy(red_event) warm_white_event.data["args"].update( - {"newValue": 20, "propertyKeyName": "Warm White"} + {"newValue": 20, "propertyKey": 0, "propertyKeyName": "Warm White"} ) cold_white_event = deepcopy(red_event) cold_white_event.data["args"].update( - {"newValue": 235, "propertyKeyName": "Cold White"} + {"newValue": 235, "propertyKey": 1, "propertyKeyName": "Cold White"} ) node.receive_event(red_event) node.receive_event(green_event) From 4aa4f7e28525d79d8b12d79ff74ba6a22bd8d5a8 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 20 Feb 2021 06:49:39 -0800 Subject: [PATCH 580/796] Rollback stream StreamOutput refactoring in PR#46610 (#46684) * Rollback PR#46610 * Update stream tests post-merge --- homeassistant/components/camera/__init__.py | 14 +- homeassistant/components/stream/__init__.py | 153 ++++++++++---------- homeassistant/components/stream/const.py | 8 +- homeassistant/components/stream/core.py | 93 +++++++++++- homeassistant/components/stream/hls.py | 92 ++++-------- homeassistant/components/stream/recorder.py | 43 ++++-- homeassistant/components/stream/worker.py | 11 +- tests/components/stream/test_hls.py | 36 ++--- tests/components/stream/test_recorder.py | 76 +++++----- tests/components/stream/test_worker.py | 9 +- 10 files changed, 295 insertions(+), 240 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 4c5d89030b4..99b5cebc2a3 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -24,11 +24,7 @@ from homeassistant.components.media_player.const import ( SERVICE_PLAY_MEDIA, ) from homeassistant.components.stream import Stream, create_stream -from homeassistant.components.stream.const import ( - FORMAT_CONTENT_TYPE, - HLS_OUTPUT, - OUTPUT_FORMATS, -) +from homeassistant.components.stream.const import FORMAT_CONTENT_TYPE, OUTPUT_FORMATS from homeassistant.const import ( ATTR_ENTITY_ID, CONF_FILENAME, @@ -259,7 +255,7 @@ async def async_setup(hass, config): if not stream: continue stream.keepalive = True - stream.hls_output() + stream.add_provider("hls") stream.start() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, preload_stream) @@ -707,8 +703,6 @@ async def async_handle_play_stream_service(camera, service_call): async def _async_stream_endpoint_url(hass, camera, fmt): - if fmt != HLS_OUTPUT: - raise ValueError("Only format {HLS_OUTPUT} is supported") stream = await camera.create_stream() if not stream: raise HomeAssistantError( @@ -719,9 +713,9 @@ async def _async_stream_endpoint_url(hass, camera, fmt): camera_prefs = hass.data[DATA_CAMERA_PREFS].get(camera.entity_id) stream.keepalive = camera_prefs.preload_stream - stream.hls_output() + stream.add_provider(fmt) stream.start() - return stream.endpoint_url() + return stream.endpoint_url(fmt) async def async_handle_record_service(camera, call): diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 6c3f0104ad0..0027152dbd6 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -7,25 +7,25 @@ a new Stream object. Stream manages: - Home Assistant URLs for viewing a stream - Access tokens for URLs for viewing a stream -A Stream consists of a background worker and multiple output streams (e.g. hls -and recorder). The worker has a callback to retrieve the current active output -streams where it writes the decoded output packets. The HLS stream has an -inactivity idle timeout that expires the access token. When all output streams -are inactive, the background worker is shut down. Alternatively, a Stream -can be configured with keepalive to always keep workers active. +A Stream consists of a background worker, and one or more output formats each +with their own idle timeout managed by the stream component. When an output +format is no longer in use, the stream component will expire it. When there +are no active output formats, the background worker is shut down and access +tokens are expired. Alternatively, a Stream can be configured with keepalive +to always keep workers active. """ import logging import secrets import threading import time -from typing import List +from types import MappingProxyType from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from .const import ( - ATTR_HLS_ENDPOINT, + ATTR_ENDPOINTS, ATTR_STREAMS, DOMAIN, MAX_SEGMENTS, @@ -33,8 +33,8 @@ from .const import ( STREAM_RESTART_INCREMENT, STREAM_RESTART_RESET_TIME, ) -from .core import IdleTimer, StreamOutput -from .hls import HlsStreamOutput, async_setup_hls +from .core import PROVIDERS, IdleTimer +from .hls import async_setup_hls _LOGGER = logging.getLogger(__name__) @@ -75,10 +75,12 @@ async def async_setup(hass, config): from .recorder import async_setup_recorder hass.data[DOMAIN] = {} + hass.data[DOMAIN][ATTR_ENDPOINTS] = {} hass.data[DOMAIN][ATTR_STREAMS] = [] # Setup HLS - hass.data[DOMAIN][ATTR_HLS_ENDPOINT] = async_setup_hls(hass) + hls_endpoint = async_setup_hls(hass) + hass.data[DOMAIN][ATTR_ENDPOINTS]["hls"] = hls_endpoint # Setup Recorder async_setup_recorder(hass) @@ -87,6 +89,7 @@ async def async_setup(hass, config): def shutdown(event): """Stop all stream workers.""" for stream in hass.data[DOMAIN][ATTR_STREAMS]: + stream.keepalive = False stream.stop() _LOGGER.info("Stopped stream workers") @@ -107,54 +110,58 @@ class Stream: self.access_token = None self._thread = None self._thread_quit = threading.Event() - self._hls = None - self._hls_timer = None - self._recorder = None + self._outputs = {} self._fast_restart_once = False if self.options is None: self.options = {} - def endpoint_url(self) -> str: - """Start the stream and returns a url for the hls endpoint.""" - if not self._hls: - raise ValueError("Stream is not configured for hls") + def endpoint_url(self, fmt): + """Start the stream and returns a url for the output format.""" + if fmt not in self._outputs: + raise ValueError(f"Stream is not configured for format '{fmt}'") if not self.access_token: self.access_token = secrets.token_hex() - return self.hass.data[DOMAIN][ATTR_HLS_ENDPOINT].format(self.access_token) + return self.hass.data[DOMAIN][ATTR_ENDPOINTS][fmt].format(self.access_token) - def outputs(self) -> List[StreamOutput]: - """Return the active stream outputs.""" - return [output for output in [self._hls, self._recorder] if output] + def outputs(self): + """Return a copy of the stream outputs.""" + # A copy is returned so the caller can iterate through the outputs + # without concern about self._outputs being modified from another thread. + return MappingProxyType(self._outputs.copy()) - def hls_output(self) -> StreamOutput: - """Return the hls output stream, creating if not already active.""" - if not self._hls: - self._hls = HlsStreamOutput(self.hass) - self._hls_timer = IdleTimer(self.hass, OUTPUT_IDLE_TIMEOUT, self._hls_idle) - self._hls_timer.start() - self._hls_timer.awake() - return self._hls + def add_provider(self, fmt, timeout=OUTPUT_IDLE_TIMEOUT): + """Add provider output stream.""" + if not self._outputs.get(fmt): - @callback - def _hls_idle(self): - """Reset access token and cleanup stream due to inactivity.""" - self.access_token = None - if not self.keepalive: - if self._hls: - self._hls.cleanup() - self._hls = None - self._hls_timer = None - self._check_idle() + @callback + def idle_callback(): + if not self.keepalive and fmt in self._outputs: + self.remove_provider(self._outputs[fmt]) + self.check_idle() - def _check_idle(self): - """Check if all outputs are idle and shut down worker.""" - if self.keepalive or self.outputs(): - return - self.stop() + provider = PROVIDERS[fmt]( + self.hass, IdleTimer(self.hass, timeout, idle_callback) + ) + self._outputs[fmt] = provider + return self._outputs[fmt] + + def remove_provider(self, provider): + """Remove provider output stream.""" + if provider.name in self._outputs: + self._outputs[provider.name].cleanup() + del self._outputs[provider.name] + + if not self._outputs: + self.stop() + + def check_idle(self): + """Reset access token if all providers are idle.""" + if all([p.idle for p in self._outputs.values()]): + self.access_token = None def start(self): - """Start stream decode worker.""" + """Start a stream.""" if self._thread is None or not self._thread.is_alive(): if self._thread is not None: # The thread must have crashed/exited. Join to clean up the @@ -210,21 +217,21 @@ class Stream: def _worker_finished(self): """Schedule cleanup of all outputs.""" - self.hass.loop.call_soon_threadsafe(self.stop) + + @callback + def remove_outputs(): + for provider in self.outputs().values(): + self.remove_provider(provider) + + self.hass.loop.call_soon_threadsafe(remove_outputs) def stop(self): """Remove outputs and access token.""" + self._outputs = {} self.access_token = None - if self._hls_timer: - self._hls_timer.clear() - self._hls_timer = None - if self._hls: - self._hls.cleanup() - self._hls = None - if self._recorder: - self._recorder.save() - self._recorder = None - self._stop() + + if not self.keepalive: + self._stop() def _stop(self): """Stop worker thread.""" @@ -237,35 +244,25 @@ class Stream: async def async_record(self, video_path, duration=30, lookback=5): """Make a .mp4 recording from a provided stream.""" - # Keep import here so that we can import stream integration without installing reqs - # pylint: disable=import-outside-toplevel - from .recorder import RecorderOutput - # Check for file access if not self.hass.config.is_allowed_path(video_path): raise HomeAssistantError(f"Can't write {video_path}, no access to path!") # Add recorder - if self._recorder: + recorder = self.outputs().get("recorder") + if recorder: raise HomeAssistantError( - f"Stream already recording to {self._recorder.video_path}!" + f"Stream already recording to {recorder.video_path}!" ) - self._recorder = RecorderOutput(self.hass) - self._recorder.video_path = video_path + recorder = self.add_provider("recorder", timeout=duration) + recorder.video_path = video_path + self.start() # Take advantage of lookback - if lookback > 0 and self._hls: - num_segments = min(int(lookback // self._hls.target_duration), MAX_SEGMENTS) + hls = self.outputs().get("hls") + if lookback > 0 and hls: + num_segments = min(int(lookback // hls.target_duration), MAX_SEGMENTS) # Wait for latest segment, then add the lookback - await self._hls.recv() - self._recorder.prepend(list(self._hls.get_segment())[-num_segments:]) - - @callback - def save_recording(): - if self._recorder: - self._recorder.save() - self._recorder = None - self._check_idle() - - IdleTimer(self.hass, duration, save_recording).start() + await hls.recv() + recorder.prepend(list(hls.get_segment())[-num_segments:]) diff --git a/homeassistant/components/stream/const.py b/homeassistant/components/stream/const.py index 55f447a9a69..41df806d020 100644 --- a/homeassistant/components/stream/const.py +++ b/homeassistant/components/stream/const.py @@ -1,14 +1,10 @@ """Constants for Stream component.""" DOMAIN = "stream" -ATTR_HLS_ENDPOINT = "hls_endpoint" +ATTR_ENDPOINTS = "endpoints" ATTR_STREAMS = "streams" -HLS_OUTPUT = "hls" -OUTPUT_FORMATS = [HLS_OUTPUT] -OUTPUT_CONTAINER_FORMAT = "mp4" -OUTPUT_VIDEO_CODECS = {"hevc", "h264"} -OUTPUT_AUDIO_CODECS = {"aac", "mp3"} +OUTPUT_FORMATS = ["hls"] FORMAT_CONTENT_TYPE = {"hls": "application/vnd.apple.mpegurl"} diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 7a46de547d7..eba6a069698 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -1,7 +1,8 @@ """Provides core stream functionality.""" -import abc +import asyncio +from collections import deque import io -from typing import Callable +from typing import Any, Callable, List from aiohttp import web import attr @@ -9,8 +10,11 @@ import attr from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.event import async_call_later +from homeassistant.util.decorator import Registry -from .const import ATTR_STREAMS, DOMAIN +from .const import ATTR_STREAMS, DOMAIN, MAX_SEGMENTS + +PROVIDERS = Registry() @attr.s @@ -76,18 +80,86 @@ class IdleTimer: self._callback() -class StreamOutput(abc.ABC): +class StreamOutput: """Represents a stream output.""" - def __init__(self, hass: HomeAssistant): + def __init__(self, hass: HomeAssistant, idle_timer: IdleTimer) -> None: """Initialize a stream output.""" self._hass = hass + self._idle_timer = idle_timer + self._cursor = None + self._event = asyncio.Event() + self._segments = deque(maxlen=MAX_SEGMENTS) + + @property + def name(self) -> str: + """Return provider name.""" + return None + + @property + def idle(self) -> bool: + """Return True if the output is idle.""" + return self._idle_timer.idle + + @property + def format(self) -> str: + """Return container format.""" + return None + + @property + def audio_codecs(self) -> str: + """Return desired audio codecs.""" + return None + + @property + def video_codecs(self) -> tuple: + """Return desired video codecs.""" + return None @property def container_options(self) -> Callable[[int], dict]: """Return Callable which takes a sequence number and returns container options.""" return None + @property + def segments(self) -> List[int]: + """Return current sequence from segments.""" + return [s.sequence for s in self._segments] + + @property + def target_duration(self) -> int: + """Return the max duration of any given segment in seconds.""" + segment_length = len(self._segments) + if not segment_length: + return 1 + durations = [s.duration for s in self._segments] + return round(max(durations)) or 1 + + def get_segment(self, sequence: int = None) -> Any: + """Retrieve a specific segment, or the whole list.""" + self._idle_timer.awake() + + if not sequence: + return self._segments + + for segment in self._segments: + if segment.sequence == sequence: + return segment + return None + + async def recv(self) -> Segment: + """Wait for and retrieve the latest segment.""" + last_segment = max(self.segments, default=0) + if self._cursor is None or self._cursor <= last_segment: + await self._event.wait() + + if not self._segments: + return None + + segment = self.get_segment()[-1] + self._cursor = segment.sequence + return segment + def put(self, segment: Segment) -> None: """Store output.""" self._hass.loop.call_soon_threadsafe(self._async_put, segment) @@ -95,6 +167,17 @@ class StreamOutput(abc.ABC): @callback def _async_put(self, segment: Segment) -> None: """Store output from event loop.""" + # Start idle timeout when we start receiving data + self._idle_timer.start() + self._segments.append(segment) + self._event.set() + self._event.clear() + + def cleanup(self): + """Handle cleanup.""" + self._event.set() + self._idle_timer.clear() + self._segments = deque(maxlen=MAX_SEGMENTS) class StreamView(HomeAssistantView): diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index 85102d208e7..28d6a300ae7 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -1,15 +1,13 @@ """Provide functionality to stream HLS.""" -import asyncio -from collections import deque import io -from typing import Any, Callable, List +from typing import Callable from aiohttp import web from homeassistant.core import callback -from .const import FORMAT_CONTENT_TYPE, MAX_SEGMENTS, NUM_PLAYLIST_SEGMENTS -from .core import Segment, StreamOutput, StreamView +from .const import FORMAT_CONTENT_TYPE, NUM_PLAYLIST_SEGMENTS +from .core import PROVIDERS, StreamOutput, StreamView from .fmp4utils import get_codec_string, get_init, get_m4s @@ -50,7 +48,8 @@ class HlsMasterPlaylistView(StreamView): async def handle(self, request, stream, sequence): """Return m3u8 playlist.""" - track = stream.hls_output() + track = stream.add_provider("hls") + stream.start() # Wait for a segment to be ready if not track.segments: if not await track.recv(): @@ -109,7 +108,8 @@ class HlsPlaylistView(StreamView): async def handle(self, request, stream, sequence): """Return m3u8 playlist.""" - track = stream.hls_output() + track = stream.add_provider("hls") + stream.start() # Wait for a segment to be ready if not track.segments: if not await track.recv(): @@ -127,7 +127,7 @@ class HlsInitView(StreamView): async def handle(self, request, stream, sequence): """Return init.mp4.""" - track = stream.hls_output() + track = stream.add_provider("hls") segments = track.get_segment() if not segments: return web.HTTPNotFound() @@ -144,7 +144,7 @@ class HlsSegmentView(StreamView): async def handle(self, request, stream, sequence): """Return fmp4 segment.""" - track = stream.hls_output() + track = stream.add_provider("hls") segment = track.get_segment(int(sequence)) if not segment: return web.HTTPNotFound() @@ -155,15 +155,29 @@ class HlsSegmentView(StreamView): ) +@PROVIDERS.register("hls") class HlsStreamOutput(StreamOutput): """Represents HLS Output formats.""" - def __init__(self, hass) -> None: - """Initialize HlsStreamOutput.""" - super().__init__(hass) - self._cursor = None - self._event = asyncio.Event() - self._segments = deque(maxlen=MAX_SEGMENTS) + @property + def name(self) -> str: + """Return provider name.""" + return "hls" + + @property + def format(self) -> str: + """Return container format.""" + return "mp4" + + @property + def audio_codecs(self) -> str: + """Return desired audio codecs.""" + return {"aac", "mp3"} + + @property + def video_codecs(self) -> tuple: + """Return desired video codecs.""" + return {"hevc", "h264"} @property def container_options(self) -> Callable[[int], dict]: @@ -174,51 +188,3 @@ class HlsStreamOutput(StreamOutput): "avoid_negative_ts": "make_non_negative", "fragment_index": str(sequence), } - - @property - def segments(self) -> List[int]: - """Return current sequence from segments.""" - return [s.sequence for s in self._segments] - - @property - def target_duration(self) -> int: - """Return the max duration of any given segment in seconds.""" - segment_length = len(self._segments) - if not segment_length: - return 1 - durations = [s.duration for s in self._segments] - return round(max(durations)) or 1 - - def get_segment(self, sequence: int = None) -> Any: - """Retrieve a specific segment, or the whole list.""" - if not sequence: - return self._segments - - for segment in self._segments: - if segment.sequence == sequence: - return segment - return None - - async def recv(self) -> Segment: - """Wait for and retrieve the latest segment.""" - last_segment = max(self.segments, default=0) - if self._cursor is None or self._cursor <= last_segment: - await self._event.wait() - - if not self._segments: - return None - - segment = self.get_segment()[-1] - self._cursor = segment.sequence - return segment - - def _async_put(self, segment: Segment) -> None: - """Store output from event loop.""" - self._segments.append(segment) - self._event.set() - self._event.clear() - - def cleanup(self): - """Handle cleanup.""" - self._event.set() - self._segments = deque(maxlen=MAX_SEGMENTS) diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index 96531233771..0b77d0ba630 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -6,10 +6,9 @@ from typing import List import av -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback -from .const import OUTPUT_CONTAINER_FORMAT -from .core import Segment, StreamOutput +from .core import PROVIDERS, IdleTimer, Segment, StreamOutput _LOGGER = logging.getLogger(__name__) @@ -19,7 +18,7 @@ def async_setup_recorder(hass): """Only here so Provider Registry works.""" -def recorder_save_worker(file_out: str, segments: List[Segment], container_format): +def recorder_save_worker(file_out: str, segments: List[Segment], container_format: str): """Handle saving stream.""" if not os.path.exists(os.path.dirname(file_out)): os.makedirs(os.path.dirname(file_out), exist_ok=True) @@ -76,31 +75,51 @@ def recorder_save_worker(file_out: str, segments: List[Segment], container_forma output.close() +@PROVIDERS.register("recorder") class RecorderOutput(StreamOutput): """Represents HLS Output formats.""" - def __init__(self, hass) -> None: + def __init__(self, hass: HomeAssistant, idle_timer: IdleTimer) -> None: """Initialize recorder output.""" - super().__init__(hass) + super().__init__(hass, idle_timer) self.video_path = None self._segments = [] - def _async_put(self, segment: Segment) -> None: - """Store output.""" - self._segments.append(segment) + @property + def name(self) -> str: + """Return provider name.""" + return "recorder" + + @property + def format(self) -> str: + """Return container format.""" + return "mp4" + + @property + def audio_codecs(self) -> str: + """Return desired audio codec.""" + return {"aac", "mp3"} + + @property + def video_codecs(self) -> tuple: + """Return desired video codecs.""" + return {"hevc", "h264"} def prepend(self, segments: List[Segment]) -> None: """Prepend segments to existing list.""" - segments = [s for s in segments if s.sequence not in self._segments] + own_segments = self.segments + segments = [s for s in segments if s.sequence not in own_segments] self._segments = segments + self._segments - def save(self): + def cleanup(self): """Write recording and clean up.""" _LOGGER.debug("Starting recorder worker thread") thread = threading.Thread( name="recorder_save_worker", target=recorder_save_worker, - args=(self.video_path, self._segments, OUTPUT_CONTAINER_FORMAT), + args=(self.video_path, self._segments, self.format), ) thread.start() + + super().cleanup() self._segments = [] diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 2592a74584e..61d4f5db17a 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -9,9 +9,6 @@ from .const import ( MAX_MISSING_DTS, MAX_TIMESTAMP_GAP, MIN_SEGMENT_DURATION, - OUTPUT_AUDIO_CODECS, - OUTPUT_CONTAINER_FORMAT, - OUTPUT_VIDEO_CODECS, PACKETS_TO_WAIT_FOR_AUDIO, STREAM_TIMEOUT, ) @@ -32,7 +29,7 @@ def create_stream_buffer(stream_output, video_stream, audio_stream, sequence): output = av.open( segment, mode="w", - format=OUTPUT_CONTAINER_FORMAT, + format=stream_output.format, container_options={ "video_track_timescale": str(int(1 / video_stream.time_base)), **container_options, @@ -41,7 +38,7 @@ def create_stream_buffer(stream_output, video_stream, audio_stream, sequence): vstream = output.add_stream(template=video_stream) # Check if audio is requested astream = None - if audio_stream and audio_stream.name in OUTPUT_AUDIO_CODECS: + if audio_stream and audio_stream.name in stream_output.audio_codecs: astream = output.add_stream(template=audio_stream) return StreamBuffer(segment, output, vstream, astream) @@ -74,8 +71,8 @@ class SegmentBuffer: # Fetch the latest StreamOutputs, which may have changed since the # worker started. self._outputs = [] - for stream_output in self._outputs_callback(): - if self._video_stream.name not in OUTPUT_VIDEO_CODECS: + for stream_output in self._outputs_callback().values(): + if self._video_stream.name not in stream_output.video_codecs: continue buffer = create_stream_buffer( stream_output, self._video_stream, self._audio_stream, self._sequence diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index ffe32d13c61..c11576d2570 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -45,7 +45,7 @@ def hls_stream(hass, hass_client): async def create_client_for_stream(stream): http_client = await hass_client() - parsed_url = urlparse(stream.endpoint_url()) + parsed_url = urlparse(stream.endpoint_url("hls")) return HlsClient(http_client, parsed_url) return create_client_for_stream @@ -91,7 +91,7 @@ async def test_hls_stream(hass, hls_stream, stream_worker_sync): stream = create_stream(hass, source) # Request stream - stream.hls_output() + stream.add_provider("hls") stream.start() hls_client = await hls_stream(stream) @@ -132,9 +132,9 @@ async def test_stream_timeout(hass, hass_client, stream_worker_sync): stream = create_stream(hass, source) # Request stream - stream.hls_output() + stream.add_provider("hls") stream.start() - url = stream.endpoint_url() + url = stream.endpoint_url("hls") http_client = await hass_client() @@ -174,16 +174,8 @@ async def test_stream_timeout_after_stop(hass, hass_client, stream_worker_sync): stream = create_stream(hass, source) # Request stream - stream.hls_output() + stream.add_provider("hls") stream.start() - url = stream.endpoint_url() - - http_client = await hass_client() - - # Fetch playlist - parsed_url = urlparse(url) - playlist_response = await http_client.get(parsed_url.path) - assert playlist_response.status == 200 stream_worker_sync.resume() stream.stop() @@ -204,10 +196,12 @@ async def test_stream_ended(hass, stream_worker_sync): # Setup demo HLS track source = generate_h264_video() stream = create_stream(hass, source) + track = stream.add_provider("hls") # Request stream - track = stream.hls_output() + stream.add_provider("hls") stream.start() + stream.endpoint_url("hls") # Run it dead while True: @@ -233,7 +227,7 @@ async def test_stream_keepalive(hass): # Setup demo HLS track source = "test_stream_keepalive_source" stream = create_stream(hass, source) - track = stream.hls_output() + track = stream.add_provider("hls") track.num_segments = 2 stream.start() @@ -264,12 +258,12 @@ async def test_stream_keepalive(hass): stream.stop() -async def test_hls_playlist_view_no_output(hass, hls_stream): +async def test_hls_playlist_view_no_output(hass, hass_client, hls_stream): """Test rendering the hls playlist with no output segments.""" await async_setup_component(hass, "stream", {"stream": {}}) stream = create_stream(hass, STREAM_SOURCE) - stream.hls_output() + stream.add_provider("hls") hls_client = await hls_stream(stream) @@ -284,7 +278,7 @@ async def test_hls_playlist_view(hass, hls_stream, stream_worker_sync): stream = create_stream(hass, STREAM_SOURCE) stream_worker_sync.pause() - hls = stream.hls_output() + hls = stream.add_provider("hls") hls.put(Segment(1, SEQUENCE_BYTES, DURATION)) await hass.async_block_till_done() @@ -313,7 +307,7 @@ async def test_hls_max_segments(hass, hls_stream, stream_worker_sync): stream = create_stream(hass, STREAM_SOURCE) stream_worker_sync.pause() - hls = stream.hls_output() + hls = stream.add_provider("hls") hls_client = await hls_stream(stream) @@ -358,7 +352,7 @@ async def test_hls_playlist_view_discontinuity(hass, hls_stream, stream_worker_s stream = create_stream(hass, STREAM_SOURCE) stream_worker_sync.pause() - hls = stream.hls_output() + hls = stream.add_provider("hls") hls.put(Segment(1, SEQUENCE_BYTES, DURATION, stream_id=0)) hls.put(Segment(2, SEQUENCE_BYTES, DURATION, stream_id=0)) @@ -388,7 +382,7 @@ async def test_hls_max_segments_discontinuity(hass, hls_stream, stream_worker_sy stream = create_stream(hass, STREAM_SOURCE) stream_worker_sync.pause() - hls = stream.hls_output() + hls = stream.add_provider("hls") hls_client = await hls_stream(stream) diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index e8ff540ba41..9d418c360b1 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -1,12 +1,10 @@ """The tests for hls streams.""" -import asyncio from datetime import timedelta import logging import os import threading from unittest.mock import patch -import async_timeout import av import pytest @@ -34,30 +32,23 @@ class SaveRecordWorkerSync: def __init__(self): """Initialize SaveRecordWorkerSync.""" self.reset() - self._segments = None - def recorder_save_worker(self, file_out, segments, container_format): + def recorder_save_worker(self, *args, **kwargs): """Mock method for patch.""" logging.debug("recorder_save_worker thread started") - self._segments = segments assert self._save_thread is None self._save_thread = threading.current_thread() self._save_event.set() - async def get_segments(self): - """Verify save worker thread was invoked and return saved segments.""" - with async_timeout.timeout(TEST_TIMEOUT): - assert await self._save_event.wait() - return self._segments - def join(self): - """Block until the record worker thread exist to ensure cleanup.""" + """Verify save worker was invoked and block on shutdown.""" + assert self._save_event.wait(timeout=TEST_TIMEOUT) self._save_thread.join() def reset(self): """Reset callback state for reuse in tests.""" self._save_thread = None - self._save_event = asyncio.Event() + self._save_event = threading.Event() @pytest.fixture() @@ -72,7 +63,7 @@ def record_worker_sync(hass): yield sync -async def test_record_stream(hass, hass_client, record_worker_sync): +async def test_record_stream(hass, hass_client, stream_worker_sync, record_worker_sync): """ Test record stream. @@ -82,14 +73,28 @@ async def test_record_stream(hass, hass_client, record_worker_sync): """ await async_setup_component(hass, "stream", {"stream": {}}) + stream_worker_sync.pause() + # Setup demo track source = generate_h264_video() stream = create_stream(hass, source) with patch.object(hass.config, "is_allowed_path", return_value=True): await stream.async_record("/example/path") - segments = await record_worker_sync.get_segments() - assert len(segments) > 1 + recorder = stream.add_provider("recorder") + while True: + segment = await recorder.recv() + if not segment: + break + segments = segment.sequence + if segments > 1: + stream_worker_sync.resume() + + stream.stop() + assert segments > 1 + + # Verify that the save worker was invoked, then block until its + # thread completes and is shutdown completely to avoid thread leaks. record_worker_sync.join() @@ -102,24 +107,19 @@ async def test_record_lookback( source = generate_h264_video() stream = create_stream(hass, source) - # Don't let the stream finish (and clean itself up) until the test has had - # a chance to perform lookback - stream_worker_sync.pause() - # Start an HLS feed to enable lookback - stream.hls_output() + stream.add_provider("hls") + stream.start() with patch.object(hass.config, "is_allowed_path", return_value=True): await stream.async_record("/example/path", lookback=4) # This test does not need recorder cleanup since it is not fully exercised - stream_worker_sync.resume() + stream.stop() -async def test_recorder_timeout( - hass, hass_client, stream_worker_sync, record_worker_sync -): +async def test_recorder_timeout(hass, hass_client, stream_worker_sync): """ Test recorder timeout. @@ -137,8 +137,9 @@ async def test_recorder_timeout( stream = create_stream(hass, source) with patch.object(hass.config, "is_allowed_path", return_value=True): await stream.async_record("/example/path") + recorder = stream.add_provider("recorder") - assert not mock_timeout.called + await recorder.recv() # Wait a minute future = dt_util.utcnow() + timedelta(minutes=1) @@ -148,10 +149,6 @@ async def test_recorder_timeout( assert mock_timeout.called stream_worker_sync.resume() - # Verify worker is invoked, and do clean shutdown of worker thread - await record_worker_sync.get_segments() - record_worker_sync.join() - stream.stop() await hass.async_block_till_done() await hass.async_block_till_done() @@ -183,7 +180,9 @@ async def test_recorder_save(tmpdir): assert os.path.exists(filename) -async def test_record_stream_audio(hass, hass_client, record_worker_sync): +async def test_record_stream_audio( + hass, hass_client, stream_worker_sync, record_worker_sync +): """ Test treatment of different audio inputs. @@ -199,6 +198,7 @@ async def test_record_stream_audio(hass, hass_client, record_worker_sync): (None, 0), # no audio stream ): record_worker_sync.reset() + stream_worker_sync.pause() # Setup demo track source = generate_h264_video( @@ -207,14 +207,22 @@ async def test_record_stream_audio(hass, hass_client, record_worker_sync): stream = create_stream(hass, source) with patch.object(hass.config, "is_allowed_path", return_value=True): await stream.async_record("/example/path") + recorder = stream.add_provider("recorder") - segments = await record_worker_sync.get_segments() - last_segment = segments[-1] + while True: + segment = await recorder.recv() + if not segment: + break + last_segment = segment + stream_worker_sync.resume() result = av.open(last_segment.segment, "r", format="mp4") assert len(result.streams.audio) == expected_audio_streams result.close() - stream.stop() + await hass.async_block_till_done() + + # Verify that the save worker was invoked, then block until its + # thread completes and is shutdown completely to avoid thread leaks. record_worker_sync.join() diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index f7952b7db44..2c202a290ce 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -31,6 +31,7 @@ from homeassistant.components.stream.worker import SegmentBuffer, stream_worker STREAM_SOURCE = "some-stream-source" # Formats here are arbitrary, not exercised by tests +STREAM_OUTPUT_FORMAT = "hls" AUDIO_STREAM_FORMAT = "mp3" VIDEO_STREAM_FORMAT = "h264" VIDEO_FRAME_RATE = 12 @@ -187,7 +188,7 @@ class MockPyAv: async def async_decode_stream(hass, packets, py_av=None): """Start a stream worker that decodes incoming stream packets into output segments.""" stream = Stream(hass, STREAM_SOURCE) - stream.hls_output() + stream.add_provider(STREAM_OUTPUT_FORMAT) if not py_av: py_av = MockPyAv() @@ -207,7 +208,7 @@ async def async_decode_stream(hass, packets, py_av=None): async def test_stream_open_fails(hass): """Test failure on stream open.""" stream = Stream(hass, STREAM_SOURCE) - stream.hls_output() + stream.add_provider(STREAM_OUTPUT_FORMAT) with patch("av.open") as av_open: av_open.side_effect = av.error.InvalidDataError(-2, "error") segment_buffer = SegmentBuffer(stream.outputs) @@ -484,7 +485,7 @@ async def test_stream_stopped_while_decoding(hass): worker_wake = threading.Event() stream = Stream(hass, STREAM_SOURCE) - stream.hls_output() + stream.add_provider(STREAM_OUTPUT_FORMAT) py_av = MockPyAv() py_av.container.packets = PacketSequence(TEST_SEQUENCE_LENGTH) @@ -511,7 +512,7 @@ async def test_update_stream_source(hass): worker_wake = threading.Event() stream = Stream(hass, STREAM_SOURCE) - stream.hls_output() + stream.add_provider(STREAM_OUTPUT_FORMAT) # Note that keepalive is not set here. The stream is "restarted" even though # it is not stopping due to failure. From b5c7493b60efce7f1bd275a474c7ffa1fc4dbd47 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 20 Feb 2021 17:26:21 +0100 Subject: [PATCH 581/796] Upgrade TwitterAPI to 2.6.6 (#46812) --- homeassistant/components/twitter/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/twitter/manifest.json b/homeassistant/components/twitter/manifest.json index cd3a12255a2..873c9e12ab1 100644 --- a/homeassistant/components/twitter/manifest.json +++ b/homeassistant/components/twitter/manifest.json @@ -2,6 +2,6 @@ "domain": "twitter", "name": "Twitter", "documentation": "https://www.home-assistant.io/integrations/twitter", - "requirements": ["TwitterAPI==2.6.5"], + "requirements": ["TwitterAPI==2.6.6"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index d8d91f2213c..6dbcf46ea51 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -81,7 +81,7 @@ RtmAPI==0.7.2 TravisPy==0.3.5 # homeassistant.components.twitter -TwitterAPI==2.6.5 +TwitterAPI==2.6.6 # homeassistant.components.tof # VL53L1X2==0.1.5 From 65e8835f2825a1583c92d8d750870def2264941d Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sat, 20 Feb 2021 10:59:22 -0600 Subject: [PATCH 582/796] Update rokuecp to 0.8.0 (#46799) * update rokuecp to 0.8.0 * Update requirements_test_all.txt * Update requirements_all.txt --- homeassistant/components/roku/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index f1509edb6fb..10cef13cda0 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -2,7 +2,7 @@ "domain": "roku", "name": "Roku", "documentation": "https://www.home-assistant.io/integrations/roku", - "requirements": ["rokuecp==0.6.0"], + "requirements": ["rokuecp==0.8.0"], "homekit": { "models": [ "3810X", diff --git a/requirements_all.txt b/requirements_all.txt index 6dbcf46ea51..3440eb9557e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1962,7 +1962,7 @@ rjpl==0.3.6 rocketchat-API==0.6.1 # homeassistant.components.roku -rokuecp==0.6.0 +rokuecp==0.8.0 # homeassistant.components.roomba roombapy==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 65c9a788f31..f843874b5bc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1005,7 +1005,7 @@ rflink==0.0.58 ring_doorbell==0.6.2 # homeassistant.components.roku -rokuecp==0.6.0 +rokuecp==0.8.0 # homeassistant.components.roomba roombapy==1.6.2 From adf480025d0ee380e5a514d90dfc16fec7467920 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Feb 2021 08:03:40 -1000 Subject: [PATCH 583/796] Add support for bond up and down lights (#46233) --- homeassistant/components/bond/entity.py | 3 + homeassistant/components/bond/light.py | 94 ++++++++++-- homeassistant/components/bond/utils.py | 36 +++-- tests/components/bond/test_light.py | 186 +++++++++++++++++++++++- 4 files changed, 294 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index f6165eb7890..5b2e27b94cc 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -52,6 +52,9 @@ class BondEntity(Entity): @property def name(self) -> Optional[str]: """Get entity name.""" + if self._sub_device: + sub_device_name = self._sub_device.replace("_", " ").title() + return f"{self._device.name} {sub_device_name}" return self._device.name @property diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py index 8d0dfe85246..194a009a857 100644 --- a/homeassistant/components/bond/light.py +++ b/homeassistant/components/bond/light.py @@ -34,7 +34,21 @@ async def async_setup_entry( fan_lights: List[Entity] = [ BondLight(hub, device, bpup_subs) for device in hub.devices - if DeviceType.is_fan(device.type) and device.supports_light() + if DeviceType.is_fan(device.type) + and device.supports_light() + and not (device.supports_up_light() and device.supports_down_light()) + ] + + fan_up_lights: List[Entity] = [ + BondUpLight(hub, device, bpup_subs, "up_light") + for device in hub.devices + if DeviceType.is_fan(device.type) and device.supports_up_light() + ] + + fan_down_lights: List[Entity] = [ + BondDownLight(hub, device, bpup_subs, "down_light") + for device in hub.devices + if DeviceType.is_fan(device.type) and device.supports_down_light() ] fireplaces: List[Entity] = [ @@ -55,10 +69,13 @@ async def async_setup_entry( if DeviceType.is_light(device.type) ] - async_add_entities(fan_lights + fireplaces + fp_lights + lights, True) + async_add_entities( + fan_lights + fan_up_lights + fan_down_lights + fireplaces + fp_lights + lights, + True, + ) -class BondLight(BondEntity, LightEntity): +class BondBaseLight(BondEntity, LightEntity): """Representation of a Bond light.""" def __init__( @@ -68,10 +85,34 @@ class BondLight(BondEntity, LightEntity): bpup_subs: BPUPSubscriptions, sub_device: Optional[str] = None, ): - """Create HA entity representing Bond fan.""" + """Create HA entity representing Bond light.""" + super().__init__(hub, device, bpup_subs, sub_device) + self._light: Optional[int] = None + + @property + def is_on(self) -> bool: + """Return if light is currently on.""" + return self._light == 1 + + @property + def supported_features(self) -> Optional[int]: + """Flag supported features.""" + return 0 + + +class BondLight(BondBaseLight, BondEntity, LightEntity): + """Representation of a Bond light.""" + + def __init__( + self, + hub: BondHub, + device: BondDevice, + bpup_subs: BPUPSubscriptions, + sub_device: Optional[str] = None, + ): + """Create HA entity representing Bond light.""" super().__init__(hub, device, bpup_subs, sub_device) self._brightness: Optional[int] = None - self._light: Optional[int] = None def _apply_state(self, state: dict): self._light = state.get("light") @@ -84,11 +125,6 @@ class BondLight(BondEntity, LightEntity): return SUPPORT_BRIGHTNESS return 0 - @property - def is_on(self) -> bool: - """Return if light is currently on.""" - return self._light == 1 - @property def brightness(self) -> int: """Return the brightness of this light between 1..255.""" @@ -113,6 +149,44 @@ class BondLight(BondEntity, LightEntity): await self._hub.bond.action(self._device.device_id, Action.turn_light_off()) +class BondDownLight(BondBaseLight, BondEntity, LightEntity): + """Representation of a Bond light.""" + + def _apply_state(self, state: dict): + self._light = state.get("down_light") and state.get("light") + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the light.""" + await self._hub.bond.action( + self._device.device_id, Action(Action.TURN_DOWN_LIGHT_ON) + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the light.""" + await self._hub.bond.action( + self._device.device_id, Action(Action.TURN_DOWN_LIGHT_OFF) + ) + + +class BondUpLight(BondBaseLight, BondEntity, LightEntity): + """Representation of a Bond light.""" + + def _apply_state(self, state: dict): + self._light = state.get("up_light") and state.get("light") + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the light.""" + await self._hub.bond.action( + self._device.device_id, Action(Action.TURN_UP_LIGHT_ON) + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the light.""" + await self._hub.bond.action( + self._device.device_id, Action(Action.TURN_UP_LIGHT_OFF) + ) + + class BondFireplace(BondEntity, LightEntity): """Representation of a Bond-controlled fireplace.""" diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py index 6d3aacf5e42..55ef81778f0 100644 --- a/homeassistant/components/bond/utils.py +++ b/homeassistant/components/bond/utils.py @@ -1,7 +1,7 @@ """Reusable utilities for the Bond component.""" import asyncio import logging -from typing import List, Optional +from typing import List, Optional, Set from aiohttp import ClientResponseError from bond_api import Action, Bond @@ -58,31 +58,39 @@ class BondDevice: """Check if Trust State is turned on.""" return self.props.get("trust_state", False) + def _has_any_action(self, actions: Set[str]): + """Check to see if the device supports any of the actions.""" + supported_actions: List[str] = self._attrs["actions"] + for action in supported_actions: + if action in actions: + return True + return False + def supports_speed(self) -> bool: """Return True if this device supports any of the speed related commands.""" - actions: List[str] = self._attrs["actions"] - return bool([action for action in actions if action in [Action.SET_SPEED]]) + return self._has_any_action({Action.SET_SPEED}) def supports_direction(self) -> bool: """Return True if this device supports any of the direction related commands.""" - actions: List[str] = self._attrs["actions"] - return bool([action for action in actions if action in [Action.SET_DIRECTION]]) + return self._has_any_action({Action.SET_DIRECTION}) def supports_light(self) -> bool: """Return True if this device supports any of the light related commands.""" - actions: List[str] = self._attrs["actions"] - return bool( - [ - action - for action in actions - if action in [Action.TURN_LIGHT_ON, Action.TURN_LIGHT_OFF] - ] + return self._has_any_action({Action.TURN_LIGHT_ON, Action.TURN_LIGHT_OFF}) + + def supports_up_light(self) -> bool: + """Return true if the device has an up light.""" + return self._has_any_action({Action.TURN_UP_LIGHT_ON, Action.TURN_UP_LIGHT_OFF}) + + def supports_down_light(self) -> bool: + """Return true if the device has a down light.""" + return self._has_any_action( + {Action.TURN_DOWN_LIGHT_ON, Action.TURN_DOWN_LIGHT_OFF} ) def supports_set_brightness(self) -> bool: """Return True if this device supports setting a light brightness.""" - actions: List[str] = self._attrs["actions"] - return bool([action for action in actions if action in [Action.SET_BRIGHTNESS]]) + return self._has_any_action({Action.SET_BRIGHTNESS}) class BondHub: diff --git a/tests/components/bond/test_light.py b/tests/components/bond/test_light.py index e4cd4e4e3e9..59d051fbe86 100644 --- a/tests/components/bond/test_light.py +++ b/tests/components/bond/test_light.py @@ -56,6 +56,24 @@ def dimmable_ceiling_fan(name: str): } +def down_light_ceiling_fan(name: str): + """Create a ceiling fan (that has built-in down light) with given name.""" + return { + "name": name, + "type": DeviceType.CEILING_FAN, + "actions": [Action.TURN_DOWN_LIGHT_ON, Action.TURN_DOWN_LIGHT_OFF], + } + + +def up_light_ceiling_fan(name: str): + """Create a ceiling fan (that has built-in down light) with given name.""" + return { + "name": name, + "type": DeviceType.CEILING_FAN, + "actions": [Action.TURN_UP_LIGHT_ON, Action.TURN_UP_LIGHT_OFF], + } + + def fireplace(name: str): """Create a fireplace with given name.""" return { @@ -94,6 +112,36 @@ async def test_fan_entity_registry(hass: core.HomeAssistant): assert entity.unique_id == "test-hub-id_test-device-id" +async def test_fan_up_light_entity_registry(hass: core.HomeAssistant): + """Tests that fan with up light devices are registered in the entity registry.""" + await setup_platform( + hass, + LIGHT_DOMAIN, + up_light_ceiling_fan("fan-name"), + bond_version={"bondid": "test-hub-id"}, + bond_device_id="test-device-id", + ) + + registry: EntityRegistry = await hass.helpers.entity_registry.async_get_registry() + entity = registry.entities["light.fan_name_up_light"] + assert entity.unique_id == "test-hub-id_test-device-id_up_light" + + +async def test_fan_down_light_entity_registry(hass: core.HomeAssistant): + """Tests that fan with down light devices are registered in the entity registry.""" + await setup_platform( + hass, + LIGHT_DOMAIN, + down_light_ceiling_fan("fan-name"), + bond_version={"bondid": "test-hub-id"}, + bond_device_id="test-device-id", + ) + + registry: EntityRegistry = await hass.helpers.entity_registry.async_get_registry() + entity = registry.entities["light.fan_name_down_light"] + assert entity.unique_id == "test-hub-id_test-device-id_down_light" + + async def test_fireplace_entity_registry(hass: core.HomeAssistant): """Tests that flame fireplace devices are registered in the entity registry.""" await setup_platform( @@ -122,7 +170,7 @@ async def test_fireplace_with_light_entity_registry(hass: core.HomeAssistant): registry: EntityRegistry = await hass.helpers.entity_registry.async_get_registry() entity_flame = registry.entities["light.fireplace_name"] assert entity_flame.unique_id == "test-hub-id_test-device-id" - entity_light = registry.entities["light.fireplace_name_2"] + entity_light = registry.entities["light.fireplace_name_light"] assert entity_light.unique_id == "test-hub-id_test-device-id_light" @@ -269,6 +317,98 @@ async def test_turn_on_light_with_brightness(hass: core.HomeAssistant): ) +async def test_turn_on_up_light(hass: core.HomeAssistant): + """Tests that turn on command, on an up light, delegates to API.""" + await setup_platform( + hass, + LIGHT_DOMAIN, + up_light_ceiling_fan("name-1"), + bond_device_id="test-device-id", + ) + + with patch_bond_action() as mock_turn_on, patch_bond_device_state(): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.name_1_up_light"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_turn_on.assert_called_once_with( + "test-device-id", Action(Action.TURN_UP_LIGHT_ON) + ) + + +async def test_turn_off_up_light(hass: core.HomeAssistant): + """Tests that turn off command, on an up light, delegates to API.""" + await setup_platform( + hass, + LIGHT_DOMAIN, + up_light_ceiling_fan("name-1"), + bond_device_id="test-device-id", + ) + + with patch_bond_action() as mock_turn_off, patch_bond_device_state(): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.name_1_up_light"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_turn_off.assert_called_once_with( + "test-device-id", Action(Action.TURN_UP_LIGHT_OFF) + ) + + +async def test_turn_on_down_light(hass: core.HomeAssistant): + """Tests that turn on command, on a down light, delegates to API.""" + await setup_platform( + hass, + LIGHT_DOMAIN, + down_light_ceiling_fan("name-1"), + bond_device_id="test-device-id", + ) + + with patch_bond_action() as mock_turn_on, patch_bond_device_state(): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.name_1_down_light"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_turn_on.assert_called_once_with( + "test-device-id", Action(Action.TURN_DOWN_LIGHT_ON) + ) + + +async def test_turn_off_down_light(hass: core.HomeAssistant): + """Tests that turn off command, on a down light, delegates to API.""" + await setup_platform( + hass, + LIGHT_DOMAIN, + down_light_ceiling_fan("name-1"), + bond_device_id="test-device-id", + ) + + with patch_bond_action() as mock_turn_off, patch_bond_device_state(): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.name_1_down_light"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_turn_off.assert_called_once_with( + "test-device-id", Action(Action.TURN_DOWN_LIGHT_OFF) + ) + + async def test_update_reports_light_is_on(hass: core.HomeAssistant): """Tests that update command sets correct state when Bond API reports the light is on.""" await setup_platform(hass, LIGHT_DOMAIN, ceiling_fan("name-1")) @@ -291,6 +431,50 @@ async def test_update_reports_light_is_off(hass: core.HomeAssistant): assert hass.states.get("light.name_1").state == "off" +async def test_update_reports_up_light_is_on(hass: core.HomeAssistant): + """Tests that update command sets correct state when Bond API reports the up light is on.""" + await setup_platform(hass, LIGHT_DOMAIN, up_light_ceiling_fan("name-1")) + + with patch_bond_device_state(return_value={"up_light": 1, "light": 1}): + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + assert hass.states.get("light.name_1_up_light").state == "on" + + +async def test_update_reports_up_light_is_off(hass: core.HomeAssistant): + """Tests that update command sets correct state when Bond API reports the up light is off.""" + await setup_platform(hass, LIGHT_DOMAIN, up_light_ceiling_fan("name-1")) + + with patch_bond_device_state(return_value={"up_light": 0, "light": 0}): + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + assert hass.states.get("light.name_1_up_light").state == "off" + + +async def test_update_reports_down_light_is_on(hass: core.HomeAssistant): + """Tests that update command sets correct state when Bond API reports the down light is on.""" + await setup_platform(hass, LIGHT_DOMAIN, down_light_ceiling_fan("name-1")) + + with patch_bond_device_state(return_value={"down_light": 1, "light": 1}): + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + assert hass.states.get("light.name_1_down_light").state == "on" + + +async def test_update_reports_down_light_is_off(hass: core.HomeAssistant): + """Tests that update command sets correct state when Bond API reports the down light is off.""" + await setup_platform(hass, LIGHT_DOMAIN, down_light_ceiling_fan("name-1")) + + with patch_bond_device_state(return_value={"down_light": 0, "light": 0}): + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + assert hass.states.get("light.name_1_down_light").state == "off" + + async def test_turn_on_fireplace_with_brightness(hass: core.HomeAssistant): """Tests that turn on command delegates to set flame API.""" await setup_platform( From 39da0dc409347d6e84cec75b7b30d3c9be4b68b1 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sun, 21 Feb 2021 02:11:50 +0800 Subject: [PATCH 584/796] Add rtsp transport options to generic camera (#46623) * Add rtsp transport options to generic camera --- homeassistant/components/generic/camera.py | 12 ++++++++++-- tests/components/generic/test_camera.py | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 28db66b4f3e..56b490e165a 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -34,6 +34,9 @@ CONF_LIMIT_REFETCH_TO_URL_CHANGE = "limit_refetch_to_url_change" CONF_STILL_IMAGE_URL = "still_image_url" CONF_STREAM_SOURCE = "stream_source" CONF_FRAMERATE = "framerate" +CONF_RTSP_TRANSPORT = "rtsp_transport" +FFMPEG_OPTION_MAP = {CONF_RTSP_TRANSPORT: "rtsp_transport"} +ALLOWED_RTSP_TRANSPORT_PROTOCOLS = {"tcp", "udp", "udp_multicast", "http"} DEFAULT_NAME = "Generic Camera" GET_IMAGE_TIMEOUT = 10 @@ -54,6 +57,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( cv.small_float, cv.positive_int ), vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, + vol.Optional(CONF_RTSP_TRANSPORT): vol.In(ALLOWED_RTSP_TRANSPORT_PROTOCOLS), } ) @@ -85,15 +89,19 @@ class GenericCamera(Camera): self._supported_features = SUPPORT_STREAM if self._stream_source else 0 self.content_type = device_info[CONF_CONTENT_TYPE] self.verify_ssl = device_info[CONF_VERIFY_SSL] + if device_info.get(CONF_RTSP_TRANSPORT): + self.stream_options[FFMPEG_OPTION_MAP[CONF_RTSP_TRANSPORT]] = device_info[ + CONF_RTSP_TRANSPORT + ] username = device_info.get(CONF_USERNAME) password = device_info.get(CONF_PASSWORD) if username and password: if self._authentication == HTTP_DIGEST_AUTHENTICATION: - self._auth = httpx.DigestAuth(username, password) + self._auth = httpx.DigestAuth(username=username, password=password) else: - self._auth = httpx.BasicAuth(username, password=password) + self._auth = httpx.BasicAuth(username=username, password=password) else: self._auth = None diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index 7f5b3bb3b53..3e2f07b446b 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -250,6 +250,28 @@ async def test_stream_source_error(aioclient_mock, hass, hass_client, hass_ws_cl } +async def test_setup_alternative_options(hass, hass_ws_client): + """Test that the stream source is setup with different config options.""" + assert await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "still_image_url": "https://example.com", + "authentication": "digest", + "username": "user", + "password": "pass", + "stream_source": "rtsp://example.com:554/rtsp/", + "rtsp_transport": "udp", + }, + }, + ) + await hass.async_block_till_done() + assert hass.data["camera"].get_entity("camera.config_test") + + async def test_no_stream_source(aioclient_mock, hass, hass_client, hass_ws_client): """Test a stream request without stream source option set.""" assert await async_setup_component( From da1808226ee0133512f2cd5cbe0bd63fb08c3e10 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 20 Feb 2021 19:42:37 +0100 Subject: [PATCH 585/796] change log level to info (#46823) --- homeassistant/components/nut/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index f4fbbdef932..174405e22e2 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -67,7 +67,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) ) else: - _LOGGER.warning( + _LOGGER.info( "Sensor type: %s does not appear in the NUT status " "output, cannot add", sensor_type, From 2940df09e9e302c2941b91e70f4535f64a896ef4 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 20 Feb 2021 19:45:04 +0100 Subject: [PATCH 586/796] Update xknx to 0.17.0 (#46809) --- homeassistant/components/knx/__init__.py | 1 + homeassistant/components/knx/binary_sensor.py | 4 +++- homeassistant/components/knx/factory.py | 8 +++++++- homeassistant/components/knx/manifest.json | 2 +- homeassistant/components/knx/schema.py | 10 ++++++++-- homeassistant/components/knx/sensor.py | 2 +- homeassistant/components/knx/weather.py | 7 ++++++- requirements_all.txt | 2 +- 8 files changed, 28 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 1879d9a6415..5a8b0d1c351 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -376,6 +376,7 @@ class KNXModule: self.telegram_received_cb, address_filters=address_filters, group_addresses=[], + match_for_outgoing=True, ) async def service_event_register_modify(self, call): diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index 35feb09dc1d..f7ec3e80fa1 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -40,7 +40,9 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity): @property def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return device specific state attributes.""" - return {ATTR_COUNTER: self._device.counter} + if self._device.counter is not None: + return {ATTR_COUNTER: self._device.counter} + return None @property def force_update(self) -> bool: diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index 20a887b628d..8d3464b25ad 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -264,6 +264,9 @@ def _create_climate(knx_module: XKNX, config: ConfigType) -> XknxClimate: max_temp=config.get(ClimateSchema.CONF_MAX_TEMP), mode=climate_mode, on_off_invert=config[ClimateSchema.CONF_ON_OFF_INVERT], + create_temperature_sensors=config.get( + ClimateSchema.CONF_CREATE_TEMPERATURE_SENSORS + ), ) @@ -332,7 +335,7 @@ def _create_weather(knx_module: XKNX, config: ConfigType) -> XknxWeather: knx_module, name=config[CONF_NAME], sync_state=config[WeatherSchema.CONF_SYNC_STATE], - expose_sensors=config[WeatherSchema.CONF_KNX_EXPOSE_SENSORS], + create_sensors=config[WeatherSchema.CONF_KNX_CREATE_SENSORS], group_address_temperature=config[WeatherSchema.CONF_KNX_TEMPERATURE_ADDRESS], group_address_brightness_south=config.get( WeatherSchema.CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS @@ -347,6 +350,9 @@ def _create_weather(knx_module: XKNX, config: ConfigType) -> XknxWeather: WeatherSchema.CONF_KNX_BRIGHTNESS_NORTH_ADDRESS ), group_address_wind_speed=config.get(WeatherSchema.CONF_KNX_WIND_SPEED_ADDRESS), + group_address_wind_bearing=config.get( + WeatherSchema.CONF_KNX_WIND_BEARING_ADDRESS + ), group_address_rain_alarm=config.get(WeatherSchema.CONF_KNX_RAIN_ALARM_ADDRESS), group_address_frost_alarm=config.get( WeatherSchema.CONF_KNX_FROST_ALARM_ADDRESS diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 60fb097128c..834e1604e9e 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.16.3"], + "requirements": ["xknx==0.17.0"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver" } diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index a9b65b85352..54134365b95 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -240,6 +240,7 @@ class ClimateSchema: CONF_ON_OFF_INVERT = "on_off_invert" CONF_MIN_TEMP = "min_temp" CONF_MAX_TEMP = "max_temp" + CONF_CREATE_TEMPERATURE_SENSORS = "create_temperature_sensors" DEFAULT_NAME = "KNX Climate" DEFAULT_SETPOINT_SHIFT_MODE = "DPT6010" @@ -295,6 +296,9 @@ class ClimateSchema: ), vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), vol.Optional(CONF_MAX_TEMP): vol.Coerce(float), + vol.Optional( + CONF_CREATE_TEMPERATURE_SENSORS, default=False + ): cv.boolean, } ), ) @@ -397,13 +401,14 @@ class WeatherSchema: CONF_KNX_BRIGHTNESS_WEST_ADDRESS = "address_brightness_west" CONF_KNX_BRIGHTNESS_NORTH_ADDRESS = "address_brightness_north" CONF_KNX_WIND_SPEED_ADDRESS = "address_wind_speed" + CONF_KNX_WIND_BEARING_ADDRESS = "address_wind_bearing" CONF_KNX_RAIN_ALARM_ADDRESS = "address_rain_alarm" CONF_KNX_FROST_ALARM_ADDRESS = "address_frost_alarm" CONF_KNX_WIND_ALARM_ADDRESS = "address_wind_alarm" CONF_KNX_DAY_NIGHT_ADDRESS = "address_day_night" CONF_KNX_AIR_PRESSURE_ADDRESS = "address_air_pressure" CONF_KNX_HUMIDITY_ADDRESS = "address_humidity" - CONF_KNX_EXPOSE_SENSORS = "expose_sensors" + CONF_KNX_CREATE_SENSORS = "create_sensors" DEFAULT_NAME = "KNX Weather Station" @@ -415,13 +420,14 @@ class WeatherSchema: cv.boolean, cv.string, ), - vol.Optional(CONF_KNX_EXPOSE_SENSORS, default=False): cv.boolean, + vol.Optional(CONF_KNX_CREATE_SENSORS, default=False): cv.boolean, vol.Required(CONF_KNX_TEMPERATURE_ADDRESS): cv.string, vol.Optional(CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS): cv.string, vol.Optional(CONF_KNX_BRIGHTNESS_EAST_ADDRESS): cv.string, vol.Optional(CONF_KNX_BRIGHTNESS_WEST_ADDRESS): cv.string, vol.Optional(CONF_KNX_BRIGHTNESS_NORTH_ADDRESS): cv.string, vol.Optional(CONF_KNX_WIND_SPEED_ADDRESS): cv.string, + vol.Optional(CONF_KNX_WIND_BEARING_ADDRESS): cv.string, vol.Optional(CONF_KNX_RAIN_ALARM_ADDRESS): cv.string, vol.Optional(CONF_KNX_FROST_ALARM_ADDRESS): cv.string, vol.Optional(CONF_KNX_WIND_ALARM_ADDRESS): cv.string, diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index dc9ffcb61b7..2409d7a6425 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -30,7 +30,7 @@ class KNXSensor(KnxEntity, Entity): return self._device.resolve_state() @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return self._device.unit_of_measurement() diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index 097fa661f4a..031af9f5af0 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -53,7 +53,12 @@ class KNXWeather(KnxEntity, WeatherEntity): @property def humidity(self): """Return current humidity.""" - return self._device.humidity if self._device.humidity is not None else None + return self._device.humidity + + @property + def wind_bearing(self): + """Return current wind bearing in degrees.""" + return self._device.wind_bearing @property def wind_speed(self): diff --git a/requirements_all.txt b/requirements_all.txt index 3440eb9557e..d4ed3e54fb4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2321,7 +2321,7 @@ xbox-webapi==2.0.8 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.16.3 +xknx==0.17.0 # homeassistant.components.bluesound # homeassistant.components.rest From 2ae790283c0c3c1a26ef6ebf103b7a9c768b24f0 Mon Sep 17 00:00:00 2001 From: kpine Date: Sat, 20 Feb 2021 10:52:23 -0800 Subject: [PATCH 587/796] Detect iBlinds v2.0 switch value as a cover not light (#46807) * Remove unused "fibaro_fgs222" discovery hint * Simplify multilevel switch current value discovery schema * Force iBlinds v2.0 devices to be discovered as cover entities * Rename discovery test file --- .../components/zwave_js/discovery.py | 71 ++-- tests/components/zwave_js/conftest.py | 14 + tests/components/zwave_js/test_discovery.py | 14 + .../zwave_js/cover_iblinds_v2_state.json | 359 ++++++++++++++++++ 4 files changed, 410 insertions(+), 48 deletions(-) create mode 100644 tests/components/zwave_js/test_discovery.py create mode 100644 tests/fixtures/zwave_js/cover_iblinds_v2_state.json diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 7231d18e186..9379ab69b34 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -83,6 +83,12 @@ class ZWaveDiscoverySchema: allow_multi: bool = False +SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA = ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, +) + # For device class mapping see: # https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/deviceClasses.json DISCOVERY_SCHEMAS = [ @@ -93,11 +99,7 @@ DISCOVERY_SCHEMAS = [ manufacturer_id={0x0039}, product_id={0x3131}, product_type={0x4944}, - primary_value=ZWaveValueDiscoverySchema( - command_class={CommandClass.SWITCH_MULTILEVEL}, - property={"currentValue"}, - type={"number"}, - ), + primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, ), # GE/Jasco fan controllers using switch multilevel CC ZWaveDiscoverySchema( @@ -105,11 +107,7 @@ DISCOVERY_SCHEMAS = [ manufacturer_id={0x0063}, product_id={0x3034, 0x3131, 0x3138}, product_type={0x4944}, - primary_value=ZWaveValueDiscoverySchema( - command_class={CommandClass.SWITCH_MULTILEVEL}, - property={"currentValue"}, - type={"number"}, - ), + primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, ), # Leviton ZW4SF fan controllers using switch multilevel CC ZWaveDiscoverySchema( @@ -117,50 +115,39 @@ DISCOVERY_SCHEMAS = [ manufacturer_id={0x001D}, product_id={0x0002}, product_type={0x0038}, - primary_value=ZWaveValueDiscoverySchema( - command_class={CommandClass.SWITCH_MULTILEVEL}, - property={"currentValue"}, - type={"number"}, - ), + primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, ), # Fibaro Shutter Fibaro FGS222 ZWaveDiscoverySchema( platform="cover", - hint="fibaro_fgs222", manufacturer_id={0x010F}, product_id={0x1000}, product_type={0x0302}, - primary_value=ZWaveValueDiscoverySchema( - command_class={CommandClass.SWITCH_MULTILEVEL}, - property={"currentValue"}, - type={"number"}, - ), + primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, ), # Qubino flush shutter ZWaveDiscoverySchema( platform="cover", - hint="fibaro_fgs222", manufacturer_id={0x0159}, product_id={0x0052}, product_type={0x0003}, - primary_value=ZWaveValueDiscoverySchema( - command_class={CommandClass.SWITCH_MULTILEVEL}, - property={"currentValue"}, - type={"number"}, - ), + primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, ), # Graber/Bali/Spring Fashion Covers ZWaveDiscoverySchema( platform="cover", - hint="fibaro_fgs222", manufacturer_id={0x026E}, product_id={0x5A31}, product_type={0x4353}, - primary_value=ZWaveValueDiscoverySchema( - command_class={CommandClass.SWITCH_MULTILEVEL}, - property={"currentValue"}, - type={"number"}, - ), + primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, + ), + # iBlinds v2 window blind motor + ZWaveDiscoverySchema( + platform="cover", + manufacturer_id={0x0287}, + product_id={0x000D}, + product_type={0x0003}, + primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, ), # ====== START OF GENERIC MAPPING SCHEMAS ======= # locks @@ -248,11 +235,7 @@ DISCOVERY_SCHEMAS = [ "Multilevel Scene Switch", "Unused", }, - primary_value=ZWaveValueDiscoverySchema( - command_class={CommandClass.SWITCH_MULTILEVEL}, - property={"currentValue"}, - type={"number"}, - ), + primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, ), # binary sensors ZWaveDiscoverySchema( @@ -370,11 +353,7 @@ DISCOVERY_SCHEMAS = [ "Motor Control Class C", "Multiposition Motor", }, - primary_value=ZWaveValueDiscoverySchema( - command_class={CommandClass.SWITCH_MULTILEVEL}, - property={"currentValue"}, - type={"number"}, - ), + primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, ), # cover # motorized barriers @@ -400,11 +379,7 @@ DISCOVERY_SCHEMAS = [ hint="fan", device_class_generic={"Multilevel Switch"}, device_class_specific={"Fan Switch"}, - primary_value=ZWaveValueDiscoverySchema( - command_class={CommandClass.SWITCH_MULTILEVEL}, - property={"currentValue"}, - type={"number"}, - ), + primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, ), ] diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 31b7d795a60..02b11970376 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -164,6 +164,12 @@ def motorized_barrier_cover_state_fixture(): return json.loads(load_fixture("zwave_js/cover_zw062_state.json")) +@pytest.fixture(name="iblinds_v2_state", scope="session") +def iblinds_v2_state_fixture(): + """Load the iBlinds v2 node state fixture data.""" + return json.loads(load_fixture("zwave_js/cover_iblinds_v2_state.json")) + + @pytest.fixture(name="client") def mock_client_fixture(controller_state, version_state): """Mock a client.""" @@ -359,3 +365,11 @@ def motorized_barrier_cover_fixture(client, gdc_zw062_state): node = Node(client, gdc_zw062_state) client.driver.controller.nodes[node.node_id] = node return node + + +@pytest.fixture(name="iblinds_v2") +def iblinds_cover_fixture(client, iblinds_v2_state): + """Mock an iBlinds v2.0 window cover node.""" + node = Node(client, iblinds_v2_state) + client.driver.controller.nodes[node.node_id] = node + return node diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py new file mode 100644 index 00000000000..f7d26f07d21 --- /dev/null +++ b/tests/components/zwave_js/test_discovery.py @@ -0,0 +1,14 @@ +"""Test discovery of entities for device-specific schemas for the Z-Wave JS integration.""" + + +async def test_iblinds_v2(hass, client, iblinds_v2, integration): + """Test that an iBlinds v2.0 multilevel switch value is discovered as a cover.""" + + node = iblinds_v2 + assert node.device_class.specific == "Unused" + + state = hass.states.get("light.window_blind_controller") + assert not state + + state = hass.states.get("cover.window_blind_controller") + assert state diff --git a/tests/fixtures/zwave_js/cover_iblinds_v2_state.json b/tests/fixtures/zwave_js/cover_iblinds_v2_state.json new file mode 100644 index 00000000000..7cb6f94a6f0 --- /dev/null +++ b/tests/fixtures/zwave_js/cover_iblinds_v2_state.json @@ -0,0 +1,359 @@ +{ + "nodeId": 54, + "index": 0, + "installerIcon": 6400, + "userIcon": 6400, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Routing Slave", + "generic": "Multilevel Switch", + "specific": "Unused", + "mandatorySupportedCCs": [ + "Basic", + "Multilevel Switch" + ], + "mandatoryControlCCs": [] + }, + "isListening": false, + "isFrequentListening": true, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 647, + "productId": 13, + "productType": 3, + "firmwareVersion": "1.65", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 7, + "deviceConfig": { + "manufacturerId": 647, + "manufacturer": "HAB Home Intelligence, LLC", + "label": "IB2.0", + "description": "Window Blind Controller", + "devices": [ + { + "productType": "0x0003", + "productId": "0x000d" + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + } + }, + "label": "IB2.0", + "neighbors": [ + 1, + 2, + 3, + 7, + 8, + 11, + 15, + 18, + 19, + 22, + 26, + 27, + 44, + 52 + ], + "interviewAttempts": 1, + "interviewStage": 7, + "endpoints": [ + { + "nodeId": 54, + "index": 0, + "installerIcon": 6400, + "userIcon": 6400 + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value" + }, + "value": true + }, + { + "endpoint": 0, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value" + }, + "value": true + }, + { + "endpoint": 0, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Transition duration" + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 99, + "label": "Target value" + }, + "value": 99 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 4, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Transition duration" + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 99, + "label": "Current value" + }, + "value": 30 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + } + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + } + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 647 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 13 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 100, + "unit": "%", + "label": "Battery level" + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.33" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "1.65" + ] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + } + ] +} From cf694152720308fa9e61110c9cccab7dc5eb13be Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sat, 20 Feb 2021 12:57:24 -0600 Subject: [PATCH 588/796] Implement suggested area in roku (#46819) * implement suggested area in roku * Update const.py * Update test_media_player.py * Update test_media_player.py * Update test_media_player.py * Update test_media_player.py --- homeassistant/components/roku/__init__.py | 2 ++ homeassistant/components/roku/const.py | 1 + tests/components/roku/test_media_player.py | 29 +++++++++++++++++++++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index af2e0ee946f..a75fc813fb9 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -27,6 +27,7 @@ from .const import ( ATTR_MANUFACTURER, ATTR_MODEL, ATTR_SOFTWARE_VERSION, + ATTR_SUGGESTED_AREA, DOMAIN, ) @@ -161,4 +162,5 @@ class RokuEntity(CoordinatorEntity): ATTR_MANUFACTURER: self.coordinator.data.info.brand, ATTR_MODEL: self.coordinator.data.info.model_name, ATTR_SOFTWARE_VERSION: self.coordinator.data.info.version, + ATTR_SUGGESTED_AREA: self.coordinator.data.info.device_location, } diff --git a/homeassistant/components/roku/const.py b/homeassistant/components/roku/const.py index 4abbd9e109a..dc458c88cd0 100644 --- a/homeassistant/components/roku/const.py +++ b/homeassistant/components/roku/const.py @@ -7,6 +7,7 @@ ATTR_KEYWORD = "keyword" ATTR_MANUFACTURER = "manufacturer" ATTR_MODEL = "model" ATTR_SOFTWARE_VERSION = "sw_version" +ATTR_SUGGESTED_AREA = "suggested_area" # Default Values DEFAULT_PORT = 8060 diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index 1a1b46117bd..09124ecdf37 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -65,14 +65,18 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed -from tests.components.roku import UPNP_SERIAL, setup_integration +from tests.components.roku import NAME_ROKUTV, UPNP_SERIAL, setup_integration from tests.test_util.aiohttp import AiohttpClientMocker MAIN_ENTITY_ID = f"{MP_DOMAIN}.my_roku_3" TV_ENTITY_ID = f"{MP_DOMAIN}.58_onn_roku_tv" TV_HOST = "192.168.1.161" +TV_LOCATION = "Living room" +TV_MANUFACTURER = "Onn" +TV_MODEL = "100005844" TV_SERIAL = "YN00H5555555" +TV_SW_VERSION = "9.2.0" async def test_setup( @@ -304,6 +308,29 @@ async def test_tv_attributes( assert state.attributes.get(ATTR_MEDIA_TITLE) == "Airwolf" +async def test_tv_device_registry( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test device registered for Roku TV in the device registry.""" + await setup_integration( + hass, + aioclient_mock, + device="rokutv", + app="tvinput-dtv", + host=TV_HOST, + unique_id=TV_SERIAL, + ) + + device_registry = await hass.helpers.device_registry.async_get_registry() + reg_device = device_registry.async_get_device(identifiers={(DOMAIN, TV_SERIAL)}) + + assert reg_device.model == TV_MODEL + assert reg_device.sw_version == TV_SW_VERSION + assert reg_device.manufacturer == TV_MANUFACTURER + assert reg_device.suggested_area == TV_LOCATION + assert reg_device.name == NAME_ROKUTV + + async def test_services( hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker ) -> None: From 9f4874bb81c615c90edf0312b4b862052518c5a2 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 20 Feb 2021 19:57:46 +0100 Subject: [PATCH 589/796] Explicitly create_task for asyncio.wait (#46325) --- homeassistant/components/script/__init__.py | 5 ++++- homeassistant/helpers/service.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index eab30e01ee2..5de3cb8264f 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -181,7 +181,10 @@ async def async_setup(hass, config): return await asyncio.wait( - [script_entity.async_turn_off() for script_entity in script_entities] + [ + asyncio.create_task(script_entity.async_turn_off()) + for script_entity in script_entities + ] ) async def toggle_service(service): diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index d4eacbc0503..01fb76ec1bc 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -629,7 +629,7 @@ async def entity_service_call( # Context expires if the turn on commands took a long time. # Set context again so it's there when we update entity.async_set_context(call.context) - tasks.append(entity.async_update_ha_state(True)) + tasks.append(asyncio.create_task(entity.async_update_ha_state(True))) if tasks: done, pending = await asyncio.wait(tasks) From c6c0e2416c257234ded5674ff242481c2def6aac Mon Sep 17 00:00:00 2001 From: marecabo <23156476+marecabo@users.noreply.github.com> Date: Sat, 20 Feb 2021 20:05:35 +0100 Subject: [PATCH 590/796] Validate icon and device_class of ESPHome sensor entities (#46709) --- homeassistant/components/esphome/sensor.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index ad23afd4744..fe9922cf0ed 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -3,8 +3,11 @@ import math from typing import Optional from aioesphomeapi import SensorInfo, SensorState, TextSensorInfo, TextSensorState +import voluptuous as vol +from homeassistant.components.sensor import DEVICE_CLASSES from homeassistant.config_entries import ConfigEntry +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry @@ -54,7 +57,7 @@ class EsphomeSensor(EsphomeEntity): """Return the icon.""" if not self._static_info.icon or self._static_info.device_class: return None - return self._static_info.icon + return vol.Schema(cv.icon)(self._static_info.icon) @property def force_update(self) -> bool: @@ -80,7 +83,7 @@ class EsphomeSensor(EsphomeEntity): @property def device_class(self) -> str: """Return the class of this device, from component DEVICE_CLASSES.""" - if not self._static_info.device_class: + if self._static_info.device_class not in DEVICE_CLASSES: return None return self._static_info.device_class From 2cf46330b1ec3fde09599af5aaf51149e847af42 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 20 Feb 2021 20:08:59 +0100 Subject: [PATCH 591/796] Enable KNX routing optional local_ip (#46133) --- homeassistant/components/knx/__init__.py | 9 ++++++--- homeassistant/components/knx/schema.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 5a8b0d1c351..c957dbe5195 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -298,9 +298,12 @@ class KNXModule: def connection_config_routing(self): """Return the connection_config if routing is configured.""" - local_ip = self.config[DOMAIN][CONF_KNX_ROUTING].get( - ConnectionSchema.CONF_KNX_LOCAL_IP - ) + local_ip = None + # all configuration values are optional + if self.config[DOMAIN][CONF_KNX_ROUTING] is not None: + local_ip = self.config[DOMAIN][CONF_KNX_ROUTING].get( + ConnectionSchema.CONF_KNX_LOCAL_IP + ) return ConnectionConfig( connection_type=ConnectionType.ROUTING, local_ip=local_ip ) diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 54134365b95..61909013739 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -38,7 +38,7 @@ class ConnectionSchema: } ) - ROUTING_SCHEMA = vol.Schema({vol.Optional(CONF_KNX_LOCAL_IP): cv.string}) + ROUTING_SCHEMA = vol.Maybe(vol.Schema({vol.Optional(CONF_KNX_LOCAL_IP): cv.string})) class CoverSchema: From 194c0d2b089b650ca7a41257df53b17669a12c45 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 20 Feb 2021 20:09:23 +0100 Subject: [PATCH 592/796] knx-read-service (#46670) --- homeassistant/components/knx/__init__.py | 28 +++++++++++++++++++++- homeassistant/components/knx/services.yaml | 6 +++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index c957dbe5195..e4280e6bddc 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -15,7 +15,7 @@ from xknx.io import ( ConnectionType, ) from xknx.telegram import AddressFilter, GroupAddress, Telegram -from xknx.telegram.apci import GroupValueResponse, GroupValueWrite +from xknx.telegram.apci import GroupValueRead, GroupValueResponse, GroupValueWrite from homeassistant.const import ( CONF_ENTITY_ID, @@ -75,6 +75,7 @@ SERVICE_KNX_ATTR_PAYLOAD = "payload" SERVICE_KNX_ATTR_TYPE = "type" SERVICE_KNX_ATTR_REMOVE = "remove" SERVICE_KNX_EVENT_REGISTER = "event_register" +SERVICE_KNX_READ = "read" CONFIG_SCHEMA = vol.Schema( { @@ -166,6 +167,15 @@ SERVICE_KNX_SEND_SCHEMA = vol.Any( ), ) +SERVICE_KNX_READ_SCHEMA = vol.Schema( + { + vol.Required(SERVICE_KNX_ATTR_ADDRESS): vol.All( + cv.ensure_list, + [cv.string], + ) + } +) + SERVICE_KNX_EVENT_REGISTER_SCHEMA = vol.Schema( { vol.Required(SERVICE_KNX_ATTR_ADDRESS): cv.string, @@ -210,6 +220,13 @@ async def async_setup(hass, config): schema=SERVICE_KNX_SEND_SCHEMA, ) + hass.services.async_register( + DOMAIN, + SERVICE_KNX_READ, + hass.data[DOMAIN].service_read_to_knx_bus, + schema=SERVICE_KNX_READ_SCHEMA, + ) + async_register_admin_service( hass, DOMAIN, @@ -423,6 +440,15 @@ class KNXModule: ) await self.xknx.telegrams.put(telegram) + async def service_read_to_knx_bus(self, call): + """Service for sending a GroupValueRead telegram to the KNX bus.""" + for address in call.data.get(SERVICE_KNX_ATTR_ADDRESS): + telegram = Telegram( + destination_address=GroupAddress(address), + payload=GroupValueRead(), + ) + await self.xknx.telegrams.put(telegram) + class KNXExposeTime: """Object to Expose Time/Date object to KNX bus.""" diff --git a/homeassistant/components/knx/services.yaml b/homeassistant/components/knx/services.yaml index cab8c100b01..aa946459cfd 100644 --- a/homeassistant/components/knx/services.yaml +++ b/homeassistant/components/knx/services.yaml @@ -10,6 +10,12 @@ send: type: description: "Optional. If set, the payload will not be sent as raw bytes, but encoded as given DPT. Knx sensor types are valid values (see https://www.home-assistant.io/integrations/sensor.knx)." example: "temperature" +read: + description: "Send GroupValueRead requests to the KNX bus. Response can be used from `knx_event` and will be processed in KNX entities." + fields: + address: + description: "Group address(es) to send read request to. Lists will read multiple group addresses." + example: "1/1/0" event_register: description: "Add or remove single group address to knx_event filter for triggering `knx_event`s. Only addresses added with this service can be removed." fields: From 6bb848455fdb59b0da6e9164b661cfc3b4d3db55 Mon Sep 17 00:00:00 2001 From: Martin Date: Sat, 20 Feb 2021 20:09:43 +0100 Subject: [PATCH 593/796] Add open/close tilt support to KNX cover (#46583) --- homeassistant/components/knx/cover.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 33da600976e..86afd467be1 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -7,7 +7,9 @@ from homeassistant.components.cover import ( DEVICE_CLASS_BLIND, DEVICE_CLASSES, SUPPORT_CLOSE, + SUPPORT_CLOSE_TILT, SUPPORT_OPEN, + SUPPORT_OPEN_TILT, SUPPORT_SET_POSITION, SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, @@ -61,7 +63,9 @@ class KNXCover(KnxEntity, CoverEntity): if self._device.supports_stop: supported_features |= SUPPORT_STOP if self._device.supports_angle: - supported_features |= SUPPORT_SET_TILT_POSITION + supported_features |= ( + SUPPORT_SET_TILT_POSITION | SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT + ) return supported_features @property @@ -127,6 +131,14 @@ class KNXCover(KnxEntity, CoverEntity): knx_tilt_position = 100 - kwargs[ATTR_TILT_POSITION] await self._device.set_angle(knx_tilt_position) + async def async_open_cover_tilt(self, **kwargs): + """Open the cover tilt.""" + await self._device.set_short_up() + + async def async_close_cover_tilt(self, **kwargs): + """Close the cover tilt.""" + await self._device.set_short_down() + def start_auto_updater(self): """Start the autoupdater to update Home Assistant while cover is moving.""" if self._unsubscribe_auto_updater is None: From 806369ddc1328b23a2eaa62ff91d0d662331a6b1 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Sat, 20 Feb 2021 11:21:01 -0800 Subject: [PATCH 594/796] Bump pywemo to 0.6.3 (#46825) --- 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 23911b31be2..9d91ab7ef96 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.6.2"], + "requirements": ["pywemo==0.6.3"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/requirements_all.txt b/requirements_all.txt index d4ed3e54fb4..45fd1d9850b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1902,7 +1902,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.6.2 +pywemo==0.6.3 # homeassistant.components.wilight pywilight==0.0.68 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f843874b5bc..cec8c469d4b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -981,7 +981,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.6.2 +pywemo==0.6.3 # homeassistant.components.wilight pywilight==0.0.68 From fe4cf611f7afb9ae38243f2ce101f4b44b0b02a4 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Sat, 20 Feb 2021 11:37:48 -0800 Subject: [PATCH 595/796] Disable update polling for Wemo when devices can push updates (#46806) --- homeassistant/components/wemo/entity.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index 65183a6f7a4..4fac786af9a 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -39,6 +39,7 @@ class WemoEntity(Entity): self._state = None self._available = True self._update_lock = None + self._has_polled = False @property def name(self) -> str: @@ -103,6 +104,7 @@ class WemoEntity(Entity): """Try updating within an async lock.""" async with self._update_lock: await self.hass.async_add_executor_job(self._update, force_update) + self._has_polled = True # When the timeout expires HomeAssistant is no longer waiting for an # update from the device. Instead, the state needs to be updated # asynchronously. This also handles the case where an update came @@ -136,6 +138,24 @@ class WemoSubscriptionEntity(WemoEntity): """Return true if the state is on. Standby is on.""" return self._state + @property + def should_poll(self) -> bool: + """Return True if the the device requires local polling, False otherwise. + + Polling can be disabled if three conditions are met: + 1. The device has polled to get the initial state (self._has_polled). + 2. The polling was successful and the device is in a healthy state + (self.available). + 3. The pywemo subscription registry reports that there is an active + subscription and the subscription has been confirmed by receiving an + initial event. This confirms that device push notifications are + working correctly (registry.is_subscribed - this method is async safe). + """ + registry = self.hass.data[WEMO_DOMAIN]["registry"] + return not ( + self.available and self._has_polled and registry.is_subscribed(self.wemo) + ) + async def async_added_to_hass(self) -> None: """Wemo device added to Home Assistant.""" await super().async_added_to_hass() From 43a5852561b6c464177fa016ab2a3216137fe418 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 20 Feb 2021 20:59:59 +0100 Subject: [PATCH 596/796] Fix habitica entry unload clean up (#46798) * Fix habitica entry unload clean up * Fix service remove * Add entry setup and unload test * Fix config flow tests * Assert service --- homeassistant/components/habitica/__init__.py | 4 +-- tests/components/habitica/test_config_flow.py | 29 ++++++++------- tests/components/habitica/test_init.py | 36 +++++++++++++++++++ 3 files changed, 52 insertions(+), 17 deletions(-) create mode 100644 tests/components/habitica/test_init.py diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 36e50db6c20..ca3837ef8ca 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -169,6 +169,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - if len(hass.config_entries.async_entries) == 1: - hass.components.webhook.async_unregister(SERVICE_API_CALL) + if len(hass.config_entries.async_entries(DOMAIN)) == 1: + hass.services.async_remove(DOMAIN, SERVICE_API_CALL) return unload_ok diff --git a/tests/components/habitica/test_config_flow.py b/tests/components/habitica/test_config_flow.py index 8ae92bcc0e2..d02a9031d63 100644 --- a/tests/components/habitica/test_config_flow.py +++ b/tests/components/habitica/test_config_flow.py @@ -1,11 +1,10 @@ """Test the habitica config flow.""" -from asyncio import Future from unittest.mock import AsyncMock, MagicMock, patch +from aiohttp import ClientResponseError + from homeassistant import config_entries, setup -from homeassistant.components.habitica.config_flow import InvalidAuth from homeassistant.components.habitica.const import DEFAULT_URL, DOMAIN -from homeassistant.const import HTTP_OK from tests.common import MockConfigEntry @@ -19,12 +18,8 @@ async def test_form(hass): assert result["type"] == "form" assert result["errors"] == {} - request_mock = MagicMock() - type(request_mock).status_code = HTTP_OK - mock_obj = MagicMock() - mock_obj.user.get.return_value = Future() - mock_obj.user.get.return_value.set_result(None) + mock_obj.user.get = AsyncMock() with patch( "homeassistant.components.habitica.config_flow.HabitipyAsync", @@ -58,9 +53,12 @@ async def test_form_invalid_credentials(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) + mock_obj = MagicMock() + mock_obj.user.get = AsyncMock(side_effect=ClientResponseError(MagicMock(), ())) + with patch( - "habitipy.aio.HabitipyAsync", - side_effect=InvalidAuth, + "homeassistant.components.habitica.config_flow.HabitipyAsync", + return_value=mock_obj, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -81,9 +79,12 @@ async def test_form_unexpected_exception(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) + mock_obj = MagicMock() + mock_obj.user.get = AsyncMock(side_effect=Exception) + with patch( "homeassistant.components.habitica.config_flow.HabitipyAsync", - side_effect=Exception, + return_value=mock_obj, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -98,7 +99,7 @@ async def test_form_unexpected_exception(hass): assert result2["errors"] == {"base": "unknown"} -async def test_manual_flow_config_exist(hass, aioclient_mock): +async def test_manual_flow_config_exist(hass): """Test config flow discovers only already configured config.""" MockConfigEntry( domain=DOMAIN, @@ -114,9 +115,7 @@ async def test_manual_flow_config_exist(hass, aioclient_mock): assert result["step_id"] == "user" mock_obj = MagicMock() - mock_obj.user.get.side_effect = AsyncMock( - return_value={"api_user": "test-api-user"} - ) + mock_obj.user.get = AsyncMock(return_value={"api_user": "test-api-user"}) with patch( "homeassistant.components.habitica.config_flow.HabitipyAsync", diff --git a/tests/components/habitica/test_init.py b/tests/components/habitica/test_init.py new file mode 100644 index 00000000000..5f7e4b7fbf5 --- /dev/null +++ b/tests/components/habitica/test_init.py @@ -0,0 +1,36 @@ +"""Test the habitica init module.""" +from homeassistant.components.habitica.const import ( + DEFAULT_URL, + DOMAIN, + SERVICE_API_CALL, +) + +from tests.common import MockConfigEntry + + +async def test_entry_setup_unload(hass, aioclient_mock): + """Test integration setup and unload.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="test-api-user", + data={ + "api_user": "test-api-user", + "api_key": "test-api-key", + "url": DEFAULT_URL, + }, + ) + entry.add_to_hass(hass) + + aioclient_mock.get( + "https://habitica.com/api/v3/user", + json={"data": {"api_user": "test-api-user", "profile": {"name": "test_user"}}}, + ) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.services.has_service(DOMAIN, SERVICE_API_CALL) + + assert await hass.config_entries.async_unload(entry.entry_id) + + assert not hass.services.has_service(DOMAIN, SERVICE_API_CALL) From 6e52b26c06098052d379065b00f570c2a44653e1 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Sat, 20 Feb 2021 13:16:50 -0800 Subject: [PATCH 597/796] Speed-up wemo discovery (#46821) * Speed-up wemo discovery * Use gather_with_concurrency to limit concurrent executor usage * Comment fixup: asyncio executors -> executor threads --- homeassistant/components/wemo/__init__.py | 48 +++++++++++++++++++---- tests/components/wemo/test_init.py | 33 +++++++++++----- 2 files changed, 64 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index db380ae11ca..df737f101ba 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -1,5 +1,4 @@ """Support for WeMo device discovery.""" -import asyncio import logging import pywemo @@ -16,9 +15,14 @@ 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 homeassistant.util.async_ import gather_with_concurrency from .const import DOMAIN +# Max number of devices to initialize at once. This limit is in place to +# avoid tying up too many executor threads with WeMo device setup. +MAX_CONCURRENCY = 3 + # Mapping from Wemo model_name to domain. WEMO_MODEL_DISPATCH = { "Bridge": LIGHT_DOMAIN, @@ -114,11 +118,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): static_conf = config.get(CONF_STATIC, []) if static_conf: _LOGGER.debug("Adding statically configured WeMo devices...") - for device in await asyncio.gather( + for device in await gather_with_concurrency( + MAX_CONCURRENCY, *[ hass.async_add_executor_job(validate_static_config, host, port) for host, port in static_conf - ] + ], ): if device: wemo_dispatcher.async_add_unique_device(hass, device) @@ -187,15 +192,44 @@ class WemoDiscovery: self._wemo_dispatcher = wemo_dispatcher self._stop = None self._scan_delay = 0 + self._upnp_entries = set() + + async def async_add_from_upnp_entry(self, entry: pywemo.ssdp.UPNPEntry) -> None: + """Create a WeMoDevice from an UPNPEntry and add it to the dispatcher. + + Uses the self._upnp_entries set to avoid interrogating the same device + multiple times. + """ + if entry in self._upnp_entries: + return + try: + device = await self._hass.async_add_executor_job( + pywemo.discovery.device_from_uuid_and_location, + entry.udn, + entry.location, + ) + except pywemo.PyWeMoException as err: + _LOGGER.error("Unable to setup WeMo %r (%s)", entry, err) + else: + self._wemo_dispatcher.async_add_unique_device(self._hass, device) + self._upnp_entries.add(entry) 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) + # pywemo.ssdp.scan is a light-weight UDP UPnP scan for WeMo devices. + entries = await self._hass.async_add_executor_job(pywemo.ssdp.scan) + + # async_add_from_upnp_entry causes multiple HTTP requests to be sent + # to the WeMo device for the initial setup of the WeMoDevice + # instance. This may take some time to complete. The per-device + # setup work is done in parallel to speed up initial setup for the + # component. + await gather_with_concurrency( + MAX_CONCURRENCY, + *[self.async_add_from_upnp_entry(entry) for entry in entries], + ) finally: # Run discovery more frequently after hass has just started. self._scan_delay = min( diff --git a/tests/components/wemo/test_init.py b/tests/components/wemo/test_init.py index 374222d8688..7c2b43dfd8c 100644 --- a/tests/components/wemo/test_init.py +++ b/tests/components/wemo/test_init.py @@ -100,28 +100,41 @@ async def test_static_config_with_invalid_host(hass): async def test_discovery(hass, pywemo_registry): """Verify that discovery dispatches devices to the platform for setup.""" - def create_device(counter): + def create_device(uuid, location): """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.host = location + device.port = MOCK_PORT + device.name = f"{MOCK_NAME}_{uuid}" + device.serialnumber = f"{MOCK_SERIAL_NUMBER}_{uuid}" device.model_name = "Motion" device.get_state.return_value = 0 # Default to Off return device - pywemo_devices = [create_device(0), create_device(1)] + def create_upnp_entry(counter): + return pywemo.ssdp.UPNPEntry.from_response( + "\r\n".join( + [ + "", + f"LOCATION: http://192.168.1.100:{counter}/setup.xml", + f"USN: uuid:Socket-1_0-SERIAL{counter}::upnp:rootdevice", + "", + ] + ) + ) + + upnp_entries = [create_upnp_entry(0), create_upnp_entry(1)] # Setup the component and start discovery. with patch( - "pywemo.discover_devices", return_value=pywemo_devices - ) as mock_discovery: + "pywemo.discovery.device_from_uuid_and_location", side_effect=create_device + ), patch("pywemo.ssdp.scan", return_value=upnp_entries) as mock_scan: 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)) + mock_scan.assert_called() + # Add two of the same entries to test deduplication. + upnp_entries.extend([create_upnp_entry(2), create_upnp_entry(2)]) # Test that discovery runs periodically and the async_dispatcher_send code works. async_fire_time_changed( From 4af619d3838406cfda66b1e6a5e4b100f00b1b80 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Sat, 20 Feb 2021 22:55:23 +0100 Subject: [PATCH 598/796] Add Rituals Perfume Genie integration (#46218) Co-authored-by: J. Nick Koston --- .coveragerc | 2 + CODEOWNERS | 1 + .../rituals_perfume_genie/__init__.py | 61 ++++++++++ .../rituals_perfume_genie/config_flow.py | 60 ++++++++++ .../components/rituals_perfume_genie/const.py | 5 + .../rituals_perfume_genie/manifest.json | 12 ++ .../rituals_perfume_genie/strings.json | 21 ++++ .../rituals_perfume_genie/switch.py | 92 +++++++++++++++ .../translations/en.json | 21 ++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + .../rituals_perfume_genie/__init__.py | 1 + .../rituals_perfume_genie/test_config_flow.py | 109 ++++++++++++++++++ 14 files changed, 392 insertions(+) create mode 100644 homeassistant/components/rituals_perfume_genie/__init__.py create mode 100644 homeassistant/components/rituals_perfume_genie/config_flow.py create mode 100644 homeassistant/components/rituals_perfume_genie/const.py create mode 100644 homeassistant/components/rituals_perfume_genie/manifest.json create mode 100644 homeassistant/components/rituals_perfume_genie/strings.json create mode 100644 homeassistant/components/rituals_perfume_genie/switch.py create mode 100644 homeassistant/components/rituals_perfume_genie/translations/en.json create mode 100644 tests/components/rituals_perfume_genie/__init__.py create mode 100644 tests/components/rituals_perfume_genie/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 51a539b4aba..2b71ba546cc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -787,6 +787,8 @@ omit = homeassistant/components/rest/switch.py homeassistant/components/ring/camera.py homeassistant/components/ripple/sensor.py + homeassistant/components/rituals_perfume_genie/switch.py + homeassistant/components/rituals_perfume_genie/__init__.py homeassistant/components/rocketchat/notify.py homeassistant/components/roomba/binary_sensor.py homeassistant/components/roomba/braava.py diff --git a/CODEOWNERS b/CODEOWNERS index ab10b4dfd60..f3c7487a520 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -383,6 +383,7 @@ homeassistant/components/rflink/* @javicalle homeassistant/components/rfxtrx/* @danielhiversen @elupus @RobBie1221 homeassistant/components/ring/* @balloob homeassistant/components/risco/* @OnFreund +homeassistant/components/rituals_perfume_genie/* @milanmeu homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/roku/* @ctalkington homeassistant/components/roomba/* @pschmitt @cyr-ius @shenxn diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py new file mode 100644 index 00000000000..ba11206d496 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -0,0 +1,61 @@ +"""The Rituals Perfume Genie integration.""" +import asyncio +import logging + +from aiohttp.client_exceptions import ClientConnectorError +from pyrituals import Account + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import ACCOUNT_HASH, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +EMPTY_CREDENTIALS = "" + +PLATFORMS = ["switch"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Rituals Perfume Genie component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Rituals Perfume Genie from a config entry.""" + session = async_get_clientsession(hass) + account = Account(EMPTY_CREDENTIALS, EMPTY_CREDENTIALS, session) + account.data = {ACCOUNT_HASH: entry.data.get(ACCOUNT_HASH)} + + try: + await account.get_devices() + except ClientConnectorError as ex: + raise ConfigEntryNotReady from ex + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = account + + 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/rituals_perfume_genie/config_flow.py b/homeassistant/components/rituals_perfume_genie/config_flow.py new file mode 100644 index 00000000000..59e442df538 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/config_flow.py @@ -0,0 +1,60 @@ +"""Config flow for Rituals Perfume Genie integration.""" +import logging + +from aiohttp import ClientResponseError +from pyrituals import Account, AuthenticationException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import ACCOUNT_HASH, DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_EMAIL): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Rituals Perfume Genie.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + if user_input is None: + return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) + + errors = {} + + session = async_get_clientsession(self.hass) + account = Account(user_input[CONF_EMAIL], user_input[CONF_PASSWORD], session) + + try: + await account.authenticate() + except ClientResponseError: + errors["base"] = "cannot_connect" + except AuthenticationException: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(account.data[CONF_EMAIL]) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=account.data[CONF_EMAIL], + data={ACCOUNT_HASH: account.data[ACCOUNT_HASH]}, + ) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/rituals_perfume_genie/const.py b/homeassistant/components/rituals_perfume_genie/const.py new file mode 100644 index 00000000000..075d79ec8de --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/const.py @@ -0,0 +1,5 @@ +"""Constants for the Rituals Perfume Genie integration.""" + +DOMAIN = "rituals_perfume_genie" + +ACCOUNT_HASH = "account_hash" diff --git a/homeassistant/components/rituals_perfume_genie/manifest.json b/homeassistant/components/rituals_perfume_genie/manifest.json new file mode 100644 index 00000000000..8be7e98b939 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "rituals_perfume_genie", + "name": "Rituals Perfume Genie", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/rituals_perfume_genie", + "requirements": [ + "pyrituals==0.0.2" + ], + "codeowners": [ + "@milanmeu" + ] +} diff --git a/homeassistant/components/rituals_perfume_genie/strings.json b/homeassistant/components/rituals_perfume_genie/strings.json new file mode 100644 index 00000000000..8824923c313 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "title": "Connect to your Rituals account", + "data": { + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/rituals_perfume_genie/switch.py b/homeassistant/components/rituals_perfume_genie/switch.py new file mode 100644 index 00000000000..7041d22f4b8 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/switch.py @@ -0,0 +1,92 @@ +"""Support for Rituals Perfume Genie switches.""" +from datetime import timedelta + +from homeassistant.components.switch import SwitchEntity + +from .const import DOMAIN + +SCAN_INTERVAL = timedelta(seconds=30) + +ON_STATE = "1" +AVAILABLE_STATE = 1 + +MANUFACTURER = "Rituals Cosmetics" +MODEL = "Diffuser" +ICON = "mdi:fan" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the diffuser switch.""" + account = hass.data[DOMAIN][config_entry.entry_id] + diffusers = await account.get_devices() + + entities = [] + for diffuser in diffusers: + entities.append(DiffuserSwitch(diffuser)) + + async_add_entities(entities, True) + + +class DiffuserSwitch(SwitchEntity): + """Representation of a diffuser switch.""" + + def __init__(self, diffuser): + """Initialize the switch.""" + self._diffuser = diffuser + + @property + def device_info(self): + """Return information about the device.""" + return { + "name": self._diffuser.data["hub"]["attributes"]["roomnamec"], + "identifiers": {(DOMAIN, self._diffuser.data["hub"]["hublot"])}, + "manufacturer": MANUFACTURER, + "model": MODEL, + "sw_version": self._diffuser.data["hub"]["sensors"]["versionc"], + } + + @property + def unique_id(self): + """Return the unique ID of the device.""" + return self._diffuser.data["hub"]["hublot"] + + @property + def available(self): + """Return if the device is available.""" + return self._diffuser.data["hub"]["status"] == AVAILABLE_STATE + + @property + def name(self): + """Return the name of the device.""" + return self._diffuser.data["hub"]["attributes"]["roomnamec"] + + @property + def icon(self): + """Return the icon of the device.""" + return ICON + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attributes = { + "fan_speed": self._diffuser.data["hub"]["attributes"]["speedc"], + "room_size": self._diffuser.data["hub"]["attributes"]["roomc"], + } + return attributes + + @property + def is_on(self): + """If the device is currently on or off.""" + return self._diffuser.data["hub"]["attributes"]["fanc"] == ON_STATE + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + await self._diffuser.turn_on() + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + await self._diffuser.turn_off() + + async def async_update(self): + """Update the data of the device.""" + await self._diffuser.update_data() diff --git a/homeassistant/components/rituals_perfume_genie/translations/en.json b/homeassistant/components/rituals_perfume_genie/translations/en.json new file mode 100644 index 00000000000..21207b1e7ed --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Password" + }, + "title": "Connect to your Rituals account" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 53d3c8294d2..dfb2f56b29e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -182,6 +182,7 @@ FLOWS = [ "rfxtrx", "ring", "risco", + "rituals_perfume_genie", "roku", "roomba", "roon", diff --git a/requirements_all.txt b/requirements_all.txt index 45fd1d9850b..35a4d925225 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1652,6 +1652,9 @@ pyrepetier==3.0.5 # homeassistant.components.risco pyrisco==0.3.1 +# homeassistant.components.rituals_perfume_genie +pyrituals==0.0.2 + # homeassistant.components.ruckus_unleashed pyruckus==0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cec8c469d4b..7f920b735e7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -876,6 +876,9 @@ pyqwikswitch==0.93 # homeassistant.components.risco pyrisco==0.3.1 +# homeassistant.components.rituals_perfume_genie +pyrituals==0.0.2 + # homeassistant.components.ruckus_unleashed pyruckus==0.12 diff --git a/tests/components/rituals_perfume_genie/__init__.py b/tests/components/rituals_perfume_genie/__init__.py new file mode 100644 index 00000000000..bd90242f14c --- /dev/null +++ b/tests/components/rituals_perfume_genie/__init__.py @@ -0,0 +1 @@ +"""Tests for the Rituals Perfume Genie integration.""" diff --git a/tests/components/rituals_perfume_genie/test_config_flow.py b/tests/components/rituals_perfume_genie/test_config_flow.py new file mode 100644 index 00000000000..60ec389a371 --- /dev/null +++ b/tests/components/rituals_perfume_genie/test_config_flow.py @@ -0,0 +1,109 @@ +"""Test the Rituals Perfume Genie config flow.""" +from unittest.mock import patch + +from aiohttp import ClientResponseError +from pyrituals import AuthenticationException + +from homeassistant import config_entries +from homeassistant.components.rituals_perfume_genie.const import ACCOUNT_HASH, DOMAIN +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD + +TEST_EMAIL = "rituals@example.com" +VALID_PASSWORD = "passw0rd" +WRONG_PASSWORD = "wrong-passw0rd" + + +async def test_form(hass): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.rituals_perfume_genie.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.rituals_perfume_genie.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: TEST_EMAIL, + CONF_PASSWORD: VALID_PASSWORD, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == TEST_EMAIL + assert isinstance(result2["data"][ACCOUNT_HASH], str) + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.rituals_perfume_genie.config_flow.Account.authenticate", + side_effect=AuthenticationException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: TEST_EMAIL, + CONF_PASSWORD: WRONG_PASSWORD, + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_auth_exception(hass): + """Test we handle auth exception.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.rituals_perfume_genie.config_flow.Account.authenticate", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: TEST_EMAIL, + CONF_PASSWORD: VALID_PASSWORD, + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.rituals_perfume_genie.config_flow.Account.authenticate", + side_effect=ClientResponseError(None, None, status=500), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: TEST_EMAIL, + CONF_PASSWORD: VALID_PASSWORD, + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} From 2b6619f815adfd391a78ad86d7cafc752095f7a7 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 20 Feb 2021 16:57:16 -0500 Subject: [PATCH 599/796] Bump zigpy-znp from 0.3.0 to 0.4.0 (#46828) --- 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 a24c20872f2..ad2bf5f17c5 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -13,7 +13,7 @@ "zigpy==0.32.0", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", - "zigpy-znp==0.3.0" + "zigpy-znp==0.4.0" ], "codeowners": ["@dmulcahey", "@adminiuga"] } diff --git a/requirements_all.txt b/requirements_all.txt index 35a4d925225..4d3d7d5bebf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2379,7 +2379,7 @@ zigpy-xbee==0.13.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.3.0 +zigpy-znp==0.4.0 # homeassistant.components.zha zigpy==0.32.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f920b735e7..80d325100c8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1219,7 +1219,7 @@ zigpy-xbee==0.13.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.3.0 +zigpy-znp==0.4.0 # homeassistant.components.zha zigpy==0.32.0 From 115fe266425654d042d633a20c5119918ffbcbcc Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Sat, 20 Feb 2021 14:25:02 -0800 Subject: [PATCH 600/796] Add smarttub sensor platform and state sensor (#46775) --- homeassistant/components/smarttub/__init__.py | 2 +- homeassistant/components/smarttub/climate.py | 5 --- homeassistant/components/smarttub/entity.py | 5 +++ homeassistant/components/smarttub/sensor.py | 30 ++++++++++++++++++ tests/components/smarttub/__init__.py | 14 +++++++++ tests/components/smarttub/conftest.py | 11 ++++++- tests/components/smarttub/test_climate.py | 31 ++----------------- tests/components/smarttub/test_config_flow.py | 2 +- tests/components/smarttub/test_init.py | 6 ++-- tests/components/smarttub/test_sensor.py | 18 +++++++++++ 10 files changed, 84 insertions(+), 40 deletions(-) create mode 100644 homeassistant/components/smarttub/sensor.py create mode 100644 tests/components/smarttub/test_sensor.py diff --git a/homeassistant/components/smarttub/__init__.py b/homeassistant/components/smarttub/__init__.py index 4700b0df4de..2b80c92510f 100644 --- a/homeassistant/components/smarttub/__init__.py +++ b/homeassistant/components/smarttub/__init__.py @@ -7,7 +7,7 @@ from .controller import SmartTubController _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["climate"] +PLATFORMS = ["climate", "sensor"] async def async_setup(hass, config): diff --git a/homeassistant/components/smarttub/climate.py b/homeassistant/components/smarttub/climate.py index 5f16bea8cf7..02d627d383e 100644 --- a/homeassistant/components/smarttub/climate.py +++ b/homeassistant/components/smarttub/climate.py @@ -36,11 +36,6 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity): """Initialize the entity.""" super().__init__(coordinator, spa, "thermostat") - @property - def unique_id(self) -> str: - """Return a unique id for the entity.""" - return f"{self.spa.id}-{self._entity_type}" - @property def temperature_unit(self): """Return the unit of measurement used by the platform.""" diff --git a/homeassistant/components/smarttub/entity.py b/homeassistant/components/smarttub/entity.py index 95d89971cb7..0e84c92e3e1 100644 --- a/homeassistant/components/smarttub/entity.py +++ b/homeassistant/components/smarttub/entity.py @@ -32,6 +32,11 @@ class SmartTubEntity(CoordinatorEntity): self.spa = spa self._entity_type = entity_type + @property + def unique_id(self) -> str: + """Return a unique id for the entity.""" + return f"{self.spa.id}-{self._entity_type}" + @property def device_info(self) -> str: """Return device info.""" diff --git a/homeassistant/components/smarttub/sensor.py b/homeassistant/components/smarttub/sensor.py new file mode 100644 index 00000000000..320f288f36a --- /dev/null +++ b/homeassistant/components/smarttub/sensor.py @@ -0,0 +1,30 @@ +"""Platform for sensor integration.""" +import logging + +from .const import DOMAIN, SMARTTUB_CONTROLLER +from .entity import SmartTubEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up climate entity for the thermostat in the tub.""" + + controller = hass.data[DOMAIN][entry.entry_id][SMARTTUB_CONTROLLER] + + entities = [SmartTubState(controller.coordinator, spa) for spa in controller.spas] + + async_add_entities(entities) + + +class SmartTubState(SmartTubEntity): + """The state of the spa.""" + + def __init__(self, coordinator, spa): + """Initialize the entity.""" + super().__init__(coordinator, spa, "state") + + @property + def state(self) -> str: + """Return the current state of the sensor.""" + return self.get_spa_status("state").lower() diff --git a/tests/components/smarttub/__init__.py b/tests/components/smarttub/__init__.py index afbf271eb63..b19af1ee59a 100644 --- a/tests/components/smarttub/__init__.py +++ b/tests/components/smarttub/__init__.py @@ -1 +1,15 @@ """Tests for the smarttub integration.""" + +from datetime import timedelta + +from homeassistant.components.smarttub.const import SCAN_INTERVAL +from homeassistant.util import dt + +from tests.common import async_fire_time_changed + + +async def trigger_update(hass): + """Trigger a polling update by moving time forward.""" + new_time = dt.utcnow() + timedelta(seconds=SCAN_INTERVAL + 1) + async_fire_time_changed(hass, new_time) + await hass.async_block_till_done() diff --git a/tests/components/smarttub/conftest.py b/tests/components/smarttub/conftest.py index 3519d4f85ce..d1bd7c377e3 100644 --- a/tests/components/smarttub/conftest.py +++ b/tests/components/smarttub/conftest.py @@ -46,6 +46,7 @@ def mock_spa(): "setTemperature": 39, "water": {"temperature": 38}, "heater": "ON", + "state": "NORMAL", } return mock_spa @@ -60,7 +61,7 @@ def mock_account(spa): return mock_account -@pytest.fixture(name="smarttub_api") +@pytest.fixture(name="smarttub_api", autouse=True) def mock_api(account, spa): """Mock the SmartTub API.""" @@ -71,3 +72,11 @@ def mock_api(account, spa): api_mock = api_class_mock.return_value api_mock.get_account.return_value = account yield api_mock + + +@pytest.fixture +async def setup_entry(hass, config_entry): + """Initialize the config entry.""" + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/smarttub/test_climate.py b/tests/components/smarttub/test_climate.py index ad47e3ede04..69fb642aab4 100644 --- a/tests/components/smarttub/test_climate.py +++ b/tests/components/smarttub/test_climate.py @@ -1,7 +1,5 @@ """Test the SmartTub climate platform.""" -from datetime import timedelta - import smarttub from homeassistant.components.climate.const import ( @@ -19,35 +17,19 @@ from homeassistant.components.climate.const import ( SERVICE_SET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.components.smarttub.const import ( - DEFAULT_MAX_TEMP, - DEFAULT_MIN_TEMP, - SCAN_INTERVAL, -) +from homeassistant.components.smarttub.const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, ) -from homeassistant.util import dt -from tests.common import async_fire_time_changed +from . import trigger_update -async def test_thermostat_update(spa, hass, config_entry, smarttub_api): +async def test_thermostat_update(spa, setup_entry, hass): """Test the thermostat entity.""" - spa.get_status.return_value = { - "heater": "ON", - "water": { - "temperature": 38, - }, - "setTemperature": 39, - } - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - entity_id = f"climate.{spa.brand}_{spa.model}_thermostat" state = hass.states.get(entity_id) assert state @@ -87,10 +69,3 @@ async def test_thermostat_update(spa, hass, config_entry, smarttub_api): spa.get_status.side_effect = smarttub.APIError await trigger_update(hass) # should not fail - - -async def trigger_update(hass): - """Trigger a polling update by moving time forward.""" - new_time = dt.utcnow() + timedelta(seconds=SCAN_INTERVAL + 1) - async_fire_time_changed(hass, new_time) - await hass.async_block_till_done() diff --git a/tests/components/smarttub/test_config_flow.py b/tests/components/smarttub/test_config_flow.py index a57eb43eef7..2608d867c0d 100644 --- a/tests/components/smarttub/test_config_flow.py +++ b/tests/components/smarttub/test_config_flow.py @@ -7,7 +7,7 @@ from homeassistant import config_entries from homeassistant.components.smarttub.const import DOMAIN -async def test_form(hass, smarttub_api): +async def test_form(hass): """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} diff --git a/tests/components/smarttub/test_init.py b/tests/components/smarttub/test_init.py index 13a447529d4..01989818d3b 100644 --- a/tests/components/smarttub/test_init.py +++ b/tests/components/smarttub/test_init.py @@ -39,15 +39,13 @@ async def test_setup_auth_failed(setup_component, hass, config_entry, smarttub_a assert config_entry.state == ENTRY_STATE_SETUP_ERROR -async def test_config_passed_to_config_entry( - hass, config_entry, config_data, smarttub_api -): +async def test_config_passed_to_config_entry(hass, config_entry, config_data): """Test that configured options are loaded via config entry.""" config_entry.add_to_hass(hass) assert await async_setup_component(hass, smarttub.DOMAIN, config_data) -async def test_unload_entry(hass, config_entry, smarttub_api): +async def test_unload_entry(hass, config_entry): """Test being able to unload an entry.""" config_entry.add_to_hass(hass) diff --git a/tests/components/smarttub/test_sensor.py b/tests/components/smarttub/test_sensor.py new file mode 100644 index 00000000000..7d62440295e --- /dev/null +++ b/tests/components/smarttub/test_sensor.py @@ -0,0 +1,18 @@ +"""Test the SmartTub sensor platform.""" + +from . import trigger_update + + +async def test_state_update(spa, setup_entry, hass, smarttub_api): + """Test the state entity.""" + + entity_id = f"sensor.{spa.brand}_{spa.model}_state" + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "normal" + + spa.get_status.return_value["state"] = "BAD" + await trigger_update(hass) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "bad" From f045c0512b063569a18d7d40fa7c36ac757b3beb Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Sat, 20 Feb 2021 18:00:18 -0500 Subject: [PATCH 601/796] Fix Insteon config flow with add X10 and device override (#45854) --- homeassistant/components/insteon/schemas.py | 48 ++++++++++---------- tests/components/insteon/test_config_flow.py | 8 +++- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/insteon/schemas.py b/homeassistant/components/insteon/schemas.py index adc7e945eba..8698a358b21 100644 --- a/homeassistant/components/insteon/schemas.py +++ b/homeassistant/components/insteon/schemas.py @@ -187,47 +187,47 @@ def add_device_override(config_data, new_override): except ValueError as err: raise ValueError("Incorrect values") from err - overrides = config_data.get(CONF_OVERRIDE, []) + overrides = [] + + for override in config_data.get(CONF_OVERRIDE, []): + if override[CONF_ADDRESS] != address: + overrides.append(override) + curr_override = {} - - # If this address has an override defined, remove it - for override in overrides: - if override[CONF_ADDRESS] == address: - curr_override = override - break - if curr_override: - overrides.remove(curr_override) - curr_override[CONF_ADDRESS] = address curr_override[CONF_CAT] = cat curr_override[CONF_SUBCAT] = subcat overrides.append(curr_override) - config_data[CONF_OVERRIDE] = overrides - return config_data + + new_config = {} + if config_data.get(CONF_X10): + new_config[CONF_X10] = config_data[CONF_X10] + new_config[CONF_OVERRIDE] = overrides + return new_config def add_x10_device(config_data, new_x10): """Add a new X10 device to X10 device list.""" - curr_device = {} - x10_devices = config_data.get(CONF_X10, []) - for x10_device in x10_devices: + x10_devices = [] + for x10_device in config_data.get(CONF_X10, []): if ( - x10_device[CONF_HOUSECODE] == new_x10[CONF_HOUSECODE] - and x10_device[CONF_UNITCODE] == new_x10[CONF_UNITCODE] + x10_device[CONF_HOUSECODE] != new_x10[CONF_HOUSECODE] + or x10_device[CONF_UNITCODE] != new_x10[CONF_UNITCODE] ): - curr_device = x10_device - break - - if curr_device: - x10_devices.remove(curr_device) + x10_devices.append(x10_device) + curr_device = {} curr_device[CONF_HOUSECODE] = new_x10[CONF_HOUSECODE] curr_device[CONF_UNITCODE] = new_x10[CONF_UNITCODE] curr_device[CONF_PLATFORM] = new_x10[CONF_PLATFORM] curr_device[CONF_DIM_STEPS] = new_x10[CONF_DIM_STEPS] x10_devices.append(curr_device) - config_data[CONF_X10] = x10_devices - return config_data + + new_config = {} + if config_data.get(CONF_OVERRIDE): + new_config[CONF_OVERRIDE] = config_data[CONF_OVERRIDE] + new_config[CONF_X10] = x10_devices + return new_config def build_device_override_schema( diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index f1940b1eb39..1b08317ca30 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -369,13 +369,16 @@ async def test_options_add_device_override(hass: HomeAssistantType): CONF_CAT: "05", CONF_SUBCAT: "bb", } - await _options_form(hass, result2["flow_id"], user_input) + result3, _ = await _options_form(hass, result2["flow_id"], user_input) assert len(config_entry.options[CONF_OVERRIDE]) == 2 assert config_entry.options[CONF_OVERRIDE][1][CONF_ADDRESS] == "4D.5E.6F" assert config_entry.options[CONF_OVERRIDE][1][CONF_CAT] == 5 assert config_entry.options[CONF_OVERRIDE][1][CONF_SUBCAT] == 187 + # If result1 eq result2 the changes will not save + assert result["data"] != result3["data"] + async def test_options_remove_device_override(hass: HomeAssistantType): """Test removing a device override.""" @@ -477,6 +480,9 @@ async def test_options_add_x10_device(hass: HomeAssistantType): assert config_entry.options[CONF_X10][1][CONF_PLATFORM] == "binary_sensor" assert config_entry.options[CONF_X10][1][CONF_DIM_STEPS] == 15 + # If result2 eq result3 the changes will not save + assert result2["data"] != result3["data"] + async def test_options_remove_x10_device(hass: HomeAssistantType): """Test removing an X10 device.""" From 0cb1f61deb3393dc1f3701e88cc00be5376ef813 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 21 Feb 2021 00:07:04 +0000 Subject: [PATCH 602/796] [ci skip] Translation update --- .../components/abode/translations/ko.json | 13 ++++++- .../accuweather/translations/ko.json | 20 +++++++++++ .../components/acmeda/translations/ko.json | 3 ++ .../components/adguard/translations/ko.json | 9 +++-- .../advantage_air/translations/ko.json | 18 ++++++++++ .../components/aemet/translations/cs.json | 3 +- .../components/aemet/translations/ko.json | 21 ++++++++++++ .../components/agent_dvr/translations/ko.json | 3 +- .../components/airly/translations/ko.json | 5 +-- .../components/airnow/translations/ko.json | 21 ++++++++++++ .../components/airvisual/translations/cs.json | 4 ++- .../components/airvisual/translations/ko.json | 27 ++++++++++++--- .../alarmdecoder/translations/ko.json | 5 ++- .../components/almond/translations/ko.json | 7 ++-- .../ambiclimate/translations/ko.json | 8 +++-- .../ambient_station/translations/ko.json | 4 +-- .../components/apple_tv/translations/ko.json | 23 +++++++++++++ .../components/arcam_fmj/translations/ko.json | 3 +- .../components/asuswrt/translations/ko.json | 24 +++++++++++++ .../components/atag/translations/ko.json | 5 +-- .../components/august/translations/ko.json | 5 +-- .../components/aurora/translations/ko.json | 16 +++++++++ .../components/awair/translations/ko.json | 8 +++-- .../components/axis/translations/ko.json | 6 ++-- .../azure_devops/translations/ko.json | 12 +++++++ .../components/blebox/translations/ko.json | 6 ++-- .../components/blink/translations/ko.json | 4 +-- .../bmw_connected_drive/translations/ko.json | 19 +++++++++++ .../components/bond/translations/ko.json | 6 ++++ .../components/braviatv/translations/ko.json | 9 +++-- .../components/broadlink/translations/ko.json | 21 +++++++----- .../components/brother/translations/ko.json | 3 +- .../components/bsblan/translations/ko.json | 7 +++- .../components/canary/translations/ko.json | 10 +++--- .../components/cast/translations/ko.json | 6 ++-- .../cert_expiry/translations/ko.json | 2 +- .../cloudflare/translations/ko.json | 19 +++++++++++ .../coolmaster/translations/ko.json | 1 + .../coronavirus/translations/ko.json | 2 +- .../components/daikin/translations/ko.json | 8 ++++- .../components/deconz/translations/ko.json | 2 +- .../components/denonavr/translations/ko.json | 4 +-- .../devolo_home_control/translations/ko.json | 11 +++--- .../components/dexcom/translations/ko.json | 5 +++ .../dialogflow/translations/ko.json | 4 +++ .../components/doorbird/translations/ko.json | 2 +- .../components/dsmr/translations/ko.json | 2 +- .../components/dunehd/translations/ko.json | 2 +- .../components/eafm/translations/ko.json | 1 + .../components/ecobee/translations/ko.json | 3 ++ .../components/econet/translations/ko.json | 21 ++++++++++++ .../components/elgato/translations/ko.json | 6 +++- .../components/elkm1/translations/ko.json | 2 +- .../emulated_roku/translations/ko.json | 7 ++-- .../components/epson/translations/ko.json | 16 +++++++++ .../components/esphome/translations/ko.json | 5 +-- .../fireservicerota/translations/ko.json | 27 +++++++++++++++ .../components/firmata/translations/ko.json | 2 +- .../flick_electric/translations/ko.json | 4 +-- .../components/flo/translations/ko.json | 12 ++++--- .../components/flume/translations/ko.json | 4 +-- .../flunearyou/translations/ko.json | 5 ++- .../forked_daapd/translations/ko.json | 2 +- .../components/foscam/translations/ko.json | 22 ++++++++++++ .../components/freebox/translations/ko.json | 6 ++-- .../components/fritzbox/translations/ko.json | 17 ++++++++-- .../fritzbox_callmonitor/translations/ko.json | 21 ++++++++++++ .../garmin_connect/translations/ko.json | 4 +-- .../components/gdacs/translations/ko.json | 2 +- .../components/geofency/translations/ko.json | 4 +++ .../geonetnz_quakes/translations/ko.json | 2 +- .../geonetnz_volcano/translations/ko.json | 3 ++ .../components/gios/translations/ko.json | 6 ++-- .../components/glances/translations/ko.json | 8 ++--- .../components/goalzero/translations/ko.json | 20 +++++++++++ .../components/gogogate2/translations/ko.json | 2 +- .../components/gpslogger/translations/ko.json | 4 +++ .../components/gree/translations/ko.json | 13 +++++++ .../components/griddy/translations/ko.json | 4 +-- .../components/guardian/translations/ko.json | 5 +-- .../components/habitica/translations/cs.json | 16 +++++++++ .../components/habitica/translations/it.json | 20 +++++++++++ .../components/habitica/translations/ko.json | 17 ++++++++++ .../components/hangouts/translations/ko.json | 4 +-- .../components/harmony/translations/ko.json | 2 +- .../components/heos/translations/ko.json | 6 ++++ .../hisense_aehw4a1/translations/ko.json | 4 +-- .../components/hlk_sw16/translations/ko.json | 21 ++++++++++++ .../home_connect/translations/ko.json | 6 ++-- .../components/homekit/translations/ko.json | 34 +++++++++++++------ .../homekit_controller/translations/ko.json | 12 +++---- .../homematicip_cloud/translations/ko.json | 10 +++--- .../huawei_lte/translations/ko.json | 7 ++-- .../components/hue/translations/ko.json | 10 +++--- .../huisbaasje/translations/ko.json | 21 ++++++++++++ .../translations/ko.json | 2 +- .../hvv_departures/translations/ko.json | 2 +- .../components/hyperion/translations/ko.json | 22 ++++++++++++ .../components/iaqualink/translations/ko.json | 6 ++++ .../components/icloud/translations/ko.json | 14 ++++++-- .../components/ifttt/translations/ko.json | 4 +++ .../components/insteon/translations/ko.json | 20 +++++++---- .../components/ios/translations/ko.json | 4 +-- .../components/ipp/translations/ko.json | 6 ++-- .../components/iqvia/translations/ko.json | 2 +- .../islamic_prayer_times/translations/ko.json | 3 ++ .../components/izone/translations/ko.json | 4 +-- .../components/juicenet/translations/ko.json | 6 ++-- .../keenetic_ndms2/translations/cs.json | 21 ++++++++++++ .../keenetic_ndms2/translations/ko.json | 30 ++++++++++++++++ .../components/kodi/translations/ko.json | 26 ++++++++++---- .../components/konnected/translations/ko.json | 12 +++---- .../components/kulersky/translations/ko.json | 13 +++++++ .../components/life360/translations/ko.json | 9 ++++- .../components/lifx/translations/ko.json | 4 +-- .../components/local_ip/translations/ko.json | 3 +- .../components/locative/translations/ko.json | 6 +++- .../logi_circle/translations/ko.json | 8 +++-- .../components/luftdaten/translations/ko.json | 2 ++ .../lutron_caseta/translations/ko.json | 11 ++++-- .../components/lyric/translations/ko.json | 16 +++++++++ .../components/mailgun/translations/ko.json | 4 +++ .../components/mazda/translations/cs.json | 6 ++-- .../components/mazda/translations/ko.json | 27 +++++++++++++++ .../components/melcloud/translations/ko.json | 2 +- .../components/met/translations/ko.json | 3 ++ .../meteo_france/translations/ko.json | 4 +-- .../components/metoffice/translations/ko.json | 4 +-- .../components/mikrotik/translations/ko.json | 5 +-- .../components/mill/translations/ko.json | 5 ++- .../minecraft_server/translations/ko.json | 2 +- .../components/monoprice/translations/ko.json | 2 +- .../motion_blinds/translations/ko.json | 27 +++++++++++++++ .../components/mqtt/translations/ko.json | 7 ++-- .../components/myq/translations/ko.json | 4 +-- .../components/mysensors/translations/cs.json | 5 ++- .../components/mysensors/translations/ko.json | 16 +++++++++ .../components/neato/translations/ko.json | 19 +++++++++-- .../components/nest/translations/ko.json | 23 ++++++++++--- .../components/netatmo/translations/ko.json | 3 +- .../components/nexia/translations/ko.json | 4 +-- .../nightscout/translations/ko.json | 12 ++++++- .../components/notion/translations/ko.json | 3 +- .../components/nuheat/translations/ko.json | 4 +-- .../components/nuki/translations/ko.json | 18 ++++++++++ .../components/nut/translations/ko.json | 2 +- .../components/nws/translations/ko.json | 6 ++-- .../components/nzbget/translations/ko.json | 14 ++++---- .../components/omnilogic/translations/ko.json | 12 +++---- .../ondilo_ico/translations/ko.json | 16 +++++++++ .../components/onewire/translations/ko.json | 18 ++++++++++ .../components/onvif/translations/ko.json | 7 ++-- .../opentherm_gw/translations/ko.json | 3 +- .../components/openuv/translations/ko.json | 2 +- .../openweathermap/translations/ko.json | 10 ++++-- .../ovo_energy/translations/ko.json | 13 ++++++- .../components/owntracks/translations/ko.json | 3 ++ .../components/ozw/translations/ko.json | 12 ++++++- .../panasonic_viera/translations/ko.json | 10 +++--- .../philips_js/translations/ko.json | 18 ++++++++++ .../components/pi_hole/translations/ko.json | 9 +++-- .../components/plaato/translations/ko.json | 11 ++++-- .../components/plex/translations/ko.json | 9 ++--- .../components/plugwise/translations/ko.json | 16 ++++++--- .../plum_lightpad/translations/ko.json | 2 +- .../components/point/translations/ko.json | 5 +-- .../components/poolsense/translations/ko.json | 2 +- .../components/powerwall/translations/it.json | 2 +- .../components/powerwall/translations/ko.json | 9 +++-- .../components/profiler/translations/ko.json | 12 +++++++ .../progettihwsw/translations/ko.json | 6 ++-- .../components/ps4/translations/ko.json | 14 ++++---- .../pvpc_hourly_pricing/translations/ko.json | 2 +- .../components/rachio/translations/ko.json | 6 ++-- .../rainmachine/translations/ko.json | 5 ++- .../recollect_waste/translations/ko.json | 7 ++++ .../components/rfxtrx/translations/ko.json | 25 +++++++++++++- .../components/risco/translations/ko.json | 19 ++++++++--- .../translations/ca.json | 21 ++++++++++++ .../translations/it.json | 21 ++++++++++++ .../components/roku/translations/ko.json | 1 + .../components/roomba/translations/ko.json | 21 +++++++++++- .../components/roon/translations/ko.json | 2 +- .../components/rpi_power/translations/ko.json | 2 +- .../ruckus_unleashed/translations/ko.json | 21 ++++++++++++ .../components/samsungtv/translations/ko.json | 5 +-- .../components/sense/translations/ko.json | 2 +- .../components/sentry/translations/ko.json | 3 ++ .../components/sharkiq/translations/ko.json | 21 ++++++------ .../components/shelly/translations/ko.json | 14 ++++++-- .../shopping_list/translations/ko.json | 8 ++--- .../simplisafe/translations/ko.json | 13 +++++-- .../components/smappee/translations/ko.json | 7 ++-- .../components/smarthab/translations/ko.json | 4 ++- .../components/smarttub/translations/cs.json | 21 ++++++++++++ .../components/smarttub/translations/it.json | 22 ++++++++++++ .../components/smarttub/translations/ko.json | 20 +++++++++++ .../components/solaredge/translations/ko.json | 3 ++ .../components/solarlog/translations/ko.json | 2 +- .../components/soma/translations/ko.json | 2 +- .../components/somfy/translations/ko.json | 12 +++---- .../somfy_mylink/translations/ko.json | 25 ++++++++++++++ .../components/sonarr/translations/ko.json | 8 +++-- .../components/sonos/translations/ko.json | 4 +-- .../speedtestdotnet/translations/ko.json | 3 +- .../components/spider/translations/ko.json | 15 ++++++-- .../components/spotify/translations/ko.json | 6 ++-- .../srp_energy/translations/ko.json | 20 +++++++++++ .../synology_dsm/translations/ko.json | 16 +++++---- .../components/tado/translations/ko.json | 2 +- .../components/tasmota/translations/ko.json | 7 ++++ .../tellduslive/translations/ko.json | 12 ++++--- .../components/tesla/translations/ko.json | 9 +++++ .../components/tibber/translations/ko.json | 3 +- .../components/tile/translations/ko.json | 5 ++- .../components/toon/translations/ko.json | 5 +-- .../totalconnect/translations/ko.json | 5 ++- .../components/tplink/translations/ko.json | 4 +-- .../components/traccar/translations/ko.json | 6 +++- .../components/tradfri/translations/ko.json | 6 ++-- .../transmission/translations/ko.json | 5 +-- .../components/tuya/translations/it.json | 2 ++ .../components/tuya/translations/ko.json | 10 ++++++ .../twentemilieu/translations/ko.json | 4 +++ .../components/twilio/translations/ko.json | 6 +++- .../components/twinkly/translations/ko.json | 10 ++++++ .../components/unifi/translations/ko.json | 5 +-- .../components/upb/translations/ko.json | 7 ++-- .../components/upcloud/translations/ko.json | 16 +++++++++ .../components/upnp/translations/ko.json | 4 +-- .../components/velbus/translations/ko.json | 7 ++++ .../components/vera/translations/ko.json | 4 +-- .../components/vesync/translations/ko.json | 6 ++++ .../components/vilfo/translations/ko.json | 8 ++--- .../components/vizio/translations/ko.json | 3 +- .../components/volumio/translations/ko.json | 19 +++++++++++ .../components/wemo/translations/ko.json | 4 +-- .../components/wiffi/translations/ko.json | 2 +- .../components/wilight/translations/ko.json | 1 + .../components/withings/translations/ko.json | 11 +++--- .../components/wled/translations/ko.json | 6 +++- .../wolflink/translations/sensor.ko.json | 3 +- .../components/xbox/translations/ko.json | 17 ++++++++++ .../xiaomi_aqara/translations/ko.json | 9 ++--- .../xiaomi_miio/translations/cs.json | 6 ++++ .../xiaomi_miio/translations/it.json | 12 ++++++- .../xiaomi_miio/translations/ko.json | 11 ++++-- .../components/yeelight/translations/ko.json | 6 ++-- .../components/zha/translations/ko.json | 4 +-- .../zoneminder/translations/ko.json | 14 +++++--- .../components/zwave/translations/ko.json | 5 +-- .../components/zwave_js/translations/ko.json | 30 ++++++++++++++++ 252 files changed, 1910 insertions(+), 406 deletions(-) create mode 100644 homeassistant/components/advantage_air/translations/ko.json create mode 100644 homeassistant/components/aemet/translations/ko.json create mode 100644 homeassistant/components/airnow/translations/ko.json create mode 100644 homeassistant/components/apple_tv/translations/ko.json create mode 100644 homeassistant/components/asuswrt/translations/ko.json create mode 100644 homeassistant/components/aurora/translations/ko.json create mode 100644 homeassistant/components/azure_devops/translations/ko.json create mode 100644 homeassistant/components/bmw_connected_drive/translations/ko.json create mode 100644 homeassistant/components/cloudflare/translations/ko.json create mode 100644 homeassistant/components/econet/translations/ko.json create mode 100644 homeassistant/components/epson/translations/ko.json create mode 100644 homeassistant/components/fireservicerota/translations/ko.json create mode 100644 homeassistant/components/foscam/translations/ko.json create mode 100644 homeassistant/components/fritzbox_callmonitor/translations/ko.json create mode 100644 homeassistant/components/goalzero/translations/ko.json create mode 100644 homeassistant/components/gree/translations/ko.json create mode 100644 homeassistant/components/habitica/translations/cs.json create mode 100644 homeassistant/components/habitica/translations/it.json create mode 100644 homeassistant/components/habitica/translations/ko.json create mode 100644 homeassistant/components/hlk_sw16/translations/ko.json create mode 100644 homeassistant/components/huisbaasje/translations/ko.json create mode 100644 homeassistant/components/hyperion/translations/ko.json create mode 100644 homeassistant/components/keenetic_ndms2/translations/cs.json create mode 100644 homeassistant/components/keenetic_ndms2/translations/ko.json create mode 100644 homeassistant/components/kulersky/translations/ko.json create mode 100644 homeassistant/components/lyric/translations/ko.json create mode 100644 homeassistant/components/mazda/translations/ko.json create mode 100644 homeassistant/components/motion_blinds/translations/ko.json create mode 100644 homeassistant/components/mysensors/translations/ko.json create mode 100644 homeassistant/components/nuki/translations/ko.json create mode 100644 homeassistant/components/ondilo_ico/translations/ko.json create mode 100644 homeassistant/components/onewire/translations/ko.json create mode 100644 homeassistant/components/philips_js/translations/ko.json create mode 100644 homeassistant/components/profiler/translations/ko.json create mode 100644 homeassistant/components/recollect_waste/translations/ko.json create mode 100644 homeassistant/components/rituals_perfume_genie/translations/ca.json create mode 100644 homeassistant/components/rituals_perfume_genie/translations/it.json create mode 100644 homeassistant/components/ruckus_unleashed/translations/ko.json create mode 100644 homeassistant/components/smarttub/translations/cs.json create mode 100644 homeassistant/components/smarttub/translations/it.json create mode 100644 homeassistant/components/smarttub/translations/ko.json create mode 100644 homeassistant/components/somfy_mylink/translations/ko.json create mode 100644 homeassistant/components/srp_energy/translations/ko.json create mode 100644 homeassistant/components/tasmota/translations/ko.json create mode 100644 homeassistant/components/twinkly/translations/ko.json create mode 100644 homeassistant/components/upcloud/translations/ko.json create mode 100644 homeassistant/components/volumio/translations/ko.json create mode 100644 homeassistant/components/xbox/translations/ko.json create mode 100644 homeassistant/components/zwave_js/translations/ko.json diff --git a/homeassistant/components/abode/translations/ko.json b/homeassistant/components/abode/translations/ko.json index 06c301550b4..a9756447adf 100644 --- a/homeassistant/components/abode/translations/ko.json +++ b/homeassistant/components/abode/translations/ko.json @@ -1,9 +1,20 @@ { "config": { "abort": { - "single_instance_allowed": "\ud558\ub098\uc758 Abode \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c" + } + }, "user": { "data": { "password": "\ube44\ubc00\ubc88\ud638", diff --git a/homeassistant/components/accuweather/translations/ko.json b/homeassistant/components/accuweather/translations/ko.json index b04778c8cb2..2f0a01e094b 100644 --- a/homeassistant/components/accuweather/translations/ko.json +++ b/homeassistant/components/accuweather/translations/ko.json @@ -1,9 +1,29 @@ { "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, "step": { "user": { + "data": { + "api_key": "API \ud0a4", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "name": "\uc774\ub984" + }, "description": "\uad6c\uc131\uc5d0 \ub300\ud55c \ub3c4\uc6c0\uc774 \ud544\uc694\ud55c \uacbd\uc6b0 \ub2e4\uc74c\uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694:\nhttps://www.home-assistant.io/integrations/accuweather/\n\n\uc77c\ubd80 \uc13c\uc11c\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc5f0\ub3d9 \uad6c\uc131 \ud6c4 \uad6c\uc131\uc694\uc18c \ub808\uc9c0\uc2a4\ud2b8\ub9ac\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\n\uc77c\uae30\uc608\ubcf4\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc5f0\ub3d9 \uc635\uc158\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." } } + }, + "options": { + "step": { + "user": { + "description": "\ubb34\ub8cc \ubc84\uc804\uc758 AccuWeather API \ud0a4\ub85c \uc77c\uae30\uc608\ubcf4\ub97c \ud65c\uc131\ud654\ud55c \uacbd\uc6b0 \uc81c\ud55c\uc0ac\ud56d\uc73c\ub85c \uc778\ud574 \uc5c5\ub370\uc774\ud2b8\ub294 40 \ubd84\uc774 \uc544\ub2cc 80 \ubd84\ub9c8\ub2e4 \uc218\ud589\ub429\ub2c8\ub2e4." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/ko.json b/homeassistant/components/acmeda/translations/ko.json index 345628eef02..098d3a952f5 100644 --- a/homeassistant/components/acmeda/translations/ko.json +++ b/homeassistant/components/acmeda/translations/ko.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/adguard/translations/ko.json b/homeassistant/components/adguard/translations/ko.json index e17bca1f0a2..b14e627ca56 100644 --- a/homeassistant/components/adguard/translations/ko.json +++ b/homeassistant/components/adguard/translations/ko.json @@ -2,7 +2,10 @@ "config": { "abort": { "existing_instance_updated": "\uae30\uc874 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4.", - "single_instance_allowed": "\ud558\ub098\uc758 AdGuard Home \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "hassio_confirm": { @@ -14,9 +17,9 @@ "host": "\ud638\uc2a4\ud2b8", "password": "\ube44\ubc00\ubc88\ud638", "port": "\ud3ec\ud2b8", - "ssl": "AdGuard Home \uc740 SSL \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4", + "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", - "verify_ssl": "AdGuard Home \uc740 \uc62c\ubc14\ub978 \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4" + "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" }, "description": "\ubaa8\ub2c8\ud130\ub9c1 \ubc0f \uc81c\uc5b4\uac00 \uac00\ub2a5\ud558\ub3c4\ub85d AdGuard Home \uc778\uc2a4\ud134\uc2a4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694." } diff --git a/homeassistant/components/advantage_air/translations/ko.json b/homeassistant/components/advantage_air/translations/ko.json new file mode 100644 index 00000000000..444d8d38285 --- /dev/null +++ b/homeassistant/components/advantage_air/translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \uc8fc\uc18c", + "port": "\ud3ec\ud2b8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/cs.json b/homeassistant/components/aemet/translations/cs.json index d31920a8745..d892d4c6dc3 100644 --- a/homeassistant/components/aemet/translations/cs.json +++ b/homeassistant/components/aemet/translations/cs.json @@ -11,7 +11,8 @@ "data": { "api_key": "Kl\u00ed\u010d API", "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", - "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka" + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", + "name": "N\u00e1zev integrace" } } } diff --git a/homeassistant/components/aemet/translations/ko.json b/homeassistant/components/aemet/translations/ko.json new file mode 100644 index 00000000000..edfb023a88b --- /dev/null +++ b/homeassistant/components/aemet/translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "api_key": "API \ud0a4", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc774\ub984" + }, + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/ko.json b/homeassistant/components/agent_dvr/translations/ko.json index 30b96c00b63..add0b917100 100644 --- a/homeassistant/components/agent_dvr/translations/ko.json +++ b/homeassistant/components/agent_dvr/translations/ko.json @@ -4,7 +4,8 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4." + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/airly/translations/ko.json b/homeassistant/components/airly/translations/ko.json index a0b20ed8c44..95981ea5eb1 100644 --- a/homeassistant/components/airly/translations/ko.json +++ b/homeassistant/components/airly/translations/ko.json @@ -1,9 +1,10 @@ { "config": { "abort": { - "already_configured": "\uc774 \uc88c\ud45c\uc5d0 \ub300\ud55c Airly \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "error": { + "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "wrong_location": "\uc774 \uc9c0\uc5ed\uc5d0\ub294 Airly \uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc774 \uc5c6\uc2b5\ub2c8\ub2e4." }, "step": { @@ -12,7 +13,7 @@ "api_key": "API \ud0a4", "latitude": "\uc704\ub3c4", "longitude": "\uacbd\ub3c4", - "name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc758 \uc774\ub984" + "name": "\uc774\ub984" }, "description": "Airly \uacf5\uae30 \ud488\uc9c8 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://developer.airly.eu/register \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694", "title": "Airly" diff --git a/homeassistant/components/airnow/translations/ko.json b/homeassistant/components/airnow/translations/ko.json new file mode 100644 index 00000000000..6da62dffa2c --- /dev/null +++ b/homeassistant/components/airnow/translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "api_key": "API \ud0a4", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/cs.json b/homeassistant/components/airvisual/translations/cs.json index 0ff97bbacfe..75720b0f30b 100644 --- a/homeassistant/components/airvisual/translations/cs.json +++ b/homeassistant/components/airvisual/translations/cs.json @@ -26,7 +26,9 @@ }, "geography_by_name": { "data": { - "api_key": "Kl\u00ed\u010d API" + "api_key": "Kl\u00ed\u010d API", + "city": "M\u011bsto", + "country": "Zem\u011b" } }, "node_pro": { diff --git a/homeassistant/components/airvisual/translations/ko.json b/homeassistant/components/airvisual/translations/ko.json index d25df4c213f..8cf450e597b 100644 --- a/homeassistant/components/airvisual/translations/ko.json +++ b/homeassistant/components/airvisual/translations/ko.json @@ -1,11 +1,13 @@ { "config": { "abort": { - "already_configured": "\uc88c\ud45c\uac12 \ub610\ub294 Node/Pro ID \uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "Node/Pro ID \uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uac70\ub098 \uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "general_error": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", - "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "general_error": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", + "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "geography": { @@ -17,14 +19,31 @@ "description": "AirVisual \ud074\ub77c\uc6b0\ub4dc API \ub97c \uc0ac\uc6a9\ud558\uc5ec \uc9c0\ub9ac\uc801 \uc704\uce58\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4.", "title": "\uc9c0\ub9ac\uc801 \uc704\uce58 \uad6c\uc131\ud558\uae30" }, + "geography_by_coords": { + "data": { + "api_key": "API \ud0a4", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4" + } + }, + "geography_by_name": { + "data": { + "api_key": "API \ud0a4" + } + }, "node_pro": { "data": { - "ip_address": "\uae30\uae30 IP \uc8fc\uc18c/\ud638\uc2a4\ud2b8 \uc774\ub984", + "ip_address": "\ud638\uc2a4\ud2b8", "password": "\ube44\ubc00\ubc88\ud638" }, "description": "\uc0ac\uc6a9\uc790\uc758 AirVisual \uae30\uae30\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4. \uae30\uae30\uc758 UI \uc5d0\uc11c \ube44\ubc00\ubc88\ud638\ub97c \ucc3e\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "title": "AirVisual Node/Pro \uad6c\uc131\ud558\uae30" }, + "reauth_confirm": { + "data": { + "api_key": "API \ud0a4" + } + }, "user": { "data": { "cloud_api": "\uc9c0\ub9ac\uc801 \uc704\uce58", diff --git a/homeassistant/components/alarmdecoder/translations/ko.json b/homeassistant/components/alarmdecoder/translations/ko.json index ed29a3260ef..08383d37151 100644 --- a/homeassistant/components/alarmdecoder/translations/ko.json +++ b/homeassistant/components/alarmdecoder/translations/ko.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "already_configured": "\uc7a5\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4." + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "create_entry": { "default": "AlarmDecoder\uc5d0 \uc131\uacf5\uc801\uc73c\ub85c \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, "step": { "protocol": { "data": { diff --git a/homeassistant/components/almond/translations/ko.json b/homeassistant/components/almond/translations/ko.json index eff796699e3..062ef885c70 100644 --- a/homeassistant/components/almond/translations/ko.json +++ b/homeassistant/components/almond/translations/ko.json @@ -1,9 +1,10 @@ { "config": { "abort": { - "cannot_connect": "Almond \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", - "missing_configuration": "Almond \uc124\uc815 \ubc29\ubc95\uc5d0 \ub300\ud55c \uc124\uba85\uc11c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694.", - "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", + "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/ambiclimate/translations/ko.json b/homeassistant/components/ambiclimate/translations/ko.json index 2a5e9280aa7..c28affbebb3 100644 --- a/homeassistant/components/ambiclimate/translations/ko.json +++ b/homeassistant/components/ambiclimate/translations/ko.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070 \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4." + "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070 \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." }, "create_entry": { - "default": "Ambi Climate \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { "follow_link": "\ud655\uc778\uc744 \ud074\ub9ad\ud558\uae30 \uc804\uc5d0 \ub9c1\ud06c\ub97c \ub530\ub77c \uc778\uc99d\uc744 \ubc1b\uc544\uc8fc\uc138\uc694", @@ -12,7 +14,7 @@ }, "step": { "auth": { - "description": "[\ub9c1\ud06c]({authorization_url}) \ub97c \ud074\ub9ad\ud558\uc5ec Ambi Climate \uacc4\uc815\uc5d0 \ub300\ud574 **\ud5c8\uc6a9**\ud55c \ub2e4\uc74c, \ub2e4\uc2dc \ub3cc\uc544\uc640\uc11c \ud558\ub2e8\uc758 **\ud655\uc778**\uc744 \ud074\ub9ad\ud574\uc8fc\uc138\uc694. \n(\ucf5c\ubc31 url \uc744 {cb_url} \ub85c \uad6c\uc131\ud588\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694)", + "description": "[\ub9c1\ud06c]({authorization_url}) \ub97c \ud074\ub9ad\ud558\uc5ec Ambiclimate \uacc4\uc815\uc5d0 \ub300\ud574 **\ud5c8\uc6a9**\ud55c \ub2e4\uc74c, \ub2e4\uc2dc \ub3cc\uc544\uc640\uc11c \ud558\ub2e8\uc758 **\ud655\uc778**\uc744 \ud074\ub9ad\ud574\uc8fc\uc138\uc694.\n(\ucf5c\ubc31 URL \uc774 {cb_url} \ub85c \uc9c0\uc815\ub418\uc5c8\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694)", "title": "Ambi Climate \uc778\uc99d\ud558\uae30" } } diff --git a/homeassistant/components/ambient_station/translations/ko.json b/homeassistant/components/ambient_station/translations/ko.json index d4e227656c2..6fc8f4b17fc 100644 --- a/homeassistant/components/ambient_station/translations/ko.json +++ b/homeassistant/components/ambient_station/translations/ko.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\uc774 \uc571 \ud0a4\ub294 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4." + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "invalid_key": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "no_devices": "\uacc4\uc815\uc5d0 \uae30\uae30\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" }, "step": { diff --git a/homeassistant/components/apple_tv/translations/ko.json b/homeassistant/components/apple_tv/translations/ko.json new file mode 100644 index 00000000000..c7e664b0638 --- /dev/null +++ b/homeassistant/components/apple_tv/translations/ko.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured_device": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "pair_with_pin": { + "data": { + "pin": "PIN \ucf54\ub4dc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/ko.json b/homeassistant/components/arcam_fmj/translations/ko.json index 62b5a54928e..532e5ef4c5f 100644 --- a/homeassistant/components/arcam_fmj/translations/ko.json +++ b/homeassistant/components/arcam_fmj/translations/ko.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4." + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "flow_title": "Arcam FMJ: {host}", "step": { diff --git a/homeassistant/components/asuswrt/translations/ko.json b/homeassistant/components/asuswrt/translations/ko.json new file mode 100644 index 00000000000..de3de06e6b1 --- /dev/null +++ b/homeassistant/components/asuswrt/translations/ko.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_host": "\ud638\uc2a4\ud2b8\uba85 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "mode": "\ubaa8\ub4dc", + "name": "\uc774\ub984", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/ko.json b/homeassistant/components/atag/translations/ko.json index c09b4f7b249..9b0c1ea1b36 100644 --- a/homeassistant/components/atag/translations/ko.json +++ b/homeassistant/components/atag/translations/ko.json @@ -1,15 +1,16 @@ { "config": { "abort": { - "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 HomeAssistant \uc5d0 \ucd94\uac00\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "unauthorized": "\ud398\uc5b4\ub9c1\uc774 \uac70\ubd80\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc778\uc99d \uc694\uccad \uae30\uae30\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694" }, "step": { "user": { "data": { - "email": "\uc774\uba54\uc77c (\uc120\ud0dd \uc0ac\ud56d)", + "email": "\uc774\uba54\uc77c", "host": "\ud638\uc2a4\ud2b8", "port": "\ud3ec\ud2b8" }, diff --git a/homeassistant/components/august/translations/ko.json b/homeassistant/components/august/translations/ko.json index 52f939c45a0..e7aed3d4c2c 100644 --- a/homeassistant/components/august/translations/ko.json +++ b/homeassistant/components/august/translations/ko.json @@ -1,10 +1,11 @@ { "config": { "abort": { - "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, diff --git a/homeassistant/components/aurora/translations/ko.json b/homeassistant/components/aurora/translations/ko.json new file mode 100644 index 00000000000..ea10c059f03 --- /dev/null +++ b/homeassistant/components/aurora/translations/ko.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "name": "\uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/ko.json b/homeassistant/components/awair/translations/ko.json index 977532de45d..22677f8ab45 100644 --- a/homeassistant/components/awair/translations/ko.json +++ b/homeassistant/components/awair/translations/ko.json @@ -1,11 +1,13 @@ { "config": { "abort": { - "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "reauth_successful": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc131\uacf5\uc801\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "unknown": "\uc54c \uc218 \uc5c6\ub294 Awair API \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4." + "invalid_access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "reauth": { diff --git a/homeassistant/components/axis/translations/ko.json b/homeassistant/components/axis/translations/ko.json index f73d467fbf7..d9e0114a97a 100644 --- a/homeassistant/components/axis/translations/ko.json +++ b/homeassistant/components/axis/translations/ko.json @@ -7,9 +7,11 @@ }, "error": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4." + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, - "flow_title": "Axis \uae30\uae30: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/azure_devops/translations/ko.json b/homeassistant/components/azure_devops/translations/ko.json new file mode 100644 index 00000000000..555c548a142 --- /dev/null +++ b/homeassistant/components/azure_devops/translations/ko.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/ko.json b/homeassistant/components/blebox/translations/ko.json index ff3fa740092..81c7bf3af48 100644 --- a/homeassistant/components/blebox/translations/ko.json +++ b/homeassistant/components/blebox/translations/ko.json @@ -2,11 +2,11 @@ "config": { "abort": { "address_already_configured": "BleBox \uae30\uae30\uac00 {address} \ub85c \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "already_configured": "BleBox \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "BleBox \uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. (\ub85c\uadf8\uc5d0\uc11c \uc624\ub958 \ub0b4\uc6a9\uc744 \ud655\uc778\ud574\ubcf4\uc138\uc694.)", - "unknown": "BleBox \uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. (\ub85c\uadf8\uc5d0\uc11c \uc624\ub958 \ub0b4\uc6a9\uc744 \ud655\uc778\ud574\ubcf4\uc138\uc694.)", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", "unsupported_version": "BleBox \uae30\uae30 \ud38c\uc6e8\uc5b4\uac00 \uc624\ub798\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uba3c\uc800 \uc5c5\uadf8\ub808\uc774\ub4dc\ud574\uc8fc\uc138\uc694." }, "flow_title": "BleBox \uae30\uae30: {name} ({host})", diff --git a/homeassistant/components/blink/translations/ko.json b/homeassistant/components/blink/translations/ko.json index ac8c96e4f2d..35ef0cefdef 100644 --- a/homeassistant/components/blink/translations/ko.json +++ b/homeassistant/components/blink/translations/ko.json @@ -4,8 +4,8 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", - "invalid_access_token": "\uc798\ubabb\ub41c \uc778\uc99d", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, diff --git a/homeassistant/components/bmw_connected_drive/translations/ko.json b/homeassistant/components/bmw_connected_drive/translations/ko.json new file mode 100644 index 00000000000..9cc079cf1cd --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/ko.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/ko.json b/homeassistant/components/bond/translations/ko.json index 61576d70431..b44db53f7c8 100644 --- a/homeassistant/components/bond/translations/ko.json +++ b/homeassistant/components/bond/translations/ko.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", @@ -8,6 +11,9 @@ "flow_title": "\ubcf8\ub4dc : {bond_id} ( {host} )", "step": { "confirm": { + "data": { + "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070" + }, "description": "{bond_id} \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" }, "user": { diff --git a/homeassistant/components/braviatv/translations/ko.json b/homeassistant/components/braviatv/translations/ko.json index 3a82c38f904..0bfb6b3f1b2 100644 --- a/homeassistant/components/braviatv/translations/ko.json +++ b/homeassistant/components/braviatv/translations/ko.json @@ -1,16 +1,19 @@ { "config": { "abort": { - "already_configured": "\uc774 TV \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "no_ip_control": "TV \uc5d0\uc11c IP \uc81c\uc5b4\uac00 \ube44\ud65c\uc131\ud654\ub418\uc5c8\uac70\ub098 TV \uac00 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8 \ub610\ub294 PIN \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "invalid_host": "\ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_host": "\ud638\uc2a4\ud2b8\uba85 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unsupported_model": "\uc774 TV \ubaa8\ub378\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." }, "step": { "authorize": { + "data": { + "pin": "PIN \ucf54\ub4dc" + }, "description": "Sony Bravia TV \uc5d0 \ud45c\uc2dc\ub41c PIN \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\nPIN \ucf54\ub4dc\uac00 \ud45c\uc2dc\ub418\uc9c0 \uc54a\uc73c\uba74 TV \uc5d0\uc11c Home Assistant \ub97c \ub4f1\ub85d \ud574\uc81c\ud558\uc5ec\uc57c \ud569\ub2c8\ub2e4. Settings -> Network -> Remote device settings -> Unregister remote device \ub85c \uc774\ub3d9\ud558\uc5ec \ub4f1\ub85d\uc744 \ud574\uc81c\ud574\uc8fc\uc138\uc694.", "title": "Sony Bravia TV \uc2b9\uc778\ud558\uae30" }, diff --git a/homeassistant/components/broadlink/translations/ko.json b/homeassistant/components/broadlink/translations/ko.json index b27391e1100..13cd17a8475 100644 --- a/homeassistant/components/broadlink/translations/ko.json +++ b/homeassistant/components/broadlink/translations/ko.json @@ -1,16 +1,17 @@ { "config": { "abort": { - "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "already_in_progress": "\uc774 \uae30\uae30\uc5d0 \ub300\ud574 \uc774\ubbf8 \uc9c4\ud589\uc911\uc778 \uad6c\uc131\uc774 \uc788\uc2b5\ub2c8\ub2e4.", - "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", - "invalid_host": "\uc798\ubabb\ub41c \ud638\uc2a4\ud2b8\uba85 \ub610\ub294 IP \uc8fc\uc18c", + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_host": "\ud638\uc2a4\ud2b8\uba85 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "not_supported": "\uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \uc7a5\uce58", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_host": "\ud638\uc2a4\ud2b8\uba85 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "flow_title": "{name} ({host} \uc758 {model})", "step": { @@ -18,6 +19,9 @@ "title": "\uc7a5\uce58\uc5d0 \uc778\uc99d" }, "finish": { + "data": { + "name": "\uc774\ub984" + }, "title": "\uc7a5\uce58 \uc774\ub984\uc744 \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624" }, "reset": { @@ -27,11 +31,12 @@ "data": { "unlock": "\uc608" }, - "description": "\uc7a5\uce58\uac00 \uc7a0\uaca8 \uc788\uc2b5\ub2c8\ub2e4. \uc774\ub85c \uc778\ud574 Home Assistant\uc5d0\uc11c \uc778\uc99d \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc7a0\uae08\uc744 \ud574\uc81c \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "{name} ({host} \uc758 {model}) \uc774(\uac00) \uc7a0\uaca8 \uc788\uc2b5\ub2c8\ub2e4. \uc774\ub85c \uc778\ud574 Home Assistant \uc5d0\uc11c \uc778\uc99d \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc7a0\uae08\uc744 \ud574\uc81c\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "\uc7a5\uce58 \uc7a0\uae08 \ud574\uc81c (\uc635\uc158)" }, "user": { "data": { + "host": "\ud638\uc2a4\ud2b8", "timeout": "\uc81c\ud55c \uc2dc\uac04" }, "title": "\uc7a5\uce58\uc5d0 \uc5f0\uacb0" diff --git a/homeassistant/components/brother/translations/ko.json b/homeassistant/components/brother/translations/ko.json index a54aea7f108..47722afdae5 100644 --- a/homeassistant/components/brother/translations/ko.json +++ b/homeassistant/components/brother/translations/ko.json @@ -1,10 +1,11 @@ { "config": { "abort": { - "already_configured": "\uc774 \ud504\ub9b0\ud130\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unsupported_model": "\uc774 \ud504\ub9b0\ud130 \ubaa8\ub378\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." }, "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "snmp_error": "SNMP \uc11c\ubc84\uac00 \uaebc\uc838 \uc788\uac70\ub098 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \ud504\ub9b0\ud130\uc785\ub2c8\ub2e4.", "wrong_host": "\ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, diff --git a/homeassistant/components/bsblan/translations/ko.json b/homeassistant/components/bsblan/translations/ko.json index 41b421ff817..85703c7eeb7 100644 --- a/homeassistant/components/bsblan/translations/ko.json +++ b/homeassistant/components/bsblan/translations/ko.json @@ -3,13 +3,18 @@ "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, "flow_title": "BSB-Lan: {name}", "step": { "user": { "data": { "host": "\ud638\uc2a4\ud2b8", "passkey": "\ud328\uc2a4\ud0a4 \ubb38\uc790\uc5f4", - "port": "\ud3ec\ud2b8" + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, "description": "Home Assistant \uc5d0 BSB-Lan \uae30\uae30 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4.", "title": "BSB-Lan \uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uae30" diff --git a/homeassistant/components/canary/translations/ko.json b/homeassistant/components/canary/translations/ko.json index 0b1d82bb20a..d02344a9027 100644 --- a/homeassistant/components/canary/translations/ko.json +++ b/homeassistant/components/canary/translations/ko.json @@ -1,18 +1,18 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uc124\uc815\ub418\uc5b4 \uc788\uc74c. \ud558\ub098\uc758 \uc124\uc815\ub9cc \uac00\ub2a5\ud568.", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec \ubc1c\uc0dd" + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328" + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "flow_title": "Canary: {name}", "step": { "user": { "data": { - "password": "\uc554\ud638", - "username": "\uc0ac\uc6a9\uc790\uba85" + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, "title": "Canary\uc5d0 \uc5f0\uacb0" } diff --git a/homeassistant/components/cast/translations/ko.json b/homeassistant/components/cast/translations/ko.json index e57fceb7705..7011a61f757 100644 --- a/homeassistant/components/cast/translations/ko.json +++ b/homeassistant/components/cast/translations/ko.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "no_devices_found": "Google \uce90\uc2a4\ud2b8 \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", - "single_instance_allowed": "\ud558\ub098\uc758 Google \uce90\uc2a4\ud2b8\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." }, "step": { "confirm": { - "description": "Google \uce90\uc2a4\ud2b8\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" } } } diff --git a/homeassistant/components/cert_expiry/translations/ko.json b/homeassistant/components/cert_expiry/translations/ko.json index ee912a33695..87827769771 100644 --- a/homeassistant/components/cert_expiry/translations/ko.json +++ b/homeassistant/components/cert_expiry/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "import_failed": "\uad6c\uc131\uc5d0\uc11c \uac00\uc838\uc624\uae30 \uc2e4\ud328" }, "error": { diff --git a/homeassistant/components/cloudflare/translations/ko.json b/homeassistant/components/cloudflare/translations/ko.json new file mode 100644 index 00000000000..d4f4eee49a8 --- /dev/null +++ b/homeassistant/components/cloudflare/translations/ko.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "api_token": "API \ud1a0\ud070" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/translations/ko.json b/homeassistant/components/coolmaster/translations/ko.json index 5d0636bddcd..82b88394431 100644 --- a/homeassistant/components/coolmaster/translations/ko.json +++ b/homeassistant/components/coolmaster/translations/ko.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "no_units": "CoolMasterNet \ud638\uc2a4\ud2b8\uc5d0\uc11c HVAC \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." }, "step": { diff --git a/homeassistant/components/coronavirus/translations/ko.json b/homeassistant/components/coronavirus/translations/ko.json index 65eec9e8bb7..873aca88e30 100644 --- a/homeassistant/components/coronavirus/translations/ko.json +++ b/homeassistant/components/coronavirus/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uc774 \uad6d\uac00\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/daikin/translations/ko.json b/homeassistant/components/daikin/translations/ko.json index 9c4a6c8d50c..e87db9f29d3 100644 --- a/homeassistant/components/daikin/translations/ko.json +++ b/homeassistant/components/daikin/translations/ko.json @@ -4,13 +4,19 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, "step": { "user": { "data": { + "api_key": "API \ud0a4", "host": "\ud638\uc2a4\ud2b8", "password": "\ube44\ubc00\ubc88\ud638" }, - "description": "\ub2e4\uc774\ud0a8 \uc5d0\uc5b4\ucee8\uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\nAPI \ud0a4 \ubc0f \ube44\ubc00\ubc88\ud638\ub294 BRP072Cxx \uc640 SKYFi \uae30\uae30\uc5d0\uc11c \uc0ac\uc6a9\ub41c\ub2e4\ub294 \uc810\uc5d0 \uc720\uc758\ud558\uc138\uc694.", + "description": "\ub2e4\uc774\ud0a8 \uc5d0\uc5b4\ucee8\uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\nAPI \ud0a4 \ubc0f \ube44\ubc00\ubc88\ud638\ub294 BRP072Cxx \uc640 SKYFi \uae30\uae30\uc5d0\uc11c \uc0ac\uc6a9\ub41c\ub2e4\ub294 \uc810\uc5d0 \uc720\uc758\ud574\uc8fc\uc138\uc694.", "title": "\ub2e4\uc774\ud0a8 \uc5d0\uc5b4\ucee8 \uad6c\uc131\ud558\uae30" } } diff --git a/homeassistant/components/deconz/translations/ko.json b/homeassistant/components/deconz/translations/ko.json index 6c7dde04e31..bd8aef75dd6 100644 --- a/homeassistant/components/deconz/translations/ko.json +++ b/homeassistant/components/deconz/translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\ube0c\ub9ac\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "already_in_progress": "\ube0c\ub9ac\uc9c0 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", "no_bridges": "\ubc1c\uacac\ub41c deCONZ \ube0c\ub9ac\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", "not_deconz_bridge": "deCONZ \ube0c\ub9ac\uc9c0\uac00 \uc544\ub2d9\ub2c8\ub2e4", "updated_instance": "deCONZ \uc778\uc2a4\ud134\uc2a4\ub97c \uc0c8\ub85c\uc6b4 \ud638\uc2a4\ud2b8 \uc8fc\uc18c\ub85c \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/denonavr/translations/ko.json b/homeassistant/components/denonavr/translations/ko.json index f995b852a57..71562ac53a4 100644 --- a/homeassistant/components/denonavr/translations/ko.json +++ b/homeassistant/components/denonavr/translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "already_in_progress": "Denon AVR \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", "not_denonavr_manufacturer": "Denon AVR \ub124\ud2b8\uc6cc\ud06c \ub9ac\uc2dc\ubc84\uac00 \uc544\ub2d9\ub2c8\ub2e4. \ubc1c\uacac\ub41c \uc81c\uc870\uc0ac\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", "not_denonavr_missing": "Denon AVR \ub124\ud2b8\uc6cc\ud06c \ub9ac\uc2dc\ubc84\uac00 \uc544\ub2d9\ub2c8\ub2e4. \uac80\uc0c9 \uc815\ubcf4\uac00 \uc644\uc804\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" }, @@ -17,7 +17,7 @@ }, "select": { "data": { - "select_host": "\ub9ac\uc2dc\ubc84 IP" + "select_host": "\ub9ac\uc2dc\ubc84 IP \uc8fc\uc18c" }, "description": "\ub9ac\uc2dc\ubc84 \uc5f0\uacb0\uc744 \ucd94\uac00\ud558\ub824\uba74 \uc124\uc815\uc744 \ub2e4\uc2dc \uc2e4\ud589\ud574\uc8fc\uc138\uc694", "title": "\uc5f0\uacb0\ud560 \ub9ac\uc2dc\ubc84\ub97c \uc120\ud0dd\ud558\uae30" diff --git a/homeassistant/components/devolo_home_control/translations/ko.json b/homeassistant/components/devolo_home_control/translations/ko.json index 17d4fe28a56..f21122bff70 100644 --- a/homeassistant/components/devolo_home_control/translations/ko.json +++ b/homeassistant/components/devolo_home_control/translations/ko.json @@ -1,15 +1,18 @@ { "config": { "abort": { - "already_configured": "\uc774 Home Control Central \uc720\ub2db\uc740 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4." + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "user": { "data": { - "home_control_url": "Home Control URL", - "mydevolo_url": "mydevolo URL", + "home_control_url": "Home Control URL \uc8fc\uc18c", + "mydevolo_url": "mydevolo URL \uc8fc\uc18c", "password": "\ube44\ubc00\ubc88\ud638", - "username": "\uc774\uba54\uc77c \uc8fc\uc18c / devolo ID" + "username": "\uc774\uba54\uc77c / devolo ID" } } } diff --git a/homeassistant/components/dexcom/translations/ko.json b/homeassistant/components/dexcom/translations/ko.json index 35129cbfbde..c3daac03356 100644 --- a/homeassistant/components/dexcom/translations/ko.json +++ b/homeassistant/components/dexcom/translations/ko.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { diff --git a/homeassistant/components/dialogflow/translations/ko.json b/homeassistant/components/dialogflow/translations/ko.json index 7afeb6da74c..2b1be9657b4 100644 --- a/homeassistant/components/dialogflow/translations/ko.json +++ b/homeassistant/components/dialogflow/translations/ko.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", + "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4." + }, "create_entry": { "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Dialogflow \uc6f9 \ud6c5]({dialogflow_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/json\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, diff --git a/homeassistant/components/doorbird/translations/ko.json b/homeassistant/components/doorbird/translations/ko.json index 74057a94d26..819b3b51d10 100644 --- a/homeassistant/components/doorbird/translations/ko.json +++ b/homeassistant/components/doorbird/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uc774 DoorBird \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "link_local_address": "\ub85c\uceec \uc8fc\uc18c \uc5f0\uacb0\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", "not_doorbird_device": "\uc774 \uae30\uae30\ub294 DoorBird \uac00 \uc544\ub2d9\ub2c8\ub2e4" }, diff --git a/homeassistant/components/dsmr/translations/ko.json b/homeassistant/components/dsmr/translations/ko.json index 9c8fbbe80a9..17dee71d640 100644 --- a/homeassistant/components/dsmr/translations/ko.json +++ b/homeassistant/components/dsmr/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uc7a5\uce58\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4." + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" } } } \ No newline at end of file diff --git a/homeassistant/components/dunehd/translations/ko.json b/homeassistant/components/dunehd/translations/ko.json index 1ddcadf8350..45a59b4d75b 100644 --- a/homeassistant/components/dunehd/translations/ko.json +++ b/homeassistant/components/dunehd/translations/ko.json @@ -6,7 +6,7 @@ "error": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "invalid_host": "\ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "invalid_host": "\ud638\uc2a4\ud2b8\uba85 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/eafm/translations/ko.json b/homeassistant/components/eafm/translations/ko.json index 4e7bfc9dc93..36af97756ee 100644 --- a/homeassistant/components/eafm/translations/ko.json +++ b/homeassistant/components/eafm/translations/ko.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "no_stations": "\ud64d\uc218 \ubaa8\ub2c8\ud130\ub9c1 \uc2a4\ud14c\uc774\uc158\uc774 \uc5c6\uc2b5\ub2c8\ub2e4." }, "step": { diff --git a/homeassistant/components/ecobee/translations/ko.json b/homeassistant/components/ecobee/translations/ko.json index 8be4c28bfbb..674b087620a 100644 --- a/homeassistant/components/ecobee/translations/ko.json +++ b/homeassistant/components/ecobee/translations/ko.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, "error": { "pin_request_failed": "ecobee \ub85c\ubd80\ud130 PIN \uc694\uccad\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4; API \ud0a4\uac00 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", "token_request_failed": "ecobee \ub85c\ubd80\ud130 \ud1a0\ud070 \uc694\uccad\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4; \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." diff --git a/homeassistant/components/econet/translations/ko.json b/homeassistant/components/econet/translations/ko.json new file mode 100644 index 00000000000..f5c1381b8b1 --- /dev/null +++ b/homeassistant/components/econet/translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "email": "\uc774\uba54\uc77c", + "password": "\ube44\ubc00\ubc88\ud638" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/translations/ko.json b/homeassistant/components/elgato/translations/ko.json index d11b106e28b..f2deb818431 100644 --- a/homeassistant/components/elgato/translations/ko.json +++ b/homeassistant/components/elgato/translations/ko.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Elgato Key Light \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "flow_title": "Elgato Key Light: {serial_number}", "step": { diff --git a/homeassistant/components/elkm1/translations/ko.json b/homeassistant/components/elkm1/translations/ko.json index d074946b7ad..fb8c22ba5b2 100644 --- a/homeassistant/components/elkm1/translations/ko.json +++ b/homeassistant/components/elkm1/translations/ko.json @@ -5,7 +5,7 @@ "already_configured": "\uc774 \uc811\ub450\uc0ac\ub85c ElkM1 \uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, diff --git a/homeassistant/components/emulated_roku/translations/ko.json b/homeassistant/components/emulated_roku/translations/ko.json index e9d1d134af2..32b4202bc78 100644 --- a/homeassistant/components/emulated_roku/translations/ko.json +++ b/homeassistant/components/emulated_roku/translations/ko.json @@ -1,11 +1,14 @@ { "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, "step": { "user": { "data": { - "advertise_ip": "\uad11\uace0 IP", + "advertise_ip": "\uad11\uace0 IP \uc8fc\uc18c", "advertise_port": "\uad11\uace0 \ud3ec\ud2b8", - "host_ip": "\ud638\uc2a4\ud2b8 IP", + "host_ip": "\ud638\uc2a4\ud2b8 IP \uc8fc\uc18c", "listen_port": "\uc218\uc2e0 \ud3ec\ud2b8", "name": "\uc774\ub984", "upnp_bind_multicast": "\uba40\ud2f0 \uce90\uc2a4\ud2b8 \ud560\ub2f9 (\ucc38/\uac70\uc9d3)" diff --git a/homeassistant/components/epson/translations/ko.json b/homeassistant/components/epson/translations/ko.json new file mode 100644 index 00000000000..1ee9afdcf75 --- /dev/null +++ b/homeassistant/components/epson/translations/ko.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "name": "\uc774\ub984", + "port": "\ud3ec\ud2b8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/ko.json b/homeassistant/components/esphome/translations/ko.json index 82b9490757b..18827f69024 100644 --- a/homeassistant/components/esphome/translations/ko.json +++ b/homeassistant/components/esphome/translations/ko.json @@ -1,11 +1,12 @@ { "config": { "abort": { - "already_configured": "ESP \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "already_in_progress": "ESP \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4." + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4" }, "error": { "connection_error": "ESP \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. YAML \ud30c\uc77c\uc5d0 'api:' \ub97c \uad6c\uc131\ud588\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "resolve_error": "ESP \uc758 \uc8fc\uc18c\ub97c \ud655\uc778\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uac00 \uacc4\uc18d \ubc1c\uc0dd\ud558\uba74 \uace0\uc815 IP \uc8fc\uc18c\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "flow_title": "ESPHome: {name}", diff --git a/homeassistant/components/fireservicerota/translations/ko.json b/homeassistant/components/fireservicerota/translations/ko.json new file mode 100644 index 00000000000..f705fd9873c --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/ko.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, + "create_entry": { + "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "reauth": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638" + } + }, + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/firmata/translations/ko.json b/homeassistant/components/firmata/translations/ko.json index 753a5851811..b5b8d46f329 100644 --- a/homeassistant/components/firmata/translations/ko.json +++ b/homeassistant/components/firmata/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cannot_connect": "\uc124\uce58\ud558\ub294 \ub3d9\uc548 Firmata \ubcf4\ub4dc\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" } } } \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/ko.json b/homeassistant/components/flick_electric/translations/ko.json index 82d095f2755..e5b69253fa7 100644 --- a/homeassistant/components/flick_electric/translations/ko.json +++ b/homeassistant/components/flick_electric/translations/ko.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\ud574\ub2f9 \uacc4\uc815\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, diff --git a/homeassistant/components/flo/translations/ko.json b/homeassistant/components/flo/translations/ko.json index ab85b70afa7..9ba063c37dd 100644 --- a/homeassistant/components/flo/translations/ko.json +++ b/homeassistant/components/flo/translations/ko.json @@ -1,17 +1,19 @@ { "config": { "abort": { - "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", - "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "user": { "data": { - "host": "\ud638\uc2a4\ud2b8" + "host": "\ud638\uc2a4\ud2b8", + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" } } } diff --git a/homeassistant/components/flume/translations/ko.json b/homeassistant/components/flume/translations/ko.json index faac5e9c579..b700854ab57 100644 --- a/homeassistant/components/flume/translations/ko.json +++ b/homeassistant/components/flume/translations/ko.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\uc774 \uacc4\uc815\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, diff --git a/homeassistant/components/flunearyou/translations/ko.json b/homeassistant/components/flunearyou/translations/ko.json index 68e65d3c349..f6528e85cca 100644 --- a/homeassistant/components/flunearyou/translations/ko.json +++ b/homeassistant/components/flunearyou/translations/ko.json @@ -1,7 +1,10 @@ { "config": { "abort": { - "already_configured": "\uc88c\ud45c\uac12\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/forked_daapd/translations/ko.json b/homeassistant/components/forked_daapd/translations/ko.json index 5522eda3a76..5ae487a4096 100644 --- a/homeassistant/components/forked_daapd/translations/ko.json +++ b/homeassistant/components/forked_daapd/translations/ko.json @@ -5,7 +5,7 @@ "not_forked_daapd": "\uae30\uae30\uac00 forked-daapd \uc11c\ubc84\uac00 \uc544\ub2d9\ub2c8\ub2e4." }, "error": { - "unknown_error": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958", + "unknown_error": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", "websocket_not_enabled": "forked-daapd \uc11c\ubc84 \uc6f9\uc18c\ucf13\uc774 \ube44\ud65c\uc131\ud654 \ub418\uc5b4\uc788\uc2b5\ub2c8\ub2e4.", "wrong_host_or_port": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694.", "wrong_password": "\ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", diff --git a/homeassistant/components/foscam/translations/ko.json b/homeassistant/components/foscam/translations/ko.json new file mode 100644 index 00000000000..bfd8e952671 --- /dev/null +++ b/homeassistant/components/foscam/translations/ko.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/ko.json b/homeassistant/components/freebox/translations/ko.json index 986f345b3ec..a8b9a1edc7a 100644 --- a/homeassistant/components/freebox/translations/ko.json +++ b/homeassistant/components/freebox/translations/ko.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "register_failed": "\ub4f1\ub85d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", - "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uc785\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694" + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "link": { diff --git a/homeassistant/components/fritzbox/translations/ko.json b/homeassistant/components/fritzbox/translations/ko.json index b04b6905284..dfdcc0ad4eb 100644 --- a/homeassistant/components/fritzbox/translations/ko.json +++ b/homeassistant/components/fritzbox/translations/ko.json @@ -1,9 +1,14 @@ { "config": { "abort": { - "already_configured": "\uc774 AVM FRITZ!Box \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "already_in_progress": "AVM FRITZ!Box \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.", - "not_supported": "AVM FRITZ!Box \uc5d0 \uc5f0\uacb0\ub418\uc5c8\uc9c0\ub9cc \uc2a4\ub9c8\ud2b8 \ud648 \uae30\uae30\ub97c \uc81c\uc5b4\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "not_supported": "AVM FRITZ!Box \uc5d0 \uc5f0\uacb0\ub418\uc5c8\uc9c0\ub9cc \uc2a4\ub9c8\ud2b8 \ud648 \uae30\uae30\ub97c \uc81c\uc5b4\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "flow_title": "AVM FRITZ!Box: {name}", "step": { @@ -14,6 +19,12 @@ }, "description": "{name} \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" }, + "reauth_confirm": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + }, "user": { "data": { "host": "\ud638\uc2a4\ud2b8", diff --git a/homeassistant/components/fritzbox_callmonitor/translations/ko.json b/homeassistant/components/fritzbox_callmonitor/translations/ko.json new file mode 100644 index 00000000000..b8fd442cd03 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" + }, + "error": { + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/ko.json b/homeassistant/components/garmin_connect/translations/ko.json index fee07e579fe..4d5330a824f 100644 --- a/homeassistant/components/garmin_connect/translations/ko.json +++ b/homeassistant/components/garmin_connect/translations/ko.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "too_many_requests": "\uc694\uccad\uc774 \ub108\ubb34 \ub9ce\uc2b5\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/gdacs/translations/ko.json b/homeassistant/components/gdacs/translations/ko.json index 1aeaf219288..b91d512039a 100644 --- a/homeassistant/components/gdacs/translations/ko.json +++ b/homeassistant/components/gdacs/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/geofency/translations/ko.json b/homeassistant/components/geofency/translations/ko.json index 8bab38a8a34..fd49c57e249 100644 --- a/homeassistant/components/geofency/translations/ko.json +++ b/homeassistant/components/geofency/translations/ko.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", + "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4." + }, "create_entry": { "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Geofency \uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, diff --git a/homeassistant/components/geonetnz_quakes/translations/ko.json b/homeassistant/components/geonetnz_quakes/translations/ko.json index b231629e856..277aa945792 100644 --- a/homeassistant/components/geonetnz_quakes/translations/ko.json +++ b/homeassistant/components/geonetnz_quakes/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/geonetnz_volcano/translations/ko.json b/homeassistant/components/geonetnz_volcano/translations/ko.json index 26e83789e8f..1aeaf219288 100644 --- a/homeassistant/components/geonetnz_volcano/translations/ko.json +++ b/homeassistant/components/geonetnz_volcano/translations/ko.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/gios/translations/ko.json b/homeassistant/components/gios/translations/ko.json index 2ad64efadc1..7895dafe8ce 100644 --- a/homeassistant/components/gios/translations/ko.json +++ b/homeassistant/components/gios/translations/ko.json @@ -1,17 +1,17 @@ { "config": { "abort": { - "already_configured": "\uc774 \uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc5d0 \ub300\ud55c GIO\u015a \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "error": { - "cannot_connect": "GIO\u015a \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_sensors_data": "\uc774 \uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc5d0 \ub300\ud55c \uc13c\uc11c \ub370\uc774\ud130\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "wrong_station_id": "\uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc758 ID \uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." }, "step": { "user": { "data": { - "name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc758 \uc774\ub984", + "name": "\uc774\ub984", "station_id": "\uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc758 ID" }, "description": "\ud3f4\ub780\ub4dc \ud658\uacbd\uccad (GIO\u015a) \ub300\uae30\uc9c8 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. \uad6c\uc131\uc5d0 \ub3c4\uc6c0\uc774 \ud544\uc694\ud55c \uacbd\uc6b0 https://www.home-assistant.io/integrations/gios \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694", diff --git a/homeassistant/components/glances/translations/ko.json b/homeassistant/components/glances/translations/ko.json index 336c9f3b3e5..47f24d2edf1 100644 --- a/homeassistant/components/glances/translations/ko.json +++ b/homeassistant/components/glances/translations/ko.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\ud638\uc2a4\ud2b8\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "wrong_version": "\ud574\ub2f9 \ubc84\uc804\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4 (2 \ub610\ub294 3\ub9cc \uc9c0\uc6d0)" }, "step": { @@ -14,9 +14,9 @@ "name": "\uc774\ub984", "password": "\ube44\ubc00\ubc88\ud638", "port": "\ud3ec\ud2b8", - "ssl": "SSL/TLS \ub97c \uc0ac\uc6a9\ud558\uc5ec Glances \uc2dc\uc2a4\ud15c\uc5d0 \uc5f0\uacb0", + "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", - "verify_ssl": "\uc2dc\uc2a4\ud15c \uc778\uc99d \ud655\uc778", + "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778", "version": "Glances API \ubc84\uc804 (2 \ub610\ub294 3)" }, "title": "Glances \uc124\uce58\ud558\uae30" diff --git a/homeassistant/components/goalzero/translations/ko.json b/homeassistant/components/goalzero/translations/ko.json new file mode 100644 index 00000000000..f15f5827448 --- /dev/null +++ b/homeassistant/components/goalzero/translations/ko.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_host": "\ud638\uc2a4\ud2b8\uba85 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "name": "\uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/ko.json b/homeassistant/components/gogogate2/translations/ko.json index 55b32812bfa..dc37928db76 100644 --- a/homeassistant/components/gogogate2/translations/ko.json +++ b/homeassistant/components/gogogate2/translations/ko.json @@ -15,7 +15,7 @@ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, "description": "\uc544\ub798\uc5d0 \ud544\uc218 \uc815\ubcf4\ub97c \uc81c\uacf5\ud574\uc8fc\uc138\uc694.", - "title": "GogoGate2 \uc124\uce58\ud558\uae30" + "title": "GogoGate2 \ub610\ub294 iSmartGate \uc124\uce58\ud558\uae30" } } } diff --git a/homeassistant/components/gpslogger/translations/ko.json b/homeassistant/components/gpslogger/translations/ko.json index a6d95a0e51b..e73d72c06b7 100644 --- a/homeassistant/components/gpslogger/translations/ko.json +++ b/homeassistant/components/gpslogger/translations/ko.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", + "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4." + }, "create_entry": { "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 GPSLogger \uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, diff --git a/homeassistant/components/gree/translations/ko.json b/homeassistant/components/gree/translations/ko.json new file mode 100644 index 00000000000..7011a61f757 --- /dev/null +++ b/homeassistant/components/gree/translations/ko.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/ko.json b/homeassistant/components/griddy/translations/ko.json index a17db380aa0..df9178fab93 100644 --- a/homeassistant/components/griddy/translations/ko.json +++ b/homeassistant/components/griddy/translations/ko.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\uc774 \uc804\ub825 \uacf5\uae09 \uc9c0\uc5ed\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { diff --git a/homeassistant/components/guardian/translations/ko.json b/homeassistant/components/guardian/translations/ko.json index da40b674009..d9f70ad2d33 100644 --- a/homeassistant/components/guardian/translations/ko.json +++ b/homeassistant/components/guardian/translations/ko.json @@ -1,8 +1,9 @@ { "config": { "abort": { - "already_configured": "\uc774 Guardian \uae30\uae30\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "already_in_progress": "Guardian \uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4." + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/habitica/translations/cs.json b/homeassistant/components/habitica/translations/cs.json new file mode 100644 index 00000000000..5ebfec2cf12 --- /dev/null +++ b/homeassistant/components/habitica/translations/cs.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_credentials": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/it.json b/homeassistant/components/habitica/translations/it.json new file mode 100644 index 00000000000..2bef21519b6 --- /dev/null +++ b/homeassistant/components/habitica/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "invalid_credentials": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "api_user": "ID utente API di Habitica", + "name": "Sostituisci il nome utente di Habitica. Verr\u00e0 utilizzato per le chiamate di servizio", + "url": "URL" + }, + "description": "Collega il tuo profilo Habitica per consentire il monitoraggio del profilo e delle attivit\u00e0 dell'utente. Nota che api_id e api_key devono essere ottenuti da https://habitica.com/user/settings/api" + } + } + }, + "title": "Habitica" +} \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/ko.json b/homeassistant/components/habitica/translations/ko.json new file mode 100644 index 00000000000..3fd04a4477b --- /dev/null +++ b/homeassistant/components/habitica/translations/ko.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_credentials": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "api_key": "API \ud0a4", + "url": "URL \uc8fc\uc18c" + } + } + } + }, + "title": "Habitica" +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/ko.json b/homeassistant/components/hangouts/translations/ko.json index 51bd857e358..3c23effaf4f 100644 --- a/homeassistant/components/hangouts/translations/ko.json +++ b/homeassistant/components/hangouts/translations/ko.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Google \ud589\uc544\uc6c3\uc740 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", - "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { "invalid_2fa": "2\ub2e8\uacc4 \uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", diff --git a/homeassistant/components/harmony/translations/ko.json b/homeassistant/components/harmony/translations/ko.json index 528f5e9cc7e..026e751b788 100644 --- a/homeassistant/components/harmony/translations/ko.json +++ b/homeassistant/components/harmony/translations/ko.json @@ -4,7 +4,7 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "flow_title": "Logitech Harmony Hub: {name}", diff --git a/homeassistant/components/heos/translations/ko.json b/homeassistant/components/heos/translations/ko.json index fc20a77d7b8..d17cbd0e4b7 100644 --- a/homeassistant/components/heos/translations/ko.json +++ b/homeassistant/components/heos/translations/ko.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/hisense_aehw4a1/translations/ko.json b/homeassistant/components/hisense_aehw4a1/translations/ko.json index 27d0ff88f6a..491887280c0 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/ko.json +++ b/homeassistant/components/hisense_aehw4a1/translations/ko.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Hisense AEH-W4A1 \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", - "single_instance_allowed": "\ud558\ub098\uc758 Hisense AEH-W4A1 \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." }, "step": { "confirm": { diff --git a/homeassistant/components/hlk_sw16/translations/ko.json b/homeassistant/components/hlk_sw16/translations/ko.json new file mode 100644 index 00000000000..9ba063c37dd --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_connect/translations/ko.json b/homeassistant/components/home_connect/translations/ko.json index 8d1f5554e7f..425968d1460 100644 --- a/homeassistant/components/home_connect/translations/ko.json +++ b/homeassistant/components/home_connect/translations/ko.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "missing_configuration": "Home Connect \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", - "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" + "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", + "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "create_entry": { - "default": "Home Connect \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "pick_implementation": { diff --git a/homeassistant/components/homekit/translations/ko.json b/homeassistant/components/homekit/translations/ko.json index 1b7276d4171..bc8e138fbaf 100644 --- a/homeassistant/components/homekit/translations/ko.json +++ b/homeassistant/components/homekit/translations/ko.json @@ -4,17 +4,23 @@ "port_name_in_use": "\uc774\ub984\uc774\ub098 \ud3ec\ud2b8\uac00 \uac19\uc740 \ube0c\ub9ac\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "step": { + "bridge_mode": { + "data": { + "include_domains": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778" + } + }, "pairing": { - "description": "{name} \ube0c\ub9ac\uc9c0\uac00 \uc900\ube44\ub418\uba74 \"\uc54c\ub9bc\"\uc5d0\uc11c \"HomeKit \ube0c\ub9ac\uc9c0 \uc124\uc815\"\uc73c\ub85c \ud398\uc5b4\ub9c1\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "title": "HomeKit \ube0c\ub9ac\uc9c0 \ud398\uc5b4\ub9c1\ud558\uae30" + "description": "{name} \uc774(\uac00) \uc900\ube44\ub418\uba74 \"\uc54c\ub9bc\"\uc5d0\uc11c \"HomeKit \ube0c\ub9ac\uc9c0 \uc124\uc815\"\uc73c\ub85c \ud398\uc5b4\ub9c1\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "HomeKit \ud398\uc5b4\ub9c1\ud558\uae30" }, "user": { "data": { "auto_start": "\uc790\ub3d9 \uc2dc\uc791 (Z-Wave \ub610\ub294 \uae30\ud0c0 \uc9c0\uc5f0\ub41c \uc2dc\uc791 \uc2dc\uc2a4\ud15c\uc744 \uc0ac\uc6a9\ud558\ub294 \uacbd\uc6b0 \ube44\ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)", - "include_domains": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778" + "include_domains": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778", + "mode": "\ubaa8\ub4dc" }, - "description": "HomeKit \ube0c\ub9ac\uc9c0\ub97c \uc0ac\uc6a9\ud558\uba74 HomeKit \uc5d0\uc11c Home Assistant \uad6c\uc131\uc694\uc18c\uc5d0 \uc561\uc138\uc2a4\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. HomeKit \ube0c\ub9ac\uc9c0\ub294 \ube0c\ub9ac\uc9c0 \uc790\uccb4\ub97c \ud3ec\ud568\ud558\uc5ec \uc778\uc2a4\ud134\uc2a4\ub2f9 150\uac1c\uc758 \uc561\uc138\uc11c\ub9ac\ub85c \uc81c\ud55c\ub429\ub2c8\ub2e4. \ucd5c\ub300 \uc561\uc138\uc11c\ub9ac \uc218\ub97c \ucd08\uacfc\ud558\uc5ec \ube0c\ub9ac\uc9d5\ud558\ub824\uba74 \uc5ec\ub7ec \ub3c4\uba54\uc778\uc5d0 \ub300\ud574 \uc5ec\ub7ec \uac1c\uc758 \ud648\ud0b7 \ube0c\ub9ac\uc9c0\ub97c \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \uc88b\uc2b5\ub2c8\ub2e4. \uad6c\uc131\uc694\uc18c\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 \uae30\ubcf8 \ube0c\ub9ac\uc9c0\uc758 YAML \uc744 \ud1b5\ud574\uc11c\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "title": "HomeKit \ube0c\ub9ac\uc9c0 \ud65c\uc131\ud654\ud558\uae30" + "description": "HomeKit \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \ud1b5\ud574 HomeKit \uc758 Home Assistant \uad6c\uc131\uc694\uc18c\uc5d0 \uc561\uc138\uc2a4\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ube0c\ub9ac\uc9c0 \ubaa8\ub4dc\uc5d0\uc11c HomeKit \ube0c\ub9ac\uc9c0\ub294 \ube0c\ub9ac\uc9c0 \uc790\uccb4\ub97c \ud3ec\ud568\ud558\uc5ec \uc778\uc2a4\ud134\uc2a4\ub2f9 150 \uac1c\uc758 \uc561\uc138\uc11c\ub9ac\ub85c \uc81c\ud55c\ub429\ub2c8\ub2e4. \ucd5c\ub300 \uc561\uc138\uc11c\ub9ac \uac1c\uc218\ubcf4\ub2e4 \ub9ce\uc740 \uc218\uc758 \ube0c\ub9ac\uc9c0\ub97c \uc0ac\uc6a9\ud558\ub824\ub294 \uacbd\uc6b0, \uc11c\ub85c \ub2e4\ub978 \ub3c4\uba54\uc778\uc5d0 \ub300\ud574 \uc5ec\ub7ec\uac1c\uc758 HomeKit \ube0c\ub9ac\uc9c0\ub97c \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \uc88b\uc2b5\ub2c8\ub2e4. \uad6c\uc131\uc694\uc18c\uc758 \uc790\uc138\ud55c \uad6c\uc131\uc740 YAML \uc744 \ud1b5\ud574\uc11c\ub9cc \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ucd5c\uc0c1\uc758 \uc131\ub2a5\uacfc \uc608\uae30\uce58 \uc54a\uc740 \uc0ac\uc6a9 \ubd88\uac00\ub2a5\ud55c \uc0c1\ud0dc\ub97c \ubc29\uc9c0\ud558\ub824\uba74 \uac01\uac01\uc758 TV \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4\uc640 \uce74\uba54\ub77c\uc5d0 \ub300\ud574 \uc561\uc138\uc11c\ub9ac \ubaa8\ub4dc\uc5d0\uc11c \ubcc4\ub3c4\uc758 HomeKit \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud558\uace0 \ud398\uc5b4\ub9c1\ud574\uc8fc\uc138\uc694.", + "title": "HomeKit \ud65c\uc131\ud654\ud558\uae30" } } }, @@ -22,10 +28,10 @@ "step": { "advanced": { "data": { - "auto_start": "\uc790\ub3d9 \uc2dc\uc791 (Z-Wave \ub610\ub294 \uae30\ud0c0 \uc9c0\uc5f0\ub41c \uc2dc\uc791 \uc2dc\uc2a4\ud15c\uc744 \uc0ac\uc6a9\ud558\ub294 \uacbd\uc6b0 \ube44\ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)", + "auto_start": "\uc790\ub3d9 \uc2dc\uc791 (homekit.start \uc11c\ube44\uc2a4\ub97c \uc218\ub3d9\uc73c\ub85c \ud638\ucd9c\ud558\ub824\uba74 \ube44\ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)", "safe_mode": "\uc548\uc804 \ubaa8\ub4dc (\ud398\uc5b4\ub9c1\uc774 \uc2e4\ud328\ud55c \uacbd\uc6b0\uc5d0\ub9cc \ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)" }, - "description": "\uc774 \uc124\uc815\uc740 HomeKit \ube0c\ub9ac\uc9c0\uac00 \uc791\ub3d9\ud558\uc9c0 \uc54a\ub294 \uacbd\uc6b0\uc5d0\ub9cc \uc124\uc815\ud574\uc8fc\uc138\uc694.", + "description": "\uc774 \uc124\uc815\uc740 HomeKit \uac00 \uc791\ub3d9\ud558\uc9c0 \uc54a\ub294 \uacbd\uc6b0\uc5d0\ub9cc \uc124\uc815\ud574\uc8fc\uc138\uc694.", "title": "\uace0\uae09 \uad6c\uc131\ud558\uae30" }, "cameras": { @@ -35,16 +41,22 @@ "description": "\ub124\uc774\ud2f0\ube0c H.264 \uc2a4\ud2b8\ub9bc\uc744 \uc9c0\uc6d0\ud558\ub294 \ubaa8\ub4e0 \uce74\uba54\ub77c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694. \uce74\uba54\ub77c\uac00 H.264 \uc2a4\ud2b8\ub9bc\uc744 \ucd9c\ub825\ud558\uc9c0 \uc54a\uc73c\uba74 \uc2dc\uc2a4\ud15c\uc740 \ube44\ub514\uc624\ub97c HomeKit \uc6a9 H.264 \ud3ec\ub9f7\uc73c\ub85c \ubcc0\ud658\uc2dc\ud0b5\ub2c8\ub2e4. \ud2b8\ub79c\uc2a4\ucf54\ub529 \ubcc0\ud658\uc5d0\ub294 \ub192\uc740 CPU \uc131\ub2a5\uc774 \ud544\uc694\ud558\uba70 Raspberry Pi \uc640 \uac19\uc740 \ub2e8\uc77c \ubcf4\ub4dc \ucef4\ud4e8\ud130\uc5d0\uc11c\ub294 \uc791\ub3d9\ud558\uc9c0 \uc54a\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "title": "\uce74\uba54\ub77c \ube44\ub514\uc624 \ucf54\ub371 \uc120\ud0dd\ud558\uae30" }, + "include_exclude": { + "data": { + "mode": "\ubaa8\ub4dc" + } + }, "init": { "data": { - "include_domains": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778" + "include_domains": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778", + "mode": "\ubaa8\ub4dc" }, - "description": "\"\ud3ec\ud568\ud560 \ub3c4\uba54\uc778\"\uc758 \uad6c\uc131\uc694\uc18c\ub294 HomeKit \uc5d0 \uc5f0\uacb0\ub429\ub2c8\ub2e4. \ub2e4\uc74c \ud654\uba74\uc5d0\uc11c \uc774 \ubaa9\ub85d\uc758 \uc81c\uc678\ud560 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "title": "\ube0c\ub9ac\uc9c0 \ud560 \ub3c4\uba54\uc778 \uc120\ud0dd\ud558\uae30" + "description": "HomeKit \ub294 \ube0c\ub9ac\uc9c0 \ub610\ub294 \uc561\uc138\uc11c\ub9ac\ub97c \ub178\ucd9c\ud558\ub3c4\ub85d \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc561\uc138\uc11c\ub9ac \ubaa8\ub4dc\uc5d0\uc11c\ub294 \ub2e8\uc77c \uad6c\uc131\uc694\uc18c\ub9cc \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. TV \uae30\uae30 \ud074\ub798\uc2a4\uac00 \uc788\ub294 \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4\uac00 \uc81c\ub300\ub85c \uc791\ub3d9\ud558\ub824\uba74 \uc561\uc138\uc11c\ub9ac \ubaa8\ub4dc\uac00 \ud544\uc694\ud569\ub2c8\ub2e4. \"\ud3ec\ud568\ud560 \ub3c4\uba54\uc778\"\uc758 \uad6c\uc131\uc694\uc18c\ub294 HomeKit \uc5d0 \ud3ec\ud568\ub429\ub2c8\ub2e4. \ub2e4\uc74c \ud654\uba74\uc5d0\uc11c \uc774 \ubaa9\ub85d\uc5d0 \ud3ec\ud568\ud558\uac70\ub098 \uc81c\uc678\ud560 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694." }, "yaml": { "description": "\uc774 \ud56d\ubaa9\uc740 YAML \uc744 \ud1b5\ud574 \uc81c\uc5b4\ub429\ub2c8\ub2e4", - "title": "HomeKit \ube0c\ub9ac\uc9c0 \uc635\uc158 \uc870\uc815\ud558\uae30" + "title": "HomeKit \uc635\uc158 \uc870\uc815\ud558\uae30" } } } diff --git a/homeassistant/components/homekit_controller/translations/ko.json b/homeassistant/components/homekit_controller/translations/ko.json index 2c41447b5a0..7314f43545e 100644 --- a/homeassistant/components/homekit_controller/translations/ko.json +++ b/homeassistant/components/homekit_controller/translations/ko.json @@ -3,7 +3,7 @@ "abort": { "accessory_not_found_error": "\uae30\uae30\ub97c \ub354 \uc774\uc0c1 \ucc3e\uc744 \uc218 \uc5c6\uc73c\ubbc0\ub85c \ud398\uc5b4\ub9c1\uc744 \ucd94\uac00 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "already_configured": "\uc561\uc138\uc11c\ub9ac\uac00 \ucee8\ud2b8\ub864\ub7ec\uc5d0 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", "already_paired": "\uc774 \uc561\uc138\uc11c\ub9ac\ub294 \uc774\ubbf8 \ub2e4\ub978 \uae30\uae30\uc640 \ud398\uc5b4\ub9c1\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4. \uc561\uc138\uc11c\ub9ac\ub97c \uc7ac\uc124\uc815\ud558\uace0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "ignored_model": "\uc774 \ubaa8\ub378\uc5d0 \ub300\ud55c HomeKit \uc9c0\uc6d0\uc740 \ub354 \ub9ce\uc740 \uae30\ub2a5\uc744 \uc81c\uacf5\ud558\ub294 \uae30\ubcf8 \uad6c\uc131\uc694\uc18c\ub85c \uc778\ud574 \ucc28\ub2e8\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "invalid_config_entry": "\uc774 \uae30\uae30\ub294 \ud398\uc5b4\ub9c1 \ud560 \uc900\ube44\uac00 \ub418\uc5c8\uc9c0\ub9cc Home Assistant \uc5d0 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \ucda9\ub3cc\ud558\ub294 \uad6c\uc131\uc694\uc18c\uac00 \uc788\uc2b5\ub2c8\ub2e4. \uba3c\uc800 \ud574\ub2f9 \uad6c\uc131\uc694\uc18c\ub97c \uc81c\uac70\ud574\uc8fc\uc138\uc694.", @@ -17,7 +17,7 @@ "unable_to_pair": "\ud398\uc5b4\ub9c1 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "unknown_error": "\uae30\uae30\uc5d0\uc11c \uc54c \uc218\uc5c6\ub294 \uc624\ub958\ub97c \ubcf4\uace0\ud588\uc2b5\ub2c8\ub2e4. \ud398\uc5b4\ub9c1\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4." }, - "flow_title": "HomeKit \uc561\uc138\uc11c\ub9ac: {name}", + "flow_title": "HomeKit \uc561\uc138\uc11c\ub9ac \ud504\ub85c\ud1a0\ucf5c\uc744 \ud1b5\ud55c {name}", "step": { "busy_error": { "description": "\ubaa8\ub4e0 \ucee8\ud2b8\ub864\ub7ec\uc5d0\uc11c \ud398\uc5b4\ub9c1\uc744 \uc911\ub2e8\ud558\uac70\ub098 \uae30\uae30\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ub2e4\uc74c \ud398\uc5b4\ub9c1\uc744 \uacc4\uc18d\ud574\uc8fc\uc138\uc694.", @@ -31,8 +31,8 @@ "data": { "pairing_code": "\ud398\uc5b4\ub9c1 \ucf54\ub4dc" }, - "description": "\uc774 \uc561\uc138\uc11c\ub9ac\ub97c \uc0ac\uc6a9\ud558\ub824\uba74 HomeKit \ud398\uc5b4\ub9c1 \ucf54\ub4dc (XXX-XX-XXX \ud615\uc2dd) \ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", - "title": "HomeKit \uc561\uc138\uc11c\ub9ac \ud398\uc5b4\ub9c1\ud558\uae30" + "description": "HomeKit \ucee8\ud2b8\ub864\ub7ec\ub294 \ubcc4\ub3c4\uc758 HomeKit \ucee8\ud2b8\ub864\ub7ec \ub610\ub294 iCloud \uc5c6\uc774 \uc554\ud638\ud654\ub41c \ubcf4\uc548 \uc5f0\uacb0\uc744 \uc0ac\uc6a9\ud558\uc5ec \ub85c\uceec \uc601\uc5ed \ub124\ud2b8\uc6cc\ud06c \uc0c1\uc5d0\uc11c {name} \uacfc(\uc640) \ud1b5\uc2e0\ud569\ub2c8\ub2e4. \uc774 \uc561\uc138\uc11c\ub9ac\ub97c \uc0ac\uc6a9\ud558\ub824\uba74 HomeKit \ud398\uc5b4\ub9c1 \ucf54\ub4dc (XX-XX-XXX \ud615\uc2dd) \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc774 \ucf54\ub4dc\ub294 \uc77c\ubc18\uc801\uc73c\ub85c \uae30\uae30\ub098 \ud3ec\uc7a5 \ubc15\uc2a4\uc5d0 \ud45c\uc2dc\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "HomeKit \uc561\uc138\uc11c\ub9ac \ud504\ub85c\ud1a0\ucf5c\uc744 \ud1b5\ud574 \uae30\uae30\uc640 \ud398\uc5b4\ub9c1 \ud558\uae30" }, "protocol_error": { "description": "\uae30\uae30\uac00 \ud398\uc5b4\ub9c1 \ubaa8\ub4dc\uc5d0 \uc788\uc9c0 \uc54a\uc744 \uc218 \uc788\uc73c\uba70 \ubb3c\ub9ac\uc801 \ub610\ub294 \uac00\uc0c1 \uc758 \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc57c \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uae30\uae30\uac00 \ud398\uc5b4\ub9c1 \ubaa8\ub4dc\uc5d0 \uc788\ub294\uc9c0 \ud655\uc778\ud558\uac70\ub098 \uae30\uae30\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ub2e4\uc74c \ud398\uc5b4\ub9c1\uc744 \uacc4\uc18d\ud574\uc8fc\uc138\uc694.", @@ -42,8 +42,8 @@ "data": { "device": "\uae30\uae30" }, - "description": "\ud398\uc5b4\ub9c1 \ud560 \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694", - "title": "HomeKit \uc561\uc138\uc11c\ub9ac \ud398\uc5b4\ub9c1\ud558\uae30" + "description": "HomeKit \ucee8\ud2b8\ub864\ub7ec\ub294 \ubcc4\ub3c4\uc758 HomeKit \ucee8\ud2b8\ub864\ub7ec \ub610\ub294 iCloud \uc5c6\uc774 \uc554\ud638\ud654\ub41c \ubcf4\uc548 \uc5f0\uacb0\uc744 \uc0ac\uc6a9\ud558\uc5ec \ub85c\uceec \uc601\uc5ed \ub124\ud2b8\uc6cc\ud06c \uc0c1\uc5d0\uc11c \uae30\uae30\uc640 \ud1b5\uc2e0\ud569\ub2c8\ub2e4. \ud398\uc5b4\ub9c1 \ud560 \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694:", + "title": "\uae30\uae30 \uc120\ud0dd\ud558\uae30" } } }, diff --git a/homeassistant/components/homematicip_cloud/translations/ko.json b/homeassistant/components/homematicip_cloud/translations/ko.json index b85b8ac00b1..6a15b21de84 100644 --- a/homeassistant/components/homematicip_cloud/translations/ko.json +++ b/homeassistant/components/homematicip_cloud/translations/ko.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "\uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "connection_aborted": "HMIP \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "connection_aborted": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "invalid_sgtin_or_pin": "PIN\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "invalid_sgtin_or_pin": "\uc798\ubabb\ub41c SGTIN \uc774\uac70\ub098 PIN \ucf54\ub4dc \uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "press_the_button": "\ud30c\ub780\uc0c9 \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.", "register_failed": "\ub4f1\ub85d\uc5d0 \uc2e4\ud328\ud558\uc600\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "timeout_button": "\uc815\ud574\uc9c4 \uc2dc\uac04\ub0b4\uc5d0 \ud30c\ub780\uc0c9 \ubc84\ud2bc\uc744 \ub20c\ub974\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." @@ -16,7 +16,7 @@ "data": { "hapid": "\uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8 ID (SGTIN)", "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d, \ubaa8\ub4e0 \uae30\uae30 \uc774\ub984\uc758 \uc811\ub450\uc5b4\ub85c \uc0ac\uc6a9)", - "pin": "PIN \ucf54\ub4dc (\uc120\ud0dd\uc0ac\ud56d)" + "pin": "PIN \ucf54\ub4dc" }, "title": "HomematicIP \uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8 \uc120\ud0dd\ud558\uae30" }, diff --git a/homeassistant/components/huawei_lte/translations/ko.json b/homeassistant/components/huawei_lte/translations/ko.json index 6469bf6a696..73274d15bfb 100644 --- a/homeassistant/components/huawei_lte/translations/ko.json +++ b/homeassistant/components/huawei_lte/translations/ko.json @@ -2,22 +2,25 @@ "config": { "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "already_in_progress": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", "not_huawei_lte": "\ud654\uc6e8\uc774 LTE \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4" }, "error": { "connection_timeout": "\uc811\uc18d \uc2dc\uac04 \ucd08\uacfc", "incorrect_password": "\ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", "incorrect_username": "\uc0ac\uc6a9\uc790 \uc774\ub984\uc774 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_url": "URL \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "login_attempts_exceeded": "\ucd5c\ub300 \ub85c\uadf8\uc778 \uc2dc\ub3c4 \ud69f\uc218\ub97c \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694", - "response_error": "\uae30\uae30\uc5d0\uc11c \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + "response_error": "\uae30\uae30\uc5d0\uc11c \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "flow_title": "Huawei LTE: {name}", "step": { "user": { "data": { "password": "\ube44\ubc00\ubc88\ud638", + "url": "URL \uc8fc\uc18c", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, "description": "\uae30\uae30 \uc561\uc138\uc2a4 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc124\uc815\ud558\ub294 \uac83\uc740 \uc120\ud0dd \uc0ac\ud56d\uc774\uc9c0\ub9cc \ub354 \ub9ce\uc740 \uae30\ub2a5\uc744 \uc9c0\uc6d0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ubc18\uba74, \uc778\uc99d\ub41c \uc5f0\uacb0\uc744 \uc0ac\uc6a9\ud558\uba74, \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \ud65c\uc131\ud654\ub41c \uc0c1\ud0dc\uc5d0\uc11c \ub2e4\ub978 \ubc29\ubc95\uc73c\ub85c Home Assistant \uc758 \uc678\ubd80\uc5d0\uc11c \uae30\uae30\uc758 \uc6f9 \uc778\ud130\ud398\uc774\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud558\ub294 \ub370 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", diff --git a/homeassistant/components/hue/translations/ko.json b/homeassistant/components/hue/translations/ko.json index 050b5c51c97..846ea937515 100644 --- a/homeassistant/components/hue/translations/ko.json +++ b/homeassistant/components/hue/translations/ko.json @@ -2,16 +2,16 @@ "config": { "abort": { "all_configured": "\ubaa8\ub4e0 \ud544\ub9bd\uc2a4 Hue \ube0c\ub9ac\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "already_configured": "\ube0c\ub9ac\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "already_in_progress": "\ube0c\ub9ac\uc9c0 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.", - "cannot_connect": "\ube0c\ub9ac\uc9c0\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "discover_timeout": "Hue \ube0c\ub9ac\uc9c0\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "no_bridges": "\ubc1c\uacac\ub41c \ud544\ub9bd\uc2a4 Hue \ube0c\ub9ac\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", "not_hue_bridge": "Hue \ube0c\ub9ac\uc9c0\uac00 \uc544\ub2d9\ub2c8\ub2e4", - "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "linking": "\uc54c \uc218 \uc5c6\ub294 \uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "linking": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", "register_failed": "\ub4f1\ub85d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694" }, "step": { diff --git a/homeassistant/components/huisbaasje/translations/ko.json b/homeassistant/components/huisbaasje/translations/ko.json new file mode 100644 index 00000000000..bd25569d7c7 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "connection_exception": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unauthenticated_exception": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/ko.json b/homeassistant/components/hunterdouglas_powerview/translations/ko.json index cba1b761682..d16945084d0 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/ko.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/ko.json @@ -4,7 +4,7 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { diff --git a/homeassistant/components/hvv_departures/translations/ko.json b/homeassistant/components/hvv_departures/translations/ko.json index 41c7f44be7f..ea6ef8bc23a 100644 --- a/homeassistant/components/hvv_departures/translations/ko.json +++ b/homeassistant/components/hvv_departures/translations/ko.json @@ -4,7 +4,7 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "no_results": "\uacb0\uacfc\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\ub978 \uc2a4\ud14c\uc774\uc158\uc774\ub098 \uc8fc\uc18c\ub97c \uc0ac\uc6a9\ud574\uc8fc\uc138\uc694" }, diff --git a/homeassistant/components/hyperion/translations/ko.json b/homeassistant/components/hyperion/translations/ko.json new file mode 100644 index 00000000000..295d418da12 --- /dev/null +++ b/homeassistant/components/hyperion/translations/ko.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/translations/ko.json b/homeassistant/components/iaqualink/translations/ko.json index 6396d5d250e..1386480fca4 100644 --- a/homeassistant/components/iaqualink/translations/ko.json +++ b/homeassistant/components/iaqualink/translations/ko.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/icloud/translations/ko.json b/homeassistant/components/icloud/translations/ko.json index 045042362b7..5e02fb02993 100644 --- a/homeassistant/components/icloud/translations/ko.json +++ b/homeassistant/components/icloud/translations/ko.json @@ -1,14 +1,22 @@ { "config": { "abort": { - "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "no_device": "\"\ub098\uc758 iPhone \ucc3e\uae30\"\uac00 \ud65c\uc131\ud654\ub41c \uae30\uae30\uac00 \uc5c6\uc2b5\ub2c8\ub2e4" + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "no_device": "\"\ub098\uc758 iPhone \ucc3e\uae30\"\uac00 \ud65c\uc131\ud654\ub41c \uae30\uae30\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, "error": { + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "send_verification_code": "\uc778\uc99d \ucf54\ub4dc\ub97c \ubcf4\ub0b4\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "validate_verification_code": "\uc778\uc99d \ucf54\ub4dc\ub97c \ud655\uc778\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \uc2e0\ub8b0\ud560 \uc218 \uc788\ub294 \uae30\uae30\ub97c \uc120\ud0dd\ud558\uace0 \uc778\uc99d\uc744 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694" + "validate_verification_code": "\uc778\uc99d \ucf54\ub4dc \ud655\uc778\uc5d0 \uc2e4\ud328\ud558\uc600\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694" }, "step": { + "reauth": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638" + }, + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d" + }, "trusted_device": { "data": { "trusted_device": "\uc2e0\ub8b0\ud560 \uc218 \uc788\ub294 \uae30\uae30" diff --git a/homeassistant/components/ifttt/translations/ko.json b/homeassistant/components/ifttt/translations/ko.json index 93daad9e182..bc561027fc3 100644 --- a/homeassistant/components/ifttt/translations/ko.json +++ b/homeassistant/components/ifttt/translations/ko.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", + "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4." + }, "create_entry": { "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\uae30 \uc704\ud574\uc11c\ub294 [IFTTT \uc6f9 \ud6c5 \uc560\ud50c\ub9bf]({applet_url}) \uc5d0\uc11c \"Make a web request\" \ub97c \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c\uc758 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\nHome Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, diff --git a/homeassistant/components/insteon/translations/ko.json b/homeassistant/components/insteon/translations/ko.json index 76ef9566725..1cd65afc9e9 100644 --- a/homeassistant/components/insteon/translations/ko.json +++ b/homeassistant/components/insteon/translations/ko.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub428. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." }, "error": { - "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "select_single": "\ud558\ub098\uc758 \uc635\uc158\uc744 \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624." }, "step": { @@ -20,9 +20,9 @@ "hubv2": { "data": { "host": "IP \uc8fc\uc18c", - "password": "\uc554\ud638", + "password": "\ube44\ubc00\ubc88\ud638", "port": "\ud3ec\ud2b8", - "username": "\uc0ac\uc6a9\uc790\uba85" + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, "description": "Insteon Hub \ubc84\uc804 2\ub97c \uad6c\uc131\ud569\ub2c8\ub2e4.", "title": "Insteon Hub \ubc84\uc804 2" @@ -43,7 +43,7 @@ }, "options": { "error": { - "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "select_single": "\uc635\uc158 \uc120\ud0dd" }, "step": { @@ -59,6 +59,14 @@ }, "description": "Insteon Hub \ube44\ubc00\ubc88\ud638\ub97c \ubcc0\uacbd\ud569\ub2c8\ub2e4." }, + "change_hub_config": { + "data": { + "host": "IP \uc8fc\uc18c", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + }, "init": { "data": { "add_override": "\uc7a5\uce58 Override \ucd94\uac00", diff --git a/homeassistant/components/ios/translations/ko.json b/homeassistant/components/ios/translations/ko.json index 6abe9380473..f5da462c1ab 100644 --- a/homeassistant/components/ios/translations/ko.json +++ b/homeassistant/components/ios/translations/ko.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "\ud558\ub098\uc758 Home Assistant iOS \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." }, "step": { "confirm": { - "description": "Home Assistant iOS \ucef4\ud3ec\ub10c\ud2b8\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" } } } diff --git a/homeassistant/components/ipp/translations/ko.json b/homeassistant/components/ipp/translations/ko.json index bc2e18cb5c2..28e79ffe281 100644 --- a/homeassistant/components/ipp/translations/ko.json +++ b/homeassistant/components/ipp/translations/ko.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "connection_upgrade": "\ud504\ub9b0\ud130\uc5d0 \uc5f0\uacb0\ud558\ub824\uba74 \uc5f0\uacb0\uc744 \uc5c5\uadf8\ub808\uc774\ub4dc\ud574\uc57c \ud569\ub2c8\ub2e4.", "ipp_error": "IPP \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "ipp_version_error": "\ud504\ub9b0\ud130\uc5d0\uc11c IPP \ubc84\uc804\uc744 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", @@ -9,6 +10,7 @@ "unique_id_required": "\uae30\uae30 \uac80\uc0c9\uc5d0 \ud544\uc694\ud55c \uace0\uc720\ud55c ID \uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." }, "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "connection_upgrade": "\ud504\ub9b0\ud130\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. SSL/TLS \uc635\uc158\uc744 \ud655\uc778\ud558\uace0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." }, "flow_title": "\ud504\ub9b0\ud130: {name}", @@ -18,8 +20,8 @@ "base_path": "\ud504\ub9b0\ud130\uc758 \uc0c1\ub300 \uacbd\ub85c", "host": "\ud638\uc2a4\ud2b8", "port": "\ud3ec\ud2b8", - "ssl": "\ud504\ub9b0\ud130\ub294 SSL/TLS \ub97c \ud1b5\ud55c \ud1b5\uc2e0\uc744 \uc9c0\uc6d0\ud569\ub2c8\ub2e4", - "verify_ssl": "\ud504\ub9b0\ud130\ub294 \uc62c\ubc14\ub978 SSL \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4" + "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9", + "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" }, "description": "\uc778\ud130\ub137 \uc778\uc1c4 \ud504\ub85c\ud1a0\ucf5c (IPP) \ub97c \ud1b5\ud574 \ud504\ub9b0\ud130\ub97c \uc124\uc815\ud558\uc5ec Home Assistant \uc640 \uc5f0\ub3d9\ud569\ub2c8\ub2e4.", "title": "\ud504\ub9b0\ud130 \uc5f0\uacb0\ud558\uae30" diff --git a/homeassistant/components/iqvia/translations/ko.json b/homeassistant/components/iqvia/translations/ko.json index f6a914bd07d..1b0dfd980ec 100644 --- a/homeassistant/components/iqvia/translations/ko.json +++ b/homeassistant/components/iqvia/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uc774 \uc6b0\ud3b8 \ubc88\ud638\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { "invalid_zip_code": "\uc6b0\ud3b8\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/islamic_prayer_times/translations/ko.json b/homeassistant/components/islamic_prayer_times/translations/ko.json index 52ac6869855..240ad6f57dc 100644 --- a/homeassistant/components/islamic_prayer_times/translations/ko.json +++ b/homeassistant/components/islamic_prayer_times/translations/ko.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, "step": { "user": { "description": "\uc774\uc2ac\ub78c \uae30\ub3c4 \uc2dc\uac04\uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", diff --git a/homeassistant/components/izone/translations/ko.json b/homeassistant/components/izone/translations/ko.json index 85aec276562..b6eae170bec 100644 --- a/homeassistant/components/izone/translations/ko.json +++ b/homeassistant/components/izone/translations/ko.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "iZone \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", - "single_instance_allowed": "\ud558\ub098\uc758 iZone \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." }, "step": { "confirm": { diff --git a/homeassistant/components/juicenet/translations/ko.json b/homeassistant/components/juicenet/translations/ko.json index 50b824ec82f..1e1ae6aaa88 100644 --- a/homeassistant/components/juicenet/translations/ko.json +++ b/homeassistant/components/juicenet/translations/ko.json @@ -1,17 +1,17 @@ { "config": { "abort": { - "already_configured": "\uc774 JuiceNet \uacc4\uc815\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "user": { "data": { - "api_token": "JuiceNet API \ud1a0\ud070" + "api_token": "API \ud1a0\ud070" }, "description": "https://home.juice.net/Manage \uc758 API \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4.", "title": "JuiceNet \uc5d0 \uc5f0\uacb0\ud558\uae30" diff --git a/homeassistant/components/keenetic_ndms2/translations/cs.json b/homeassistant/components/keenetic_ndms2/translations/cs.json new file mode 100644 index 00000000000..f34807f3fee --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "name": "N\u00e1zev", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/ko.json b/homeassistant/components/keenetic_ndms2/translations/ko.json new file mode 100644 index 00000000000..3281ddbe3d4 --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/ko.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "name": "\uc774\ub984", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "\uc2a4\uce94 \uac04\uaca9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/ko.json b/homeassistant/components/kodi/translations/ko.json index 64b08475b68..233cd068a1e 100644 --- a/homeassistant/components/kodi/translations/ko.json +++ b/homeassistant/components/kodi/translations/ko.json @@ -1,17 +1,23 @@ { "config": { "abort": { - "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", - "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d" + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", - "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, + "flow_title": "Kodi: {name}", "step": { "credentials": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, "description": "Kodi \uc0ac\uc6a9\uc790\uba85\uacfc \uc554\ud638\ub97c \uc785\ub825\ud558\uc2ed\uc2dc\uc624. \uc774\ub7ec\ud55c \ub0b4\uc6a9\uc740 \uc2dc\uc2a4\ud15c/\uc124\uc815/\ub124\ud2b8\uc6cc\ud06c/\uc11c\ube44\uc2a4\uc5d0\uc11c \ucc3e\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4." }, "discovery_confirm": { @@ -19,9 +25,17 @@ "title": "Kodi \ubc1c\uacac" }, "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8", + "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9" + }, "description": "Kodi \uc5f0\uacb0 \uc815\ubcf4. \uc2dc\uc2a4\ud15c / \uc124\uc815 / \ub124\ud2b8\uc6cc\ud06c / \uc11c\ube44\uc2a4\uc5d0\uc11c \"HTTP\ub97c \ud1b5\ud55c Kodi \uc81c\uc5b4 \ud5c8\uc6a9\"\uc744 \ud65c\uc131\ud654\ud588\ub294\uc9c0 \ud655\uc778\ud558\uc2ed\uc2dc\uc624." }, "ws_port": { + "data": { + "ws_port": "\ud3ec\ud2b8" + }, "description": "WebSocket \ud3ec\ud2b8 (Kodi\uc5d0\uc11c TCP \ud3ec\ud2b8\ub77c\uace0\ub3c4 \ud568). WebSocket\uc744 \ud1b5\ud574 \uc5f0\uacb0\ud558\ub824\uba74 \uc2dc\uc2a4\ud15c / \uc124\uc815 / \ub124\ud2b8\uc6cc\ud06c / \uc11c\ube44\uc2a4\uc5d0\uc11c \"\ud504\ub85c\uadf8\ub7a8\uc774 Kodi\ub97c \uc81c\uc5b4\ud558\ub3c4\ub85d \ud5c8\uc6a9\"\uc744 \ud65c\uc131\ud654\ud574\uc57c\ud569\ub2c8\ub2e4. WebSocket\uc774 \ud65c\uc131\ud654\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \ud3ec\ud2b8\ub97c \uc81c\uac70\ud558\uace0 \ube44\uc6cc \ub461\ub2c8\ub2e4." } } diff --git a/homeassistant/components/konnected/translations/ko.json b/homeassistant/components/konnected/translations/ko.json index d8d2b70d909..fe5b9a0347a 100644 --- a/homeassistant/components/konnected/translations/ko.json +++ b/homeassistant/components/konnected/translations/ko.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", "not_konn_panel": "\uc778\uc2dd\ub41c Konnected.io \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4", - "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "{host}:{port} \uc758 Konnected \ud328\ub110\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "confirm": { @@ -38,7 +38,7 @@ "options_binary": { "data": { "inverse": "\uc5f4\ub9bc / \ub2eb\ud798 \uc0c1\ud0dc \ubc18\uc804", - "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d)", + "name": "\uc774\ub984 (\uc120\ud0dd\uc0ac\ud56d)", "type": "\uc774\uc9c4 \uc13c\uc11c \uc720\ud615" }, "description": "{zone} \uc635\uc158", @@ -46,7 +46,7 @@ }, "options_digital": { "data": { - "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d)", + "name": "\uc774\ub984 (\uc120\ud0dd\uc0ac\ud56d)", "poll_interval": "\ud3f4\ub9c1 \uac04\uaca9 (\ubd84) (\uc120\ud0dd \uc0ac\ud56d)", "type": "\uc13c\uc11c \uc720\ud615" }, @@ -96,7 +96,7 @@ "activation": "\uc2a4\uc704\uce58\uac00 \ucf1c\uc9c8 \ub54c \ucd9c\ub825", "momentary": "\ud384\uc2a4 \uc9c0\uc18d\uc2dc\uac04 (ms) (\uc120\ud0dd \uc0ac\ud56d)", "more_states": "\uc774 \uad6c\uc5ed\uc5d0 \ub300\ud55c \ucd94\uac00 \uc0c1\ud0dc \uad6c\uc131", - "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d)", + "name": "\uc774\ub984 (\uc120\ud0dd\uc0ac\ud56d)", "pause": "\ud384\uc2a4 \uac04 \uc77c\uc2dc\uc815\uc9c0 \uc2dc\uac04 (ms) (\uc120\ud0dd \uc0ac\ud56d)", "repeat": "\ubc18\ubcf5 \uc2dc\uac04 (-1 = \ubb34\ud55c) (\uc120\ud0dd \uc0ac\ud56d)" }, diff --git a/homeassistant/components/kulersky/translations/ko.json b/homeassistant/components/kulersky/translations/ko.json new file mode 100644 index 00000000000..7011a61f757 --- /dev/null +++ b/homeassistant/components/kulersky/translations/ko.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/ko.json b/homeassistant/components/life360/translations/ko.json index d419c5fdc02..d2ebd7c674f 100644 --- a/homeassistant/components/life360/translations/ko.json +++ b/homeassistant/components/life360/translations/ko.json @@ -1,10 +1,17 @@ { "config": { + "abort": { + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, "create_entry": { "default": "\uace0\uae09 \uc635\uc158\uc744 \uc124\uc815\ud558\ub824\uba74 [Life360 \uc124\uba85\uc11c]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "error": { - "invalid_username": "\uc0ac\uc6a9\uc790 \uc774\ub984\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_username": "\uc0ac\uc6a9\uc790 \uc774\ub984\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/lifx/translations/ko.json b/homeassistant/components/lifx/translations/ko.json index 040ac405e2d..34bec9c3aee 100644 --- a/homeassistant/components/lifx/translations/ko.json +++ b/homeassistant/components/lifx/translations/ko.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "LIFX \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", - "single_instance_allowed": "\ud558\ub098\uc758 LIFX \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." }, "step": { "confirm": { diff --git a/homeassistant/components/local_ip/translations/ko.json b/homeassistant/components/local_ip/translations/ko.json index 050229dbf08..3b543f87f79 100644 --- a/homeassistant/components/local_ip/translations/ko.json +++ b/homeassistant/components/local_ip/translations/ko.json @@ -1,13 +1,14 @@ { "config": { "abort": { - "single_instance_allowed": "\ud558\ub098\uc758 \ub85c\uceec IP \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." }, "step": { "user": { "data": { "name": "\uc13c\uc11c \uc774\ub984" }, + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "\ub85c\uceec IP \uc8fc\uc18c" } } diff --git a/homeassistant/components/locative/translations/ko.json b/homeassistant/components/locative/translations/ko.json index eb10a8ca167..5930e7edf1b 100644 --- a/homeassistant/components/locative/translations/ko.json +++ b/homeassistant/components/locative/translations/ko.json @@ -1,11 +1,15 @@ { "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", + "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4." + }, "create_entry": { "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Locative \uc571\uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { - "description": "Locative \uc6f9 \ud6c5\uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Locative \uc6f9 \ud6c5 \uc124\uc815\ud558\uae30" } } diff --git a/homeassistant/components/logi_circle/translations/ko.json b/homeassistant/components/logi_circle/translations/ko.json index 3fe8ce4824e..2300bbb27c6 100644 --- a/homeassistant/components/logi_circle/translations/ko.json +++ b/homeassistant/components/logi_circle/translations/ko.json @@ -1,11 +1,15 @@ { "config": { "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "external_error": "\ub2e4\ub978 \uad6c\uc131 \ub2e8\uacc4\uc5d0\uc11c \uc608\uc678\uc0ac\ud56d\uc774 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", - "external_setup": "Logi Circle \uc774 \ub2e4\ub978 \uad6c\uc131 \ub2e8\uacc4\uc5d0\uc11c \uc131\uacf5\uc801\uc73c\ub85c \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "external_setup": "Logi Circle \uc774 \ub2e4\ub978 \uad6c\uc131 \ub2e8\uacc4\uc5d0\uc11c \uc131\uacf5\uc801\uc73c\ub85c \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." }, "error": { - "follow_link": "\ud655\uc778\uc744 \ud074\ub9ad\ud558\uae30 \uc804\uc5d0 \ub9c1\ud06c\ub97c \ub530\ub77c \uc778\uc99d\uc744 \ubc1b\uc544\uc8fc\uc138\uc694" + "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "follow_link": "\ud655\uc778\uc744 \ud074\ub9ad\ud558\uae30 \uc804\uc5d0 \ub9c1\ud06c\ub97c \ub530\ub77c \uc778\uc99d\uc744 \ubc1b\uc544\uc8fc\uc138\uc694", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "auth": { diff --git a/homeassistant/components/luftdaten/translations/ko.json b/homeassistant/components/luftdaten/translations/ko.json index eb69dfb64a2..fbb5a26e7ee 100644 --- a/homeassistant/components/luftdaten/translations/ko.json +++ b/homeassistant/components/luftdaten/translations/ko.json @@ -1,6 +1,8 @@ { "config": { "error": { + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_sensor": "\uc13c\uc11c\ub97c \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uac70\ub098 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" }, "step": { diff --git a/homeassistant/components/lutron_caseta/translations/ko.json b/homeassistant/components/lutron_caseta/translations/ko.json index 8c5caec998e..af7fed5829c 100644 --- a/homeassistant/components/lutron_caseta/translations/ko.json +++ b/homeassistant/components/lutron_caseta/translations/ko.json @@ -1,16 +1,21 @@ { "config": { "abort": { - "already_configured": "Cas\u00e9ta \ube0c\ub9ac\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "cannot_connect": "Cas\u00e9ta \ube0c\ub9ac\uc9c0 \uc5f0\uacb0 \uc2e4\ud328\ub85c \uc124\uc815\uc774 \ucde8\uc18c\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "Cas\u00e9ta \ube0c\ub9ac\uc9c0\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8 \ubc0f \uc778\uc99d\uc11c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694." + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "import_failed": { "description": "configuration.yaml \uc5d0\uc11c \uac00\uc838\uc628 \ube0c\ub9ac\uc9c0 (\ud638\uc2a4\ud2b8:{host}) \ub97c \uc124\uc815\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "title": "Cas\u00e9ta \ube0c\ub9ac\uc9c0 \uad6c\uc131\uc744 \uac00\uc838\uc624\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4." + }, + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8" + } } } } diff --git a/homeassistant/components/lyric/translations/ko.json b/homeassistant/components/lyric/translations/ko.json new file mode 100644 index 00000000000..fa000ea1c06 --- /dev/null +++ b/homeassistant/components/lyric/translations/ko.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." + }, + "create_entry": { + "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "pick_implementation": { + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/ko.json b/homeassistant/components/mailgun/translations/ko.json index 43b6586b14f..b757a27f4a0 100644 --- a/homeassistant/components/mailgun/translations/ko.json +++ b/homeassistant/components/mailgun/translations/ko.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", + "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4." + }, "create_entry": { "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Mailgun \uc6f9 \ud6c5]({mailgun_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/json\n \nHome Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, diff --git a/homeassistant/components/mazda/translations/cs.json b/homeassistant/components/mazda/translations/cs.json index 8a929fb58d7..89fde600735 100644 --- a/homeassistant/components/mazda/translations/cs.json +++ b/homeassistant/components/mazda/translations/cs.json @@ -13,13 +13,15 @@ "reauth": { "data": { "email": "E-mail", - "password": "Heslo" + "password": "Heslo", + "region": "Region" } }, "user": { "data": { "email": "E-mail", - "password": "Heslo" + "password": "Heslo", + "region": "Region" } } } diff --git a/homeassistant/components/mazda/translations/ko.json b/homeassistant/components/mazda/translations/ko.json new file mode 100644 index 00000000000..31495b0d8e3 --- /dev/null +++ b/homeassistant/components/mazda/translations/ko.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "reauth": { + "data": { + "email": "\uc774\uba54\uc77c", + "password": "\ube44\ubc00\ubc88\ud638" + } + }, + "user": { + "data": { + "email": "\uc774\uba54\uc77c", + "password": "\ube44\ubc00\ubc88\ud638" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/translations/ko.json b/homeassistant/components/melcloud/translations/ko.json index a43d4cfbcb3..2e1f1b535e1 100644 --- a/homeassistant/components/melcloud/translations/ko.json +++ b/homeassistant/components/melcloud/translations/ko.json @@ -4,7 +4,7 @@ "already_configured": "\uc774 \uc774\uba54\uc77c\uc5d0 \ub300\ud55c MELCloud \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uac31\uc2e0\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, diff --git a/homeassistant/components/met/translations/ko.json b/homeassistant/components/met/translations/ko.json index e7263aba3d2..17175c196c0 100644 --- a/homeassistant/components/met/translations/ko.json +++ b/homeassistant/components/met/translations/ko.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/meteo_france/translations/ko.json b/homeassistant/components/meteo_france/translations/ko.json index 4b8dc3204dd..ec48103bbff 100644 --- a/homeassistant/components/meteo_france/translations/ko.json +++ b/homeassistant/components/meteo_france/translations/ko.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\ub3c4\uc2dc\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uc785\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694" + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { "empty": "\ub3c4\uc2dc \uac80\uc0c9 \uacb0\uacfc \uc5c6\uc74c: \ub3c4\uc2dc \ud544\ub4dc\ub97c \ud655\uc778\ud558\uc2ed\uc2dc\uc624." diff --git a/homeassistant/components/metoffice/translations/ko.json b/homeassistant/components/metoffice/translations/ko.json index b1af2afaf30..b2f09a4a9e5 100644 --- a/homeassistant/components/metoffice/translations/ko.json +++ b/homeassistant/components/metoffice/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "\uc601\uad6d \uae30\uc0c1\uccad DataPoint API \ud0a4", + "api_key": "API \ud0a4", "latitude": "\uc704\ub3c4", "longitude": "\uacbd\ub3c4" }, diff --git a/homeassistant/components/mikrotik/translations/ko.json b/homeassistant/components/mikrotik/translations/ko.json index f32e2260501..05a8f50066c 100644 --- a/homeassistant/components/mikrotik/translations/ko.json +++ b/homeassistant/components/mikrotik/translations/ko.json @@ -1,10 +1,11 @@ { "config": { "abort": { - "already_configured": "Mikrotik \uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4" }, "step": { diff --git a/homeassistant/components/mill/translations/ko.json b/homeassistant/components/mill/translations/ko.json index d2c6fd74284..48c8cdc6eaa 100644 --- a/homeassistant/components/mill/translations/ko.json +++ b/homeassistant/components/mill/translations/ko.json @@ -1,7 +1,10 @@ { "config": { "abort": { - "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/minecraft_server/translations/ko.json b/homeassistant/components/minecraft_server/translations/ko.json index 30605d72936..98ab72e94fc 100644 --- a/homeassistant/components/minecraft_server/translations/ko.json +++ b/homeassistant/components/minecraft_server/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { "cannot_connect": "\uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\ub97c \ud655\uc778\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \ub610\ud55c \uc11c\ubc84\uc5d0\uc11c Minecraft \ubc84\uc804 1.7 \uc774\uc0c1\uc744 \uc2e4\ud589 \uc911\uc778\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", diff --git a/homeassistant/components/monoprice/translations/ko.json b/homeassistant/components/monoprice/translations/ko.json index 23e19173535..6afed3aa6d6 100644 --- a/homeassistant/components/monoprice/translations/ko.json +++ b/homeassistant/components/monoprice/translations/ko.json @@ -4,7 +4,7 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { diff --git a/homeassistant/components/motion_blinds/translations/ko.json b/homeassistant/components/motion_blinds/translations/ko.json new file mode 100644 index 00000000000..9d2f0eead3d --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/ko.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", + "connection_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "connect": { + "data": { + "api_key": "API \ud0a4" + } + }, + "select": { + "data": { + "select_ip": "IP \uc8fc\uc18c" + } + }, + "user": { + "data": { + "api_key": "API \ud0a4", + "host": "IP \uc8fc\uc18c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/ko.json b/homeassistant/components/mqtt/translations/ko.json index f713d564438..fd79863fadf 100644 --- a/homeassistant/components/mqtt/translations/ko.json +++ b/homeassistant/components/mqtt/translations/ko.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "single_instance_allowed": "\ud558\ub098\uc758 MQTT \ube0c\ub85c\ucee4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." }, "error": { - "cannot_connect": "MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "broker": { @@ -52,7 +52,7 @@ "error": { "bad_birth": "Birth \ud1a0\ud53d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "bad_will": "Will \ud1a0\ud53d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "cannot_connect": "MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "broker": { @@ -71,6 +71,7 @@ "birth_retain": "Birth \uba54\uc2dc\uc9c0 \ub9ac\ud14c\uc778", "birth_topic": "Birth \uba54\uc2dc\uc9c0 \ud1a0\ud53d", "discovery": "\uc7a5\uce58 \uac80\uc0c9 \ud65c\uc131\ud654", + "will_enable": "Will \uba54\uc2dc\uc9c0 \ud65c\uc131\ud654", "will_payload": "Will \uba54\uc2dc\uc9c0 \ud398\uc774\ub85c\ub4dc", "will_qos": "Will \uba54\uc2dc\uc9c0 QoS", "will_retain": "Will \uba54\uc2dc\uc9c0 \ub9ac\ud14c\uc778", diff --git a/homeassistant/components/myq/translations/ko.json b/homeassistant/components/myq/translations/ko.json index 31e3f8646e6..23ba2eecea7 100644 --- a/homeassistant/components/myq/translations/ko.json +++ b/homeassistant/components/myq/translations/ko.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "MyQ \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, diff --git a/homeassistant/components/mysensors/translations/cs.json b/homeassistant/components/mysensors/translations/cs.json index 9d3cfbd2508..abe47f046ff 100644 --- a/homeassistant/components/mysensors/translations/cs.json +++ b/homeassistant/components/mysensors/translations/cs.json @@ -1,13 +1,16 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "error": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" } }, "title": "MySensors" diff --git a/homeassistant/components/mysensors/translations/ko.json b/homeassistant/components/mysensors/translations/ko.json new file mode 100644 index 00000000000..bb38f94bc92 --- /dev/null +++ b/homeassistant/components/mysensors/translations/ko.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/ko.json b/homeassistant/components/neato/translations/ko.json index 00d1ae3b467..359aeefcc78 100644 --- a/homeassistant/components/neato/translations/ko.json +++ b/homeassistant/components/neato/translations/ko.json @@ -1,12 +1,27 @@ { "config": { "abort": { - "already_configured": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", + "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, "create_entry": { - "default": "[Neato \uc124\uba85\uc11c]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { + "pick_implementation": { + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" + }, + "reauth_confirm": { + "title": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + }, "user": { "data": { "password": "\ube44\ubc00\ubc88\ud638", diff --git a/homeassistant/components/nest/translations/ko.json b/homeassistant/components/nest/translations/ko.json index 798f191e34a..f5a0fcf39d1 100644 --- a/homeassistant/components/nest/translations/ko.json +++ b/homeassistant/components/nest/translations/ko.json @@ -1,20 +1,29 @@ { "config": { "abort": { - "authorize_url_fail": "\uc778\uc99d url \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", - "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "authorize_url_fail": "\uc778\uc99d URL \uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", + "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", + "unknown_authorize_url_generation": "\uc778\uc99d URL \uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4." + }, + "create_entry": { + "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { "internal_error": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac\uc5d0 \ub0b4\ubd80 \uc624\ub958 \ubc1c\uc0dd", + "invalid_pin": "PIN \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "timeout": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac \uc2dc\uac04 \ucd08\uacfc", - "unknown": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958 \ubc1c\uc0dd" + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "init": { "data": { "flow_impl": "\uacf5\uae09\uc790" }, - "description": "Nest \ub97c \uc778\uc99d\ud558\uae30 \uc704\ud55c \uc778\uc99d \uacf5\uae09\uc790\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", + "description": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30", "title": "\uc778\uc99d \uacf5\uae09\uc790" }, "link": { @@ -23,6 +32,12 @@ }, "description": "Nest \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74, [\uacc4\uc815 \uc5f0\uacb0 \uc2b9\uc778]({url}) \uc744 \ud574\uc8fc\uc138\uc694.\n\n\uc2b9\uc778 \ud6c4, \uc544\ub798\uc758 PIN \ucf54\ub4dc\ub97c \ubcf5\uc0ac\ud558\uc5ec \ubd99\uc5ec\ub123\uc73c\uc138\uc694.", "title": "Nest \uacc4\uc815 \uc5f0\uacb0\ud558\uae30" + }, + "pick_implementation": { + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" + }, + "reauth_confirm": { + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d" } } } diff --git a/homeassistant/components/netatmo/translations/ko.json b/homeassistant/components/netatmo/translations/ko.json index 8165941f0d8..320df466515 100644 --- a/homeassistant/components/netatmo/translations/ko.json +++ b/homeassistant/components/netatmo/translations/ko.json @@ -3,7 +3,8 @@ "abort": { "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", - "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" + "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." }, "create_entry": { "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/nexia/translations/ko.json b/homeassistant/components/nexia/translations/ko.json index a918de60fe6..170411f5f73 100644 --- a/homeassistant/components/nexia/translations/ko.json +++ b/homeassistant/components/nexia/translations/ko.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "nexia home \uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, diff --git a/homeassistant/components/nightscout/translations/ko.json b/homeassistant/components/nightscout/translations/ko.json index 0235c446e75..0408a1f61ab 100644 --- a/homeassistant/components/nightscout/translations/ko.json +++ b/homeassistant/components/nightscout/translations/ko.json @@ -4,7 +4,17 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "api_key": "API \ud0a4", + "url": "URL \uc8fc\uc18c" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/notion/translations/ko.json b/homeassistant/components/notion/translations/ko.json index 323ea126445..b5c7cadbe9b 100644 --- a/homeassistant/components/notion/translations/ko.json +++ b/homeassistant/components/notion/translations/ko.json @@ -1,9 +1,10 @@ { "config": { "abort": { - "already_configured": "\uc774 \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4." + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "no_devices": "\uacc4\uc815\uc5d0 \ub4f1\ub85d\ub41c \uae30\uae30\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" }, "step": { diff --git a/homeassistant/components/nuheat/translations/ko.json b/homeassistant/components/nuheat/translations/ko.json index 1476e8beb0d..a533cd69093 100644 --- a/homeassistant/components/nuheat/translations/ko.json +++ b/homeassistant/components/nuheat/translations/ko.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\uc628\ub3c4 \uc870\uc808\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_thermostat": "\uc628\ub3c4 \uc870\uc808\uae30\uc758 \uc2dc\ub9ac\uc5bc \ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/nuki/translations/ko.json b/homeassistant/components/nuki/translations/ko.json new file mode 100644 index 00000000000..68f43847d6c --- /dev/null +++ b/homeassistant/components/nuki/translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8", + "token": "\uc561\uc138\uc2a4 \ud1a0\ud070" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/translations/ko.json b/homeassistant/components/nut/translations/ko.json index 81fe8d88a90..0fb8339ddfc 100644 --- a/homeassistant/components/nut/translations/ko.json +++ b/homeassistant/components/nut/translations/ko.json @@ -4,7 +4,7 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { diff --git a/homeassistant/components/nws/translations/ko.json b/homeassistant/components/nws/translations/ko.json index 552099c7193..9fbdf026558 100644 --- a/homeassistant/components/nws/translations/ko.json +++ b/homeassistant/components/nws/translations/ko.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { @@ -15,7 +15,7 @@ "longitude": "\uacbd\ub3c4", "station": "METAR \uc2a4\ud14c\uc774\uc158 \ucf54\ub4dc" }, - "description": "METAR \uc2a4\ud14c\uc774\uc158 \ucf54\ub4dc\ub97c \uc9c0\uc815\ud558\uc9c0 \uc54a\uc73c\uba74 \uac00\uae4c\uc6b4 \uc2a4\ud14c\uc774\uc158\uc744 \ucc3e\ub294\ub370 \uc704\ub3c4\uc640 \uacbd\ub3c4\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4.", + "description": "METAR \uc2a4\ud14c\uc774\uc158 \ucf54\ub4dc\uac00 \uc9c0\uc815\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \uc704\ub3c4 \ubc0f \uacbd\ub3c4\uac00 \uac00\uc7a5 \uac00\uae4c\uc6b4 \uc2a4\ud14c\uc774\uc158\uc744 \ucc3e\ub294 \ub370 \uc0ac\uc6a9\ub429\ub2c8\ub2e4. \ud604\uc7ac API Key \ub294 \uc544\ubb34 \ud0a4\ub098 \ub123\uc5b4\ub3c4 \uc0c1\uad00 \uc5c6\uc2b5\ub2c8\ub2e4\ub9cc, \uc62c\ubc14\ub978 \uc774\uba54\uc77c \uc8fc\uc18c\ub97c \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \uad8c\uc7a5\ud569\ub2c8\ub2e4.", "title": "\ubbf8\uad6d \uae30\uc0c1\uccad\uc5d0 \uc5f0\uacb0\ud558\uae30" } } diff --git a/homeassistant/components/nzbget/translations/ko.json b/homeassistant/components/nzbget/translations/ko.json index dd53d52a236..58d38b66361 100644 --- a/homeassistant/components/nzbget/translations/ko.json +++ b/homeassistant/components/nzbget/translations/ko.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub428. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328" + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "flow_title": "NZBGet : {name}", "step": { @@ -13,11 +13,11 @@ "data": { "host": "\ud638\uc2a4\ud2b8", "name": "\uc774\ub984", - "password": "\uc554\ud638", + "password": "\ube44\ubc00\ubc88\ud638", "port": "\ud3ec\ud2b8", - "ssl": "NZBGet\uc740 SSL \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4.", - "username": "\uc0ac\uc6a9\uc790\uba85", - "verify_ssl": "NZBGet\uc740 \uc801\uc808\ud55c \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4." + "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", + "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" }, "title": "NZBGet\uc5d0 \uc5f0\uacb0" } diff --git a/homeassistant/components/omnilogic/translations/ko.json b/homeassistant/components/omnilogic/translations/ko.json index 5389207cdda..74786104624 100644 --- a/homeassistant/components/omnilogic/translations/ko.json +++ b/homeassistant/components/omnilogic/translations/ko.json @@ -1,18 +1,18 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uc124\uc815\ub418\uc5b4 \uc788\uc74c. \ud558\ub098\uc758 \uc124\uc815\ub9cc \uac00\ub2a5\ud568." + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." }, "error": { - "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", - "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "user": { "data": { - "password": "\uc554\ud638", - "username": "\uc0ac\uc6a9\uc790\uba85" + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" } } } diff --git a/homeassistant/components/ondilo_ico/translations/ko.json b/homeassistant/components/ondilo_ico/translations/ko.json new file mode 100644 index 00000000000..fa000ea1c06 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/ko.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." + }, + "create_entry": { + "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "pick_implementation": { + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/ko.json b/homeassistant/components/onewire/translations/ko.json new file mode 100644 index 00000000000..871482b766b --- /dev/null +++ b/homeassistant/components/onewire/translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "owserver": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/ko.json b/homeassistant/components/onvif/translations/ko.json index 3b992e8d35f..173cdb88512 100644 --- a/homeassistant/components/onvif/translations/ko.json +++ b/homeassistant/components/onvif/translations/ko.json @@ -1,12 +1,15 @@ { "config": { "abort": { - "already_configured": "ONVIF \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "already_in_progress": "ONVIF \uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.", + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", "no_h264": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c H264 \uc2a4\ud2b8\ub9bc\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uae30\uae30\uc5d0\uc11c \ud504\ub85c\ud544 \uad6c\uc131\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694.", "no_mac": "ONVIF \uae30\uae30\uc758 \uace0\uc720 ID \ub97c \uad6c\uc131\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "onvif_error": "ONVIF \uae30\uae30 \uc124\uc815 \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 \ub85c\uadf8\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694." }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, "step": { "auth": { "data": { diff --git a/homeassistant/components/opentherm_gw/translations/ko.json b/homeassistant/components/opentherm_gw/translations/ko.json index eece1492002..6f3ac939ad1 100644 --- a/homeassistant/components/opentherm_gw/translations/ko.json +++ b/homeassistant/components/opentherm_gw/translations/ko.json @@ -1,7 +1,8 @@ { "config": { "error": { - "already_configured": "OpenTherm Gateway \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "id_exists": "OpenTherm Gateway id \uac00 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4" }, "step": { diff --git a/homeassistant/components/openuv/translations/ko.json b/homeassistant/components/openuv/translations/ko.json index 480b745fe36..ee211d3cbd5 100644 --- a/homeassistant/components/openuv/translations/ko.json +++ b/homeassistant/components/openuv/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uc88c\ud45c\uac12\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "error": { "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/openweathermap/translations/ko.json b/homeassistant/components/openweathermap/translations/ko.json index 9560f447250..1514ada24ec 100644 --- a/homeassistant/components/openweathermap/translations/ko.json +++ b/homeassistant/components/openweathermap/translations/ko.json @@ -1,17 +1,21 @@ { "config": { "abort": { - "already_configured": "\uc774\ub7ec\ud55c \uc88c\ud45c\uc5d0 \ub300\ud55c OpenWeatherMap \ud1b5\ud569\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4." + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "user": { "data": { - "api_key": "OpenWeatherMap API \ud0a4", + "api_key": "API \ud0a4", "language": "\uc5b8\uc5b4", "latitude": "\uc704\ub3c4", "longitude": "\uacbd\ub3c4", "mode": "\ubaa8\ub4dc", - "name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uba85" + "name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc774\ub984" }, "description": "OpenWeatherMap \ud1b5\ud569\uc744 \uc124\uc815\ud558\uc138\uc694. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://openweathermap.org/appid\ub85c \uc774\ub3d9\ud558\uc2ed\uc2dc\uc624.", "title": "OpenWeatherMap" diff --git a/homeassistant/components/ovo_energy/translations/ko.json b/homeassistant/components/ovo_energy/translations/ko.json index 26372afc28e..07ef8d8e166 100644 --- a/homeassistant/components/ovo_energy/translations/ko.json +++ b/homeassistant/components/ovo_energy/translations/ko.json @@ -1,10 +1,21 @@ { "config": { "error": { - "already_configured": "\uacc4\uc815\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { + "reauth": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638" + } + }, "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, "title": "OVO Energy \uacc4\uc815 \ucd94\uac00\ud558\uae30" } } diff --git a/homeassistant/components/owntracks/translations/ko.json b/homeassistant/components/owntracks/translations/ko.json index 3cde37528c2..6e558e54627 100644 --- a/homeassistant/components/owntracks/translations/ko.json +++ b/homeassistant/components/owntracks/translations/ko.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, "create_entry": { "default": "\n\nAndroid \uc778 \uacbd\uc6b0, [OwnTracks \uc571]({android_url}) \uc744 \uc5f4\uace0 preferences -> connection \uc73c\ub85c \uc774\ub3d9\ud558\uc5ec \ub2e4\uc74c\uacfc \uac19\uc774 \uc124\uc815\ud574\uc8fc\uc138\uc694:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: `''`\n - Device ID: `''`\n\niOS \uc778 \uacbd\uc6b0, [OwnTracks \uc571]({ios_url}) \uc744 \uc5f4\uace0 \uc67c\ucabd \uc0c1\ub2e8\uc758 (i) \uc544\uc774\ucf58\uc744 \ud0ed\ud558\uc5ec \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud558\uc5ec \ub2e4\uc74c\uacfc \uac19\uc774 \uc124\uc815\ud574\uc8fc\uc138\uc694:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: `''`\n\n{secret} \n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, diff --git a/homeassistant/components/ozw/translations/ko.json b/homeassistant/components/ozw/translations/ko.json index 98b965d5dd2..ba37dccdd68 100644 --- a/homeassistant/components/ozw/translations/ko.json +++ b/homeassistant/components/ozw/translations/ko.json @@ -1,7 +1,17 @@ { "config": { "abort": { - "mqtt_required": "MQTT \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \uc124\uc815\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4" + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", + "mqtt_required": "MQTT \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \uc124\uc815\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, + "step": { + "start_addon": { + "data": { + "usb_path": "USB \uc7a5\uce58 \uacbd\ub85c" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/ko.json b/homeassistant/components/panasonic_viera/translations/ko.json index fc2fd7827ab..0f3252a4ab1 100644 --- a/homeassistant/components/panasonic_viera/translations/ko.json +++ b/homeassistant/components/panasonic_viera/translations/ko.json @@ -1,18 +1,20 @@ { "config": { "abort": { - "already_configured": "\uc774 Panasonic Viera TV \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uc785\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 \ub85c\uadf8\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694" + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_pin_code": "\uc785\ub825\ud55c PIN \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "pairing": { "data": { - "pin": "PIN" + "pin": "PIN \ucf54\ub4dc" }, - "description": "TV \uc5d0 \ud45c\uc2dc\ub41c PIN \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", + "description": "TV \uc5d0 \ud45c\uc2dc\ub41c PIN \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", "title": "\ud398\uc5b4\ub9c1\ud558\uae30" }, "user": { diff --git a/homeassistant/components/philips_js/translations/ko.json b/homeassistant/components/philips_js/translations/ko.json new file mode 100644 index 00000000000..85281856809 --- /dev/null +++ b/homeassistant/components/philips_js/translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/ko.json b/homeassistant/components/pi_hole/translations/ko.json index 4653cc8564d..7261742b2a6 100644 --- a/homeassistant/components/pi_hole/translations/ko.json +++ b/homeassistant/components/pi_hole/translations/ko.json @@ -7,6 +7,11 @@ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "step": { + "api_key": { + "data": { + "api_key": "API \ud0a4" + } + }, "user": { "data": { "api_key": "API \ud0a4", @@ -14,8 +19,8 @@ "location": "\uc704\uce58", "name": "\uc774\ub984", "port": "\ud3ec\ud2b8", - "ssl": "SSL \uc0ac\uc6a9", - "verify_ssl": "SSL \uc778\uc99d\uc11c \uac80\uc99d" + "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9", + "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" } } } diff --git a/homeassistant/components/plaato/translations/ko.json b/homeassistant/components/plaato/translations/ko.json index 6eeb6a9c061..753653c88b2 100644 --- a/homeassistant/components/plaato/translations/ko.json +++ b/homeassistant/components/plaato/translations/ko.json @@ -1,12 +1,17 @@ { "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", + "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4." + }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Plaato Airlock \uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4.\n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "**{device_name}** \uc758 Plaato {device_type} \uc774(\uac00) \uc131\uacf5\uc801\uc73c\ub85c \uc124\uc815\ub418\uc5c8\uc2b5\ub2c8\ub2e4!" }, "step": { "user": { - "description": "Plaato Airlock \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Plaato \uc6f9 \ud6c5 \uc124\uc815\ud558\uae30" + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Plaato \uae30\uae30 \uc124\uc815\ud558\uae30" } } } diff --git a/homeassistant/components/plex/translations/ko.json b/homeassistant/components/plex/translations/ko.json index 7c461fe1673..df728533467 100644 --- a/homeassistant/components/plex/translations/ko.json +++ b/homeassistant/components/plex/translations/ko.json @@ -3,9 +3,10 @@ "abort": { "all_configured": "\uc774\ubbf8 \uad6c\uc131\ub41c \ubaa8\ub4e0 \uc5f0\uacb0\ub41c \uc11c\ubc84", "already_configured": "\uc774 Plex \uc11c\ubc84\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "already_in_progress": "Plex \ub97c \uad6c\uc131 \uc911\uc785\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4", "token_request_timeout": "\ud1a0\ud070 \ud68d\ub4dd \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4", - "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc774\uc720\ub85c \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4" + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { "faulty_credentials": "\uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. \ud1a0\ud070\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694", @@ -20,9 +21,9 @@ "data": { "host": "\ud638\uc2a4\ud2b8", "port": "\ud3ec\ud2b8", - "ssl": "SSL \uc0ac\uc6a9", + "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9", "token": "\ud1a0\ud070 (\uc120\ud0dd \uc0ac\ud56d)", - "verify_ssl": "SSL \uc778\uc99d\uc11c \uac80\uc99d" + "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" }, "title": "Plex \uc9c1\uc811 \uad6c\uc131\ud558\uae30" }, diff --git a/homeassistant/components/plugwise/translations/ko.json b/homeassistant/components/plugwise/translations/ko.json index 4af12098b7f..7af503f0a66 100644 --- a/homeassistant/components/plugwise/translations/ko.json +++ b/homeassistant/components/plugwise/translations/ko.json @@ -1,18 +1,24 @@ { "config": { "abort": { - "already_configured": "\uc774 Smile \uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694", - "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. 8\uc790\uc758 Smile ID \ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "flow_title": "Smile: {name}", "step": { "user": { - "description": "\uc138\ubd80 \uc815\ubcf4", - "title": "Smile \uc5d0 \uc5f0\uacb0\ud558\uae30" + "description": "\uc81c\ud488:", + "title": "Plugwise \uc720\ud615" + }, + "user_gateway": { + "data": { + "host": "IP \uc8fc\uc18c", + "port": "\ud3ec\ud2b8" + } } } }, diff --git a/homeassistant/components/plum_lightpad/translations/ko.json b/homeassistant/components/plum_lightpad/translations/ko.json index 008177f1cec..be71285cbb7 100644 --- a/homeassistant/components/plum_lightpad/translations/ko.json +++ b/homeassistant/components/plum_lightpad/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/point/translations/ko.json b/homeassistant/components/point/translations/ko.json index 6ea58e95834..f4ca6002036 100644 --- a/homeassistant/components/point/translations/ko.json +++ b/homeassistant/components/point/translations/ko.json @@ -2,10 +2,11 @@ "config": { "abort": { "already_setup": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", - "authorize_url_fail": "\uc778\uc99d url \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "authorize_url_fail": "\uc778\uc99d URL \uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "external_setup": "Point \uac00 \ub2e4\ub978 \uad6c\uc131 \ub2e8\uacc4\uc5d0\uc11c \uc131\uacf5\uc801\uc73c\ub85c \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "no_flows": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." + "no_flows": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", + "unknown_authorize_url_generation": "\uc778\uc99d URL \uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4." }, "create_entry": { "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/poolsense/translations/ko.json b/homeassistant/components/poolsense/translations/ko.json index 42a6654592f..ec8c7dfc90f 100644 --- a/homeassistant/components/poolsense/translations/ko.json +++ b/homeassistant/components/poolsense/translations/ko.json @@ -12,7 +12,7 @@ "email": "\uc774\uba54\uc77c", "password": "\ube44\ubc00\ubc88\ud638" }, - "description": "[%key:common::config_flow::description%]", + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "PoolSense" } } diff --git a/homeassistant/components/powerwall/translations/it.json b/homeassistant/components/powerwall/translations/it.json index d136c385aca..48cd7c04743 100644 --- a/homeassistant/components/powerwall/translations/it.json +++ b/homeassistant/components/powerwall/translations/it.json @@ -17,7 +17,7 @@ "ip_address": "Indirizzo IP", "password": "Password" }, - "description": "La password di solito \u00e8 costituita dagli ultimi 5 caratteri del numero di serie per il Backup Gateway e pu\u00f2 essere trovata nell'app Telsa; oppure dagli ultimi 5 caratteri della password trovata all'interno della porta per il Backup Gateway 2.", + "description": "La password di solito \u00e8 costituita dagli ultimi 5 caratteri del numero di serie per il Backup Gateway e pu\u00f2 essere trovata nell'app Tesla; oppure dagli ultimi 5 caratteri della password trovata all'interno della porta per il Backup Gateway 2.", "title": "Connessione al Powerwall" } } diff --git a/homeassistant/components/powerwall/translations/ko.json b/homeassistant/components/powerwall/translations/ko.json index 9ba7004899f..11c638fa9bd 100644 --- a/homeassistant/components/powerwall/translations/ko.json +++ b/homeassistant/components/powerwall/translations/ko.json @@ -1,17 +1,20 @@ { "config": { "abort": { - "already_configured": "powerwall \uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", "wrong_version": "Powerwall \uc774 \ud604\uc7ac \uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \ubc84\uc804\uc758 \uc18c\ud504\ud2b8\uc6e8\uc5b4\ub97c \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4. \uc774 \ubb38\uc81c\ub97c \ud574\uacb0\ud558\ub824\uba74 \uc5c5\uadf8\ub808\uc774\ub4dc\ud558\uac70\ub098 \uc774 \ub0b4\uc6a9\uc744 \uc54c\ub824\uc8fc\uc138\uc694." }, "step": { "user": { "data": { - "ip_address": "IP \uc8fc\uc18c" + "ip_address": "IP \uc8fc\uc18c", + "password": "\ube44\ubc00\ubc88\ud638" }, "title": "powerwall \uc5d0 \uc5f0\uacb0\ud558\uae30" } diff --git a/homeassistant/components/profiler/translations/ko.json b/homeassistant/components/profiler/translations/ko.json new file mode 100644 index 00000000000..0c38052b826 --- /dev/null +++ b/homeassistant/components/profiler/translations/ko.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, + "step": { + "user": { + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/ko.json b/homeassistant/components/progettihwsw/translations/ko.json index 02fca814811..ab21d8427bd 100644 --- a/homeassistant/components/progettihwsw/translations/ko.json +++ b/homeassistant/components/progettihwsw/translations/ko.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\uc7a5\uce58\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4." + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "relay_modes": { diff --git a/homeassistant/components/ps4/translations/ko.json b/homeassistant/components/ps4/translations/ko.json index 0e62a64d1c6..76762a0bec6 100644 --- a/homeassistant/components/ps4/translations/ko.json +++ b/homeassistant/components/ps4/translations/ko.json @@ -1,15 +1,17 @@ { "config": { "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "credential_error": "\uc790\uaca9 \uc99d\uba85\uc744 \uac00\uc838\uc624\ub294 \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", - "no_devices_found": "PlayStation 4 \uae30\uae30\ub97c \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "port_987_bind_error": "\ud3ec\ud2b8 987 \uc5d0 \ubc14\uc778\ub529 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucd94\uac00 \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/components/ps4/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", "port_997_bind_error": "\ud3ec\ud2b8 997 \uc5d0 \ubc14\uc778\ub529 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucd94\uac00 \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/components/ps4/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "credential_timeout": "\uc790\uaca9 \uc99d\uba85 \uc11c\ube44\uc2a4 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud655\uc778\uc744 \ud074\ub9ad\ud558\uc5ec \ub2e4\uc2dc \uc2dc\uc791\ud574\uc8fc\uc138\uc694.", - "login_failed": "PlayStation 4 \uc640 \ud398\uc5b4\ub9c1\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. PIN \uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", - "no_ipaddress": "\uad6c\uc131\ud558\uace0\uc790 \ud558\ub294 PlayStation 4 \uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694." + "login_failed": "PlayStation 4 \uc640 \ud398\uc5b4\ub9c1\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. PIN \ucf54\ub4dc\uac00 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "no_ipaddress": "\uad6c\uc131\ud560 PlayStation 4\uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694." }, "step": { "creds": { @@ -18,12 +20,12 @@ }, "link": { "data": { - "code": "PIN", + "code": "PIN \ucf54\ub4dc", "ip_address": "IP \uc8fc\uc18c", "name": "\uc774\ub984", "region": "\uc9c0\uc5ed" }, - "description": "PlayStation 4 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. 'PIN' \uc744 \ud655\uc778\ud558\ub824\uba74, PlayStation 4 \ucf58\uc194\uc5d0\uc11c '\uc124\uc815' \uc73c\ub85c \uc774\ub3d9\ud55c \ub4a4 '\ubaa8\ubc14\uc77c \uc571 \uc811\uc18d \uc124\uc815' \uc73c\ub85c \uc774\ub3d9\ud558\uc5ec '\uae30\uae30 \ub4f1\ub85d\ud558\uae30' \ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \ud654\uba74\uc5d0 \ud45c\uc2dc\ub41c 8\uc790\ub9ac \uc22b\uc790\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \ucd94\uac00 \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/components/ps4/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "description": "PlayStation 4 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. PIN \ucf54\ub4dc\ub97c \ud655\uc778\ud558\ub824\uba74, PlayStation 4 \ucf58\uc194\uc5d0\uc11c '\uc124\uc815' \uc73c\ub85c \uc774\ub3d9\ud55c \ub4a4 '\ubaa8\ubc14\uc77c \uc571 \uc811\uc18d \uc124\uc815' \uc73c\ub85c \uc774\ub3d9\ud558\uc5ec '\uae30\uae30 \ub4f1\ub85d\ud558\uae30' \ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \ud654\uba74\uc5d0 \ud45c\uc2dc\ub41c PIN \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \ucd94\uac00 \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/components/ps4/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", "title": "PlayStation 4" }, "mode": { @@ -31,7 +33,7 @@ "ip_address": "IP \uc8fc\uc18c (\uc790\ub3d9 \uac80\uc0c9\uc744 \uc0ac\uc6a9\ud558\ub294 \uacbd\uc6b0 \ube44\uc6cc\ub450\uc138\uc694)", "mode": "\uad6c\uc131 \ubaa8\ub4dc" }, - "description": "\uad6c\uc131 \ubaa8\ub4dc\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \uc790\ub3d9 \uac80\uc0c9\uc744 \uc120\ud0dd\ud558\uba74 \uae30\uae30\uac00 \uc790\ub3d9\uc73c\ub85c \uac80\uc0c9\ub418\ubbc0\ub85c IP \uc8fc\uc18c \ud544\ub4dc\ub294 \ube44\uc6cc\ub450\uc154\ub3c4 \ub429\ub2c8\ub2e4.", + "description": "\uad6c\uc131\ud560 \ubaa8\ub4dc\ub97c \uc120\ud0dd\ud569\ub2c8\ub2e4. \uc790\ub3d9 \uac80\uc0c9\uc744 \uc120\ud0dd\ud558\uba74 \uae30\uae30\uac00 \uc790\ub3d9\uc73c\ub85c \uac80\uc0c9\ub418\ubbc0\ub85c IP \uc8fc\uc18c \ud544\ub4dc\ub294 \ube44\uc6cc\ub450\uc154\ub3c4 \ub429\ub2c8\ub2e4.", "title": "PlayStation 4" } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ko.json b/homeassistant/components/pvpc_hourly_pricing/translations/ko.json index 35ac17a8bb8..f1f225ae525 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/ko.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \uc774\ubbf8 \ud574\ub2f9 \uc694\uae08\uc81c \uc13c\uc11c\ub85c \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/rachio/translations/ko.json b/homeassistant/components/rachio/translations/ko.json index 2f5724c7af1..298ef476745 100644 --- a/homeassistant/components/rachio/translations/ko.json +++ b/homeassistant/components/rachio/translations/ko.json @@ -4,7 +4,7 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, @@ -13,7 +13,7 @@ "data": { "api_key": "API \ud0a4" }, - "description": "https://app.rach.io/ \uc758 API \ud0a4\uac00 \ud544\uc694\ud569\ub2c8\ub2e4. \uacc4\uc815 \uc124\uc815\uc744 \uc120\ud0dd\ud55c \ub2e4\uc74c 'GET API KEY ' \ub97c \ud074\ub9ad\ud574\uc8fc\uc138\uc694.", + "description": "https://app.rach.io/ \uc758 API \ud0a4\uac00 \ud544\uc694\ud569\ub2c8\ub2e4. Settings \ub85c \uc774\ub3d9\ud55c \ub2e4\uc74c 'GET API KEY ' \ub97c \ud074\ub9ad\ud574\uc8fc\uc138\uc694.", "title": "Rachio \uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uae30" } } @@ -22,7 +22,7 @@ "step": { "init": { "data": { - "manual_run_mins": "\uc2a4\uc704\uce58\uac00 \ud65c\uc131\ud654\ub41c \uacbd\uc6b0 \uc2a4\ud14c\uc774\uc158\uc744 \ucf1c\ub294 \uc2dc\uac04(\ubd84) \uc785\ub2c8\ub2e4." + "manual_run_mins": "\uad6c\uc5ed \uc2a4\uc704\uce58\ub97c \ud65c\uc131\ud654\ud560 \ub54c \uc2e4\ud589\ud560 \uc2dc\uac04(\ubd84)" } } } diff --git a/homeassistant/components/rainmachine/translations/ko.json b/homeassistant/components/rainmachine/translations/ko.json index e1c78ae8247..08ccfd1f5b9 100644 --- a/homeassistant/components/rainmachine/translations/ko.json +++ b/homeassistant/components/rainmachine/translations/ko.json @@ -1,7 +1,10 @@ { "config": { "abort": { - "already_configured": "\uc774 RainMachine \ucee8\ud2b8\ub864\ub7ec\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/recollect_waste/translations/ko.json b/homeassistant/components/recollect_waste/translations/ko.json new file mode 100644 index 00000000000..17dee71d640 --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/ko.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/ko.json b/homeassistant/components/rfxtrx/translations/ko.json index aa8512da285..e8c83a7bfd7 100644 --- a/homeassistant/components/rfxtrx/translations/ko.json +++ b/homeassistant/components/rfxtrx/translations/ko.json @@ -1,7 +1,30 @@ { "config": { "abort": { - "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "setup_network": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8" + } + }, + "setup_serial_manual_path": { + "data": { + "device": "USB \uc7a5\uce58 \uacbd\ub85c" + } + } + } + }, + "options": { + "error": { + "already_configured_device": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" } } } \ No newline at end of file diff --git a/homeassistant/components/risco/translations/ko.json b/homeassistant/components/risco/translations/ko.json index 37d9a61307b..f3065256e7f 100644 --- a/homeassistant/components/risco/translations/ko.json +++ b/homeassistant/components/risco/translations/ko.json @@ -1,12 +1,21 @@ { "config": { "abort": { - "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", - "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "pin": "PIN \ucf54\ub4dc", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } } }, "options": { @@ -23,6 +32,8 @@ }, "init": { "data": { + "code_arm_required": "\uc124\uc815\ud558\ub824\uba74 PIN \ucf54\ub4dc\uac00 \ud544\uc694\ud569\ub2c8\ub2e4", + "code_disarm_required": "\ud574\uc81c\ud558\ub824\uba74 PIN \ucf54\ub4dc\uac00 \ud544\uc694\ud569\ub2c8\ub2e4", "scan_interval": "Risco\ub97c \ud3f4\ub9c1\ud558\ub294 \ube48\ub3c4 (\ucd08)" } }, diff --git a/homeassistant/components/rituals_perfume_genie/translations/ca.json b/homeassistant/components/rituals_perfume_genie/translations/ca.json new file mode 100644 index 00000000000..d7abccc2c25 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "email": "Correu electr\u00f2nic", + "password": "Contrasenya" + }, + "title": "Connexi\u00f3 amb el teu compte Rituals" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rituals_perfume_genie/translations/it.json b/homeassistant/components/rituals_perfume_genie/translations/it.json new file mode 100644 index 00000000000..6dfb2230285 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Password" + }, + "title": "Collegati al tuo account Rituals" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/ko.json b/homeassistant/components/roku/translations/ko.json index 054c1674884..19f4c16785f 100644 --- a/homeassistant/components/roku/translations/ko.json +++ b/homeassistant/components/roku/translations/ko.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { diff --git a/homeassistant/components/roomba/translations/ko.json b/homeassistant/components/roomba/translations/ko.json index ebf9056c037..d9c661a20dd 100644 --- a/homeassistant/components/roomba/translations/ko.json +++ b/homeassistant/components/roomba/translations/ko.json @@ -1,9 +1,28 @@ { "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "step": { + "init": { + "data": { + "host": "\ud638\uc2a4\ud2b8" + } + }, + "link_manual": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638" + } + }, + "manual": { + "data": { + "host": "\ud638\uc2a4\ud2b8" + } + }, "user": { "data": { "blid": "BLID", diff --git a/homeassistant/components/roon/translations/ko.json b/homeassistant/components/roon/translations/ko.json index ae483b0b098..7c051d49dc7 100644 --- a/homeassistant/components/roon/translations/ko.json +++ b/homeassistant/components/roon/translations/ko.json @@ -17,7 +17,7 @@ "data": { "host": "\ud638\uc2a4\ud2b8" }, - "description": "Roon \uc11c\ubc84 Hostname \ub610\ub294 IP\ub97c \uc785\ub825\ud558\uc2ed\uc2dc\uc624." + "description": "Roon \uc11c\ubc84\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8\uba85\uc774\ub098 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694." } } } diff --git a/homeassistant/components/rpi_power/translations/ko.json b/homeassistant/components/rpi_power/translations/ko.json index b9a9a1be643..445c0c34e68 100644 --- a/homeassistant/components/rpi_power/translations/ko.json +++ b/homeassistant/components/rpi_power/translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\uc774 \uad6c\uc131 \uc694\uc18c\uc5d0 \ud544\uc694\ud55c \uc2dc\uc2a4\ud15c \ud074\ub798\uc2a4\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucee4\ub110\uc774 \ucd5c\uc2e0\uc774\uace0 \ud558\ub4dc\uc6e8\uc5b4\uac00 \uc9c0\uc6d0\ub418\ub294\uc9c0 \ud655\uc778\ud558\uc2ed\uc2dc\uc624.", - "single_instance_allowed": "\uc774\ubbf8 \uc124\uc815\ub418\uc5b4 \uc788\uc74c. \ud558\ub098\uc758 \uc124\uc815\ub9cc \uac00\ub2a5\ud568." + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." }, "step": { "confirm": { diff --git a/homeassistant/components/ruckus_unleashed/translations/ko.json b/homeassistant/components/ruckus_unleashed/translations/ko.json new file mode 100644 index 00000000000..9ba063c37dd --- /dev/null +++ b/homeassistant/components/ruckus_unleashed/translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/ko.json b/homeassistant/components/samsungtv/translations/ko.json index 1c7e0b29808..14e35a7ff2e 100644 --- a/homeassistant/components/samsungtv/translations/ko.json +++ b/homeassistant/components/samsungtv/translations/ko.json @@ -1,9 +1,10 @@ { "config": { "abort": { - "already_configured": "\uc774 \uc0bc\uc131 TV \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "already_in_progress": "\uc0bc\uc131 TV \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.", + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", "auth_missing": "Home Assistant \uac00 \ud574\ub2f9 \uc0bc\uc131 TV \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc788\ub294 \uad8c\ud55c\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. TV \uc124\uc815\uc744 \ud655\uc778\ud558\uc5ec Home Assistant \ub97c \uc2b9\uc778\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "not_supported": "\uc774 \uc0bc\uc131 TV \ubaa8\ub378\uc740 \ud604\uc7ac \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." }, "flow_title": "\uc0bc\uc131 TV: {model}", diff --git a/homeassistant/components/sense/translations/ko.json b/homeassistant/components/sense/translations/ko.json index 26545db739a..517ad7af17d 100644 --- a/homeassistant/components/sense/translations/ko.json +++ b/homeassistant/components/sense/translations/ko.json @@ -4,7 +4,7 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, diff --git a/homeassistant/components/sentry/translations/ko.json b/homeassistant/components/sentry/translations/ko.json index 963695dabfd..6c00ffea2ef 100644 --- a/homeassistant/components/sentry/translations/ko.json +++ b/homeassistant/components/sentry/translations/ko.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, "error": { "bad_dsn": "DSN \uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/sharkiq/translations/ko.json b/homeassistant/components/sharkiq/translations/ko.json index 92649031534..04f400212f1 100644 --- a/homeassistant/components/sharkiq/translations/ko.json +++ b/homeassistant/components/sharkiq/translations/ko.json @@ -1,26 +1,27 @@ { "config": { "abort": { - "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", - "reauth_successful": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc131\uacf5\uc801\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", - "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "reauth": { "data": { - "password": "\uc554\ud638", - "username": "\uc0ac\uc6a9\uc790\uba85" + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" } }, "user": { "data": { - "password": "\uc554\ud638", - "username": "\uc0ac\uc6a9\uc790\uba85" + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" } } } diff --git a/homeassistant/components/shelly/translations/ko.json b/homeassistant/components/shelly/translations/ko.json index 5fb84e0ac90..914c9a46bd8 100644 --- a/homeassistant/components/shelly/translations/ko.json +++ b/homeassistant/components/shelly/translations/ko.json @@ -1,16 +1,24 @@ { "config": { "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unsupported_firmware": "\uc774 \uc7a5\uce58\ub294 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \ud38c\uc6e8\uc5b4 \ubc84\uc804\uc744 \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4." }, "error": { - "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d" + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "credentials": { "data": { - "password": "\uc554\ud638", - "username": "\uc0ac\uc6a9\uc790\uba85" + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + }, + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8" } } } diff --git a/homeassistant/components/shopping_list/translations/ko.json b/homeassistant/components/shopping_list/translations/ko.json index 247fa8d9f4d..a576567a87f 100644 --- a/homeassistant/components/shopping_list/translations/ko.json +++ b/homeassistant/components/shopping_list/translations/ko.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_configured": "\uc7a5\ubcf4\uae30\ubaa9\ub85d\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "user": { - "description": "\uc7a5\ubcf4\uae30\ubaa9\ub85d\uc744 \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "\uc7a5\ubcf4\uae30\ubaa9\ub85d" + "description": "\uc7a5\ubcf4\uae30 \ubaa9\ub85d\uc744 \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\uc7a5\ubcf4\uae30 \ubaa9\ub85d" } } }, - "title": "\uc7a5\ubcf4\uae30\ubaa9\ub85d" + "title": "\uc7a5\ubcf4\uae30 \ubaa9\ub85d" } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/ko.json b/homeassistant/components/simplisafe/translations/ko.json index 57ba4a88fc1..c5c1b057ea8 100644 --- a/homeassistant/components/simplisafe/translations/ko.json +++ b/homeassistant/components/simplisafe/translations/ko.json @@ -1,12 +1,21 @@ { "config": { "abort": { - "already_configured": "\uc774 SimpliSafe \uacc4\uc815\uc740 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4." + "already_configured": "\uc774 SimpliSafe \uacc4\uc815\uc740 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4.", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "identifier_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "identifier_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638" + }, + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d" + }, "user": { "data": { "code": "\ucf54\ub4dc (Home Assistant UI \uc5d0\uc11c \uc0ac\uc6a9\ub428)", diff --git a/homeassistant/components/smappee/translations/ko.json b/homeassistant/components/smappee/translations/ko.json index b3e37ee6d01..8509b65ca09 100644 --- a/homeassistant/components/smappee/translations/ko.json +++ b/homeassistant/components/smappee/translations/ko.json @@ -1,10 +1,11 @@ { "config": { "abort": { - "already_configured_device": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "already_configured_device": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", - "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" + "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "local": { diff --git a/homeassistant/components/smarthab/translations/ko.json b/homeassistant/components/smarthab/translations/ko.json index 4b15acd2f38..d39931cbc03 100644 --- a/homeassistant/components/smarthab/translations/ko.json +++ b/homeassistant/components/smarthab/translations/ko.json @@ -1,7 +1,9 @@ { "config": { "error": { - "service": "SmartHab \uc5d0 \uc811\uc18d\ud558\ub294 \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc11c\ube44\uc2a4\uac00 \ub2e4\uc6b4\ub418\uc5c8\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc5f0\uacb0\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694." + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "service": "SmartHab \uc5d0 \uc811\uc18d\ud558\ub294 \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc11c\ube44\uc2a4\uac00 \ub2e4\uc6b4\ub418\uc5c8\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc5f0\uacb0\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/smarttub/translations/cs.json b/homeassistant/components/smarttub/translations/cs.json new file mode 100644 index 00000000000..6be2df92286 --- /dev/null +++ b/homeassistant/components/smarttub/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Heslo" + }, + "title": "P\u0159ihl\u00e1\u0161en\u00ed" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarttub/translations/it.json b/homeassistant/components/smarttub/translations/it.json new file mode 100644 index 00000000000..64aed0996f3 --- /dev/null +++ b/homeassistant/components/smarttub/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Password" + }, + "description": "Inserisci il tuo indirizzo e-mail e la password SmartTub per accedere", + "title": "Accesso" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarttub/translations/ko.json b/homeassistant/components/smarttub/translations/ko.json new file mode 100644 index 00000000000..fab7e511034 --- /dev/null +++ b/homeassistant/components/smarttub/translations/ko.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "email": "\uc774\uba54\uc77c", + "password": "\ube44\ubc00\ubc88\ud638" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/translations/ko.json b/homeassistant/components/solaredge/translations/ko.json index eb2d8c42a14..8544cdd143d 100644 --- a/homeassistant/components/solaredge/translations/ko.json +++ b/homeassistant/components/solaredge/translations/ko.json @@ -1,9 +1,12 @@ { "config": { "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "site_exists": "\uc774 site_id \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "site_exists": "\uc774 site_id \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { diff --git a/homeassistant/components/solarlog/translations/ko.json b/homeassistant/components/solarlog/translations/ko.json index 66c6a2177d4..22002c52cef 100644 --- a/homeassistant/components/solarlog/translations/ko.json +++ b/homeassistant/components/solarlog/translations/ko.json @@ -5,7 +5,7 @@ }, "error": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8 \uc8fc\uc18c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694." + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/soma/translations/ko.json b/homeassistant/components/soma/translations/ko.json index b987c7b2b73..83c2f01ff8b 100644 --- a/homeassistant/components/soma/translations/ko.json +++ b/homeassistant/components/soma/translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "\ud558\ub098\uc758 Soma \uacc4\uc815\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "connection_error": "SOMA Connect \uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", "missing_configuration": "Soma \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", "result_error": "SOMA Connect \uac00 \uc624\ub958 \uc0c1\ud0dc\ub85c \uc751\ub2f5\ud588\uc2b5\ub2c8\ub2e4." diff --git a/homeassistant/components/somfy/translations/ko.json b/homeassistant/components/somfy/translations/ko.json index 5119670f766..8b4f4ff752f 100644 --- a/homeassistant/components/somfy/translations/ko.json +++ b/homeassistant/components/somfy/translations/ko.json @@ -1,17 +1,17 @@ { "config": { "abort": { - "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "missing_configuration": "Somfy \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", - "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})", - "single_instance_allowed": "\uc774\ubbf8 \uc124\uc815\ub418\uc5b4 \uc788\uc74c. \ud558\ub098\uc758 \uc124\uc815\ub9cc \uac00\ub2a5\ud568." + "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", + "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Somfy \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "pick_implementation": { - "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd" + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" } } } diff --git a/homeassistant/components/somfy_mylink/translations/ko.json b/homeassistant/components/somfy_mylink/translations/ko.json new file mode 100644 index 00000000000..4d4a78ee1f0 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/ko.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8" + } + } + } + }, + "options": { + "abort": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/ko.json b/homeassistant/components/sonarr/translations/ko.json index fe650991778..17e3592d509 100644 --- a/homeassistant/components/sonarr/translations/ko.json +++ b/homeassistant/components/sonarr/translations/ko.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { @@ -10,14 +11,17 @@ }, "flow_title": "Sonarr: {name}", "step": { + "reauth_confirm": { + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d" + }, "user": { "data": { "api_key": "API \ud0a4", "base_path": "API \uacbd\ub85c", "host": "\ud638\uc2a4\ud2b8", "port": "\ud3ec\ud2b8", - "ssl": "Sonarr \ub294 SSL \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4", - "verify_ssl": "Sonarr \ub294 \uc62c\ubc14\ub978 \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4" + "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9", + "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" } } } diff --git a/homeassistant/components/sonos/translations/ko.json b/homeassistant/components/sonos/translations/ko.json index c92b50a0f83..ba85f8df170 100644 --- a/homeassistant/components/sonos/translations/ko.json +++ b/homeassistant/components/sonos/translations/ko.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Sonos \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", - "single_instance_allowed": "\ud558\ub098\uc758 Sonos \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." }, "step": { "confirm": { diff --git a/homeassistant/components/speedtestdotnet/translations/ko.json b/homeassistant/components/speedtestdotnet/translations/ko.json index ede64fa0531..2951d72d201 100644 --- a/homeassistant/components/speedtestdotnet/translations/ko.json +++ b/homeassistant/components/speedtestdotnet/translations/ko.json @@ -1,11 +1,12 @@ { "config": { "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", "wrong_server_id": "\uc11c\ubc84 ID \uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "user": { - "description": "SpeedTest \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" } } }, diff --git a/homeassistant/components/spider/translations/ko.json b/homeassistant/components/spider/translations/ko.json index 1f08b96ee10..9e9ed5b0f30 100644 --- a/homeassistant/components/spider/translations/ko.json +++ b/homeassistant/components/spider/translations/ko.json @@ -1,8 +1,19 @@ { "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, "error": { - "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/ko.json b/homeassistant/components/spotify/translations/ko.json index a12162bb4fb..22a338a8d7c 100644 --- a/homeassistant/components/spotify/translations/ko.json +++ b/homeassistant/components/spotify/translations/ko.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "missing_configuration": "Spotify \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", - "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})", + "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", "reauth_account_mismatch": "\uc778\uc99d\ub41c Spotify \uacc4\uc815\uc740 \uc7ac\uc778\uc99d\uc774 \ud544\uc694\ud55c \uacc4\uc815\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." }, "create_entry": { @@ -15,7 +15,7 @@ }, "reauth_confirm": { "description": "Spotify \ud1b5\ud569\uc740 \uacc4\uc815 {account} \ub300\ud574 Spotify\ub85c \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c\ud569\ub2c8\ub2e4.", - "title": "Spotify\ub85c \uc7ac \uc778\uc99d" + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d" } } } diff --git a/homeassistant/components/srp_energy/translations/ko.json b/homeassistant/components/srp_energy/translations/ko.json new file mode 100644 index 00000000000..4b6af62638a --- /dev/null +++ b/homeassistant/components/srp_energy/translations/ko.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/ko.json b/homeassistant/components/synology_dsm/translations/ko.json index 6989f6515a1..efc20dbe03f 100644 --- a/homeassistant/components/synology_dsm/translations/ko.json +++ b/homeassistant/components/synology_dsm/translations/ko.json @@ -1,12 +1,14 @@ { "config": { "abort": { - "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "missing_data": "\ub204\ub77d\ub41c \ub370\uc774\ud130: \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud558\uac70\ub098 \ub2e4\ub978 \uad6c\uc131\uc744 \uc2dc\ub3c4\ud574\ubcf4\uc138\uc694", "otp_failed": "2\ub2e8\uacc4 \uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. \uc0c8\ub85c\uc6b4 \ud328\uc2a4 \ucf54\ub4dc\ub85c \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694", - "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uc785\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 \ub85c\uadf8\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694" + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "flow_title": "Synology DSM: {name} ({host})", "step": { @@ -20,8 +22,9 @@ "data": { "password": "\ube44\ubc00\ubc88\ud638", "port": "\ud3ec\ud2b8", - "ssl": "SSL/TLS \ub97c \uc0ac\uc6a9\ud558\uc5ec NAS \uc5d0 \uc5f0\uacb0", - "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", + "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" }, "description": "{name} ({host}) \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Synology DSM" @@ -31,8 +34,9 @@ "host": "\ud638\uc2a4\ud2b8", "password": "\ube44\ubc00\ubc88\ud638", "port": "\ud3ec\ud2b8", - "ssl": "SSL/TLS \ub97c \uc0ac\uc6a9\ud558\uc5ec NAS \uc5d0 \uc5f0\uacb0", - "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", + "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" }, "title": "Synology DSM" } diff --git a/homeassistant/components/tado/translations/ko.json b/homeassistant/components/tado/translations/ko.json index 8982b68829b..8290e32c4c8 100644 --- a/homeassistant/components/tado/translations/ko.json +++ b/homeassistant/components/tado/translations/ko.json @@ -4,7 +4,7 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "no_homes": "\uc774 Tado \uacc4\uc815\uc5d0 \uc5f0\uacb0\ub41c \uc9d1\uc774 \uc5c6\uc2b5\ub2c8\ub2e4.", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/tasmota/translations/ko.json b/homeassistant/components/tasmota/translations/ko.json new file mode 100644 index 00000000000..c6e52d209e7 --- /dev/null +++ b/homeassistant/components/tasmota/translations/ko.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/ko.json b/homeassistant/components/tellduslive/translations/ko.json index 645b7233bd7..d29dd504844 100644 --- a/homeassistant/components/tellduslive/translations/ko.json +++ b/homeassistant/components/tellduslive/translations/ko.json @@ -1,10 +1,14 @@ { "config": { "abort": { - "already_configured": "TelldusLive \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "authorize_url_fail": "\uc778\uc99d url \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", - "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "authorize_url_fail": "\uc778\uc99d URL \uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", + "unknown_authorize_url_generation": "\uc778\uc99d URL \uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4." + }, + "error": { + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "auth": { diff --git a/homeassistant/components/tesla/translations/ko.json b/homeassistant/components/tesla/translations/ko.json index 27a96518ca7..3e3893e0b75 100644 --- a/homeassistant/components/tesla/translations/ko.json +++ b/homeassistant/components/tesla/translations/ko.json @@ -1,5 +1,14 @@ { "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/tibber/translations/ko.json b/homeassistant/components/tibber/translations/ko.json index 1f99aa440b4..5ba1f62e4ed 100644 --- a/homeassistant/components/tibber/translations/ko.json +++ b/homeassistant/components/tibber/translations/ko.json @@ -1,9 +1,10 @@ { "config": { "abort": { - "already_configured": "Tibber \uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "timeout": "Tibber \uc5f0\uacb0 \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4." }, diff --git a/homeassistant/components/tile/translations/ko.json b/homeassistant/components/tile/translations/ko.json index 50ba5000a1a..d592fef112c 100644 --- a/homeassistant/components/tile/translations/ko.json +++ b/homeassistant/components/tile/translations/ko.json @@ -1,7 +1,10 @@ { "config": { "abort": { - "already_configured": "\uc774 Tile \uacc4\uc815\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/toon/translations/ko.json b/homeassistant/components/toon/translations/ko.json index 379058f68d1..faed1fe74d7 100644 --- a/homeassistant/components/toon/translations/ko.json +++ b/homeassistant/components/toon/translations/ko.json @@ -2,11 +2,12 @@ "config": { "abort": { "already_configured": "\uc120\ud0dd\ub41c \uc57d\uc815\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "authorize_url_fail": "\uc778\uc99d url \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "authorize_url_fail": "\uc778\uc99d URL \uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", "no_agreements": "\uc774 \uacc4\uc815\uc5d0\ub294 Toon \ub514\uc2a4\ud50c\ub808\uc774\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.", - "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" + "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "unknown_authorize_url_generation": "\uc778\uc99d URL \uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4." }, "step": { "agreement": { diff --git a/homeassistant/components/totalconnect/translations/ko.json b/homeassistant/components/totalconnect/translations/ko.json index 99513a64508..c074472b8f4 100644 --- a/homeassistant/components/totalconnect/translations/ko.json +++ b/homeassistant/components/totalconnect/translations/ko.json @@ -1,7 +1,10 @@ { "config": { "abort": { - "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/tplink/translations/ko.json b/homeassistant/components/tplink/translations/ko.json index dc8a6a5a8fc..e1ff7eff372 100644 --- a/homeassistant/components/tplink/translations/ko.json +++ b/homeassistant/components/tplink/translations/ko.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "TP-Link \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", - "single_instance_allowed": "\ud558\ub098\uc758 TP-Link \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." }, "step": { "confirm": { diff --git a/homeassistant/components/traccar/translations/ko.json b/homeassistant/components/traccar/translations/ko.json index 910281d4b38..04e13a9aa6f 100644 --- a/homeassistant/components/traccar/translations/ko.json +++ b/homeassistant/components/traccar/translations/ko.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", + "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4." + }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Traccar \uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c URL \uc815\ubcf4\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4: `{webhook_url}`\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Traccar \uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c \ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c\uc758 URL \uc8fc\uc18c\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4: `{webhook_url}`\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { diff --git a/homeassistant/components/tradfri/translations/ko.json b/homeassistant/components/tradfri/translations/ko.json index caa94fa8b10..067a10c6490 100644 --- a/homeassistant/components/tradfri/translations/ko.json +++ b/homeassistant/components/tradfri/translations/ko.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\ube0c\ub9ac\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "already_in_progress": "\ube0c\ub9ac\uc9c0 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4." + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_key": "\uc81c\uacf5\ub41c \ud0a4\ub85c \ub4f1\ub85d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \uc774 \ubb38\uc81c\uac00 \uacc4\uc18d \ubc1c\uc0dd\ud558\uba74 \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud574\ubcf4\uc138\uc694.", "timeout": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, diff --git a/homeassistant/components/transmission/translations/ko.json b/homeassistant/components/transmission/translations/ko.json index 7f5d67114a1..002e374e54d 100644 --- a/homeassistant/components/transmission/translations/ko.json +++ b/homeassistant/components/transmission/translations/ko.json @@ -1,10 +1,11 @@ { "config": { "abort": { - "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\ud638\uc2a4\ud2b8\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4" }, "step": { diff --git a/homeassistant/components/tuya/translations/it.json b/homeassistant/components/tuya/translations/it.json index 639f5834922..729514d3541 100644 --- a/homeassistant/components/tuya/translations/it.json +++ b/homeassistant/components/tuya/translations/it.json @@ -40,8 +40,10 @@ "max_temp": "Temperatura di destinazione massima (utilizzare min e max = 0 per impostazione predefinita)", "min_kelvin": "Temperatura colore minima supportata in kelvin", "min_temp": "Temperatura di destinazione minima (utilizzare min e max = 0 per impostazione predefinita)", + "set_temp_divided": "Utilizzare il valore temperatura diviso per impostare il comando temperatura", "support_color": "Forza il supporto del colore", "temp_divider": "Divisore dei valori di temperatura (0 = utilizzare il valore predefinito)", + "temp_step_override": "Passo della temperatura da raggiungere", "tuya_max_coltemp": "Temperatura di colore massima riportata dal dispositivo", "unit_of_measurement": "Unit\u00e0 di temperatura utilizzata dal dispositivo" }, diff --git a/homeassistant/components/tuya/translations/ko.json b/homeassistant/components/tuya/translations/ko.json index e123bc2b6f9..81dd2689b0c 100644 --- a/homeassistant/components/tuya/translations/ko.json +++ b/homeassistant/components/tuya/translations/ko.json @@ -1,8 +1,13 @@ { "config": { "abort": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." }, + "error": { + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, "flow_title": "Tuya \uad6c\uc131\ud558\uae30", "step": { "user": { @@ -16,5 +21,10 @@ "title": "Tuya" } } + }, + "options": { + "abort": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + } } } \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/translations/ko.json b/homeassistant/components/twentemilieu/translations/ko.json index 3efc227abf7..e6c19d40d06 100644 --- a/homeassistant/components/twentemilieu/translations/ko.json +++ b/homeassistant/components/twentemilieu/translations/ko.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_address": "Twente Milieu \uc11c\ube44\uc2a4 \uc9c0\uc5ed\uc5d0\uc11c \uc8fc\uc18c\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." }, "step": { diff --git a/homeassistant/components/twilio/translations/ko.json b/homeassistant/components/twilio/translations/ko.json index b6be32e1de4..72165dfb798 100644 --- a/homeassistant/components/twilio/translations/ko.json +++ b/homeassistant/components/twilio/translations/ko.json @@ -1,11 +1,15 @@ { "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", + "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4." + }, "create_entry": { "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Twilio \uc6f9 \ud6c5]({twilio_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/x-www-form-urlencoded\n \nHome Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { - "description": "Twilio \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Twilio \uc6f9 \ud6c5 \uc124\uc815\ud558\uae30" } } diff --git a/homeassistant/components/twinkly/translations/ko.json b/homeassistant/components/twinkly/translations/ko.json new file mode 100644 index 00000000000..207037cba60 --- /dev/null +++ b/homeassistant/components/twinkly/translations/ko.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "device_exists": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/ko.json b/homeassistant/components/unifi/translations/ko.json index 94160829bad..454feec0922 100644 --- a/homeassistant/components/unifi/translations/ko.json +++ b/homeassistant/components/unifi/translations/ko.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\ucee8\ud2b8\ub864\ub7ec \uc0ac\uc774\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\ucee8\ud2b8\ub864\ub7ec \uc0ac\uc774\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, "error": { "faulty_credentials": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", @@ -16,7 +17,7 @@ "port": "\ud3ec\ud2b8", "site": "\uc0ac\uc774\ud2b8 ID", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", - "verify_ssl": "\uc62c\ubc14\ub978 \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\ub294 \ucee8\ud2b8\ub864\ub7ec" + "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" }, "title": "UniFi \ucee8\ud2b8\ub864\ub7ec \uc124\uc815\ud558\uae30" } diff --git a/homeassistant/components/upb/translations/ko.json b/homeassistant/components/upb/translations/ko.json index 48cc545d87b..da357f7a136 100644 --- a/homeassistant/components/upb/translations/ko.json +++ b/homeassistant/components/upb/translations/ko.json @@ -1,9 +1,12 @@ { "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, "error": { - "cannot_connect": "UPB PIM \uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_upb_file": "UPB UPStart \ub0b4\ubcf4\ub0b4\uae30 \ud30c\uc77c\uc774 \uc5c6\uac70\ub098 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud30c\uc77c \uc774\ub984\uacfc \uacbd\ub85c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694.", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4." + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/upcloud/translations/ko.json b/homeassistant/components/upcloud/translations/ko.json new file mode 100644 index 00000000000..04360a9a8f7 --- /dev/null +++ b/homeassistant/components/upcloud/translations/ko.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/ko.json b/homeassistant/components/upnp/translations/ko.json index 1a7b5204930..7dd2e5c685c 100644 --- a/homeassistant/components/upnp/translations/ko.json +++ b/homeassistant/components/upnp/translations/ko.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_configured": "UPnP/IGD \uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "incomplete_discovery": "\uae30\uae30 \uac80\uc0c9\uc774 \uc644\uc804\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", - "no_devices_found": "UPnP/IGD \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4." + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" }, "flow_title": "UPnP/IGD: {name}", "step": { diff --git a/homeassistant/components/velbus/translations/ko.json b/homeassistant/components/velbus/translations/ko.json index 3d23ff3727d..fa9c4f7496d 100644 --- a/homeassistant/components/velbus/translations/ko.json +++ b/homeassistant/components/velbus/translations/ko.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/vera/translations/ko.json b/homeassistant/components/vera/translations/ko.json index 1556b9fe0d2..e8658528488 100644 --- a/homeassistant/components/vera/translations/ko.json +++ b/homeassistant/components/vera/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cannot_connect": "URL {base_url} \uc5d0 \ucee8\ud2b8\ub864\ub7ec\ub97c \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" + "cannot_connect": "{base_url} URL \uc8fc\uc18c\uc758 \ucee8\ud2b8\ub864\ub7ec\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" }, "step": { "user": { @@ -10,7 +10,7 @@ "lights": "Vera \uc2a4\uc704\uce58 \uae30\uae30 ID \ub294 Home Assistant \uc5d0\uc11c \uc870\uba85\uc73c\ub85c \ucde8\uae09\ub429\ub2c8\ub2e4.", "vera_controller_url": "\ucee8\ud2b8\ub864\ub7ec URL" }, - "description": "\uc544\ub798\uc5d0 Vera \ucee8\ud2b8\ub864\ub7ec URL \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. http://192.168.1.161:3480 \uacfc \uac19\uc740 \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4.", + "description": "\uc544\ub798\uc5d0 Vera \ucee8\ud2b8\ub864\ub7ec URL \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. http://192.168.1.161:3480 \uacfc \uac19\uc740 \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4.", "title": "Vera \ucee8\ud2b8\ub864\ub7ec \uc124\uc815\ud558\uae30" } } diff --git a/homeassistant/components/vesync/translations/ko.json b/homeassistant/components/vesync/translations/ko.json index 888bcd66231..d11e9f9459d 100644 --- a/homeassistant/components/vesync/translations/ko.json +++ b/homeassistant/components/vesync/translations/ko.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, + "error": { + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/vilfo/translations/ko.json b/homeassistant/components/vilfo/translations/ko.json index 70a315ae703..4b79130c750 100644 --- a/homeassistant/components/vilfo/translations/ko.json +++ b/homeassistant/components/vilfo/translations/ko.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "\uc774 Vilfo \ub77c\uc6b0\ud130\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \uc785\ub825\ud558\uc2e0 \ub0b4\uc6a9\uc744 \ud655\uc778\ud558\uc2e0 \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", - "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \ud655\uc778\ud558\uc2e0 \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", - "unknown": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud558\ub294 \uc911 \uc608\uae30\uce58 \uc54a\uc740 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4." + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/vizio/translations/ko.json b/homeassistant/components/vizio/translations/ko.json index ef10cb1f4fc..037c85d7c4e 100644 --- a/homeassistant/components/vizio/translations/ko.json +++ b/homeassistant/components/vizio/translations/ko.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured_device": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "updated_entry": "\uc774 \ud56d\ubaa9\uc740 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc9c0\ub9cc \uad6c\uc131\uc5d0 \uc815\uc758\ub41c \uc774\ub984, \uc571 \ud639\uc740 \uc635\uc158\uc774 \uc774\uc804\uc5d0 \uac00\uc838\uc628 \uad6c\uc131 \ub0b4\uc6a9\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc73c\ubbc0\ub85c \uad6c\uc131 \ud56d\ubaa9\uc774 \uadf8\uc5d0 \ub530\ub77c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "error": { @@ -12,7 +13,7 @@ "step": { "pair_tv": { "data": { - "pin": "PIN" + "pin": "PIN \ucf54\ub4dc" }, "description": "TV \uc5d0 \ucf54\ub4dc\uac00 \ud45c\uc2dc\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud574\ub2f9 \ucf54\ub4dc\ub97c \uc785\ub825\ub780\uc5d0 \uc785\ub825\ud55c \ud6c4 \ub2e4\uc74c \ub2e8\uacc4\ub97c \uacc4\uc18d\ud558\uc5ec \ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud574\uc8fc\uc138\uc694.", "title": "\ud398\uc5b4\ub9c1 \uacfc\uc815 \ub05d\ub0b4\uae30" diff --git a/homeassistant/components/volumio/translations/ko.json b/homeassistant/components/volumio/translations/ko.json new file mode 100644 index 00000000000..2c630e533ff --- /dev/null +++ b/homeassistant/components/volumio/translations/ko.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/ko.json b/homeassistant/components/wemo/translations/ko.json index a262f7ebd3e..5673c049422 100644 --- a/homeassistant/components/wemo/translations/ko.json +++ b/homeassistant/components/wemo/translations/ko.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Wemo \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", - "single_instance_allowed": "\ud558\ub098\uc758 Wemo \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." }, "step": { "confirm": { diff --git a/homeassistant/components/wiffi/translations/ko.json b/homeassistant/components/wiffi/translations/ko.json index c332d3e5f26..74c6568feef 100644 --- a/homeassistant/components/wiffi/translations/ko.json +++ b/homeassistant/components/wiffi/translations/ko.json @@ -7,7 +7,7 @@ "step": { "user": { "data": { - "port": "\uc11c\ubc84 \ud3ec\ud2b8" + "port": "\ud3ec\ud2b8" }, "title": "WIFFI \uae30\uae30\uc6a9 TCP \uc11c\ubc84 \uc124\uc815\ud558\uae30" } diff --git a/homeassistant/components/wilight/translations/ko.json b/homeassistant/components/wilight/translations/ko.json index 677b104c065..e18250811fc 100644 --- a/homeassistant/components/wilight/translations/ko.json +++ b/homeassistant/components/wilight/translations/ko.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "not_wilight_device": "\uc774 \uc7a5\uce58\ub294 WiLight\uac00 \uc544\ub2d9\ub2c8\ub2e4." }, "step": { diff --git a/homeassistant/components/withings/translations/ko.json b/homeassistant/components/withings/translations/ko.json index 902f3c77e68..38ed96dca67 100644 --- a/homeassistant/components/withings/translations/ko.json +++ b/homeassistant/components/withings/translations/ko.json @@ -2,13 +2,16 @@ "config": { "abort": { "already_configured": "\ud504\ub85c\ud544\uc5d0 \ub300\ud55c \uad6c\uc131\uc774 \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "missing_configuration": "Withings \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", - "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" + "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", + "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "create_entry": { "default": "Withings \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, + "error": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, "flow_title": "Withings: {profile}", "step": { "pick_implementation": { @@ -23,7 +26,7 @@ }, "reauth": { "description": "Withings \ub370\uc774\ud130\ub97c \uacc4\uc18d \uc218\uc2e0\ud558\ub824\uba74 \"{profile}\" \ud504\ub85c\ud544\uc744 \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c \ud569\ub2c8\ub2e4.", - "title": "\ud504\ub85c\ud544 \uc7ac\uc778\uc99d\ud558\uae30" + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d" } } } diff --git a/homeassistant/components/wled/translations/ko.json b/homeassistant/components/wled/translations/ko.json index 2adb7985fd3..d1945707b6d 100644 --- a/homeassistant/components/wled/translations/ko.json +++ b/homeassistant/components/wled/translations/ko.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "\uc774 WLED \uae30\uae30\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "flow_title": "WLED: {name}", "step": { diff --git a/homeassistant/components/wolflink/translations/sensor.ko.json b/homeassistant/components/wolflink/translations/sensor.ko.json index 5b6c33d7231..71fde05a07a 100644 --- a/homeassistant/components/wolflink/translations/sensor.ko.json +++ b/homeassistant/components/wolflink/translations/sensor.ko.json @@ -14,7 +14,8 @@ "auto_off_cool": "\ub0c9\ubc29 \uc790\ub3d9 \uaebc\uc9d0", "auto_on_cool": "\ub0c9\ubc29 \uc790\ub3d9 \ucf1c\uc9d0", "automatik_aus": "\uc790\ub3d9 \uaebc\uc9d0", - "automatik_ein": "\uc790\ub3d9 \ucf1c\uc9d0" + "automatik_ein": "\uc790\ub3d9 \ucf1c\uc9d0", + "permanent": "\uc601\uad6c\uc801" } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/ko.json b/homeassistant/components/xbox/translations/ko.json new file mode 100644 index 00000000000..7314d9e3c5c --- /dev/null +++ b/homeassistant/components/xbox/translations/ko.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, + "create_entry": { + "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "pick_implementation": { + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/ko.json b/homeassistant/components/xiaomi_aqara/translations/ko.json index 1b4e11c6ea3..7c15bc572e5 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ko.json +++ b/homeassistant/components/xiaomi_aqara/translations/ko.json @@ -2,11 +2,12 @@ "config": { "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "already_in_progress": "\uac8c\uc774\ud2b8\uc6e8\uc774 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", "not_xiaomi_aqara": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774\uac00 \uc544\ub2d9\ub2c8\ub2e4. \ubc1c\uacac\ub41c \uae30\uae30\uac00 \uc54c\ub824\uc9c4 \uac8c\uc774\ud2b8\uc6e8\uc774\uc640 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" }, "error": { "discovery_error": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \ubc1c\uacac\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. HomeAssistant \ub97c \uc778\ud130\ud398\uc774\uc2a4\ub85c \uc0ac\uc6a9\ud558\ub294 \uae30\uae30\uc758 IP \ub85c \uc2dc\ub3c4\ud574\ubcf4\uc138\uc694.", + "invalid_host": "\ud638\uc2a4\ud2b8\uba85 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694", "invalid_interface": "\ub124\ud2b8\uc6cc\ud06c \uc778\ud130\ud398\uc774\uc2a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_key": "\uac8c\uc774\ud2b8\uc6e8\uc774 \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, @@ -14,9 +15,9 @@ "step": { "select": { "data": { - "select_ip": "\uac8c\uc774\ud2b8\uc6e8\uc774 IP" + "select_ip": "IP \uc8fc\uc18c" }, - "description": "\uac8c\uc774\ud2b8\uc6e8\uc774 \uc5f0\uacb0\uc744 \ucd94\uac00\ud558\ub824\uba74 \uc124\uc815\uc744 \ub2e4\uc2dc \uc2e4\ud589\ud574\uc8fc\uc138\uc694", + "description": "\uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \ucd94\uac00 \uc5f0\uacb0\ud558\ub824\uba74 \uc124\uc815\uc744 \ub2e4\uc2dc \uc2e4\ud589\ud574\uc8fc\uc138\uc694", "title": "\uc5f0\uacb0\ud560 Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774 \uc120\ud0dd\ud558\uae30" }, "settings": { @@ -33,7 +34,7 @@ "interface": "\uc0ac\uc6a9\ud560 \ub124\ud2b8\uc6cc\ud06c \uc778\ud130\ud398\uc774\uc2a4", "mac": "Mac \uc8fc\uc18c(\uc120\ud0dd \uc0ac\ud56d)" }, - "description": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud569\ub2c8\ub2e4. IP \ubc0f Mac \uc8fc\uc18c\uac00 \uc124\uc815\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \uc790\ub3d9 \uac80\uc0c9\uc774 \uc0ac\uc6a9\ub429\ub2c8\ub2e4", + "description": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud569\ub2c8\ub2e4. IP \uc8fc\uc18c \ubc0f MAC \uc8fc\uc18c\ub97c \ube44\uc6cc\ub450\uba74 \uc790\ub3d9 \uac80\uc0c9\uc774 \uc0ac\uc6a9\ub429\ub2c8\ub2e4", "title": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774" } } diff --git a/homeassistant/components/xiaomi_miio/translations/cs.json b/homeassistant/components/xiaomi_miio/translations/cs.json index 91c30a69e54..ec275b93330 100644 --- a/homeassistant/components/xiaomi_miio/translations/cs.json +++ b/homeassistant/components/xiaomi_miio/translations/cs.json @@ -10,6 +10,12 @@ }, "flow_title": "Xiaomi Miio: {name}", "step": { + "device": { + "data": { + "host": "IP adresa", + "token": "API token" + } + }, "gateway": { "data": { "host": "IP adresa", diff --git a/homeassistant/components/xiaomi_miio/translations/it.json b/homeassistant/components/xiaomi_miio/translations/it.json index cbfc2d60621..68202e1631e 100644 --- a/homeassistant/components/xiaomi_miio/translations/it.json +++ b/homeassistant/components/xiaomi_miio/translations/it.json @@ -6,10 +6,20 @@ }, "error": { "cannot_connect": "Impossibile connettersi", - "no_device_selected": "Nessun dispositivo selezionato, selezionare un dispositivo." + "no_device_selected": "Nessun dispositivo selezionato, selezionare un dispositivo.", + "unknown_device": "Il modello del dispositivo non \u00e8 noto, non \u00e8 possibile configurare il dispositivo utilizzando il flusso di configurazione." }, "flow_title": "Xiaomi Miio: {name}", "step": { + "device": { + "data": { + "host": "Indirizzo IP", + "name": "Nome del dispositivo", + "token": "Token API" + }, + "description": "Avrai bisogno dei 32 caratteri Token API , vedi https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token per istruzioni. Tieni presente che questa Token API \u00e8 diversa dalla chiave utilizzata dall'integrazione Xiaomi Aqara.", + "title": "Connettiti a un dispositivo Xiaomi Miio o Xiaomi Gateway" + }, "gateway": { "data": { "host": "Indirizzo IP", diff --git a/homeassistant/components/xiaomi_miio/translations/ko.json b/homeassistant/components/xiaomi_miio/translations/ko.json index 52f1ed960d2..7e594fde247 100644 --- a/homeassistant/components/xiaomi_miio/translations/ko.json +++ b/homeassistant/components/xiaomi_miio/translations/ko.json @@ -2,20 +2,27 @@ "config": { "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "already_in_progress": "Xiaomi Miio \uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4." + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4" }, "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "no_device_selected": "\uc120\ud0dd\ub41c \uae30\uae30\uac00 \uc5c6\uc2b5\ub2c8\ub2e4. \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694." }, "flow_title": "Xiaomi Miio: {name}", "step": { + "device": { + "data": { + "host": "IP \uc8fc\uc18c", + "token": "API \ud1a0\ud070" + } + }, "gateway": { "data": { "host": "IP \uc8fc\uc18c", "name": "\uac8c\uc774\ud2b8\uc6e8\uc774 \uc774\ub984", "token": "API \ud1a0\ud070" }, - "description": "32 \ubb38\uc790\uc758 API \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694. \uc774 \ud1a0\ud070\uc740 Xiaomi Aqara \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0\uc11c \uc0ac\uc6a9\ub418\ub294 \ud0a4\uc640 \ub2e4\ub985\ub2c8\ub2e4.", + "description": "32 \uac1c\uc758 \ubb38\uc790\uc5f4\ub85c \uad6c\uc131\ub41c API \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694. \ucc38\uace0\ub85c \uc774 API \ud1a0\ud070\uc740 Xiaomi Aqara \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0\uc11c \uc0ac\uc6a9\ub418\ub294 \ud0a4\uc640 \ub2e4\ub985\ub2c8\ub2e4.", "title": "Xiaomi \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud558\uae30" }, "user": { diff --git a/homeassistant/components/yeelight/translations/ko.json b/homeassistant/components/yeelight/translations/ko.json index 7164e56b595..1d6974aaa61 100644 --- a/homeassistant/components/yeelight/translations/ko.json +++ b/homeassistant/components/yeelight/translations/ko.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\uc7a5\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.", - "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c \uc0c1\uc5d0 \ubc1c\uacac\ub41c \uc7a5\uce58\uac00 \uc5c6\uc2b5\ub2c8\ub2e4." + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328" + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "pick_device": { diff --git a/homeassistant/components/zha/translations/ko.json b/homeassistant/components/zha/translations/ko.json index 93582cc9202..639cc84d86f 100644 --- a/homeassistant/components/zha/translations/ko.json +++ b/homeassistant/components/zha/translations/ko.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "single_instance_allowed": "\ud558\ub098\uc758 ZHA \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." }, "error": { - "cannot_connect": "ZHA \uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "pick_radio": { diff --git a/homeassistant/components/zoneminder/translations/ko.json b/homeassistant/components/zoneminder/translations/ko.json index 3625d6e402e..e03da9ed8fa 100644 --- a/homeassistant/components/zoneminder/translations/ko.json +++ b/homeassistant/components/zoneminder/translations/ko.json @@ -2,25 +2,29 @@ "config": { "abort": { "auth_fail": "\uc0ac\uc6a9\uc790\uba85\uacfc \uc554\ud638\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", - "connection_error": "ZoneMinder \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4." + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "connection_error": "ZoneMinder \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "create_entry": { "default": "ZoneMinder \uc11c\ubc84\uac00 \ucd94\uac00\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "error": { "auth_fail": "\uc0ac\uc6a9\uc790\uba85\uacfc \uc554\ud638\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", - "connection_error": "ZoneMinder \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4." + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "connection_error": "ZoneMinder \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "flow_title": "ZoneMinder", "step": { "user": { "data": { "host": "\ud638\uc2a4\ud2b8 \ubc0f \ud3ec\ud2b8(\uc608: 10.10.0.4:8010)", - "password": "\uc554\ud638", + "password": "\ube44\ubc00\ubc88\ud638", "path": "ZMS \uacbd\ub85c", "path_zms": "ZMS \uacbd\ub85c", - "ssl": "ZoneMinder \uc5f0\uacb0\uc5d0 SSL \uc0ac\uc6a9", - "username": "\uc0ac\uc6a9\uc790\uba85", + "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" }, "title": "ZoneMinder \uc11c\ubc84\ub97c \ucd94\uac00\ud558\uc138\uc694." diff --git a/homeassistant/components/zwave/translations/ko.json b/homeassistant/components/zwave/translations/ko.json index 1357fd492c5..84c7b4ee4e3 100644 --- a/homeassistant/components/zwave/translations/ko.json +++ b/homeassistant/components/zwave/translations/ko.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Z-Wave \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." }, "error": { "option_error": "Z-Wave \uc720\ud6a8\uc131 \uac80\uc0ac\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. USB \uc2a4\ud2f1\uc758 \uacbd\ub85c\uac00 \uc815\ud655\ud569\ub2c8\uae4c?" @@ -12,7 +13,7 @@ "network_key": "\ub124\ud2b8\uc6cc\ud06c \ud0a4 (\uacf5\ub780\uc73c\ub85c \ube44\uc6cc\ub450\uba74 \uc790\ub3d9 \uc0dd\uc131\ud569\ub2c8\ub2e4)", "usb_path": "USB \uc7a5\uce58 \uacbd\ub85c" }, - "description": "\uad6c\uc131 \ubcc0\uc218\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/docs/z-wave/installation/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694", + "description": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \ub354 \uc774\uc0c1 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc0c8\ub85c\uc6b4 \uc124\uce58\uc758 \uacbd\uc6b0 Z-Wave JS \ub97c \uc0ac\uc6a9\ud574\uc8fc\uc138\uc694.\n\n\uad6c\uc131 \ubcc0\uc218\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/docs/z-wave/installation/ \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694", "title": "Z-Wave \uc124\uc815" } } diff --git a/homeassistant/components/zwave_js/translations/ko.json b/homeassistant/components/zwave_js/translations/ko.json new file mode 100644 index 00000000000..283b0aa17b6 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/ko.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "manual": { + "data": { + "url": "URL \uc8fc\uc18c" + } + }, + "start_addon": { + "data": { + "usb_path": "USB \uc7a5\uce58 \uacbd\ub85c" + } + }, + "user": { + "data": { + "url": "URL \uc8fc\uc18c" + } + } + } + } +} \ No newline at end of file From efa339ca543c62a191e8c93ee27d283656be3b9e Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sun, 21 Feb 2021 03:26:17 +0100 Subject: [PATCH 603/796] Allow upnp ignore SSDP-discoveries (#46592) --- homeassistant/components/upnp/__init__.py | 16 ++++- homeassistant/components/upnp/config_flow.py | 13 ++++ homeassistant/components/upnp/const.py | 2 + homeassistant/components/upnp/device.py | 15 ++++- homeassistant/components/upnp/sensor.py | 6 +- tests/components/upnp/mock_device.py | 9 ++- tests/components/upnp/test_config_flow.py | 67 +++++++++++++++++++- tests/components/upnp/test_init.py | 4 ++ 8 files changed, 120 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 7b46037d99d..5d251ce7dd8 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -13,6 +13,7 @@ from homeassistant.util import get_local_ip from .const import ( CONF_LOCAL_IP, + CONFIG_ENTRY_HOSTNAME, CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, DISCOVERY_LOCATION, @@ -31,7 +32,13 @@ NOTIFICATION_ID = "upnp_notification" NOTIFICATION_TITLE = "UPnP/IGD Setup" CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Schema({vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string)})}, + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string), + }, + ) + }, extra=vol.ALLOW_EXTRA, ) @@ -115,6 +122,13 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) unique_id=device.unique_id, ) + # Ensure entry has a hostname, for older entries. + if CONFIG_ENTRY_HOSTNAME not in config_entry.data: + hass.config_entries.async_update_entry( + entry=config_entry, + data={CONFIG_ENTRY_HOSTNAME: device.hostname, **config_entry.data}, + ) + # Create device registry entry. device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index d3811b7e18b..1d212441bfa 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -10,10 +10,12 @@ from homeassistant.const import CONF_SCAN_INTERVAL from homeassistant.core import callback from .const import ( + CONFIG_ENTRY_HOSTNAME, CONFIG_ENTRY_SCAN_INTERVAL, CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, DEFAULT_SCAN_INTERVAL, + DISCOVERY_HOSTNAME, DISCOVERY_LOCATION, DISCOVERY_NAME, DISCOVERY_ST, @@ -179,6 +181,16 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() + # Handle devices changing their UDN, only allow a single + existing_entries = self.hass.config_entries.async_entries(DOMAIN) + for config_entry in existing_entries: + entry_hostname = config_entry.data.get(CONFIG_ENTRY_HOSTNAME) + if entry_hostname == discovery[DISCOVERY_HOSTNAME]: + _LOGGER.debug( + "Found existing config_entry with same hostname, discovery ignored" + ) + return self.async_abort(reason="discovery_ignored") + # Store discovery. self._discoveries = [discovery] @@ -222,6 +234,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data = { CONFIG_ENTRY_UDN: discovery[DISCOVERY_UDN], CONFIG_ENTRY_ST: discovery[DISCOVERY_ST], + CONFIG_ENTRY_HOSTNAME: discovery[DISCOVERY_HOSTNAME], } return self.async_create_entry(title=title, data=data) diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py index 4ccf6d3d7ea..6575139c4a4 100644 --- a/homeassistant/components/upnp/const.py +++ b/homeassistant/components/upnp/const.py @@ -21,6 +21,7 @@ DATA_PACKETS = "packets" DATA_RATE_PACKETS_PER_SECOND = f"{DATA_PACKETS}/{TIME_SECONDS}" KIBIBYTE = 1024 UPDATE_INTERVAL = timedelta(seconds=30) +DISCOVERY_HOSTNAME = "hostname" DISCOVERY_LOCATION = "location" DISCOVERY_NAME = "name" DISCOVERY_ST = "st" @@ -30,4 +31,5 @@ DISCOVERY_USN = "usn" CONFIG_ENTRY_SCAN_INTERVAL = "scan_interval" CONFIG_ENTRY_ST = "st" CONFIG_ENTRY_UDN = "udn" +CONFIG_ENTRY_HOSTNAME = "hostname" DEFAULT_SCAN_INTERVAL = timedelta(seconds=30).seconds diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 39fd09089b4..a06ca254c87 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from ipaddress import IPv4Address from typing import List, Mapping +from urllib.parse import urlparse from async_upnp_client import UpnpFactory from async_upnp_client.aiohttp import AiohttpSessionRequester @@ -17,6 +18,7 @@ from .const import ( BYTES_RECEIVED, BYTES_SENT, CONF_LOCAL_IP, + DISCOVERY_HOSTNAME, DISCOVERY_LOCATION, DISCOVERY_NAME, DISCOVERY_ST, @@ -66,10 +68,10 @@ class Device: cls, hass: HomeAssistantType, discovery: Mapping ) -> Mapping: """Get additional data from device and supplement discovery.""" - device = await Device.async_create_device(hass, discovery[DISCOVERY_LOCATION]) + location = discovery[DISCOVERY_LOCATION] + device = await Device.async_create_device(hass, location) discovery[DISCOVERY_NAME] = device.name - - # Set unique_id. + discovery[DISCOVERY_HOSTNAME] = device.hostname discovery[DISCOVERY_UNIQUE_ID] = discovery[DISCOVERY_USN] return discovery @@ -126,6 +128,13 @@ class Device: """Get the unique id.""" return self.usn + @property + def hostname(self) -> str: + """Get the hostname.""" + url = self._igd_device.device.device_url + parsed = urlparse(url) + return parsed.hostname + def __str__(self) -> str: """Get string representation.""" return f"IGD Device: {self.name}/{self.udn}::{self.device_type}" diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 59205f49667..97d3c1a702c 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -1,6 +1,6 @@ """Support for UPnP/IGD Sensors.""" from datetime import timedelta -from typing import Any, Mapping +from typing import Any, Mapping, Optional from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_BYTES, DATA_RATE_KIBIBYTES_PER_SECOND @@ -176,7 +176,7 @@ class RawUpnpSensor(UpnpSensor): """Representation of a UPnP/IGD sensor.""" @property - def state(self) -> str: + def state(self) -> Optional[str]: """Return the state of the device.""" device_value_key = self._sensor_type["device_value_key"] value = self.coordinator.data[device_value_key] @@ -214,7 +214,7 @@ class DerivedUpnpSensor(UpnpSensor): return current_value < self._last_value @property - def state(self) -> str: + def state(self) -> Optional[str]: """Return the state of the device.""" # Can't calculate any derivative if we have only one value. device_value_key = self._sensor_type["device_value_key"] diff --git a/tests/components/upnp/mock_device.py b/tests/components/upnp/mock_device.py index a70b3fa0237..d6027608137 100644 --- a/tests/components/upnp/mock_device.py +++ b/tests/components/upnp/mock_device.py @@ -16,14 +16,14 @@ import homeassistant.util.dt as dt_util class MockDevice(Device): """Mock device for Device.""" - def __init__(self, udn): + def __init__(self, udn: str) -> None: """Initialize mock device.""" igd_device = object() super().__init__(igd_device) self._udn = udn @classmethod - async def async_create_device(cls, hass, ssdp_location): + async def async_create_device(cls, hass, ssdp_location) -> "MockDevice": """Return self.""" return cls("UDN") @@ -52,6 +52,11 @@ class MockDevice(Device): """Get the device type.""" return "urn:schemas-upnp-org:device:InternetGatewayDevice:1" + @property + def hostname(self) -> str: + """Get the hostname.""" + return "mock-hostname" + async def async_get_traffic_data(self) -> Mapping[str, any]: """Get traffic data.""" return { diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index f702d770ee6..77d04381a12 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -6,10 +6,12 @@ from unittest.mock import AsyncMock, patch from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp from homeassistant.components.upnp.const import ( + CONFIG_ENTRY_HOSTNAME, CONFIG_ENTRY_SCAN_INTERVAL, CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, DEFAULT_SCAN_INTERVAL, + DISCOVERY_HOSTNAME, DISCOVERY_LOCATION, DISCOVERY_NAME, DISCOVERY_ST, @@ -41,6 +43,7 @@ async def test_flow_ssdp_discovery(hass: HomeAssistantType): DISCOVERY_UDN: mock_device.udn, DISCOVERY_UNIQUE_ID: mock_device.unique_id, DISCOVERY_USN: mock_device.usn, + DISCOVERY_HOSTNAME: mock_device.hostname, } ] with patch.object( @@ -75,10 +78,11 @@ async def test_flow_ssdp_discovery(hass: HomeAssistantType): assert result["data"] == { CONFIG_ENTRY_ST: mock_device.device_type, CONFIG_ENTRY_UDN: mock_device.udn, + CONFIG_ENTRY_HOSTNAME: mock_device.hostname, } -async def test_flow_ssdp_discovery_incomplete(hass: HomeAssistantType): +async def test_flow_ssdp_incomplete_discovery(hass: HomeAssistantType): """Test config flow: incomplete discovery through ssdp.""" udn = "uuid:device_1" location = "dummy" @@ -89,15 +93,64 @@ async def test_flow_ssdp_discovery_incomplete(hass: HomeAssistantType): DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data={ - ssdp.ATTR_SSDP_ST: mock_device.device_type, - # ssdp.ATTR_UPNP_UDN: mock_device.udn, # Not provided. ssdp.ATTR_SSDP_LOCATION: location, + ssdp.ATTR_SSDP_ST: mock_device.device_type, + ssdp.ATTR_SSDP_USN: mock_device.usn, + # ssdp.ATTR_UPNP_UDN: mock_device.udn, # Not provided. }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "incomplete_discovery" +async def test_flow_ssdp_discovery_ignored(hass: HomeAssistantType): + """Test config flow: discovery through ssdp, but ignored.""" + udn = "uuid:device_random_1" + location = "dummy" + mock_device = MockDevice(udn) + + # Existing entry. + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONFIG_ENTRY_UDN: "uuid:device_random_2", + CONFIG_ENTRY_ST: mock_device.device_type, + CONFIG_ENTRY_HOSTNAME: mock_device.hostname, + }, + options={CONFIG_ENTRY_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, + ) + config_entry.add_to_hass(hass) + + discoveries = [ + { + DISCOVERY_LOCATION: location, + DISCOVERY_NAME: mock_device.name, + DISCOVERY_ST: mock_device.device_type, + DISCOVERY_UDN: mock_device.udn, + DISCOVERY_UNIQUE_ID: mock_device.unique_id, + DISCOVERY_USN: mock_device.usn, + DISCOVERY_HOSTNAME: mock_device.hostname, + } + ] + + with patch.object( + Device, "async_supplement_discovery", AsyncMock(return_value=discoveries[0]) + ): + # Discovered via step ssdp, but ignored. + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + ssdp.ATTR_SSDP_LOCATION: location, + ssdp.ATTR_SSDP_ST: mock_device.device_type, + ssdp.ATTR_SSDP_USN: mock_device.usn, + ssdp.ATTR_UPNP_UDN: mock_device.udn, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "discovery_ignored" + + async def test_flow_user(hass: HomeAssistantType): """Test config flow: discovered + configured through user.""" udn = "uuid:device_1" @@ -111,6 +164,7 @@ async def test_flow_user(hass: HomeAssistantType): DISCOVERY_UDN: mock_device.udn, DISCOVERY_UNIQUE_ID: mock_device.unique_id, DISCOVERY_USN: mock_device.usn, + DISCOVERY_HOSTNAME: mock_device.hostname, } ] @@ -139,6 +193,7 @@ async def test_flow_user(hass: HomeAssistantType): assert result["data"] == { CONFIG_ENTRY_ST: mock_device.device_type, CONFIG_ENTRY_UDN: mock_device.udn, + CONFIG_ENTRY_HOSTNAME: mock_device.hostname, } @@ -155,6 +210,7 @@ async def test_flow_import(hass: HomeAssistantType): DISCOVERY_UDN: mock_device.udn, DISCOVERY_UNIQUE_ID: mock_device.unique_id, DISCOVERY_USN: mock_device.usn, + DISCOVERY_HOSTNAME: mock_device.hostname, } ] @@ -175,6 +231,7 @@ async def test_flow_import(hass: HomeAssistantType): assert result["data"] == { CONFIG_ENTRY_ST: mock_device.device_type, CONFIG_ENTRY_UDN: mock_device.udn, + CONFIG_ENTRY_HOSTNAME: mock_device.hostname, } @@ -189,6 +246,7 @@ async def test_flow_import_already_configured(hass: HomeAssistantType): data={ CONFIG_ENTRY_UDN: mock_device.udn, CONFIG_ENTRY_ST: mock_device.device_type, + CONFIG_ENTRY_HOSTNAME: mock_device.hostname, }, options={CONFIG_ENTRY_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, ) @@ -216,6 +274,7 @@ async def test_flow_import_incomplete(hass: HomeAssistantType): DISCOVERY_UDN: mock_device.udn, DISCOVERY_UNIQUE_ID: mock_device.unique_id, DISCOVERY_USN: mock_device.usn, + DISCOVERY_HOSTNAME: mock_device.hostname, } ] @@ -243,6 +302,7 @@ async def test_options_flow(hass: HomeAssistantType): DISCOVERY_UDN: mock_device.udn, DISCOVERY_UNIQUE_ID: mock_device.unique_id, DISCOVERY_USN: mock_device.usn, + DISCOVERY_HOSTNAME: mock_device.hostname, } ] config_entry = MockConfigEntry( @@ -250,6 +310,7 @@ async def test_options_flow(hass: HomeAssistantType): data={ CONFIG_ENTRY_UDN: mock_device.udn, CONFIG_ENTRY_ST: mock_device.device_type, + CONFIG_ENTRY_HOSTNAME: mock_device.hostname, }, options={CONFIG_ENTRY_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, ) diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index 26b2f970fed..086fbd677ab 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, patch from homeassistant.components.upnp.const import ( CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, + DISCOVERY_HOSTNAME, DISCOVERY_LOCATION, DISCOVERY_NAME, DISCOVERY_ST, @@ -35,6 +36,7 @@ async def test_async_setup_entry_default(hass: HomeAssistantType): DISCOVERY_UDN: mock_device.udn, DISCOVERY_UNIQUE_ID: mock_device.unique_id, DISCOVERY_USN: mock_device.usn, + DISCOVERY_HOSTNAME: mock_device.hostname, } ] entry = MockConfigEntry( @@ -83,6 +85,7 @@ async def test_sync_setup_entry_multiple_discoveries(hass: HomeAssistantType): DISCOVERY_UDN: mock_device_0.udn, DISCOVERY_UNIQUE_ID: mock_device_0.unique_id, DISCOVERY_USN: mock_device_0.usn, + DISCOVERY_HOSTNAME: mock_device_0.hostname, }, { DISCOVERY_LOCATION: location_1, @@ -91,6 +94,7 @@ async def test_sync_setup_entry_multiple_discoveries(hass: HomeAssistantType): DISCOVERY_UDN: mock_device_1.udn, DISCOVERY_UNIQUE_ID: mock_device_1.unique_id, DISCOVERY_USN: mock_device_1.usn, + DISCOVERY_HOSTNAME: mock_device_1.hostname, }, ] entry = MockConfigEntry( From 3ad207a499b96d71283b18c38155e6722ed51861 Mon Sep 17 00:00:00 2001 From: Garrett <7310260+G-Two@users.noreply.github.com> Date: Sat, 20 Feb 2021 21:52:44 -0500 Subject: [PATCH 604/796] Add new Subaru integration (#35760) Co-authored-by: On Freund Co-authored-by: J. Nick Koston --- CODEOWNERS | 1 + homeassistant/components/subaru/__init__.py | 173 +++++++++++ .../components/subaru/config_flow.py | 157 ++++++++++ homeassistant/components/subaru/const.py | 47 +++ homeassistant/components/subaru/entity.py | 39 +++ homeassistant/components/subaru/manifest.json | 8 + homeassistant/components/subaru/sensor.py | 265 ++++++++++++++++ homeassistant/components/subaru/strings.json | 45 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/subaru/__init__.py | 1 + tests/components/subaru/api_responses.py | 284 ++++++++++++++++++ tests/components/subaru/conftest.py | 139 +++++++++ tests/components/subaru/test_config_flow.py | 250 +++++++++++++++ tests/components/subaru/test_init.py | 153 ++++++++++ tests/components/subaru/test_sensor.py | 67 +++++ 17 files changed, 1636 insertions(+) create mode 100644 homeassistant/components/subaru/__init__.py create mode 100644 homeassistant/components/subaru/config_flow.py create mode 100644 homeassistant/components/subaru/const.py create mode 100644 homeassistant/components/subaru/entity.py create mode 100644 homeassistant/components/subaru/manifest.json create mode 100644 homeassistant/components/subaru/sensor.py create mode 100644 homeassistant/components/subaru/strings.json create mode 100644 tests/components/subaru/__init__.py create mode 100644 tests/components/subaru/api_responses.py create mode 100644 tests/components/subaru/conftest.py create mode 100644 tests/components/subaru/test_config_flow.py create mode 100644 tests/components/subaru/test_init.py create mode 100644 tests/components/subaru/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index f3c7487a520..788f3636143 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -450,6 +450,7 @@ homeassistant/components/stiebel_eltron/* @fucm homeassistant/components/stookalert/* @fwestenberg homeassistant/components/stream/* @hunterjm @uvjustin homeassistant/components/stt/* @pvizeli +homeassistant/components/subaru/* @G-Two homeassistant/components/suez_water/* @ooii homeassistant/components/sun/* @Swamp-Ig homeassistant/components/supla/* @mwegrzynek diff --git a/homeassistant/components/subaru/__init__.py b/homeassistant/components/subaru/__init__.py new file mode 100644 index 00000000000..63bc644b50a --- /dev/null +++ b/homeassistant/components/subaru/__init__.py @@ -0,0 +1,173 @@ +"""The Subaru integration.""" +import asyncio +from datetime import timedelta +import logging +import time + +from subarulink import Controller as SubaruAPI, InvalidCredentials, SubaruException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + CONF_COUNTRY, + CONF_UPDATE_ENABLED, + COORDINATOR_NAME, + DOMAIN, + ENTRY_CONTROLLER, + ENTRY_COORDINATOR, + ENTRY_VEHICLES, + FETCH_INTERVAL, + SUPPORTED_PLATFORMS, + UPDATE_INTERVAL, + VEHICLE_API_GEN, + VEHICLE_HAS_EV, + VEHICLE_HAS_REMOTE_SERVICE, + VEHICLE_HAS_REMOTE_START, + VEHICLE_HAS_SAFETY_SERVICE, + VEHICLE_LAST_UPDATE, + VEHICLE_NAME, + VEHICLE_VIN, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, base_config): + """Do nothing since this integration does not support configuration.yml setup.""" + hass.data.setdefault(DOMAIN, {}) + return True + + +async def async_setup_entry(hass, entry): + """Set up Subaru from a config entry.""" + config = entry.data + websession = aiohttp_client.async_get_clientsession(hass) + try: + controller = SubaruAPI( + websession, + config[CONF_USERNAME], + config[CONF_PASSWORD], + config[CONF_DEVICE_ID], + config[CONF_PIN], + None, + config[CONF_COUNTRY], + update_interval=UPDATE_INTERVAL, + fetch_interval=FETCH_INTERVAL, + ) + _LOGGER.debug("Using subarulink %s", controller.version) + await controller.connect() + except InvalidCredentials: + _LOGGER.error("Invalid account") + return False + except SubaruException as err: + raise ConfigEntryNotReady(err.message) from err + + vehicle_info = {} + for vin in controller.get_vehicles(): + vehicle_info[vin] = get_vehicle_info(controller, vin) + + async def async_update_data(): + """Fetch data from API endpoint.""" + try: + return await refresh_subaru_data(entry, vehicle_info, controller) + except SubaruException as err: + raise UpdateFailed(err.message) from err + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=COORDINATOR_NAME, + update_method=async_update_data, + update_interval=timedelta(seconds=FETCH_INTERVAL), + ) + + await coordinator.async_refresh() + + hass.data[DOMAIN][entry.entry_id] = { + ENTRY_CONTROLLER: controller, + ENTRY_COORDINATOR: coordinator, + ENTRY_VEHICLES: vehicle_info, + } + + for component in SUPPORTED_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 SUPPORTED_PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok + + +async def refresh_subaru_data(config_entry, vehicle_info, controller): + """ + Refresh local data with data fetched via Subaru API. + + Subaru API calls assume a server side vehicle context + Data fetch/update must be done for each vehicle + """ + data = {} + + for vehicle in vehicle_info.values(): + vin = vehicle[VEHICLE_VIN] + + # Active subscription required + if not vehicle[VEHICLE_HAS_SAFETY_SERVICE]: + continue + + # Optionally send an "update" remote command to vehicle (throttled with update_interval) + if config_entry.options.get(CONF_UPDATE_ENABLED, False): + await update_subaru(vehicle, controller) + + # Fetch data from Subaru servers + await controller.fetch(vin, force=True) + + # Update our local data that will go to entity states + received_data = await controller.get_data(vin) + if received_data: + data[vin] = received_data + + return data + + +async def update_subaru(vehicle, controller): + """Commands remote vehicle update (polls the vehicle to update subaru API cache).""" + cur_time = time.time() + last_update = vehicle[VEHICLE_LAST_UPDATE] + + if cur_time - last_update > controller.get_update_interval(): + await controller.update(vehicle[VEHICLE_VIN], force=True) + vehicle[VEHICLE_LAST_UPDATE] = cur_time + + +def get_vehicle_info(controller, vin): + """Obtain vehicle identifiers and capabilities.""" + info = { + VEHICLE_VIN: vin, + VEHICLE_NAME: controller.vin_to_name(vin), + VEHICLE_HAS_EV: controller.get_ev_status(vin), + VEHICLE_API_GEN: controller.get_api_gen(vin), + VEHICLE_HAS_REMOTE_START: controller.get_res_status(vin), + VEHICLE_HAS_REMOTE_SERVICE: controller.get_remote_status(vin), + VEHICLE_HAS_SAFETY_SERVICE: controller.get_safety_status(vin), + VEHICLE_LAST_UPDATE: 0, + } + return info diff --git a/homeassistant/components/subaru/config_flow.py b/homeassistant/components/subaru/config_flow.py new file mode 100644 index 00000000000..4c5c476a402 --- /dev/null +++ b/homeassistant/components/subaru/config_flow.py @@ -0,0 +1,157 @@ +"""Config flow for Subaru integration.""" +from datetime import datetime +import logging + +from subarulink import ( + Controller as SubaruAPI, + InvalidCredentials, + InvalidPIN, + SubaruException, +) +from subarulink.const import COUNTRY_CAN, COUNTRY_USA +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_USERNAME +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client, config_validation as cv + +# pylint: disable=unused-import +from .const import CONF_COUNTRY, CONF_UPDATE_ENABLED, DOMAIN + +_LOGGER = logging.getLogger(__name__) +PIN_SCHEMA = vol.Schema({vol.Required(CONF_PIN): str}) + + +class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Subaru.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + config_data = {CONF_PIN: None} + controller = None + + async def async_step_user(self, user_input=None): + """Handle the start of the config flow.""" + error = None + + if user_input: + if user_input[CONF_USERNAME] in [ + entry.data[CONF_USERNAME] for entry in self._async_current_entries() + ]: + return self.async_abort(reason="already_configured") + + try: + await self.validate_login_creds(user_input) + except InvalidCredentials: + error = {"base": "invalid_auth"} + except SubaruException as ex: + _LOGGER.error("Unable to communicate with Subaru API: %s", ex.message) + return self.async_abort(reason="cannot_connect") + else: + if self.controller.is_pin_required(): + return await self.async_step_pin() + return self.async_create_entry( + title=user_input[CONF_USERNAME], data=self.config_data + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_USERNAME, + default=user_input.get(CONF_USERNAME) if user_input else "", + ): str, + vol.Required( + CONF_PASSWORD, + default=user_input.get(CONF_PASSWORD) if user_input else "", + ): str, + vol.Required( + CONF_COUNTRY, + default=user_input.get(CONF_COUNTRY) + if user_input + else COUNTRY_USA, + ): vol.In([COUNTRY_CAN, COUNTRY_USA]), + } + ), + errors=error, + ) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + async def validate_login_creds(self, data): + """Validate the user input allows us to connect. + + data: contains values provided by the user. + """ + websession = aiohttp_client.async_get_clientsession(self.hass) + now = datetime.now() + if not data.get(CONF_DEVICE_ID): + data[CONF_DEVICE_ID] = int(now.timestamp()) + date = now.strftime("%Y-%m-%d") + device_name = "Home Assistant: Added " + date + + self.controller = SubaruAPI( + websession, + username=data[CONF_USERNAME], + password=data[CONF_PASSWORD], + device_id=data[CONF_DEVICE_ID], + pin=None, + device_name=device_name, + country=data[CONF_COUNTRY], + ) + _LOGGER.debug("Using subarulink %s", self.controller.version) + _LOGGER.debug( + "Setting up first time connection to Subuaru API. This may take up to 20 seconds." + ) + if await self.controller.connect(): + _LOGGER.debug("Successfully authenticated and authorized with Subaru API") + self.config_data.update(data) + + async def async_step_pin(self, user_input=None): + """Handle second part of config flow, if required.""" + error = None + if user_input: + if self.controller.update_saved_pin(user_input[CONF_PIN]): + try: + vol.Match(r"[0-9]{4}")(user_input[CONF_PIN]) + await self.controller.test_pin() + except vol.Invalid: + error = {"base": "bad_pin_format"} + except InvalidPIN: + error = {"base": "incorrect_pin"} + else: + _LOGGER.debug("PIN successfully tested") + self.config_data.update(user_input) + return self.async_create_entry( + title=self.config_data[CONF_USERNAME], data=self.config_data + ) + return self.async_show_form(step_id="pin", data_schema=PIN_SCHEMA, errors=error) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for Subaru.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + data_schema = vol.Schema( + { + vol.Required( + CONF_UPDATE_ENABLED, + default=self.config_entry.options.get(CONF_UPDATE_ENABLED, False), + ): cv.boolean, + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/subaru/const.py b/homeassistant/components/subaru/const.py new file mode 100644 index 00000000000..7349f9c32d6 --- /dev/null +++ b/homeassistant/components/subaru/const.py @@ -0,0 +1,47 @@ +"""Constants for the Subaru integration.""" + +DOMAIN = "subaru" +FETCH_INTERVAL = 300 +UPDATE_INTERVAL = 7200 +CONF_UPDATE_ENABLED = "update_enabled" +CONF_COUNTRY = "country" + +# entry fields +ENTRY_CONTROLLER = "controller" +ENTRY_COORDINATOR = "coordinator" +ENTRY_VEHICLES = "vehicles" + +# update coordinator name +COORDINATOR_NAME = "subaru_data" + +# info fields +VEHICLE_VIN = "vin" +VEHICLE_NAME = "display_name" +VEHICLE_HAS_EV = "is_ev" +VEHICLE_API_GEN = "api_gen" +VEHICLE_HAS_REMOTE_START = "has_res" +VEHICLE_HAS_REMOTE_SERVICE = "has_remote" +VEHICLE_HAS_SAFETY_SERVICE = "has_safety" +VEHICLE_LAST_UPDATE = "last_update" +VEHICLE_STATUS = "status" + + +API_GEN_1 = "g1" +API_GEN_2 = "g2" +MANUFACTURER = "Subaru Corp." + +SUPPORTED_PLATFORMS = [ + "sensor", +] + +ICONS = { + "Avg Fuel Consumption": "mdi:leaf", + "EV Time to Full Charge": "mdi:car-electric", + "EV Range": "mdi:ev-station", + "Odometer": "mdi:road-variant", + "Range": "mdi:gas-station", + "Tire Pressure FL": "mdi:gauge", + "Tire Pressure FR": "mdi:gauge", + "Tire Pressure RL": "mdi:gauge", + "Tire Pressure RR": "mdi:gauge", +} diff --git a/homeassistant/components/subaru/entity.py b/homeassistant/components/subaru/entity.py new file mode 100644 index 00000000000..4fdeca4e484 --- /dev/null +++ b/homeassistant/components/subaru/entity.py @@ -0,0 +1,39 @@ +"""Base class for all Subaru Entities.""" +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, ICONS, MANUFACTURER, VEHICLE_NAME, VEHICLE_VIN + + +class SubaruEntity(CoordinatorEntity): + """Representation of a Subaru Entity.""" + + def __init__(self, vehicle_info, coordinator): + """Initialize the Subaru Entity.""" + super().__init__(coordinator) + self.car_name = vehicle_info[VEHICLE_NAME] + self.vin = vehicle_info[VEHICLE_VIN] + self.entity_type = "entity" + + @property + def name(self): + """Return name.""" + return f"{self.car_name} {self.entity_type}" + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return f"{self.vin}_{self.entity_type}" + + @property + def icon(self): + """Return the icon of the sensor.""" + return ICONS.get(self.entity_type) + + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self.vin)}, + "name": self.car_name, + "manufacturer": MANUFACTURER, + } diff --git a/homeassistant/components/subaru/manifest.json b/homeassistant/components/subaru/manifest.json new file mode 100644 index 00000000000..7a918c59f74 --- /dev/null +++ b/homeassistant/components/subaru/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "subaru", + "name": "Subaru", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/subaru", + "requirements": ["subarulink==0.3.12"], + "codeowners": ["@G-Two"] +} diff --git a/homeassistant/components/subaru/sensor.py b/homeassistant/components/subaru/sensor.py new file mode 100644 index 00000000000..594d18028e6 --- /dev/null +++ b/homeassistant/components/subaru/sensor.py @@ -0,0 +1,265 @@ +"""Support for Subaru sensors.""" +import subarulink.const as sc + +from homeassistant.components.sensor import DEVICE_CLASSES +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + LENGTH_KILOMETERS, + LENGTH_MILES, + PERCENTAGE, + PRESSURE_HPA, + TEMP_CELSIUS, + TIME_MINUTES, + VOLT, + VOLUME_GALLONS, + VOLUME_LITERS, +) +from homeassistant.util.distance import convert as dist_convert +from homeassistant.util.unit_system import ( + IMPERIAL_SYSTEM, + LENGTH_UNITS, + PRESSURE_UNITS, + TEMPERATURE_UNITS, +) +from homeassistant.util.volume import convert as vol_convert + +from .const import ( + API_GEN_2, + DOMAIN, + ENTRY_COORDINATOR, + ENTRY_VEHICLES, + VEHICLE_API_GEN, + VEHICLE_HAS_EV, + VEHICLE_HAS_SAFETY_SERVICE, + VEHICLE_STATUS, +) +from .entity import SubaruEntity + +L_PER_GAL = vol_convert(1, VOLUME_GALLONS, VOLUME_LITERS) +KM_PER_MI = dist_convert(1, LENGTH_MILES, LENGTH_KILOMETERS) + +# Fuel Economy Constants +FUEL_CONSUMPTION_L_PER_100KM = "L/100km" +FUEL_CONSUMPTION_MPG = "mi/gal" +FUEL_CONSUMPTION_UNITS = [FUEL_CONSUMPTION_L_PER_100KM, FUEL_CONSUMPTION_MPG] + +SENSOR_TYPE = "type" +SENSOR_CLASS = "class" +SENSOR_FIELD = "field" +SENSOR_UNITS = "units" + +# Sensor data available to "Subaru Safety Plus" subscribers with Gen1 or Gen2 vehicles +SAFETY_SENSORS = [ + { + SENSOR_TYPE: "Odometer", + SENSOR_CLASS: None, + SENSOR_FIELD: sc.ODOMETER, + SENSOR_UNITS: LENGTH_KILOMETERS, + }, +] + +# Sensor data available to "Subaru Safety Plus" subscribers with Gen2 vehicles +API_GEN_2_SENSORS = [ + { + SENSOR_TYPE: "Avg Fuel Consumption", + SENSOR_CLASS: None, + SENSOR_FIELD: sc.AVG_FUEL_CONSUMPTION, + SENSOR_UNITS: FUEL_CONSUMPTION_L_PER_100KM, + }, + { + SENSOR_TYPE: "Range", + SENSOR_CLASS: None, + SENSOR_FIELD: sc.DIST_TO_EMPTY, + SENSOR_UNITS: LENGTH_KILOMETERS, + }, + { + SENSOR_TYPE: "Tire Pressure FL", + SENSOR_CLASS: None, + SENSOR_FIELD: sc.TIRE_PRESSURE_FL, + SENSOR_UNITS: PRESSURE_HPA, + }, + { + SENSOR_TYPE: "Tire Pressure FR", + SENSOR_CLASS: None, + SENSOR_FIELD: sc.TIRE_PRESSURE_FR, + SENSOR_UNITS: PRESSURE_HPA, + }, + { + SENSOR_TYPE: "Tire Pressure RL", + SENSOR_CLASS: None, + SENSOR_FIELD: sc.TIRE_PRESSURE_RL, + SENSOR_UNITS: PRESSURE_HPA, + }, + { + SENSOR_TYPE: "Tire Pressure RR", + SENSOR_CLASS: None, + SENSOR_FIELD: sc.TIRE_PRESSURE_RR, + SENSOR_UNITS: PRESSURE_HPA, + }, + { + SENSOR_TYPE: "External Temp", + SENSOR_CLASS: DEVICE_CLASS_TEMPERATURE, + SENSOR_FIELD: sc.EXTERNAL_TEMP, + SENSOR_UNITS: TEMP_CELSIUS, + }, + { + SENSOR_TYPE: "12V Battery Voltage", + SENSOR_CLASS: DEVICE_CLASS_VOLTAGE, + SENSOR_FIELD: sc.BATTERY_VOLTAGE, + SENSOR_UNITS: VOLT, + }, +] + +# Sensor data available to "Subaru Safety Plus" subscribers with PHEV vehicles +EV_SENSORS = [ + { + SENSOR_TYPE: "EV Range", + SENSOR_CLASS: None, + SENSOR_FIELD: sc.EV_DISTANCE_TO_EMPTY, + SENSOR_UNITS: LENGTH_MILES, + }, + { + SENSOR_TYPE: "EV Battery Level", + SENSOR_CLASS: DEVICE_CLASS_BATTERY, + SENSOR_FIELD: sc.EV_STATE_OF_CHARGE_PERCENT, + SENSOR_UNITS: PERCENTAGE, + }, + { + SENSOR_TYPE: "EV Time to Full Charge", + SENSOR_CLASS: None, + SENSOR_FIELD: sc.EV_TIME_TO_FULLY_CHARGED, + SENSOR_UNITS: TIME_MINUTES, + }, +] + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Subaru sensors by config_entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id][ENTRY_COORDINATOR] + vehicle_info = hass.data[DOMAIN][config_entry.entry_id][ENTRY_VEHICLES] + entities = [] + for vin in vehicle_info.keys(): + entities.extend(create_vehicle_sensors(vehicle_info[vin], coordinator)) + async_add_entities(entities, True) + + +def create_vehicle_sensors(vehicle_info, coordinator): + """Instantiate all available sensors for the vehicle.""" + sensors_to_add = [] + if vehicle_info[VEHICLE_HAS_SAFETY_SERVICE]: + sensors_to_add.extend(SAFETY_SENSORS) + + if vehicle_info[VEHICLE_API_GEN] == API_GEN_2: + sensors_to_add.extend(API_GEN_2_SENSORS) + + if vehicle_info[VEHICLE_HAS_EV]: + sensors_to_add.extend(EV_SENSORS) + + return [ + SubaruSensor( + vehicle_info, + coordinator, + s[SENSOR_TYPE], + s[SENSOR_CLASS], + s[SENSOR_FIELD], + s[SENSOR_UNITS], + ) + for s in sensors_to_add + ] + + +class SubaruSensor(SubaruEntity): + """Class for Subaru sensors.""" + + def __init__( + self, vehicle_info, coordinator, entity_type, sensor_class, data_field, api_unit + ): + """Initialize the sensor.""" + super().__init__(vehicle_info, coordinator) + self.hass_type = "sensor" + self.current_value = None + self.entity_type = entity_type + self.sensor_class = sensor_class + self.data_field = data_field + self.api_unit = api_unit + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + if self.sensor_class in DEVICE_CLASSES: + return self.sensor_class + return super().device_class + + @property + def state(self): + """Return the state of the sensor.""" + self.current_value = self.get_current_value() + + if self.current_value is None: + return None + + if self.api_unit in TEMPERATURE_UNITS: + return round( + self.hass.config.units.temperature(self.current_value, self.api_unit), 1 + ) + + if self.api_unit in LENGTH_UNITS: + return round( + self.hass.config.units.length(self.current_value, self.api_unit), 1 + ) + + if self.api_unit in PRESSURE_UNITS: + if self.hass.config.units == IMPERIAL_SYSTEM: + return round( + self.hass.config.units.pressure(self.current_value, self.api_unit), + 1, + ) + + if self.api_unit in FUEL_CONSUMPTION_UNITS: + if self.hass.config.units == IMPERIAL_SYSTEM: + return round((100.0 * L_PER_GAL) / (KM_PER_MI * self.current_value), 1) + + return self.current_value + + @property + def unit_of_measurement(self): + """Return the unit_of_measurement of the device.""" + if self.api_unit in TEMPERATURE_UNITS: + return self.hass.config.units.temperature_unit + + if self.api_unit in LENGTH_UNITS: + return self.hass.config.units.length_unit + + if self.api_unit in PRESSURE_UNITS: + if self.hass.config.units == IMPERIAL_SYSTEM: + return self.hass.config.units.pressure_unit + return PRESSURE_HPA + + if self.api_unit in FUEL_CONSUMPTION_UNITS: + if self.hass.config.units == IMPERIAL_SYSTEM: + return FUEL_CONSUMPTION_MPG + return FUEL_CONSUMPTION_L_PER_100KM + + return self.api_unit + + @property + def available(self): + """Return if entity is available.""" + last_update_success = super().available + if last_update_success and self.vin not in self.coordinator.data: + return False + return last_update_success + + def get_current_value(self): + """Get raw value from the coordinator.""" + value = self.coordinator.data[self.vin][VEHICLE_STATUS].get(self.data_field) + if value in sc.BAD_SENSOR_VALUES: + value = None + if isinstance(value, str): + if "." in value: + value = float(value) + else: + value = int(value) + return value diff --git a/homeassistant/components/subaru/strings.json b/homeassistant/components/subaru/strings.json new file mode 100644 index 00000000000..064245e0732 --- /dev/null +++ b/homeassistant/components/subaru/strings.json @@ -0,0 +1,45 @@ +{ + "config": { + "step": { + "user": { + "title": "Subaru Starlink Configuration", + "description": "Please enter your MySubaru credentials\nNOTE: Initial setup may take up to 30 seconds", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "country": "Select country" + } + }, + "pin": { + "title": "Subaru Starlink Configuration", + "description": "Please enter your MySubaru PIN\nNOTE: All vehicles in account must have the same PIN", + "data": { + "pin": "PIN" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "incorrect_pin": "Incorrect PIN", + "bad_pin_format": "PIN should be 4 digits", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + } + }, + + "options": { + "step": { + "init": { + "title": "Subaru Starlink Options", + "description": "When enabled, vehicle polling will send a remote command to your vehicle every 2 hours to obtain new sensor data. Without vehicle polling, new sensor data is only received when the vehicle automatically pushes data (normally after engine shutdown).", + "data": { + "update_enabled": "Enable vehicle polling" + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index dfb2f56b29e..7e17a839068 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -216,6 +216,7 @@ FLOWS = [ "squeezebox", "srp_energy", "starline", + "subaru", "syncthru", "synology_dsm", "tado", diff --git a/requirements_all.txt b/requirements_all.txt index 4d3d7d5bebf..9ceb668bc58 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2142,6 +2142,9 @@ streamlabswater==1.0.1 # homeassistant.components.traccar stringcase==1.2.0 +# homeassistant.components.subaru +subarulink==0.3.12 + # homeassistant.components.ecovacs sucks==0.9.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 80d325100c8..d37ee25cbbd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1105,6 +1105,9 @@ statsd==3.2.1 # homeassistant.components.traccar stringcase==1.2.0 +# homeassistant.components.subaru +subarulink==0.3.12 + # homeassistant.components.solarlog sunwatcher==0.2.1 diff --git a/tests/components/subaru/__init__.py b/tests/components/subaru/__init__.py new file mode 100644 index 00000000000..26b81c84a1e --- /dev/null +++ b/tests/components/subaru/__init__.py @@ -0,0 +1 @@ +"""Tests for the Subaru integration.""" diff --git a/tests/components/subaru/api_responses.py b/tests/components/subaru/api_responses.py new file mode 100644 index 00000000000..b6a79ab8829 --- /dev/null +++ b/tests/components/subaru/api_responses.py @@ -0,0 +1,284 @@ +"""Sample API response data for tests.""" + +from homeassistant.components.subaru.const import ( + API_GEN_1, + API_GEN_2, + VEHICLE_API_GEN, + VEHICLE_HAS_EV, + VEHICLE_HAS_REMOTE_SERVICE, + VEHICLE_HAS_REMOTE_START, + VEHICLE_HAS_SAFETY_SERVICE, + VEHICLE_NAME, + VEHICLE_VIN, +) + +TEST_VIN_1_G1 = "JF2ABCDE6L0000001" +TEST_VIN_2_EV = "JF2ABCDE6L0000002" +TEST_VIN_3_G2 = "JF2ABCDE6L0000003" + +VEHICLE_DATA = { + TEST_VIN_1_G1: { + VEHICLE_VIN: TEST_VIN_1_G1, + VEHICLE_NAME: "test_vehicle_1", + VEHICLE_HAS_EV: False, + VEHICLE_API_GEN: API_GEN_1, + VEHICLE_HAS_REMOTE_START: True, + VEHICLE_HAS_REMOTE_SERVICE: True, + VEHICLE_HAS_SAFETY_SERVICE: False, + }, + TEST_VIN_2_EV: { + VEHICLE_VIN: TEST_VIN_2_EV, + VEHICLE_NAME: "test_vehicle_2", + VEHICLE_HAS_EV: True, + VEHICLE_API_GEN: API_GEN_2, + VEHICLE_HAS_REMOTE_START: True, + VEHICLE_HAS_REMOTE_SERVICE: True, + VEHICLE_HAS_SAFETY_SERVICE: True, + }, + TEST_VIN_3_G2: { + VEHICLE_VIN: TEST_VIN_3_G2, + VEHICLE_NAME: "test_vehicle_3", + VEHICLE_HAS_EV: False, + VEHICLE_API_GEN: API_GEN_2, + VEHICLE_HAS_REMOTE_START: True, + VEHICLE_HAS_REMOTE_SERVICE: True, + VEHICLE_HAS_SAFETY_SERVICE: True, + }, +} + +VEHICLE_STATUS_EV = { + "status": { + "AVG_FUEL_CONSUMPTION": 2.3, + "BATTERY_VOLTAGE": "12.0", + "DISTANCE_TO_EMPTY_FUEL": 707, + "DOOR_BOOT_LOCK_STATUS": "UNKNOWN", + "DOOR_BOOT_POSITION": "CLOSED", + "DOOR_ENGINE_HOOD_LOCK_STATUS": "UNKNOWN", + "DOOR_ENGINE_HOOD_POSITION": "CLOSED", + "DOOR_FRONT_LEFT_LOCK_STATUS": "UNKNOWN", + "DOOR_FRONT_LEFT_POSITION": "CLOSED", + "DOOR_FRONT_RIGHT_LOCK_STATUS": "UNKNOWN", + "DOOR_FRONT_RIGHT_POSITION": "CLOSED", + "DOOR_REAR_LEFT_LOCK_STATUS": "UNKNOWN", + "DOOR_REAR_LEFT_POSITION": "CLOSED", + "DOOR_REAR_RIGHT_LOCK_STATUS": "UNKNOWN", + "DOOR_REAR_RIGHT_POSITION": "CLOSED", + "EV_CHARGER_STATE_TYPE": "CHARGING_STOPPED", + "EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM", + "EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1", + "EV_DISTANCE_TO_EMPTY": 17, + "EV_IS_PLUGGED_IN": "UNLOCKED_CONNECTED", + "EV_STATE_OF_CHARGE_MODE": "EV_MODE", + "EV_STATE_OF_CHARGE_PERCENT": "100", + "EV_TIME_TO_FULLY_CHARGED": "65535", + "EV_VEHICLE_TIME_DAYOFWEEK": "6", + "EV_VEHICLE_TIME_HOUR": "14", + "EV_VEHICLE_TIME_MINUTE": "20", + "EV_VEHICLE_TIME_SECOND": "39", + "EXT_EXTERNAL_TEMP": "21.5", + "ODOMETER": 1234, + "POSITION_HEADING_DEGREE": "150", + "POSITION_SPEED_KMPH": "0", + "POSITION_TIMESTAMP": 1595560000.0, + "SEAT_BELT_STATUS_FRONT_LEFT": "BELTED", + "SEAT_BELT_STATUS_FRONT_MIDDLE": "NOT_EQUIPPED", + "SEAT_BELT_STATUS_FRONT_RIGHT": "BELTED", + "SEAT_BELT_STATUS_SECOND_LEFT": "UNKNOWN", + "SEAT_BELT_STATUS_SECOND_MIDDLE": "UNKNOWN", + "SEAT_BELT_STATUS_SECOND_RIGHT": "UNKNOWN", + "SEAT_BELT_STATUS_THIRD_LEFT": "UNKNOWN", + "SEAT_BELT_STATUS_THIRD_MIDDLE": "UNKNOWN", + "SEAT_BELT_STATUS_THIRD_RIGHT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_FRONT_LEFT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_FRONT_MIDDLE": "NOT_EQUIPPED", + "SEAT_OCCUPATION_STATUS_FRONT_RIGHT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_SECOND_LEFT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_SECOND_MIDDLE": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_SECOND_RIGHT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_THIRD_LEFT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_THIRD_MIDDLE": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_THIRD_RIGHT": "UNKNOWN", + "TIMESTAMP": 1595560000.0, + "TRANSMISSION_MODE": "UNKNOWN", + "TYRE_PRESSURE_FRONT_LEFT": 2550, + "TYRE_PRESSURE_FRONT_RIGHT": 2550, + "TYRE_PRESSURE_REAR_LEFT": 2450, + "TYRE_PRESSURE_REAR_RIGHT": 2350, + "TYRE_STATUS_FRONT_LEFT": "UNKNOWN", + "TYRE_STATUS_FRONT_RIGHT": "UNKNOWN", + "TYRE_STATUS_REAR_LEFT": "UNKNOWN", + "TYRE_STATUS_REAR_RIGHT": "UNKNOWN", + "VEHICLE_STATE_TYPE": "IGNITION_OFF", + "WINDOW_BACK_STATUS": "UNKNOWN", + "WINDOW_FRONT_LEFT_STATUS": "VENTED", + "WINDOW_FRONT_RIGHT_STATUS": "VENTED", + "WINDOW_REAR_LEFT_STATUS": "UNKNOWN", + "WINDOW_REAR_RIGHT_STATUS": "UNKNOWN", + "WINDOW_SUNROOF_STATUS": "UNKNOWN", + "heading": 170, + "latitude": 40.0, + "longitude": -100.0, + } +} + +VEHICLE_STATUS_G2 = { + "status": { + "AVG_FUEL_CONSUMPTION": 2.3, + "BATTERY_VOLTAGE": "12.0", + "DISTANCE_TO_EMPTY_FUEL": 707, + "DOOR_BOOT_LOCK_STATUS": "UNKNOWN", + "DOOR_BOOT_POSITION": "CLOSED", + "DOOR_ENGINE_HOOD_LOCK_STATUS": "UNKNOWN", + "DOOR_ENGINE_HOOD_POSITION": "CLOSED", + "DOOR_FRONT_LEFT_LOCK_STATUS": "UNKNOWN", + "DOOR_FRONT_LEFT_POSITION": "CLOSED", + "DOOR_FRONT_RIGHT_LOCK_STATUS": "UNKNOWN", + "DOOR_FRONT_RIGHT_POSITION": "CLOSED", + "DOOR_REAR_LEFT_LOCK_STATUS": "UNKNOWN", + "DOOR_REAR_LEFT_POSITION": "CLOSED", + "DOOR_REAR_RIGHT_LOCK_STATUS": "UNKNOWN", + "DOOR_REAR_RIGHT_POSITION": "CLOSED", + "EXT_EXTERNAL_TEMP": "21.5", + "ODOMETER": 1234, + "POSITION_HEADING_DEGREE": "150", + "POSITION_SPEED_KMPH": "0", + "POSITION_TIMESTAMP": 1595560000.0, + "SEAT_BELT_STATUS_FRONT_LEFT": "BELTED", + "SEAT_BELT_STATUS_FRONT_MIDDLE": "NOT_EQUIPPED", + "SEAT_BELT_STATUS_FRONT_RIGHT": "BELTED", + "SEAT_BELT_STATUS_SECOND_LEFT": "UNKNOWN", + "SEAT_BELT_STATUS_SECOND_MIDDLE": "UNKNOWN", + "SEAT_BELT_STATUS_SECOND_RIGHT": "UNKNOWN", + "SEAT_BELT_STATUS_THIRD_LEFT": "UNKNOWN", + "SEAT_BELT_STATUS_THIRD_MIDDLE": "UNKNOWN", + "SEAT_BELT_STATUS_THIRD_RIGHT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_FRONT_LEFT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_FRONT_MIDDLE": "NOT_EQUIPPED", + "SEAT_OCCUPATION_STATUS_FRONT_RIGHT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_SECOND_LEFT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_SECOND_MIDDLE": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_SECOND_RIGHT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_THIRD_LEFT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_THIRD_MIDDLE": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_THIRD_RIGHT": "UNKNOWN", + "TIMESTAMP": 1595560000.0, + "TRANSMISSION_MODE": "UNKNOWN", + "TYRE_PRESSURE_FRONT_LEFT": 2550, + "TYRE_PRESSURE_FRONT_RIGHT": 2550, + "TYRE_PRESSURE_REAR_LEFT": 2450, + "TYRE_PRESSURE_REAR_RIGHT": 2350, + "TYRE_STATUS_FRONT_LEFT": "UNKNOWN", + "TYRE_STATUS_FRONT_RIGHT": "UNKNOWN", + "TYRE_STATUS_REAR_LEFT": "UNKNOWN", + "TYRE_STATUS_REAR_RIGHT": "UNKNOWN", + "VEHICLE_STATE_TYPE": "IGNITION_OFF", + "WINDOW_BACK_STATUS": "UNKNOWN", + "WINDOW_FRONT_LEFT_STATUS": "VENTED", + "WINDOW_FRONT_RIGHT_STATUS": "VENTED", + "WINDOW_REAR_LEFT_STATUS": "UNKNOWN", + "WINDOW_REAR_RIGHT_STATUS": "UNKNOWN", + "WINDOW_SUNROOF_STATUS": "UNKNOWN", + "heading": 170, + "latitude": 40.0, + "longitude": -100.0, + } +} + +EXPECTED_STATE_EV_IMPERIAL = { + "AVG_FUEL_CONSUMPTION": "102.3", + "BATTERY_VOLTAGE": "12.0", + "DISTANCE_TO_EMPTY_FUEL": "439.3", + "EV_CHARGER_STATE_TYPE": "CHARGING_STOPPED", + "EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM", + "EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1", + "EV_DISTANCE_TO_EMPTY": "17", + "EV_IS_PLUGGED_IN": "UNLOCKED_CONNECTED", + "EV_STATE_OF_CHARGE_MODE": "EV_MODE", + "EV_STATE_OF_CHARGE_PERCENT": "100", + "EV_TIME_TO_FULLY_CHARGED": "unknown", + "EV_VEHICLE_TIME_DAYOFWEEK": "6", + "EV_VEHICLE_TIME_HOUR": "14", + "EV_VEHICLE_TIME_MINUTE": "20", + "EV_VEHICLE_TIME_SECOND": "39", + "EXT_EXTERNAL_TEMP": "70.7", + "ODOMETER": "766.8", + "POSITION_HEADING_DEGREE": "150", + "POSITION_SPEED_KMPH": "0", + "POSITION_TIMESTAMP": 1595560000.0, + "TIMESTAMP": 1595560000.0, + "TRANSMISSION_MODE": "UNKNOWN", + "TYRE_PRESSURE_FRONT_LEFT": "37.0", + "TYRE_PRESSURE_FRONT_RIGHT": "37.0", + "TYRE_PRESSURE_REAR_LEFT": "35.5", + "TYRE_PRESSURE_REAR_RIGHT": "34.1", + "VEHICLE_STATE_TYPE": "IGNITION_OFF", + "heading": 170, + "latitude": 40.0, + "longitude": -100.0, +} + +EXPECTED_STATE_EV_METRIC = { + "AVG_FUEL_CONSUMPTION": "2.3", + "BATTERY_VOLTAGE": "12.0", + "DISTANCE_TO_EMPTY_FUEL": "707", + "EV_CHARGER_STATE_TYPE": "CHARGING_STOPPED", + "EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM", + "EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1", + "EV_DISTANCE_TO_EMPTY": "27.4", + "EV_IS_PLUGGED_IN": "UNLOCKED_CONNECTED", + "EV_STATE_OF_CHARGE_MODE": "EV_MODE", + "EV_STATE_OF_CHARGE_PERCENT": "100", + "EV_TIME_TO_FULLY_CHARGED": "unknown", + "EV_VEHICLE_TIME_DAYOFWEEK": "6", + "EV_VEHICLE_TIME_HOUR": "14", + "EV_VEHICLE_TIME_MINUTE": "20", + "EV_VEHICLE_TIME_SECOND": "39", + "EXT_EXTERNAL_TEMP": "21.5", + "ODOMETER": "1234", + "POSITION_HEADING_DEGREE": "150", + "POSITION_SPEED_KMPH": "0", + "POSITION_TIMESTAMP": 1595560000.0, + "TIMESTAMP": 1595560000.0, + "TRANSMISSION_MODE": "UNKNOWN", + "TYRE_PRESSURE_FRONT_LEFT": "2550", + "TYRE_PRESSURE_FRONT_RIGHT": "2550", + "TYRE_PRESSURE_REAR_LEFT": "2450", + "TYRE_PRESSURE_REAR_RIGHT": "2350", + "VEHICLE_STATE_TYPE": "IGNITION_OFF", + "heading": 170, + "latitude": 40.0, + "longitude": -100.0, +} + +EXPECTED_STATE_EV_UNAVAILABLE = { + "AVG_FUEL_CONSUMPTION": "unavailable", + "BATTERY_VOLTAGE": "unavailable", + "DISTANCE_TO_EMPTY_FUEL": "unavailable", + "EV_CHARGER_STATE_TYPE": "unavailable", + "EV_CHARGE_SETTING_AMPERE_TYPE": "unavailable", + "EV_CHARGE_VOLT_TYPE": "unavailable", + "EV_DISTANCE_TO_EMPTY": "unavailable", + "EV_IS_PLUGGED_IN": "unavailable", + "EV_STATE_OF_CHARGE_MODE": "unavailable", + "EV_STATE_OF_CHARGE_PERCENT": "unavailable", + "EV_TIME_TO_FULLY_CHARGED": "unavailable", + "EV_VEHICLE_TIME_DAYOFWEEK": "unavailable", + "EV_VEHICLE_TIME_HOUR": "unavailable", + "EV_VEHICLE_TIME_MINUTE": "unavailable", + "EV_VEHICLE_TIME_SECOND": "unavailable", + "EXT_EXTERNAL_TEMP": "unavailable", + "ODOMETER": "unavailable", + "POSITION_HEADING_DEGREE": "unavailable", + "POSITION_SPEED_KMPH": "unavailable", + "POSITION_TIMESTAMP": "unavailable", + "TIMESTAMP": "unavailable", + "TRANSMISSION_MODE": "unavailable", + "TYRE_PRESSURE_FRONT_LEFT": "unavailable", + "TYRE_PRESSURE_FRONT_RIGHT": "unavailable", + "TYRE_PRESSURE_REAR_LEFT": "unavailable", + "TYRE_PRESSURE_REAR_RIGHT": "unavailable", + "VEHICLE_STATE_TYPE": "unavailable", + "heading": "unavailable", + "latitude": "unavailable", + "longitude": "unavailable", +} diff --git a/tests/components/subaru/conftest.py b/tests/components/subaru/conftest.py new file mode 100644 index 00000000000..8216ca2d2c2 --- /dev/null +++ b/tests/components/subaru/conftest.py @@ -0,0 +1,139 @@ +"""Common functions needed to setup tests for Subaru component.""" +from unittest.mock import patch + +import pytest +from subarulink.const import COUNTRY_USA + +from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN +from homeassistant.components.subaru.const import ( + CONF_COUNTRY, + CONF_UPDATE_ENABLED, + DOMAIN, + VEHICLE_API_GEN, + VEHICLE_HAS_EV, + VEHICLE_HAS_REMOTE_SERVICE, + VEHICLE_HAS_REMOTE_START, + VEHICLE_HAS_SAFETY_SERVICE, + VEHICLE_NAME, +) +from homeassistant.config_entries import ENTRY_STATE_LOADED +from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_USERNAME +from homeassistant.setup import async_setup_component + +from .api_responses import TEST_VIN_2_EV, VEHICLE_DATA, VEHICLE_STATUS_EV + +from tests.common import MockConfigEntry + +MOCK_API = "homeassistant.components.subaru.SubaruAPI." +MOCK_API_CONNECT = f"{MOCK_API}connect" +MOCK_API_IS_PIN_REQUIRED = f"{MOCK_API}is_pin_required" +MOCK_API_TEST_PIN = f"{MOCK_API}test_pin" +MOCK_API_UPDATE_SAVED_PIN = f"{MOCK_API}update_saved_pin" +MOCK_API_GET_VEHICLES = f"{MOCK_API}get_vehicles" +MOCK_API_VIN_TO_NAME = f"{MOCK_API}vin_to_name" +MOCK_API_GET_API_GEN = f"{MOCK_API}get_api_gen" +MOCK_API_GET_EV_STATUS = f"{MOCK_API}get_ev_status" +MOCK_API_GET_RES_STATUS = f"{MOCK_API}get_res_status" +MOCK_API_GET_REMOTE_STATUS = f"{MOCK_API}get_remote_status" +MOCK_API_GET_SAFETY_STATUS = f"{MOCK_API}get_safety_status" +MOCK_API_GET_GET_DATA = f"{MOCK_API}get_data" +MOCK_API_UPDATE = f"{MOCK_API}update" +MOCK_API_FETCH = f"{MOCK_API}fetch" + +TEST_USERNAME = "user@email.com" +TEST_PASSWORD = "password" +TEST_PIN = "1234" +TEST_DEVICE_ID = 1613183362 +TEST_COUNTRY = COUNTRY_USA + +TEST_CREDS = { + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_COUNTRY: TEST_COUNTRY, +} + +TEST_CONFIG = { + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_COUNTRY: TEST_COUNTRY, + CONF_PIN: TEST_PIN, + CONF_DEVICE_ID: TEST_DEVICE_ID, +} + +TEST_OPTIONS = { + CONF_UPDATE_ENABLED: True, +} + +TEST_ENTITY_ID = "sensor.test_vehicle_2_odometer" + + +async def setup_subaru_integration( + hass, + vehicle_list=None, + vehicle_data=None, + vehicle_status=None, + connect_effect=None, + fetch_effect=None, +): + """Create Subaru entry.""" + assert await async_setup_component(hass, HA_DOMAIN, {}) + assert await async_setup_component(hass, DOMAIN, {}) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=TEST_CONFIG, + options=TEST_OPTIONS, + entry_id=1, + ) + config_entry.add_to_hass(hass) + + with patch( + MOCK_API_CONNECT, + return_value=connect_effect is None, + side_effect=connect_effect, + ), patch(MOCK_API_GET_VEHICLES, return_value=vehicle_list,), patch( + MOCK_API_VIN_TO_NAME, + return_value=vehicle_data[VEHICLE_NAME], + ), patch( + MOCK_API_GET_API_GEN, + return_value=vehicle_data[VEHICLE_API_GEN], + ), patch( + MOCK_API_GET_EV_STATUS, + return_value=vehicle_data[VEHICLE_HAS_EV], + ), patch( + MOCK_API_GET_RES_STATUS, + return_value=vehicle_data[VEHICLE_HAS_REMOTE_START], + ), patch( + MOCK_API_GET_REMOTE_STATUS, + return_value=vehicle_data[VEHICLE_HAS_REMOTE_SERVICE], + ), patch( + MOCK_API_GET_SAFETY_STATUS, + return_value=vehicle_data[VEHICLE_HAS_SAFETY_SERVICE], + ), patch( + MOCK_API_GET_GET_DATA, + return_value=vehicle_status, + ), patch( + MOCK_API_UPDATE, + ), patch( + MOCK_API_FETCH, side_effect=fetch_effect + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry + + +@pytest.fixture +async def ev_entry(hass): + """Create a Subaru entry representing an EV vehicle with full STARLINK subscription.""" + entry = await setup_subaru_integration( + hass, + vehicle_list=[TEST_VIN_2_EV], + vehicle_data=VEHICLE_DATA[TEST_VIN_2_EV], + vehicle_status=VEHICLE_STATUS_EV, + ) + assert DOMAIN in hass.config_entries.async_domains() + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert hass.config_entries.async_get_entry(entry.entry_id) + assert entry.state == ENTRY_STATE_LOADED + return entry diff --git a/tests/components/subaru/test_config_flow.py b/tests/components/subaru/test_config_flow.py new file mode 100644 index 00000000000..676b876652b --- /dev/null +++ b/tests/components/subaru/test_config_flow.py @@ -0,0 +1,250 @@ +"""Tests for the Subaru component config flow.""" +# pylint: disable=redefined-outer-name +from copy import deepcopy +from unittest import mock +from unittest.mock import patch + +import pytest +from subarulink.exceptions import InvalidCredentials, InvalidPIN, SubaruException + +from homeassistant import config_entries +from homeassistant.components.subaru import config_flow +from homeassistant.components.subaru.const import CONF_UPDATE_ENABLED, DOMAIN +from homeassistant.const import CONF_DEVICE_ID, CONF_PIN + +from .conftest import ( + MOCK_API_CONNECT, + MOCK_API_IS_PIN_REQUIRED, + MOCK_API_TEST_PIN, + MOCK_API_UPDATE_SAVED_PIN, + TEST_CONFIG, + TEST_CREDS, + TEST_DEVICE_ID, + TEST_PIN, + TEST_USERNAME, +) + +from tests.common import MockConfigEntry + + +async def test_user_form_init(user_form): + """Test the initial user form for first step of the config flow.""" + expected = { + "data_schema": mock.ANY, + "description_placeholders": None, + "errors": None, + "flow_id": mock.ANY, + "handler": DOMAIN, + "step_id": "user", + "type": "form", + } + assert expected == user_form + + +async def test_user_form_repeat_identifier(hass, user_form): + """Test we handle repeat identifiers.""" + entry = MockConfigEntry( + domain=DOMAIN, title=TEST_USERNAME, data=TEST_CREDS, options=None + ) + entry.add_to_hass(hass) + + with patch( + MOCK_API_CONNECT, + return_value=True, + ) as mock_connect: + result = await hass.config_entries.flow.async_configure( + user_form["flow_id"], + TEST_CREDS, + ) + assert len(mock_connect.mock_calls) == 0 + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_user_form_cannot_connect(hass, user_form): + """Test we handle cannot connect error.""" + with patch( + MOCK_API_CONNECT, + side_effect=SubaruException(None), + ) as mock_connect: + result = await hass.config_entries.flow.async_configure( + user_form["flow_id"], + TEST_CREDS, + ) + assert len(mock_connect.mock_calls) == 1 + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" + + +async def test_user_form_invalid_auth(hass, user_form): + """Test we handle invalid auth.""" + with patch( + MOCK_API_CONNECT, + side_effect=InvalidCredentials("invalidAccount"), + ) as mock_connect: + result = await hass.config_entries.flow.async_configure( + user_form["flow_id"], + TEST_CREDS, + ) + assert len(mock_connect.mock_calls) == 1 + assert result["type"] == "form" + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_user_form_pin_not_required(hass, user_form): + """Test successful login when no PIN is required.""" + with patch(MOCK_API_CONNECT, return_value=True,) as mock_connect, patch( + MOCK_API_IS_PIN_REQUIRED, + return_value=False, + ) as mock_is_pin_required: + result = await hass.config_entries.flow.async_configure( + user_form["flow_id"], + TEST_CREDS, + ) + assert len(mock_connect.mock_calls) == 2 + assert len(mock_is_pin_required.mock_calls) == 1 + + expected = { + "title": TEST_USERNAME, + "description": None, + "description_placeholders": None, + "flow_id": mock.ANY, + "result": mock.ANY, + "handler": DOMAIN, + "type": "create_entry", + "version": 1, + "data": deepcopy(TEST_CONFIG), + } + expected["data"][CONF_PIN] = None + result["data"][CONF_DEVICE_ID] = TEST_DEVICE_ID + assert expected == result + + +async def test_pin_form_init(pin_form): + """Test the pin entry form for second step of the config flow.""" + expected = { + "data_schema": config_flow.PIN_SCHEMA, + "description_placeholders": None, + "errors": None, + "flow_id": mock.ANY, + "handler": DOMAIN, + "step_id": "pin", + "type": "form", + } + assert expected == pin_form + + +async def test_pin_form_bad_pin_format(hass, pin_form): + """Test we handle invalid pin.""" + with patch(MOCK_API_TEST_PIN,) as mock_test_pin, patch( + MOCK_API_UPDATE_SAVED_PIN, + return_value=True, + ) as mock_update_saved_pin: + result = await hass.config_entries.flow.async_configure( + pin_form["flow_id"], user_input={CONF_PIN: "abcd"} + ) + assert len(mock_test_pin.mock_calls) == 0 + assert len(mock_update_saved_pin.mock_calls) == 1 + assert result["type"] == "form" + assert result["errors"] == {"base": "bad_pin_format"} + + +async def test_pin_form_success(hass, pin_form): + """Test successful PIN entry.""" + with patch(MOCK_API_TEST_PIN, return_value=True,) as mock_test_pin, patch( + MOCK_API_UPDATE_SAVED_PIN, + return_value=True, + ) as mock_update_saved_pin: + result = await hass.config_entries.flow.async_configure( + pin_form["flow_id"], user_input={CONF_PIN: TEST_PIN} + ) + + assert len(mock_test_pin.mock_calls) == 1 + assert len(mock_update_saved_pin.mock_calls) == 1 + expected = { + "title": TEST_USERNAME, + "description": None, + "description_placeholders": None, + "flow_id": mock.ANY, + "result": mock.ANY, + "handler": DOMAIN, + "type": "create_entry", + "version": 1, + "data": TEST_CONFIG, + } + result["data"][CONF_DEVICE_ID] = TEST_DEVICE_ID + assert result == expected + + +async def test_pin_form_incorrect_pin(hass, pin_form): + """Test we handle invalid pin.""" + with patch( + MOCK_API_TEST_PIN, + side_effect=InvalidPIN("invalidPin"), + ) as mock_test_pin, patch( + MOCK_API_UPDATE_SAVED_PIN, + return_value=True, + ) as mock_update_saved_pin: + result = await hass.config_entries.flow.async_configure( + pin_form["flow_id"], user_input={CONF_PIN: TEST_PIN} + ) + assert len(mock_test_pin.mock_calls) == 1 + assert len(mock_update_saved_pin.mock_calls) == 1 + assert result["type"] == "form" + assert result["errors"] == {"base": "incorrect_pin"} + + +async def test_option_flow_form(options_form): + """Test config flow options form.""" + expected = { + "data_schema": mock.ANY, + "description_placeholders": None, + "errors": None, + "flow_id": mock.ANY, + "handler": mock.ANY, + "step_id": "init", + "type": "form", + } + assert expected == options_form + + +async def test_option_flow(hass, options_form): + """Test config flow options.""" + result = await hass.config_entries.options.async_configure( + options_form["flow_id"], + user_input={ + CONF_UPDATE_ENABLED: False, + }, + ) + assert result["type"] == "create_entry" + assert result["data"] == { + CONF_UPDATE_ENABLED: False, + } + + +@pytest.fixture +async def user_form(hass): + """Return initial form for Subaru config flow.""" + return await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + +@pytest.fixture +async def pin_form(hass, user_form): + """Return second form (PIN input) for Subaru config flow.""" + with patch(MOCK_API_CONNECT, return_value=True,), patch( + MOCK_API_IS_PIN_REQUIRED, + return_value=True, + ): + return await hass.config_entries.flow.async_configure( + user_form["flow_id"], user_input=TEST_CREDS + ) + + +@pytest.fixture +async def options_form(hass): + """Return options form for Subaru config flow.""" + entry = MockConfigEntry(domain=DOMAIN, data={}, options=None) + entry.add_to_hass(hass) + return await hass.config_entries.options.async_init(entry.entry_id) diff --git a/tests/components/subaru/test_init.py b/tests/components/subaru/test_init.py new file mode 100644 index 00000000000..13b510e8c40 --- /dev/null +++ b/tests/components/subaru/test_init.py @@ -0,0 +1,153 @@ +"""Test Subaru component setup and updates.""" +from unittest.mock import patch + +from subarulink import InvalidCredentials, SubaruException + +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.components.subaru.const import DOMAIN +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, + ENTRY_STATE_SETUP_ERROR, + ENTRY_STATE_SETUP_RETRY, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.setup import async_setup_component + +from .api_responses import ( + TEST_VIN_1_G1, + TEST_VIN_2_EV, + TEST_VIN_3_G2, + VEHICLE_DATA, + VEHICLE_STATUS_EV, + VEHICLE_STATUS_G2, +) +from .conftest import ( + MOCK_API_FETCH, + MOCK_API_UPDATE, + TEST_ENTITY_ID, + setup_subaru_integration, +) + + +async def test_setup_with_no_config(hass): + """Test DOMAIN is empty if there is no config.""" + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert DOMAIN not in hass.config_entries.async_domains() + + +async def test_setup_ev(hass, ev_entry): + """Test setup with an EV vehicle.""" + check_entry = hass.config_entries.async_get_entry(ev_entry.entry_id) + assert check_entry + assert check_entry.state == ENTRY_STATE_LOADED + + +async def test_setup_g2(hass): + """Test setup with a G2 vehcile .""" + entry = await setup_subaru_integration( + hass, + vehicle_list=[TEST_VIN_3_G2], + vehicle_data=VEHICLE_DATA[TEST_VIN_3_G2], + vehicle_status=VEHICLE_STATUS_G2, + ) + check_entry = hass.config_entries.async_get_entry(entry.entry_id) + assert check_entry + assert check_entry.state == ENTRY_STATE_LOADED + + +async def test_setup_g1(hass): + """Test setup with a G1 vehicle.""" + entry = await setup_subaru_integration( + hass, vehicle_list=[TEST_VIN_1_G1], vehicle_data=VEHICLE_DATA[TEST_VIN_1_G1] + ) + check_entry = hass.config_entries.async_get_entry(entry.entry_id) + assert check_entry + assert check_entry.state == ENTRY_STATE_LOADED + + +async def test_unsuccessful_connect(hass): + """Test unsuccessful connect due to connectivity.""" + entry = await setup_subaru_integration( + hass, + connect_effect=SubaruException("Service Unavailable"), + vehicle_list=[TEST_VIN_2_EV], + vehicle_data=VEHICLE_DATA[TEST_VIN_2_EV], + vehicle_status=VEHICLE_STATUS_EV, + ) + check_entry = hass.config_entries.async_get_entry(entry.entry_id) + assert check_entry + assert check_entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_invalid_credentials(hass): + """Test invalid credentials.""" + entry = await setup_subaru_integration( + hass, + connect_effect=InvalidCredentials("Invalid Credentials"), + vehicle_list=[TEST_VIN_2_EV], + vehicle_data=VEHICLE_DATA[TEST_VIN_2_EV], + vehicle_status=VEHICLE_STATUS_EV, + ) + check_entry = hass.config_entries.async_get_entry(entry.entry_id) + assert check_entry + assert check_entry.state == ENTRY_STATE_SETUP_ERROR + + +async def test_update_skip_unsubscribed(hass): + """Test update function skips vehicles without subscription.""" + await setup_subaru_integration( + hass, vehicle_list=[TEST_VIN_1_G1], vehicle_data=VEHICLE_DATA[TEST_VIN_1_G1] + ) + + with patch(MOCK_API_FETCH) as mock_fetch: + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, + blocking=True, + ) + + await hass.async_block_till_done() + mock_fetch.assert_not_called() + + +async def test_update_disabled(hass, ev_entry): + """Test update function disable option.""" + with patch(MOCK_API_FETCH, side_effect=SubaruException("403 Error"),), patch( + MOCK_API_UPDATE, + ) as mock_update: + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + mock_update.assert_not_called() + + +async def test_fetch_failed(hass): + """Tests when fetch fails.""" + await setup_subaru_integration( + hass, + vehicle_list=[TEST_VIN_2_EV], + vehicle_data=VEHICLE_DATA[TEST_VIN_2_EV], + vehicle_status=VEHICLE_STATUS_EV, + fetch_effect=SubaruException("403 Error"), + ) + + test_entity = hass.states.get(TEST_ENTITY_ID) + assert test_entity.state == "unavailable" + + +async def test_unload_entry(hass, ev_entry): + """Test that entry is unloaded.""" + assert ev_entry.state == ENTRY_STATE_LOADED + assert await hass.config_entries.async_unload(ev_entry.entry_id) + await hass.async_block_till_done() + assert ev_entry.state == ENTRY_STATE_NOT_LOADED diff --git a/tests/components/subaru/test_sensor.py b/tests/components/subaru/test_sensor.py new file mode 100644 index 00000000000..4344c147f22 --- /dev/null +++ b/tests/components/subaru/test_sensor.py @@ -0,0 +1,67 @@ +"""Test Subaru sensors.""" +from homeassistant.components.subaru.const import VEHICLE_NAME +from homeassistant.components.subaru.sensor import ( + API_GEN_2_SENSORS, + EV_SENSORS, + SAFETY_SENSORS, + SENSOR_FIELD, + SENSOR_TYPE, +) +from homeassistant.util import slugify +from homeassistant.util.unit_system import IMPERIAL_SYSTEM + +from .api_responses import ( + EXPECTED_STATE_EV_IMPERIAL, + EXPECTED_STATE_EV_METRIC, + EXPECTED_STATE_EV_UNAVAILABLE, + TEST_VIN_2_EV, + VEHICLE_DATA, + VEHICLE_STATUS_EV, +) + +from tests.components.subaru.conftest import setup_subaru_integration + +VEHICLE_NAME = VEHICLE_DATA[TEST_VIN_2_EV][VEHICLE_NAME] + + +async def test_sensors_ev_imperial(hass): + """Test sensors supporting imperial units.""" + hass.config.units = IMPERIAL_SYSTEM + await setup_subaru_integration( + hass, + vehicle_list=[TEST_VIN_2_EV], + vehicle_data=VEHICLE_DATA[TEST_VIN_2_EV], + vehicle_status=VEHICLE_STATUS_EV, + ) + _assert_data(hass, EXPECTED_STATE_EV_IMPERIAL) + + +async def test_sensors_ev_metric(hass, ev_entry): + """Test sensors supporting metric units.""" + _assert_data(hass, EXPECTED_STATE_EV_METRIC) + + +async def test_sensors_missing_vin_data(hass): + """Test for missing VIN dataset.""" + await setup_subaru_integration( + hass, + vehicle_list=[TEST_VIN_2_EV], + vehicle_data=VEHICLE_DATA[TEST_VIN_2_EV], + vehicle_status=None, + ) + _assert_data(hass, EXPECTED_STATE_EV_UNAVAILABLE) + + +def _assert_data(hass, expected_state): + sensor_list = EV_SENSORS + sensor_list.extend(API_GEN_2_SENSORS) + sensor_list.extend(SAFETY_SENSORS) + expected_states = {} + for item in sensor_list: + expected_states[ + f"sensor.{slugify(f'{VEHICLE_NAME} {item[SENSOR_TYPE]}')}" + ] = expected_state[item[SENSOR_FIELD]] + + for sensor in expected_states: + actual = hass.states.get(sensor) + assert actual.state == expected_states[sensor] From 2d70806035693c77e8340936defcd95d014ed918 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 21 Feb 2021 04:21:09 +0100 Subject: [PATCH 605/796] Add support for "alias" in script steps device, device_condition, and conditions (#46647) Co-authored-by: Donnie --- homeassistant/helpers/config_validation.py | 41 +++++++++---- homeassistant/helpers/script.py | 68 +++++++++------------- tests/helpers/test_condition.py | 36 ++++++++---- tests/helpers/test_config_validation.py | 5 ++ tests/helpers/test_script.py | 68 +++++++++++++++++----- 5 files changed, 146 insertions(+), 72 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 6f1c6f46599..6b0737ae346 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -888,9 +888,11 @@ def script_action(value: Any) -> dict: SCRIPT_SCHEMA = vol.All(ensure_list, [script_action]) +SCRIPT_ACTION_BASE_SCHEMA = {vol.Optional(CONF_ALIAS): string} + EVENT_SCHEMA = vol.Schema( { - vol.Optional(CONF_ALIAS): string, + **SCRIPT_ACTION_BASE_SCHEMA, vol.Required(CONF_EVENT): string, vol.Optional(CONF_EVENT_DATA): vol.All(dict, template_complex), vol.Optional(CONF_EVENT_DATA_TEMPLATE): vol.All(dict, template_complex), @@ -900,7 +902,7 @@ EVENT_SCHEMA = vol.Schema( SERVICE_SCHEMA = vol.All( vol.Schema( { - vol.Optional(CONF_ALIAS): string, + **SCRIPT_ACTION_BASE_SCHEMA, vol.Exclusive(CONF_SERVICE, "service name"): vol.Any( service, dynamic_template ), @@ -920,9 +922,12 @@ NUMERIC_STATE_THRESHOLD_SCHEMA = vol.Any( vol.Coerce(float), vol.All(str, entity_domain("input_number")) ) +CONDITION_BASE_SCHEMA = {vol.Optional(CONF_ALIAS): string} + NUMERIC_STATE_CONDITION_SCHEMA = vol.All( vol.Schema( { + **CONDITION_BASE_SCHEMA, vol.Required(CONF_CONDITION): "numeric_state", vol.Required(CONF_ENTITY_ID): entity_ids, vol.Optional(CONF_ATTRIBUTE): str, @@ -935,6 +940,7 @@ NUMERIC_STATE_CONDITION_SCHEMA = vol.All( ) STATE_CONDITION_BASE_SCHEMA = { + **CONDITION_BASE_SCHEMA, vol.Required(CONF_CONDITION): "state", vol.Required(CONF_ENTITY_ID): entity_ids, vol.Optional(CONF_ATTRIBUTE): str, @@ -975,6 +981,7 @@ def STATE_CONDITION_SCHEMA(value: Any) -> dict: # pylint: disable=invalid-name SUN_CONDITION_SCHEMA = vol.All( vol.Schema( { + **CONDITION_BASE_SCHEMA, vol.Required(CONF_CONDITION): "sun", vol.Optional("before"): sun_event, vol.Optional("before_offset"): time_period, @@ -989,6 +996,7 @@ SUN_CONDITION_SCHEMA = vol.All( TEMPLATE_CONDITION_SCHEMA = vol.Schema( { + **CONDITION_BASE_SCHEMA, vol.Required(CONF_CONDITION): "template", vol.Required(CONF_VALUE_TEMPLATE): template, } @@ -997,6 +1005,7 @@ TEMPLATE_CONDITION_SCHEMA = vol.Schema( TIME_CONDITION_SCHEMA = vol.All( vol.Schema( { + **CONDITION_BASE_SCHEMA, vol.Required(CONF_CONDITION): "time", "before": vol.Any(time, vol.All(str, entity_domain("input_datetime"))), "after": vol.Any(time, vol.All(str, entity_domain("input_datetime"))), @@ -1008,6 +1017,7 @@ TIME_CONDITION_SCHEMA = vol.All( ZONE_CONDITION_SCHEMA = vol.Schema( { + **CONDITION_BASE_SCHEMA, vol.Required(CONF_CONDITION): "zone", vol.Required(CONF_ENTITY_ID): entity_ids, "zone": entity_ids, @@ -1019,6 +1029,7 @@ ZONE_CONDITION_SCHEMA = vol.Schema( AND_CONDITION_SCHEMA = vol.Schema( { + **CONDITION_BASE_SCHEMA, vol.Required(CONF_CONDITION): "and", vol.Required(CONF_CONDITIONS): vol.All( ensure_list, @@ -1030,6 +1041,7 @@ AND_CONDITION_SCHEMA = vol.Schema( OR_CONDITION_SCHEMA = vol.Schema( { + **CONDITION_BASE_SCHEMA, vol.Required(CONF_CONDITION): "or", vol.Required(CONF_CONDITIONS): vol.All( ensure_list, @@ -1041,6 +1053,7 @@ OR_CONDITION_SCHEMA = vol.Schema( NOT_CONDITION_SCHEMA = vol.Schema( { + **CONDITION_BASE_SCHEMA, vol.Required(CONF_CONDITION): "not", vol.Required(CONF_CONDITIONS): vol.All( ensure_list, @@ -1052,6 +1065,7 @@ NOT_CONDITION_SCHEMA = vol.Schema( DEVICE_CONDITION_BASE_SCHEMA = vol.Schema( { + **CONDITION_BASE_SCHEMA, vol.Required(CONF_CONDITION): "device", vol.Required(CONF_DEVICE_ID): str, vol.Required(CONF_DOMAIN): str, @@ -1087,14 +1101,14 @@ TRIGGER_SCHEMA = vol.All( _SCRIPT_DELAY_SCHEMA = vol.Schema( { - vol.Optional(CONF_ALIAS): string, + **SCRIPT_ACTION_BASE_SCHEMA, vol.Required(CONF_DELAY): positive_time_period_template, } ) _SCRIPT_WAIT_TEMPLATE_SCHEMA = vol.Schema( { - vol.Optional(CONF_ALIAS): string, + **SCRIPT_ACTION_BASE_SCHEMA, vol.Required(CONF_WAIT_TEMPLATE): template, vol.Optional(CONF_TIMEOUT): positive_time_period_template, vol.Optional(CONF_CONTINUE_ON_TIMEOUT): boolean, @@ -1102,16 +1116,22 @@ _SCRIPT_WAIT_TEMPLATE_SCHEMA = vol.Schema( ) DEVICE_ACTION_BASE_SCHEMA = vol.Schema( - {vol.Required(CONF_DEVICE_ID): string, vol.Required(CONF_DOMAIN): str} + { + **SCRIPT_ACTION_BASE_SCHEMA, + vol.Required(CONF_DEVICE_ID): string, + vol.Required(CONF_DOMAIN): str, + } ) DEVICE_ACTION_SCHEMA = DEVICE_ACTION_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) -_SCRIPT_SCENE_SCHEMA = vol.Schema({vol.Required(CONF_SCENE): entity_domain("scene")}) +_SCRIPT_SCENE_SCHEMA = vol.Schema( + {**SCRIPT_ACTION_BASE_SCHEMA, vol.Required(CONF_SCENE): entity_domain("scene")} +) _SCRIPT_REPEAT_SCHEMA = vol.Schema( { - vol.Optional(CONF_ALIAS): string, + **SCRIPT_ACTION_BASE_SCHEMA, vol.Required(CONF_REPEAT): vol.All( { vol.Exclusive(CONF_COUNT, "repeat"): vol.Any(vol.Coerce(int), template), @@ -1130,11 +1150,12 @@ _SCRIPT_REPEAT_SCHEMA = vol.Schema( _SCRIPT_CHOOSE_SCHEMA = vol.Schema( { - vol.Optional(CONF_ALIAS): string, + **SCRIPT_ACTION_BASE_SCHEMA, vol.Required(CONF_CHOOSE): vol.All( ensure_list, [ { + vol.Optional(CONF_ALIAS): string, vol.Required(CONF_CONDITIONS): vol.All( ensure_list, [CONDITION_SCHEMA] ), @@ -1148,7 +1169,7 @@ _SCRIPT_CHOOSE_SCHEMA = vol.Schema( _SCRIPT_WAIT_FOR_TRIGGER_SCHEMA = vol.Schema( { - vol.Optional(CONF_ALIAS): string, + **SCRIPT_ACTION_BASE_SCHEMA, vol.Required(CONF_WAIT_FOR_TRIGGER): TRIGGER_SCHEMA, vol.Optional(CONF_TIMEOUT): positive_time_period_template, vol.Optional(CONF_CONTINUE_ON_TIMEOUT): boolean, @@ -1157,7 +1178,7 @@ _SCRIPT_WAIT_FOR_TRIGGER_SCHEMA = vol.Schema( _SCRIPT_SET_SCHEMA = vol.Schema( { - vol.Optional(CONF_ALIAS): string, + **SCRIPT_ACTION_BASE_SCHEMA, vol.Required(CONF_VARIABLES): SCRIPT_VARIABLES_SCHEMA, } ) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 69ba082e573..2e8348bcaf8 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -18,7 +18,7 @@ from typing import ( cast, ) -from async_timeout import timeout +import async_timeout import voluptuous as vol from homeassistant import exceptions @@ -235,6 +235,13 @@ class _ScriptRun: msg, *args, level=level, **kwargs ) + def _step_log(self, default_message, timeout=None): + self._script.last_action = self._action.get(CONF_ALIAS, default_message) + _timeout = ( + "" if timeout is None else f" (timeout: {timedelta(seconds=timeout)})" + ) + self._log("Executing step %s%s", self._script.last_action, _timeout) + async def async_run(self) -> None: """Run script.""" try: @@ -327,13 +334,12 @@ class _ScriptRun: """Handle delay.""" delay = self._get_pos_time_period_template(CONF_DELAY) - self._script.last_action = self._action.get(CONF_ALIAS, f"delay {delay}") - self._log("Executing step %s", self._script.last_action) + self._step_log(f"delay {delay}") delay = delay.total_seconds() self._changed() try: - async with timeout(delay): + async with async_timeout.timeout(delay): await self._stop.wait() except asyncio.TimeoutError: pass @@ -341,18 +347,13 @@ class _ScriptRun: async def _async_wait_template_step(self): """Handle a wait template.""" if CONF_TIMEOUT in self._action: - delay = self._get_pos_time_period_template(CONF_TIMEOUT).total_seconds() + timeout = self._get_pos_time_period_template(CONF_TIMEOUT).total_seconds() else: - delay = None + timeout = None - self._script.last_action = self._action.get(CONF_ALIAS, "wait template") - self._log( - "Executing step %s%s", - self._script.last_action, - "" if delay is None else f" (timeout: {timedelta(seconds=delay)})", - ) + self._step_log("wait template", timeout) - self._variables["wait"] = {"remaining": delay, "completed": False} + self._variables["wait"] = {"remaining": timeout, "completed": False} wait_template = self._action[CONF_WAIT_TEMPLATE] wait_template.hass = self._hass @@ -366,7 +367,7 @@ class _ScriptRun: def async_script_wait(entity_id, from_s, to_s): """Handle script after template condition is true.""" self._variables["wait"] = { - "remaining": to_context.remaining if to_context else delay, + "remaining": to_context.remaining if to_context else timeout, "completed": True, } done.set() @@ -382,7 +383,7 @@ class _ScriptRun: self._hass.async_create_task(flag.wait()) for flag in (self._stop, done) ] try: - async with timeout(delay) as to_context: + async with async_timeout.timeout(timeout) as to_context: await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) except asyncio.TimeoutError as ex: if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True): @@ -431,8 +432,7 @@ class _ScriptRun: async def _async_call_service_step(self): """Call the service specified in the action.""" - self._script.last_action = self._action.get(CONF_ALIAS, "call service") - self._log("Executing step %s", self._script.last_action) + self._step_log("call service") params = service.async_prepare_call_from_config( self._hass, self._action, self._variables @@ -467,8 +467,7 @@ class _ScriptRun: async def _async_device_step(self): """Perform the device automation specified in the action.""" - self._script.last_action = self._action.get(CONF_ALIAS, "device automation") - self._log("Executing step %s", self._script.last_action) + self._step_log("device automation") platform = await device_automation.async_get_device_automation_platform( self._hass, self._action[CONF_DOMAIN], "action" ) @@ -478,8 +477,7 @@ class _ScriptRun: async def _async_scene_step(self): """Activate the scene specified in the action.""" - self._script.last_action = self._action.get(CONF_ALIAS, "activate scene") - self._log("Executing step %s", self._script.last_action) + self._step_log("activate scene") await self._hass.services.async_call( scene.DOMAIN, SERVICE_TURN_ON, @@ -490,10 +488,7 @@ class _ScriptRun: async def _async_event_step(self): """Fire an event.""" - self._script.last_action = self._action.get( - CONF_ALIAS, self._action[CONF_EVENT] - ) - self._log("Executing step %s", self._script.last_action) + self._step_log(self._action.get(CONF_ALIAS, self._action[CONF_EVENT])) event_data = {} for conf in [CONF_EVENT_DATA, CONF_EVENT_DATA_TEMPLATE]: if conf not in self._action: @@ -627,25 +622,20 @@ class _ScriptRun: async def _async_wait_for_trigger_step(self): """Wait for a trigger event.""" if CONF_TIMEOUT in self._action: - delay = self._get_pos_time_period_template(CONF_TIMEOUT).total_seconds() + timeout = self._get_pos_time_period_template(CONF_TIMEOUT).total_seconds() else: - delay = None + timeout = None - self._script.last_action = self._action.get(CONF_ALIAS, "wait for trigger") - self._log( - "Executing step %s%s", - self._script.last_action, - "" if delay is None else f" (timeout: {timedelta(seconds=delay)})", - ) + self._step_log("wait for trigger", timeout) variables = {**self._variables} - self._variables["wait"] = {"remaining": delay, "trigger": None} + self._variables["wait"] = {"remaining": timeout, "trigger": None} done = asyncio.Event() async def async_done(variables, context=None): self._variables["wait"] = { - "remaining": to_context.remaining if to_context else delay, + "remaining": to_context.remaining if to_context else timeout, "trigger": variables["trigger"], } done.set() @@ -671,7 +661,7 @@ class _ScriptRun: self._hass.async_create_task(flag.wait()) for flag in (self._stop, done) ] try: - async with timeout(delay) as to_context: + async with async_timeout.timeout(timeout) as to_context: await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) except asyncio.TimeoutError as ex: if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True): @@ -685,8 +675,7 @@ class _ScriptRun: async def _async_variables_step(self): """Set a variable value.""" - self._script.last_action = self._action.get(CONF_ALIAS, "setting variables") - self._log("Executing step %s", self._script.last_action) + self._step_log("setting variables") self._variables = self._action[CONF_VARIABLES].async_render( self._hass, self._variables, render_as_defaults=False ) @@ -1111,10 +1100,11 @@ class Script: await self._async_get_condition(config) for config in choice.get(CONF_CONDITIONS, []) ] + choice_name = choice.get(CONF_ALIAS, f"choice {idx}") sub_script = Script( self._hass, choice[CONF_SEQUENCE], - f"{self.name}: {step_name}: choice {idx}", + f"{self.name}: {step_name}: {choice_name}", self.domain, running_description=self.running_description, script_mode=SCRIPT_MODE_PARALLEL, diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 3e7833b24dd..63ef9ba56d8 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -34,6 +34,7 @@ async def test_and_condition(hass): test = await condition.async_from_config( hass, { + "alias": "And Condition", "condition": "and", "conditions": [ { @@ -71,6 +72,7 @@ async def test_and_condition_with_template(hass): "condition": "and", "conditions": [ { + "alias": "Template Condition", "condition": "template", "value_template": '{{ states.sensor.temperature.state == "100" }}', }, @@ -98,6 +100,7 @@ async def test_or_condition(hass): test = await condition.async_from_config( hass, { + "alias": "Or Condition", "condition": "or", "conditions": [ { @@ -159,6 +162,7 @@ async def test_not_condition(hass): test = await condition.async_from_config( hass, { + "alias": "Not Condition", "condition": "not", "conditions": [ { @@ -226,36 +230,45 @@ async def test_not_condition_with_template(hass): async def test_time_window(hass): """Test time condition windows.""" - sixam = dt.parse_time("06:00:00") - sixpm = dt.parse_time("18:00:00") + sixam = "06:00:00" + sixpm = "18:00:00" + + test1 = await condition.async_from_config( + hass, + {"alias": "Time Cond", "condition": "time", "after": sixam, "before": sixpm}, + ) + test2 = await condition.async_from_config( + hass, + {"alias": "Time Cond", "condition": "time", "after": sixpm, "before": sixam}, + ) with patch( "homeassistant.helpers.condition.dt_util.now", return_value=dt.now().replace(hour=3), ): - assert not condition.time(hass, after=sixam, before=sixpm) - assert condition.time(hass, after=sixpm, before=sixam) + assert not test1(hass) + assert test2(hass) with patch( "homeassistant.helpers.condition.dt_util.now", return_value=dt.now().replace(hour=9), ): - assert condition.time(hass, after=sixam, before=sixpm) - assert not condition.time(hass, after=sixpm, before=sixam) + assert test1(hass) + assert not test2(hass) with patch( "homeassistant.helpers.condition.dt_util.now", return_value=dt.now().replace(hour=15), ): - assert condition.time(hass, after=sixam, before=sixpm) - assert not condition.time(hass, after=sixpm, before=sixam) + assert test1(hass) + assert not test2(hass) with patch( "homeassistant.helpers.condition.dt_util.now", return_value=dt.now().replace(hour=21), ): - assert not condition.time(hass, after=sixam, before=sixpm) - assert condition.time(hass, after=sixpm, before=sixam) + assert not test1(hass) + assert test2(hass) async def test_time_using_input_datetime(hass): @@ -439,6 +452,7 @@ async def test_multiple_states(hass): "condition": "and", "conditions": [ { + "alias": "State Condition", "condition": "state", "entity_id": "sensor.temperature", "state": ["100", "200"], @@ -709,6 +723,7 @@ async def test_numeric_state_multiple_entities(hass): "condition": "and", "conditions": [ { + "alias": "Numeric State Condition", "condition": "numeric_state", "entity_id": ["sensor.temperature_1", "sensor.temperature_2"], "below": 50, @@ -911,6 +926,7 @@ async def test_zone_multiple_entities(hass): "condition": "and", "conditions": [ { + "alias": "Zone Condition", "condition": "zone", "entity_id": ["device_tracker.person_1", "device_tracker.person_2"], "zone": "zone.home", diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 1397e499c7e..d0ae86f8f7e 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -358,6 +358,11 @@ def test_service_schema(): "service": "homeassistant.turn_on", "entity_id": ["light.kitchen", "light.ceiling"], }, + { + "service": "light.turn_on", + "entity_id": "all", + "alias": "turn on kitchen lights", + }, ) for value in options: cv.SERVICE_SCHEMA(value) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index a22cf27acdc..d2946fcd494 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -50,7 +50,10 @@ async def test_firing_event_basic(hass, caplog): context = Context() events = async_capture_events(hass, event) - sequence = cv.SCRIPT_SCHEMA({"event": event, "event_data": {"hello": "world"}}) + alias = "event step" + sequence = cv.SCRIPT_SCHEMA( + {"alias": alias, "event": event, "event_data": {"hello": "world"}} + ) script_obj = script.Script( hass, sequence, "Test Name", "test_domain", running_description="test script" ) @@ -63,6 +66,7 @@ async def test_firing_event_basic(hass, caplog): assert events[0].data.get("hello") == "world" assert ".test_name:" in caplog.text assert "Test Name: Running test script" in caplog.text + assert f"Executing step {alias}" in caplog.text async def test_firing_event_template(hass): @@ -107,12 +111,15 @@ async def test_firing_event_template(hass): } -async def test_calling_service_basic(hass): +async def test_calling_service_basic(hass, caplog): """Test the calling of a service.""" context = Context() calls = async_mock_service(hass, "test", "script") - sequence = cv.SCRIPT_SCHEMA({"service": "test.script", "data": {"hello": "world"}}) + alias = "service step" + sequence = cv.SCRIPT_SCHEMA( + {"alias": alias, "service": "test.script", "data": {"hello": "world"}} + ) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") await script_obj.async_run(context=context) @@ -121,6 +128,7 @@ async def test_calling_service_basic(hass): assert len(calls) == 1 assert calls[0].context is context assert calls[0].data.get("hello") == "world" + assert f"Executing step {alias}" in caplog.text async def test_calling_service_template(hass): @@ -250,12 +258,13 @@ async def test_multiple_runs_no_wait(hass): assert len(calls) == 4 -async def test_activating_scene(hass): +async def test_activating_scene(hass, caplog): """Test the activation of a scene.""" context = Context() calls = async_mock_service(hass, scene.DOMAIN, SERVICE_TURN_ON) - sequence = cv.SCRIPT_SCHEMA({"scene": "scene.hello"}) + alias = "scene step" + sequence = cv.SCRIPT_SCHEMA({"alias": alias, "scene": "scene.hello"}) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") await script_obj.async_run(context=context) @@ -264,6 +273,7 @@ async def test_activating_scene(hass): assert len(calls) == 1 assert calls[0].context is context assert calls[0].data.get(ATTR_ENTITY_ID) == "scene.hello" + assert f"Executing step {alias}" in caplog.text @pytest.mark.parametrize("count", [1, 3]) @@ -1063,14 +1073,16 @@ async def test_condition_warning(hass, caplog): assert len(events) == 1 -async def test_condition_basic(hass): +async def test_condition_basic(hass, caplog): """Test if we can use conditions in a script.""" event = "test_event" events = async_capture_events(hass, event) + alias = "condition step" sequence = cv.SCRIPT_SCHEMA( [ {"event": event}, { + "alias": alias, "condition": "template", "value_template": "{{ states.test.entity.state == 'hello' }}", }, @@ -1083,6 +1095,8 @@ async def test_condition_basic(hass): await script_obj.async_run(context=Context()) await hass.async_block_till_done() + assert f"Test condition {alias}: True" in caplog.text + caplog.clear() assert len(events) == 2 hass.states.async_set("test.entity", "goodbye") @@ -1090,6 +1104,7 @@ async def test_condition_basic(hass): await script_obj.async_run(context=Context()) await hass.async_block_till_done() + assert f"Test condition {alias}: False" in caplog.text assert len(events) == 3 @@ -1140,14 +1155,16 @@ async def test_condition_all_cached(hass): assert len(script_obj._config_cache) == 2 -async def test_repeat_count(hass): +async def test_repeat_count(hass, caplog): """Test repeat action w/ count option.""" event = "test_event" events = async_capture_events(hass, event) count = 3 + alias = "condition step" sequence = cv.SCRIPT_SCHEMA( { + "alias": alias, "repeat": { "count": count, "sequence": { @@ -1158,7 +1175,7 @@ async def test_repeat_count(hass): "last": "{{ repeat.last }}", }, }, - } + }, } ) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") @@ -1171,6 +1188,7 @@ async def test_repeat_count(hass): assert event.data.get("first") == (index == 0) assert event.data.get("index") == index + 1 assert event.data.get("last") == (index == count - 1) + assert caplog.text.count(f"Repeating {alias}") == count @pytest.mark.parametrize("condition", ["while", "until"]) @@ -1470,26 +1488,44 @@ async def test_choose_warning(hass, caplog): @pytest.mark.parametrize("var,result", [(1, "first"), (2, "second"), (3, "default")]) -async def test_choose(hass, var, result): +async def test_choose(hass, caplog, var, result): """Test choose action.""" event = "test_event" events = async_capture_events(hass, event) + alias = "choose step" + choice = {1: "choice one", 2: "choice two", 3: None} + aliases = {1: "sequence one", 2: "sequence two", 3: "default sequence"} sequence = cv.SCRIPT_SCHEMA( { + "alias": alias, "choose": [ { + "alias": choice[1], "conditions": { "condition": "template", "value_template": "{{ var == 1 }}", }, - "sequence": {"event": event, "event_data": {"choice": "first"}}, + "sequence": { + "alias": aliases[1], + "event": event, + "event_data": {"choice": "first"}, + }, }, { + "alias": choice[2], "conditions": "{{ var == 2 }}", - "sequence": {"event": event, "event_data": {"choice": "second"}}, + "sequence": { + "alias": aliases[2], + "event": event, + "event_data": {"choice": "second"}, + }, }, ], - "default": {"event": event, "event_data": {"choice": "default"}}, + "default": { + "alias": aliases[3], + "event": event, + "event_data": {"choice": "default"}, + }, } ) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") @@ -1499,6 +1535,10 @@ async def test_choose(hass, var, result): assert len(events) == 1 assert events[0].data["choice"] == result + expected_choice = choice[var] + if var == 3: + expected_choice = "default" + assert f"{alias}: {expected_choice}: Executing step {aliases[var]}" in caplog.text @pytest.mark.parametrize( @@ -2115,9 +2155,10 @@ async def test_started_action(hass, caplog): async def test_set_variable(hass, caplog): """Test setting variables in scripts.""" + alias = "variables step" sequence = cv.SCRIPT_SCHEMA( [ - {"variables": {"variable": "value"}}, + {"alias": alias, "variables": {"variable": "value"}}, {"service": "test.script", "data": {"value": "{{ variable }}"}}, ] ) @@ -2129,6 +2170,7 @@ async def test_set_variable(hass, caplog): await hass.async_block_till_done() assert mock_calls[0].data["value"] == "value" + assert f"Executing step {alias}" in caplog.text async def test_set_redefines_variable(hass, caplog): From 5e26bda52d4b4e61a0e7df415a8b0b5bd90343cf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 21 Feb 2021 04:21:39 +0100 Subject: [PATCH 606/796] Add support for disabling config entries (#46779) --- .../components/config/config_entries.py | 89 +++++++----- homeassistant/config_entries.py | 46 +++++- homeassistant/const.py | 1 + homeassistant/helpers/device_registry.py | 43 +++++- homeassistant/helpers/entity_registry.py | 44 ++++++ tests/common.py | 2 + .../components/config/test_config_entries.py | 133 ++++++++++++++++++ tests/helpers/test_device_registry.py | 38 +++++ tests/helpers/test_entity_registry.py | 80 +++++++++++ tests/test_config_entries.py | 104 ++++++++++++++ 10 files changed, 542 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index b8d9944d7af..b45b6abe468 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -1,7 +1,6 @@ """Http views to control the config manager.""" import aiohttp.web_exceptions import voluptuous as vol -import voluptuous_serialize from homeassistant import config_entries, data_entry_flow from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT @@ -10,7 +9,6 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.const import HTTP_FORBIDDEN, HTTP_NOT_FOUND from homeassistant.core import callback from homeassistant.exceptions import Unauthorized -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.data_entry_flow import ( FlowManagerIndexView, FlowManagerResourceView, @@ -30,6 +28,7 @@ async def async_setup(hass): hass.http.register_view(OptionManagerFlowIndexView(hass.config_entries.options)) hass.http.register_view(OptionManagerFlowResourceView(hass.config_entries.options)) + hass.components.websocket_api.async_register_command(config_entry_disable) hass.components.websocket_api.async_register_command(config_entry_update) hass.components.websocket_api.async_register_command(config_entries_progress) hass.components.websocket_api.async_register_command(system_options_list) @@ -39,24 +38,6 @@ async def async_setup(hass): return True -def _prepare_json(result): - """Convert result for JSON.""" - if result["type"] != data_entry_flow.RESULT_TYPE_FORM: - return result - - data = result.copy() - - schema = data["data_schema"] - if schema is None: - data["data_schema"] = [] - else: - data["data_schema"] = voluptuous_serialize.convert( - schema, custom_serializer=cv.custom_serializer - ) - - return data - - class ConfigManagerEntryIndexView(HomeAssistantView): """View to get available config entries.""" @@ -265,6 +246,21 @@ async def system_options_list(hass, connection, msg): connection.send_result(msg["id"], entry.system_options.as_dict()) +def send_entry_not_found(connection, msg_id): + """Send Config entry not found error.""" + connection.send_error( + msg_id, websocket_api.const.ERR_NOT_FOUND, "Config entry not found" + ) + + +def get_entry(hass, connection, entry_id, msg_id): + """Get entry, send error message if it doesn't exist.""" + entry = hass.config_entries.async_get_entry(entry_id) + if entry is None: + send_entry_not_found(connection, msg_id) + return entry + + @websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( @@ -279,13 +275,10 @@ async def system_options_update(hass, connection, msg): changes = dict(msg) changes.pop("id") changes.pop("type") - entry_id = changes.pop("entry_id") - entry = hass.config_entries.async_get_entry(entry_id) + changes.pop("entry_id") + entry = get_entry(hass, connection, msg["entry_id"], msg["id"]) if entry is None: - connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_FOUND, "Config entry not found" - ) return hass.config_entries.async_update_entry(entry, system_options=changes) @@ -302,20 +295,47 @@ async def config_entry_update(hass, connection, msg): changes = dict(msg) changes.pop("id") changes.pop("type") - entry_id = changes.pop("entry_id") - - entry = hass.config_entries.async_get_entry(entry_id) + changes.pop("entry_id") + entry = get_entry(hass, connection, msg["entry_id"], msg["id"]) if entry is None: - connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_FOUND, "Config entry not found" - ) return hass.config_entries.async_update_entry(entry, **changes) connection.send_result(msg["id"], entry_json(entry)) +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + "type": "config_entries/disable", + "entry_id": str, + # We only allow setting disabled_by user via API. + "disabled_by": vol.Any("user", None), + } +) +async def config_entry_disable(hass, connection, msg): + """Disable config entry.""" + disabled_by = msg["disabled_by"] + + result = False + try: + result = await hass.config_entries.async_set_disabled_by( + msg["entry_id"], disabled_by + ) + except config_entries.OperationNotAllowed: + # Failed to unload the config entry + pass + except config_entries.UnknownEntry: + send_entry_not_found(connection, msg["id"]) + return + + result = {"require_restart": not result} + + connection.send_result(msg["id"], result) + + @websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( @@ -333,9 +353,7 @@ async def ignore_config_flow(hass, connection, msg): ) if flow is None: - connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_FOUND, "Config entry not found" - ) + send_entry_not_found(connection, msg["id"]) return if "unique_id" not in flow["context"]: @@ -357,7 +375,7 @@ def entry_json(entry: config_entries.ConfigEntry) -> dict: """Return JSON value of a config entry.""" handler = config_entries.HANDLERS.get(entry.domain) supports_options = ( - # Guard in case handler is no longer registered (custom compnoent etc) + # Guard in case handler is no longer registered (custom component etc) handler is not None # pylint: disable=comparison-with-callable and handler.async_get_options_flow @@ -372,4 +390,5 @@ def entry_json(entry: config_entries.ConfigEntry) -> dict: "connection_class": entry.connection_class, "supports_options": supports_options, "supports_unload": entry.supports_unload, + "disabled_by": entry.disabled_by, } diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 7225b7c375d..dbc0dd01454 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -11,6 +11,7 @@ import weakref import attr from homeassistant import data_entry_flow, loader +from homeassistant.const import EVENT_CONFIG_ENTRY_DISABLED_BY_UPDATED from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import entity_registry @@ -68,6 +69,8 @@ ENTRY_STATE_SETUP_RETRY = "setup_retry" ENTRY_STATE_NOT_LOADED = "not_loaded" # An error occurred when trying to unload the entry ENTRY_STATE_FAILED_UNLOAD = "failed_unload" +# The config entry is disabled +ENTRY_STATE_DISABLED = "disabled" UNRECOVERABLE_STATES = (ENTRY_STATE_MIGRATION_ERROR, ENTRY_STATE_FAILED_UNLOAD) @@ -92,6 +95,8 @@ CONN_CLASS_LOCAL_POLL = "local_poll" CONN_CLASS_ASSUMED = "assumed" CONN_CLASS_UNKNOWN = "unknown" +DISABLED_USER = "user" + RELOAD_AFTER_UPDATE_DELAY = 30 @@ -126,6 +131,7 @@ class ConfigEntry: "source", "connection_class", "state", + "disabled_by", "_setup_lock", "update_listeners", "_async_cancel_retry_setup", @@ -144,6 +150,7 @@ class ConfigEntry: unique_id: Optional[str] = None, entry_id: Optional[str] = None, state: str = ENTRY_STATE_NOT_LOADED, + disabled_by: Optional[str] = None, ) -> None: """Initialize a config entry.""" # Unique id of the config entry @@ -179,6 +186,9 @@ class ConfigEntry: # Unique ID of this entry. self.unique_id = unique_id + # Config entry is disabled + self.disabled_by = disabled_by + # Supports unload self.supports_unload = False @@ -198,7 +208,7 @@ class ConfigEntry: tries: int = 0, ) -> None: """Set up an entry.""" - if self.source == SOURCE_IGNORE: + if self.source == SOURCE_IGNORE or self.disabled_by: return if integration is None: @@ -441,6 +451,7 @@ class ConfigEntry: "source": self.source, "connection_class": self.connection_class, "unique_id": self.unique_id, + "disabled_by": self.disabled_by, } @@ -711,6 +722,8 @@ class ConfigEntries: system_options=entry.get("system_options", {}), # New in 0.104 unique_id=entry.get("unique_id"), + # New in 2021.3 + disabled_by=entry.get("disabled_by"), ) for entry in config["entries"] ] @@ -759,13 +772,42 @@ class ConfigEntries: If an entry was not loaded, will just load. """ + entry = self.async_get_entry(entry_id) + + if entry is None: + raise UnknownEntry + unload_result = await self.async_unload(entry_id) - if not unload_result: + if not unload_result or entry.disabled_by: return unload_result return await self.async_setup(entry_id) + async def async_set_disabled_by( + self, entry_id: str, disabled_by: Optional[str] + ) -> bool: + """Disable an entry. + + If disabled_by is changed, the config entry will be reloaded. + """ + entry = self.async_get_entry(entry_id) + + if entry is None: + raise UnknownEntry + + if entry.disabled_by == disabled_by: + return True + + entry.disabled_by = disabled_by + self._async_schedule_save() + + self.hass.bus.async_fire( + EVENT_CONFIG_ENTRY_DISABLED_BY_UPDATED, {"config_entry_id": entry_id} + ) + + return await self.async_reload(entry_id) + @callback def async_update_entry( self, diff --git a/homeassistant/const.py b/homeassistant/const.py index 4406c8bdfc3..a0aafaad3ce 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -202,6 +202,7 @@ CONF_ZONE = "zone" # #### EVENTS #### EVENT_CALL_SERVICE = "call_service" EVENT_COMPONENT_LOADED = "component_loaded" +EVENT_CONFIG_ENTRY_DISABLED_BY_UPDATED = "config_entry_disabled_by_updated" EVENT_CORE_CONFIG_UPDATE = "core_config_updated" EVENT_HOMEASSISTANT_CLOSE = "homeassistant_close" EVENT_HOMEASSISTANT_START = "homeassistant_start" diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 3dd44364604..705f6cdd89a 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -6,7 +6,10 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union, import attr -from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.const import ( + EVENT_CONFIG_ENTRY_DISABLED_BY_UPDATED, + EVENT_HOMEASSISTANT_STARTED, +) from homeassistant.core import Event, callback from homeassistant.loader import bind_hass import homeassistant.util.uuid as uuid_util @@ -37,6 +40,7 @@ IDX_IDENTIFIERS = "identifiers" REGISTERED_DEVICE = "registered" DELETED_DEVICE = "deleted" +DISABLED_CONFIG_ENTRY = "config_entry" DISABLED_INTEGRATION = "integration" DISABLED_USER = "user" @@ -65,6 +69,7 @@ class DeviceEntry: default=None, validator=attr.validators.in_( ( + DISABLED_CONFIG_ENTRY, DISABLED_INTEGRATION, DISABLED_USER, None, @@ -138,6 +143,10 @@ class DeviceRegistry: self.hass = hass self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) self._clear_index() + self.hass.bus.async_listen( + EVENT_CONFIG_ENTRY_DISABLED_BY_UPDATED, + self.async_config_entry_disabled_by_changed, + ) @callback def async_get(self, device_id: str) -> Optional[DeviceEntry]: @@ -609,6 +618,38 @@ class DeviceRegistry: if area_id == device.area_id: self._async_update_device(dev_id, area_id=None) + @callback + def async_config_entry_disabled_by_changed(self, event: Event) -> None: + """Handle a config entry being disabled or enabled. + + Disable devices in the registry that are associated to a config entry when + the config entry is disabled. + """ + config_entry = self.hass.config_entries.async_get_entry( + event.data["config_entry_id"] + ) + + # The config entry may be deleted already if the event handling is late + if not config_entry: + return + + if not config_entry.disabled_by: + devices = async_entries_for_config_entry( + self, event.data["config_entry_id"] + ) + for device in devices: + if device.disabled_by != DISABLED_CONFIG_ENTRY: + continue + self.async_update_device(device.id, disabled_by=None) + return + + devices = async_entries_for_config_entry(self, event.data["config_entry_id"]) + for device in devices: + if device.disabled: + # Entity already disabled, do not overwrite + continue + self.async_update_device(device.id, disabled_by=DISABLED_CONFIG_ENTRY) + @callback def async_get(hass: HomeAssistantType) -> DeviceRegistry: diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 0938ea9165f..c86bd64d73e 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -31,6 +31,7 @@ from homeassistant.const import ( ATTR_RESTORED, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, + EVENT_CONFIG_ENTRY_DISABLED_BY_UPDATED, EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE, ) @@ -157,6 +158,10 @@ class EntityRegistry: self.hass.bus.async_listen( EVENT_DEVICE_REGISTRY_UPDATED, self.async_device_modified ) + self.hass.bus.async_listen( + EVENT_CONFIG_ENTRY_DISABLED_BY_UPDATED, + self.async_config_entry_disabled_by_changed, + ) @callback def async_get_device_class_lookup(self, domain_device_classes: set) -> dict: @@ -349,10 +354,49 @@ class EntityRegistry: self.async_update_entity(entity.entity_id, disabled_by=None) return + if device.disabled_by == dr.DISABLED_CONFIG_ENTRY: + # Handled by async_config_entry_disabled + return + + # Fetch entities which are not already disabled entities = async_entries_for_device(self, event.data["device_id"]) for entity in entities: self.async_update_entity(entity.entity_id, disabled_by=DISABLED_DEVICE) + @callback + def async_config_entry_disabled_by_changed(self, event: Event) -> None: + """Handle a config entry being disabled or enabled. + + Disable entities in the registry that are associated to a config entry when + the config entry is disabled. + """ + config_entry = self.hass.config_entries.async_get_entry( + event.data["config_entry_id"] + ) + + # The config entry may be deleted already if the event handling is late + if not config_entry: + return + + if not config_entry.disabled_by: + entities = async_entries_for_config_entry( + self, event.data["config_entry_id"] + ) + for entity in entities: + if entity.disabled_by != DISABLED_CONFIG_ENTRY: + continue + self.async_update_entity(entity.entity_id, disabled_by=None) + return + + entities = async_entries_for_config_entry(self, event.data["config_entry_id"]) + for entity in entities: + if entity.disabled: + # Entity already disabled, do not overwrite + continue + self.async_update_entity( + entity.entity_id, disabled_by=DISABLED_CONFIG_ENTRY + ) + @callback def async_update_entity( self, diff --git a/tests/common.py b/tests/common.py index c07716dbfc9..52d368853b3 100644 --- a/tests/common.py +++ b/tests/common.py @@ -749,6 +749,7 @@ class MockConfigEntry(config_entries.ConfigEntry): system_options={}, connection_class=config_entries.CONN_CLASS_UNKNOWN, unique_id=None, + disabled_by=None, ): """Initialize a mock config entry.""" kwargs = { @@ -761,6 +762,7 @@ class MockConfigEntry(config_entries.ConfigEntry): "title": title, "connection_class": connection_class, "unique_id": unique_id, + "disabled_by": disabled_by, } if source is not None: kwargs["source"] = source diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 87b1559a21b..6bb1f1885eb 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -68,6 +68,12 @@ async def test_get_entries(hass, client): state=core_ce.ENTRY_STATE_LOADED, connection_class=core_ce.CONN_CLASS_ASSUMED, ).add_to_hass(hass) + MockConfigEntry( + domain="comp3", + title="Test 3", + source="bla3", + disabled_by="user", + ).add_to_hass(hass) resp = await client.get("/api/config/config_entries/entry") assert resp.status == 200 @@ -83,6 +89,7 @@ async def test_get_entries(hass, client): "connection_class": "local_poll", "supports_options": True, "supports_unload": True, + "disabled_by": None, }, { "domain": "comp2", @@ -92,6 +99,17 @@ async def test_get_entries(hass, client): "connection_class": "assumed", "supports_options": False, "supports_unload": False, + "disabled_by": None, + }, + { + "domain": "comp3", + "title": "Test 3", + "source": "bla3", + "state": "not_loaded", + "connection_class": "unknown", + "supports_options": False, + "supports_unload": False, + "disabled_by": "user", }, ] @@ -680,6 +698,25 @@ async def test_update_system_options(hass, hass_ws_client): assert entry.system_options.disable_new_entities +async def test_update_system_options_nonexisting(hass, hass_ws_client): + """Test that we can update entry.""" + assert await async_setup_component(hass, "config", {}) + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + "id": 5, + "type": "config_entries/system_options/update", + "entry_id": "non_existing", + "disable_new_entities": True, + } + ) + response = await ws_client.receive_json() + + assert not response["success"] + assert response["error"]["code"] == "not_found" + + async def test_update_entry(hass, hass_ws_client): """Test that we can update entry.""" assert await async_setup_component(hass, "config", {}) @@ -722,6 +759,83 @@ async def test_update_entry_nonexisting(hass, hass_ws_client): assert response["error"]["code"] == "not_found" +async def test_disable_entry(hass, hass_ws_client): + """Test that we can disable entry.""" + assert await async_setup_component(hass, "config", {}) + ws_client = await hass_ws_client(hass) + + entry = MockConfigEntry(domain="demo", state="loaded") + entry.add_to_hass(hass) + assert entry.disabled_by is None + + # Disable + await ws_client.send_json( + { + "id": 5, + "type": "config_entries/disable", + "entry_id": entry.entry_id, + "disabled_by": "user", + } + ) + response = await ws_client.receive_json() + + assert response["success"] + assert response["result"] == {"require_restart": True} + assert entry.disabled_by == "user" + assert entry.state == "failed_unload" + + # Enable + await ws_client.send_json( + { + "id": 6, + "type": "config_entries/disable", + "entry_id": entry.entry_id, + "disabled_by": None, + } + ) + response = await ws_client.receive_json() + + assert response["success"] + assert response["result"] == {"require_restart": True} + assert entry.disabled_by is None + assert entry.state == "failed_unload" + + # Enable again -> no op + await ws_client.send_json( + { + "id": 7, + "type": "config_entries/disable", + "entry_id": entry.entry_id, + "disabled_by": None, + } + ) + response = await ws_client.receive_json() + + assert response["success"] + assert response["result"] == {"require_restart": False} + assert entry.disabled_by is None + assert entry.state == "failed_unload" + + +async def test_disable_entry_nonexisting(hass, hass_ws_client): + """Test that we can disable entry.""" + assert await async_setup_component(hass, "config", {}) + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + "id": 5, + "type": "config_entries/disable", + "entry_id": "non_existing", + "disabled_by": "user", + } + ) + response = await ws_client.receive_json() + + assert not response["success"] + assert response["error"]["code"] == "not_found" + + async def test_ignore_flow(hass, hass_ws_client): """Test we can ignore a flow.""" assert await async_setup_component(hass, "config", {}) @@ -763,3 +877,22 @@ async def test_ignore_flow(hass, hass_ws_client): assert entry.source == "ignore" assert entry.unique_id == "mock-unique-id" assert entry.title == "Test Integration" + + +async def test_ignore_flow_nonexisting(hass, hass_ws_client): + """Test we can ignore a flow.""" + assert await async_setup_component(hass, "config", {}) + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + "id": 5, + "type": "config_entries/ignore_flow", + "flow_id": "non_existing", + "title": "Test Integration", + } + ) + response = await ws_client.receive_json() + + assert not response["success"] + assert response["error"]["code"] == "not_found" diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index bc0e1c3bec9..965ebcd3e23 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -1209,3 +1209,41 @@ async def test_verify_suggested_area_does_not_overwrite_area_id( suggested_area="New Game Room", ) assert entry2.area_id == game_room_area.id + + +async def test_disable_config_entry_disables_devices(hass, registry): + """Test that we disable entities tied to a config entry.""" + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + + entry1 = registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={("mac", "12:34:56:AB:CD:EF")}, + ) + entry2 = registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={("mac", "34:56:AB:CD:EF:12")}, + disabled_by="user", + ) + + assert not entry1.disabled + assert entry2.disabled + + await hass.config_entries.async_set_disabled_by(config_entry.entry_id, "user") + await hass.async_block_till_done() + + entry1 = registry.async_get(entry1.id) + assert entry1.disabled + assert entry1.disabled_by == "config_entry" + entry2 = registry.async_get(entry2.id) + assert entry2.disabled + assert entry2.disabled_by == "user" + + await hass.config_entries.async_set_disabled_by(config_entry.entry_id, None) + await hass.async_block_till_done() + + entry1 = registry.async_get(entry1.id) + assert not entry1.disabled + entry2 = registry.async_get(entry2.id) + assert entry2.disabled + assert entry2.disabled_by == "user" diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 71cfb331591..86cdab82238 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -757,9 +757,18 @@ async def test_disable_device_disables_entities(hass, registry): device_id=device_entry.id, disabled_by="user", ) + entry3 = registry.async_get_or_create( + "light", + "hue", + "EFGH", + config_entry=config_entry, + device_id=device_entry.id, + disabled_by="config_entry", + ) assert not entry1.disabled assert entry2.disabled + assert entry3.disabled device_registry.async_update_device(device_entry.id, disabled_by="user") await hass.async_block_till_done() @@ -770,6 +779,9 @@ async def test_disable_device_disables_entities(hass, registry): entry2 = registry.async_get(entry2.entity_id) assert entry2.disabled assert entry2.disabled_by == "user" + entry3 = registry.async_get(entry3.entity_id) + assert entry3.disabled + assert entry3.disabled_by == "config_entry" device_registry.async_update_device(device_entry.id, disabled_by=None) await hass.async_block_till_done() @@ -779,6 +791,74 @@ async def test_disable_device_disables_entities(hass, registry): entry2 = registry.async_get(entry2.entity_id) assert entry2.disabled assert entry2.disabled_by == "user" + entry3 = registry.async_get(entry3.entity_id) + assert entry3.disabled + assert entry3.disabled_by == "config_entry" + + +async def test_disable_config_entry_disables_entities(hass, registry): + """Test that we disable entities tied to a config entry.""" + device_registry = mock_device_registry(hass) + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={("mac", "12:34:56:AB:CD:EF")}, + ) + + entry1 = registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry, + device_id=device_entry.id, + ) + entry2 = registry.async_get_or_create( + "light", + "hue", + "ABCD", + config_entry=config_entry, + device_id=device_entry.id, + disabled_by="user", + ) + entry3 = registry.async_get_or_create( + "light", + "hue", + "EFGH", + config_entry=config_entry, + device_id=device_entry.id, + disabled_by="device", + ) + + assert not entry1.disabled + assert entry2.disabled + assert entry3.disabled + + await hass.config_entries.async_set_disabled_by(config_entry.entry_id, "user") + await hass.async_block_till_done() + + entry1 = registry.async_get(entry1.entity_id) + assert entry1.disabled + assert entry1.disabled_by == "config_entry" + entry2 = registry.async_get(entry2.entity_id) + assert entry2.disabled + assert entry2.disabled_by == "user" + entry3 = registry.async_get(entry3.entity_id) + assert entry3.disabled + assert entry3.disabled_by == "device" + + await hass.config_entries.async_set_disabled_by(config_entry.entry_id, None) + await hass.async_block_till_done() + + entry1 = registry.async_get(entry1.entity_id) + assert not entry1.disabled + entry2 = registry.async_get(entry2.entity_id) + assert entry2.disabled + assert entry2.disabled_by == "user" + # The device was re-enabled, so entity disabled by the device will be re-enabled too + entry3 = registry.async_get(entry3.entity_id) + assert not entry3.disabled_by async def test_disabled_entities_excluded_from_entity_list(hass, registry): diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 435f2a11cc2..8a479a802e4 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1108,6 +1108,110 @@ async def test_entry_reload_error(hass, manager, state): assert entry.state == state +async def test_entry_disable_succeed(hass, manager): + """Test that we can disable an entry.""" + entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED) + entry.add_to_hass(hass) + + async_setup = AsyncMock(return_value=True) + async_setup_entry = AsyncMock(return_value=True) + async_unload_entry = AsyncMock(return_value=True) + + mock_integration( + hass, + MockModule( + "comp", + async_setup=async_setup, + async_setup_entry=async_setup_entry, + async_unload_entry=async_unload_entry, + ), + ) + mock_entity_platform(hass, "config_flow.comp", None) + + # Disable + assert await manager.async_set_disabled_by( + entry.entry_id, config_entries.DISABLED_USER + ) + assert len(async_unload_entry.mock_calls) == 1 + assert len(async_setup.mock_calls) == 0 + assert len(async_setup_entry.mock_calls) == 0 + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + + # Enable + assert await manager.async_set_disabled_by(entry.entry_id, None) + assert len(async_unload_entry.mock_calls) == 1 + assert len(async_setup.mock_calls) == 1 + assert len(async_setup_entry.mock_calls) == 1 + assert entry.state == config_entries.ENTRY_STATE_LOADED + + +async def test_entry_disable_without_reload_support(hass, manager): + """Test that we can disable an entry without reload support.""" + entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED) + entry.add_to_hass(hass) + + async_setup = AsyncMock(return_value=True) + async_setup_entry = AsyncMock(return_value=True) + + mock_integration( + hass, + MockModule( + "comp", + async_setup=async_setup, + async_setup_entry=async_setup_entry, + ), + ) + mock_entity_platform(hass, "config_flow.comp", None) + + # Disable + assert not await manager.async_set_disabled_by( + entry.entry_id, config_entries.DISABLED_USER + ) + assert len(async_setup.mock_calls) == 0 + assert len(async_setup_entry.mock_calls) == 0 + assert entry.state == config_entries.ENTRY_STATE_FAILED_UNLOAD + + # Enable + with pytest.raises(config_entries.OperationNotAllowed): + await manager.async_set_disabled_by(entry.entry_id, None) + assert len(async_setup.mock_calls) == 0 + assert len(async_setup_entry.mock_calls) == 0 + assert entry.state == config_entries.ENTRY_STATE_FAILED_UNLOAD + + +async def test_entry_enable_without_reload_support(hass, manager): + """Test that we can disable an entry without reload support.""" + entry = MockConfigEntry(domain="comp", disabled_by=config_entries.DISABLED_USER) + entry.add_to_hass(hass) + + async_setup = AsyncMock(return_value=True) + async_setup_entry = AsyncMock(return_value=True) + + mock_integration( + hass, + MockModule( + "comp", + async_setup=async_setup, + async_setup_entry=async_setup_entry, + ), + ) + mock_entity_platform(hass, "config_flow.comp", None) + + # Enable + assert await manager.async_set_disabled_by(entry.entry_id, None) + assert len(async_setup.mock_calls) == 1 + assert len(async_setup_entry.mock_calls) == 1 + assert entry.state == config_entries.ENTRY_STATE_LOADED + + # Disable + assert not await manager.async_set_disabled_by( + entry.entry_id, config_entries.DISABLED_USER + ) + assert len(async_setup.mock_calls) == 1 + assert len(async_setup_entry.mock_calls) == 1 + assert entry.state == config_entries.ENTRY_STATE_FAILED_UNLOAD + + async def test_init_custom_integration(hass): """Test initializing flow for custom integration.""" integration = loader.Integration( From d9ab1482bc5e4e0e2eea675036aa352e8e38b8d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Feb 2021 17:24:14 -1000 Subject: [PATCH 607/796] Add support for preset modes in homekit fans (#45962) --- homeassistant/components/homekit/type_fans.py | 43 ++++++++++ tests/components/homekit/test_type_fans.py | 83 +++++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index 306beb89c48..1efb3b6c8be 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -8,12 +8,15 @@ from homeassistant.components.fan import ( ATTR_OSCILLATING, ATTR_PERCENTAGE, ATTR_PERCENTAGE_STEP, + ATTR_PRESET_MODE, + ATTR_PRESET_MODES, DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SERVICE_SET_PERCENTAGE, + SERVICE_SET_PRESET_MODE, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, @@ -31,11 +34,14 @@ from homeassistant.core import callback from .accessories import TYPES, HomeAccessory from .const import ( CHAR_ACTIVE, + CHAR_NAME, + CHAR_ON, CHAR_ROTATION_DIRECTION, CHAR_ROTATION_SPEED, CHAR_SWING_MODE, PROP_MIN_STEP, SERV_FANV2, + SERV_SWITCH, ) _LOGGER = logging.getLogger(__name__) @@ -56,6 +62,7 @@ class Fan(HomeAccessory): features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) percentage_step = state.attributes.get(ATTR_PERCENTAGE_STEP, 1) + preset_modes = state.attributes.get(ATTR_PRESET_MODES) if features & SUPPORT_DIRECTION: chars.append(CHAR_ROTATION_DIRECTION) @@ -65,11 +72,13 @@ class Fan(HomeAccessory): chars.append(CHAR_ROTATION_SPEED) serv_fan = self.add_preload_service(SERV_FANV2, chars) + self.set_primary_service(serv_fan) self.char_active = serv_fan.configure_char(CHAR_ACTIVE, value=0) self.char_direction = None self.char_speed = None self.char_swing = None + self.preset_mode_chars = {} if CHAR_ROTATION_DIRECTION in chars: self.char_direction = serv_fan.configure_char( @@ -86,6 +95,22 @@ class Fan(HomeAccessory): properties={PROP_MIN_STEP: percentage_step}, ) + if preset_modes: + for preset_mode in preset_modes: + preset_serv = self.add_preload_service(SERV_SWITCH, CHAR_NAME) + serv_fan.add_linked_service(preset_serv) + preset_serv.configure_char( + CHAR_NAME, value=f"{self.display_name} {preset_mode}" + ) + + self.preset_mode_chars[preset_mode] = preset_serv.configure_char( + CHAR_ON, + value=False, + setter_callback=lambda value, preset_mode=preset_mode: self.set_preset_mode( + value, preset_mode + ), + ) + if CHAR_SWING_MODE in chars: self.char_swing = serv_fan.configure_char(CHAR_SWING_MODE, value=0) self.async_update_state(state) @@ -120,6 +145,18 @@ class Fan(HomeAccessory): if CHAR_ROTATION_SPEED in char_values: self.set_percentage(char_values[CHAR_ROTATION_SPEED]) + def set_preset_mode(self, value, preset_mode): + """Set preset_mode if call came from HomeKit.""" + _LOGGER.debug( + "%s: Set preset_mode %s to %d", self.entity_id, preset_mode, value + ) + params = {ATTR_ENTITY_ID: self.entity_id} + if value: + params[ATTR_PRESET_MODE] = preset_mode + self.async_call_service(DOMAIN, SERVICE_SET_PRESET_MODE, params) + else: + self.async_call_service(DOMAIN, SERVICE_TURN_ON, params) + def set_state(self, value): """Set state if call came from HomeKit.""" _LOGGER.debug("%s: Set state to %d", self.entity_id, value) @@ -193,3 +230,9 @@ class Fan(HomeAccessory): hk_oscillating = 1 if oscillating else 0 if self.char_swing.value != hk_oscillating: self.char_swing.set_value(hk_oscillating) + + current_preset_mode = new_state.attributes.get(ATTR_PRESET_MODE) + for preset_mode, char in self.preset_mode_chars.items(): + hk_value = 1 if preset_mode == current_preset_mode else 0 + if char.value != hk_value: + char.set_value(hk_value) diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index baa47462cdc..ba660f2f12d 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -7,11 +7,14 @@ from homeassistant.components.fan import ( ATTR_OSCILLATING, ATTR_PERCENTAGE, ATTR_PERCENTAGE_STEP, + ATTR_PRESET_MODE, + ATTR_PRESET_MODES, DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, + SUPPORT_PRESET_MODE, SUPPORT_SET_SPEED, ) from homeassistant.components.homekit.const import ATTR_VALUE, PROP_MIN_STEP @@ -557,3 +560,83 @@ async def test_fan_restore(hass, hk_driver, events): assert acc.char_direction is not None assert acc.char_speed is not None assert acc.char_swing is not None + + +async def test_fan_preset_modes(hass, hk_driver, events): + """Test fan with direction.""" + entity_id = "fan.demo" + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_PRESET_MODE, + ATTR_PRESET_MODE: "auto", + ATTR_PRESET_MODES: ["auto", "smart"], + }, + ) + await hass.async_block_till_done() + acc = Fan(hass, hk_driver, "Fan", entity_id, 1, None) + hk_driver.add_accessory(acc) + + assert acc.preset_mode_chars["auto"].value == 1 + assert acc.preset_mode_chars["smart"].value == 0 + + await acc.run() + await hass.async_block_till_done() + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_PRESET_MODE, + ATTR_PRESET_MODE: "smart", + ATTR_PRESET_MODES: ["auto", "smart"], + }, + ) + await hass.async_block_till_done() + + assert acc.preset_mode_chars["auto"].value == 0 + assert acc.preset_mode_chars["smart"].value == 1 + # Set from HomeKit + call_set_preset_mode = async_mock_service(hass, DOMAIN, "set_preset_mode") + call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") + + char_auto_iid = acc.preset_mode_chars["auto"].to_HAP()[HAP_REPR_IID] + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_auto_iid, + HAP_REPR_VALUE: 1, + }, + ] + }, + "mock_addr", + ) + await hass.async_block_till_done() + assert call_set_preset_mode[0] + assert call_set_preset_mode[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_preset_mode[0].data[ATTR_PRESET_MODE] == "auto" + assert len(events) == 1 + assert events[-1].data["service"] == "set_preset_mode" + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_auto_iid, + HAP_REPR_VALUE: 0, + }, + ] + }, + "mock_addr", + ) + await hass.async_block_till_done() + assert call_turn_on[0] + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert events[-1].data["service"] == "turn_on" + assert len(events) == 2 From e2fd255a96500e4a7de88b79b60d4f53c6d9903a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Feb 2021 22:52:41 -1000 Subject: [PATCH 608/796] Cleanup recorder tests (#46836) --- tests/components/recorder/test_init.py | 2 +- tests/components/recorder/test_util.py | 33 +++++++++++++++++++------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index ca25fe10284..dfa65944811 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -49,7 +49,7 @@ async def test_shutdown_before_startup_finishes(hass): with patch.object(hass.data[DATA_INSTANCE], "engine"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() - hass.stop() + await hass.async_stop() run_info = await hass.async_add_executor_job(run_information_with_session, session) diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index f1d55999ae4..4aba6569a41 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -8,11 +8,16 @@ import pytest from homeassistant.components.recorder import util from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.util import dt as dt_util -from .common import corrupt_db_file, wait_recording_done +from .common import corrupt_db_file -from tests.common import get_test_home_assistant, init_recorder_component +from tests.common import ( + async_init_recorder_component, + get_test_home_assistant, + init_recorder_component, +) @pytest.fixture @@ -142,18 +147,25 @@ def test_validate_or_move_away_sqlite_database_without_integrity_check( assert util.validate_or_move_away_sqlite_database(dburl, db_integrity_check) is True -def test_last_run_was_recently_clean(hass_recorder): +async def test_last_run_was_recently_clean(hass): """Test we can check if the last recorder run was recently clean.""" - hass = hass_recorder() + await async_init_recorder_component(hass) + await hass.async_block_till_done() cursor = hass.data[DATA_INSTANCE].engine.raw_connection().cursor() - assert util.last_run_was_recently_clean(cursor) is False + assert ( + await hass.async_add_executor_job(util.last_run_was_recently_clean, cursor) + is False + ) - hass.data[DATA_INSTANCE]._shutdown() - wait_recording_done(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() - assert util.last_run_was_recently_clean(cursor) is True + assert ( + await hass.async_add_executor_job(util.last_run_was_recently_clean, cursor) + is True + ) thirty_min_future_time = dt_util.utcnow() + timedelta(minutes=30) @@ -161,7 +173,10 @@ def test_last_run_was_recently_clean(hass_recorder): "homeassistant.components.recorder.dt_util.utcnow", return_value=thirty_min_future_time, ): - assert util.last_run_was_recently_clean(cursor) is False + assert ( + await hass.async_add_executor_job(util.last_run_was_recently_clean, cursor) + is False + ) def test_basic_sanity_check(hass_recorder): From d33a1a5ff85569d94b521c75a4ad1bb8378a63d0 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 21 Feb 2021 14:54:36 +0100 Subject: [PATCH 609/796] Refine printing of ConditionError (#46838) * Refine printing of ConditionError * Improve coverage * name -> type --- .../components/automation/__init__.py | 21 ++- .../homeassistant/triggers/numeric_state.py | 2 +- homeassistant/exceptions.py | 69 ++++++++- homeassistant/helpers/condition.py | 140 +++++++++++------- homeassistant/helpers/script.py | 8 +- tests/helpers/test_condition.py | 59 ++++++-- tests/test_exceptions.py | 46 ++++++ 7 files changed, 272 insertions(+), 73 deletions(-) create mode 100644 tests/test_exceptions.py diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 3a48b3e3cc2..7e07f35be45 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -32,7 +32,12 @@ from homeassistant.core import ( callback, split_entity_id, ) -from homeassistant.exceptions import ConditionError, HomeAssistantError +from homeassistant.exceptions import ( + ConditionError, + ConditionErrorContainer, + ConditionErrorIndex, + HomeAssistantError, +) from homeassistant.helpers import condition, extract_domain_configs, template import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity @@ -616,16 +621,22 @@ async def _async_process_if(hass, config, p_config): def if_action(variables=None): """AND all conditions.""" errors = [] - for check in checks: + for index, check in enumerate(checks): try: if not check(hass, variables): return False except ConditionError as ex: - errors.append(f"Error in 'condition' evaluation: {ex}") + errors.append( + ConditionErrorIndex( + "condition", index=index, total=len(checks), error=ex + ) + ) if errors: - for error in errors: - LOGGER.warning("%s", error) + LOGGER.warning( + "Error evaluating condition:\n%s", + ConditionErrorContainer("condition", errors=errors), + ) return False return True diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index 16b3fb97475..59f16c41a36 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -112,7 +112,7 @@ async def async_attach_trigger( armed_entities.add(entity_id) except exceptions.ConditionError as ex: _LOGGER.warning( - "Error initializing 'numeric_state' trigger for '%s': %s", + "Error initializing '%s' trigger: %s", automation_info["name"], ex, ) diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 852795ebb4a..0ac231fd314 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -1,5 +1,7 @@ """The exceptions used by Home Assistant.""" -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Generator, Optional, Sequence + +import attr if TYPE_CHECKING: from .core import Context # noqa: F401 pylint: disable=unused-import @@ -25,9 +27,74 @@ class TemplateError(HomeAssistantError): super().__init__(f"{exception.__class__.__name__}: {exception}") +@attr.s class ConditionError(HomeAssistantError): """Error during condition evaluation.""" + # The type of the failed condition, such as 'and' or 'numeric_state' + type: str = attr.ib() + + @staticmethod + def _indent(indent: int, message: str) -> str: + """Return indentation.""" + return " " * indent + message + + def output(self, indent: int) -> Generator: + """Yield an indented representation.""" + raise NotImplementedError() + + def __str__(self) -> str: + """Return string representation.""" + return "\n".join(list(self.output(indent=0))) + + +@attr.s +class ConditionErrorMessage(ConditionError): + """Condition error message.""" + + # A message describing this error + message: str = attr.ib() + + def output(self, indent: int) -> Generator: + """Yield an indented representation.""" + yield self._indent(indent, f"In '{self.type}' condition: {self.message}") + + +@attr.s +class ConditionErrorIndex(ConditionError): + """Condition error with index.""" + + # The zero-based index of the failed condition, for conditions with multiple parts + index: int = attr.ib() + # The total number of parts in this condition, including non-failed parts + total: int = attr.ib() + # The error that this error wraps + error: ConditionError = attr.ib() + + def output(self, indent: int) -> Generator: + """Yield an indented representation.""" + if self.total > 1: + yield self._indent( + indent, f"In '{self.type}' (item {self.index+1} of {self.total}):" + ) + else: + yield self._indent(indent, f"In '{self.type}':") + + yield from self.error.output(indent + 1) + + +@attr.s +class ConditionErrorContainer(ConditionError): + """Condition error with index.""" + + # List of ConditionErrors that this error wraps + errors: Sequence[ConditionError] = attr.ib() + + def output(self, indent: int) -> Generator: + """Yield an indented representation.""" + for item in self.errors: + yield from item.output(indent) + class PlatformNotReady(HomeAssistantError): """Error to indicate that platform is not ready.""" diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index b66ee6c7976..c20755a1780 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -36,7 +36,14 @@ from homeassistant.const import ( WEEKDAYS, ) from homeassistant.core import HomeAssistant, State, callback -from homeassistant.exceptions import ConditionError, HomeAssistantError, TemplateError +from homeassistant.exceptions import ( + ConditionError, + ConditionErrorContainer, + ConditionErrorIndex, + ConditionErrorMessage, + HomeAssistantError, + TemplateError, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.sun import get_astral_event_date from homeassistant.helpers.template import Template @@ -109,18 +116,18 @@ async def async_and_from_config( ) -> bool: """Test and condition.""" errors = [] - for check in checks: + for index, check in enumerate(checks): try: if not check(hass, variables): return False except ConditionError as ex: - errors.append(str(ex)) - except Exception as ex: # pylint: disable=broad-except - errors.append(str(ex)) + errors.append( + ConditionErrorIndex("and", index=index, total=len(checks), error=ex) + ) # Raise the errors if no check was false if errors: - raise ConditionError("Error in 'and' condition: " + ", ".join(errors)) + raise ConditionErrorContainer("and", errors=errors) return True @@ -142,18 +149,18 @@ async def async_or_from_config( ) -> bool: """Test or condition.""" errors = [] - for check in checks: + for index, check in enumerate(checks): try: if check(hass, variables): return True except ConditionError as ex: - errors.append(str(ex)) - except Exception as ex: # pylint: disable=broad-except - errors.append(str(ex)) + errors.append( + ConditionErrorIndex("or", index=index, total=len(checks), error=ex) + ) # Raise the errors if no check was true if errors: - raise ConditionError("Error in 'or' condition: " + ", ".join(errors)) + raise ConditionErrorContainer("or", errors=errors) return False @@ -175,18 +182,18 @@ async def async_not_from_config( ) -> bool: """Test not condition.""" errors = [] - for check in checks: + for index, check in enumerate(checks): try: if check(hass, variables): return False except ConditionError as ex: - errors.append(str(ex)) - except Exception as ex: # pylint: disable=broad-except - errors.append(str(ex)) + errors.append( + ConditionErrorIndex("not", index=index, total=len(checks), error=ex) + ) # Raise the errors if no check was true if errors: - raise ConditionError("Error in 'not' condition: " + ", ".join(errors)) + raise ConditionErrorContainer("not", errors=errors) return True @@ -225,20 +232,21 @@ def async_numeric_state( ) -> bool: """Test a numeric state condition.""" if entity is None: - raise ConditionError("No entity specified") + raise ConditionErrorMessage("numeric_state", "no entity specified") if isinstance(entity, str): entity_id = entity entity = hass.states.get(entity) if entity is None: - raise ConditionError(f"Unknown entity {entity_id}") + raise ConditionErrorMessage("numeric_state", f"unknown entity {entity_id}") else: entity_id = entity.entity_id if attribute is not None and attribute not in entity.attributes: - raise ConditionError( - f"Attribute '{attribute}' (of entity {entity_id}) does not exist" + raise ConditionErrorMessage( + "numeric_state", + f"attribute '{attribute}' (of entity {entity_id}) does not exist", ) value: Any = None @@ -253,16 +261,21 @@ def async_numeric_state( try: value = value_template.async_render(variables) except TemplateError as ex: - raise ConditionError(f"Template error: {ex}") from ex + raise ConditionErrorMessage( + "numeric_state", f"template error: {ex}" + ) from ex if value in (STATE_UNAVAILABLE, STATE_UNKNOWN): - raise ConditionError("State is not available") + raise ConditionErrorMessage( + "numeric_state", f"state of {entity_id} is unavailable" + ) try: fvalue = float(value) - except ValueError as ex: - raise ConditionError( - f"Entity {entity_id} state '{value}' cannot be processed as a number" + except (ValueError, TypeError) as ex: + raise ConditionErrorMessage( + "numeric_state", + f"entity {entity_id} state '{value}' cannot be processed as a number", ) from ex if below is not None: @@ -272,9 +285,17 @@ def async_numeric_state( STATE_UNAVAILABLE, STATE_UNKNOWN, ): - raise ConditionError(f"The below entity {below} is not available") - if fvalue >= float(below_entity.state): - return False + raise ConditionErrorMessage( + "numeric_state", f"the 'below' entity {below} is unavailable" + ) + try: + if fvalue >= float(below_entity.state): + return False + except (ValueError, TypeError) as ex: + raise ConditionErrorMessage( + "numeric_state", + f"the 'below' entity {below} state '{below_entity.state}' cannot be processed as a number", + ) from ex elif fvalue >= below: return False @@ -285,9 +306,17 @@ def async_numeric_state( STATE_UNAVAILABLE, STATE_UNKNOWN, ): - raise ConditionError(f"The above entity {above} is not available") - if fvalue <= float(above_entity.state): - return False + raise ConditionErrorMessage( + "numeric_state", f"the 'above' entity {above} is unavailable" + ) + try: + if fvalue <= float(above_entity.state): + return False + except (ValueError, TypeError) as ex: + raise ConditionErrorMessage( + "numeric_state", + f"the 'above' entity {above} state '{above_entity.state}' cannot be processed as a number", + ) from ex elif fvalue <= above: return False @@ -335,20 +364,20 @@ def state( Async friendly. """ if entity is None: - raise ConditionError("No entity specified") + raise ConditionErrorMessage("state", "no entity specified") if isinstance(entity, str): entity_id = entity entity = hass.states.get(entity) if entity is None: - raise ConditionError(f"Unknown entity {entity_id}") + raise ConditionErrorMessage("state", f"unknown entity {entity_id}") else: entity_id = entity.entity_id if attribute is not None and attribute not in entity.attributes: - raise ConditionError( - f"Attribute '{attribute}' (of entity {entity_id}) does not exist" + raise ConditionErrorMessage( + "state", f"attribute '{attribute}' (of entity {entity_id}) does not exist" ) assert isinstance(entity, State) @@ -370,7 +399,9 @@ def state( ): state_entity = hass.states.get(req_state_value) if not state_entity: - continue + raise ConditionErrorMessage( + "state", f"the 'state' entity {req_state_value} is unavailable" + ) state_value = state_entity.state is_state = value == state_value if is_state: @@ -495,7 +526,7 @@ def async_template( try: value: str = value_template.async_render(variables, parse_result=False) except TemplateError as ex: - raise ConditionError(f"Error in 'template' condition: {ex}") from ex + raise ConditionErrorMessage("template", str(ex)) from ex return value.lower() == "true" @@ -538,9 +569,7 @@ def time( elif isinstance(after, str): after_entity = hass.states.get(after) if not after_entity: - raise ConditionError( - f"Error in 'time' condition: The 'after' entity {after} is not available" - ) + raise ConditionErrorMessage("time", f"unknown 'after' entity {after}") after = dt_util.dt.time( after_entity.attributes.get("hour", 23), after_entity.attributes.get("minute", 59), @@ -552,9 +581,7 @@ def time( elif isinstance(before, str): before_entity = hass.states.get(before) if not before_entity: - raise ConditionError( - f"Error in 'time' condition: The 'before' entity {before} is not available" - ) + raise ConditionErrorMessage("time", f"unknown 'before' entity {before}") before = dt_util.dt.time( before_entity.attributes.get("hour", 23), before_entity.attributes.get("minute", 59), @@ -609,24 +636,24 @@ def zone( Async friendly. """ if zone_ent is None: - raise ConditionError("No zone specified") + raise ConditionErrorMessage("zone", "no zone specified") if isinstance(zone_ent, str): zone_ent_id = zone_ent zone_ent = hass.states.get(zone_ent) if zone_ent is None: - raise ConditionError(f"Unknown zone {zone_ent_id}") + raise ConditionErrorMessage("zone", f"unknown zone {zone_ent_id}") if entity is None: - raise ConditionError("No entity specified") + raise ConditionErrorMessage("zone", "no entity specified") if isinstance(entity, str): entity_id = entity entity = hass.states.get(entity) if entity is None: - raise ConditionError(f"Unknown entity {entity_id}") + raise ConditionErrorMessage("zone", f"unknown entity {entity_id}") else: entity_id = entity.entity_id @@ -634,10 +661,14 @@ def zone( longitude = entity.attributes.get(ATTR_LONGITUDE) if latitude is None: - raise ConditionError(f"Entity {entity_id} has no 'latitude' attribute") + raise ConditionErrorMessage( + "zone", f"entity {entity_id} has no 'latitude' attribute" + ) if longitude is None: - raise ConditionError(f"Entity {entity_id} has no 'longitude' attribute") + raise ConditionErrorMessage( + "zone", f"entity {entity_id} has no 'longitude' attribute" + ) return zone_cmp.in_zone( zone_ent, latitude, longitude, entity.attributes.get(ATTR_GPS_ACCURACY, 0) @@ -664,15 +695,20 @@ def zone_from_config( try: if zone(hass, zone_entity_id, entity_id): entity_ok = True - except ConditionError as ex: - errors.append(str(ex)) + except ConditionErrorMessage as ex: + errors.append( + ConditionErrorMessage( + "zone", + f"error matching {entity_id} with {zone_entity_id}: {ex.message}", + ) + ) if not entity_ok: all_ok = False # Raise the errors only if no definitive result was found if errors and not all_ok: - raise ConditionError("Error in 'zone' condition: " + ", ".join(errors)) + raise ConditionErrorContainer("zone", errors=errors) return all_ok diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 2e8348bcaf8..e4eb0d4a901 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -516,7 +516,7 @@ class _ScriptRun: try: check = cond(self._hass, self._variables) except exceptions.ConditionError as ex: - _LOGGER.warning("Error in 'condition' evaluation: %s", ex) + _LOGGER.warning("Error in 'condition' evaluation:\n%s", ex) check = False self._log("Test condition %s: %s", self._script.last_action, check) @@ -575,7 +575,7 @@ class _ScriptRun: ): break except exceptions.ConditionError as ex: - _LOGGER.warning("Error in 'while' evaluation: %s", ex) + _LOGGER.warning("Error in 'while' evaluation:\n%s", ex) break await async_run_sequence(iteration) @@ -593,7 +593,7 @@ class _ScriptRun: ): break except exceptions.ConditionError as ex: - _LOGGER.warning("Error in 'until' evaluation: %s", ex) + _LOGGER.warning("Error in 'until' evaluation:\n%s", ex) break if saved_repeat_vars: @@ -614,7 +614,7 @@ class _ScriptRun: await self._async_run_script(script) return except exceptions.ConditionError as ex: - _LOGGER.warning("Error in 'choose' evaluation: %s", ex) + _LOGGER.warning("Error in 'choose' evaluation:\n%s", ex) if choose_data["default"]: await self._async_run_script(choose_data["default"]) diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 63ef9ba56d8..2c35a3c8b15 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -386,8 +386,12 @@ async def test_if_numeric_state_raises_on_unavailable(hass, caplog): async def test_state_raises(hass): """Test that state raises ConditionError on errors.""" + # No entity + with pytest.raises(ConditionError, match="no entity"): + condition.state(hass, entity=None, req_state="missing") + # Unknown entity_id - with pytest.raises(ConditionError, match="Unknown entity"): + with pytest.raises(ConditionError, match="unknown entity"): test = await condition.async_from_config( hass, { @@ -400,7 +404,7 @@ async def test_state_raises(hass): test(hass) # Unknown attribute - with pytest.raises(ConditionError, match=r"Attribute .* does not exist"): + with pytest.raises(ConditionError, match=r"attribute .* does not exist"): test = await condition.async_from_config( hass, { @@ -414,6 +418,20 @@ async def test_state_raises(hass): hass.states.async_set("sensor.door", "open") test(hass) + # Unknown state entity + with pytest.raises(ConditionError, match="input_text.missing"): + test = await condition.async_from_config( + hass, + { + "condition": "state", + "entity_id": "sensor.door", + "state": "input_text.missing", + }, + ) + + hass.states.async_set("sensor.door", "open") + test(hass) + async def test_state_multiple_entities(hass): """Test with multiple entities in condition.""" @@ -564,7 +582,6 @@ async def test_state_using_input_entities(hass): "state": [ "input_text.hello", "input_select.hello", - "input_number.not_exist", "salut", ], }, @@ -616,7 +633,7 @@ async def test_state_using_input_entities(hass): async def test_numeric_state_raises(hass): """Test that numeric_state raises ConditionError on errors.""" # Unknown entity_id - with pytest.raises(ConditionError, match="Unknown entity"): + with pytest.raises(ConditionError, match="unknown entity"): test = await condition.async_from_config( hass, { @@ -629,7 +646,7 @@ async def test_numeric_state_raises(hass): test(hass) # Unknown attribute - with pytest.raises(ConditionError, match=r"Attribute .* does not exist"): + with pytest.raises(ConditionError, match=r"attribute .* does not exist"): test = await condition.async_from_config( hass, { @@ -659,7 +676,7 @@ async def test_numeric_state_raises(hass): test(hass) # Unavailable state - with pytest.raises(ConditionError, match="State is not available"): + with pytest.raises(ConditionError, match="state of .* is unavailable"): test = await condition.async_from_config( hass, { @@ -687,7 +704,7 @@ async def test_numeric_state_raises(hass): test(hass) # Below entity missing - with pytest.raises(ConditionError, match="below entity"): + with pytest.raises(ConditionError, match="'below' entity"): test = await condition.async_from_config( hass, { @@ -700,8 +717,16 @@ async def test_numeric_state_raises(hass): hass.states.async_set("sensor.temperature", 50) test(hass) + # Below entity not a number + with pytest.raises( + ConditionError, + match="'below'.*input_number.missing.*cannot be processed as a number", + ): + hass.states.async_set("input_number.missing", "number") + test(hass) + # Above entity missing - with pytest.raises(ConditionError, match="above entity"): + with pytest.raises(ConditionError, match="'above' entity"): test = await condition.async_from_config( hass, { @@ -714,6 +739,14 @@ async def test_numeric_state_raises(hass): hass.states.async_set("sensor.temperature", 50) test(hass) + # Above entity not a number + with pytest.raises( + ConditionError, + match="'above'.*input_number.missing.*cannot be processed as a number", + ): + hass.states.async_set("input_number.missing", "number") + test(hass) + async def test_numeric_state_multiple_entities(hass): """Test with multiple entities in condition.""" @@ -849,7 +882,10 @@ async def test_zone_raises(hass): }, ) - with pytest.raises(ConditionError, match="Unknown zone"): + with pytest.raises(ConditionError, match="no zone"): + condition.zone(hass, zone_ent=None, entity="sensor.any") + + with pytest.raises(ConditionError, match="unknown zone"): test(hass) hass.states.async_set( @@ -858,7 +894,10 @@ async def test_zone_raises(hass): {"name": "home", "latitude": 2.1, "longitude": 1.1, "radius": 10}, ) - with pytest.raises(ConditionError, match="Unknown entity"): + with pytest.raises(ConditionError, match="no entity"): + condition.zone(hass, zone_ent="zone.home", entity=None) + + with pytest.raises(ConditionError, match="unknown entity"): test(hass) hass.states.async_set( diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py new file mode 100644 index 00000000000..959f0846cae --- /dev/null +++ b/tests/test_exceptions.py @@ -0,0 +1,46 @@ +"""Test to verify that Home Assistant exceptions work.""" +from homeassistant.exceptions import ( + ConditionErrorContainer, + ConditionErrorIndex, + ConditionErrorMessage, +) + + +def test_conditionerror_format(): + """Test ConditionError stringifiers.""" + error1 = ConditionErrorMessage("test", "A test error") + assert str(error1) == "In 'test' condition: A test error" + + error2 = ConditionErrorMessage("test", "Another error") + assert str(error2) == "In 'test' condition: Another error" + + error_pos1 = ConditionErrorIndex("box", index=0, total=2, error=error1) + assert ( + str(error_pos1) + == """In 'box' (item 1 of 2): + In 'test' condition: A test error""" + ) + + error_pos2 = ConditionErrorIndex("box", index=1, total=2, error=error2) + assert ( + str(error_pos2) + == """In 'box' (item 2 of 2): + In 'test' condition: Another error""" + ) + + error_container1 = ConditionErrorContainer("box", errors=[error_pos1, error_pos2]) + print(error_container1) + assert ( + str(error_container1) + == """In 'box' (item 1 of 2): + In 'test' condition: A test error +In 'box' (item 2 of 2): + In 'test' condition: Another error""" + ) + + error_pos3 = ConditionErrorIndex("box", index=0, total=1, error=error1) + assert ( + str(error_pos3) + == """In 'box': + In 'test' condition: A test error""" + ) From c1ee9f7e4a872b3b5e2dcab21b220329afce07a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Feb 2021 09:47:38 -1000 Subject: [PATCH 610/796] Fix unmocked I/O in rituals_perfume_genie config flow test (#46862) --- .../rituals_perfume_genie/test_config_flow.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/components/rituals_perfume_genie/test_config_flow.py b/tests/components/rituals_perfume_genie/test_config_flow.py index 60ec389a371..92c3e15c247 100644 --- a/tests/components/rituals_perfume_genie/test_config_flow.py +++ b/tests/components/rituals_perfume_genie/test_config_flow.py @@ -1,5 +1,5 @@ """Test the Rituals Perfume Genie config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, MagicMock, patch from aiohttp import ClientResponseError from pyrituals import AuthenticationException @@ -13,6 +13,13 @@ VALID_PASSWORD = "passw0rd" WRONG_PASSWORD = "wrong-passw0rd" +def _mock_account(*_): + account = MagicMock() + account.authenticate = AsyncMock() + account.data = {CONF_EMAIL: TEST_EMAIL, ACCOUNT_HASH: "any"} + return account + + async def test_form(hass): """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -22,6 +29,9 @@ async def test_form(hass): assert result["errors"] is None with patch( + "homeassistant.components.rituals_perfume_genie.config_flow.Account", + side_effect=_mock_account, + ), patch( "homeassistant.components.rituals_perfume_genie.async_setup", return_value=True ) as mock_setup, patch( "homeassistant.components.rituals_perfume_genie.async_setup_entry", From 50a07f6d25ab40a1e3d435a885029839e56364a7 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Sun, 21 Feb 2021 16:42:06 -0500 Subject: [PATCH 611/796] Log zwave_js connection errors (#46867) --- homeassistant/components/zwave_js/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index d5624551b27..9c8c245e910 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -181,6 +181,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async with timeout(CONNECT_TIMEOUT): await client.connect() except (asyncio.TimeoutError, BaseZwaveJSServerError) as err: + LOGGER.error("Failed to connect: %s", err) raise ConfigEntryNotReady from err else: LOGGER.info("Connected to Zwave JS Server") @@ -268,8 +269,8 @@ async def client_listen( await client.listen(driver_ready) except asyncio.CancelledError: should_reload = False - except BaseZwaveJSServerError: - pass + except BaseZwaveJSServerError as err: + LOGGER.error("Failed to listen: %s", err) # The entry needs to be reloaded since a new driver state # will be acquired on reconnect. From 871427f5f1a6fd6cc339118fbb6b760fe8dcd4be Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 22 Feb 2021 00:06:36 +0000 Subject: [PATCH 612/796] [ci skip] Translation update --- .../components/habitica/translations/pl.json | 20 +++++++++ .../keenetic_ndms2/translations/pl.json | 2 +- .../translations/et.json | 21 +++++++++ .../translations/nl.json | 21 +++++++++ .../translations/pl.json | 21 +++++++++ .../components/smarttub/translations/pl.json | 22 ++++++++++ .../components/subaru/translations/ca.json | 44 +++++++++++++++++++ .../components/subaru/translations/en.json | 44 +++++++++++++++++++ .../components/subaru/translations/et.json | 44 +++++++++++++++++++ .../components/subaru/translations/pl.json | 44 +++++++++++++++++++ 10 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/habitica/translations/pl.json create mode 100644 homeassistant/components/rituals_perfume_genie/translations/et.json create mode 100644 homeassistant/components/rituals_perfume_genie/translations/nl.json create mode 100644 homeassistant/components/rituals_perfume_genie/translations/pl.json create mode 100644 homeassistant/components/smarttub/translations/pl.json create mode 100644 homeassistant/components/subaru/translations/ca.json create mode 100644 homeassistant/components/subaru/translations/en.json create mode 100644 homeassistant/components/subaru/translations/et.json create mode 100644 homeassistant/components/subaru/translations/pl.json diff --git a/homeassistant/components/habitica/translations/pl.json b/homeassistant/components/habitica/translations/pl.json new file mode 100644 index 00000000000..f06f1a0e1aa --- /dev/null +++ b/homeassistant/components/habitica/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "invalid_credentials": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "api_user": "Identyfikator API u\u017cytkownika Habitica", + "name": "Nadpisanie nazwy u\u017cytkownika Habitica. B\u0119dzie u\u017cywany do wywo\u0142a\u0144 serwisowych.", + "url": "URL" + }, + "description": "Po\u0142\u0105cz sw\u00f3j profil Habitica, aby umo\u017cliwi\u0107 monitorowanie profilu i zada\u0144 u\u017cytkownika. Pami\u0119taj, \u017ce api_id i api_key musz\u0105 zosta\u0107 pobrane z https://habitica.com/user/settings/api" + } + } + }, + "title": "Habitica" +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/pl.json b/homeassistant/components/keenetic_ndms2/translations/pl.json index 30e7b9a4fae..13bcbfb91ba 100644 --- a/homeassistant/components/keenetic_ndms2/translations/pl.json +++ b/homeassistant/components/keenetic_ndms2/translations/pl.json @@ -28,7 +28,7 @@ "include_associated": "U\u017cyj danych skojarze\u0144 WiFi AP (ignorowane, je\u015bli u\u017cywane s\u0105 dane hotspotu)", "interfaces": "Wybierz interfejsy do skanowania", "scan_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania", - "try_hotspot": "U\u017cyj danych \u201eip hotspot\u201d (najdok\u0142adniejsze)" + "try_hotspot": "U\u017cyj danych \u201eIP hotspot\u201d (najdok\u0142adniejsze)" } } } diff --git a/homeassistant/components/rituals_perfume_genie/translations/et.json b/homeassistant/components/rituals_perfume_genie/translations/et.json new file mode 100644 index 00000000000..17720a59792 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "email": "E-posti aadress", + "password": "Salas\u00f5na" + }, + "title": "Loo \u00fchendus oma Ritualsi kontoga" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rituals_perfume_genie/translations/nl.json b/homeassistant/components/rituals_perfume_genie/translations/nl.json new file mode 100644 index 00000000000..432079cac25 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Wachtwoord" + }, + "title": "Verbind met uw Rituals account" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rituals_perfume_genie/translations/pl.json b/homeassistant/components/rituals_perfume_genie/translations/pl.json new file mode 100644 index 00000000000..9e8c9839cbe --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "email": "Adres e-mail", + "password": "Has\u0142o" + }, + "title": "Po\u0142\u0105czenie z kontem Rituals" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarttub/translations/pl.json b/homeassistant/components/smarttub/translations/pl.json new file mode 100644 index 00000000000..2c3f097d6d0 --- /dev/null +++ b/homeassistant/components/smarttub/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "email": "Adres e-mail", + "password": "Has\u0142o" + }, + "description": "Wprowad\u017a sw\u00f3j adres e-mail SmartTub oraz has\u0142o, aby si\u0119 zalogowa\u0107", + "title": "Logowanie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/ca.json b/homeassistant/components/subaru/translations/ca.json new file mode 100644 index 00000000000..51cd44b4ce2 --- /dev/null +++ b/homeassistant/components/subaru/translations/ca.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja ha estat configurat", + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "error": { + "bad_pin_format": "El PIN ha de tenir 4 d\u00edgits", + "cannot_connect": "Ha fallat la connexi\u00f3", + "incorrect_pin": "PIN incorrecte", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "pin": { + "data": { + "pin": "PIN" + }, + "description": "Introdueix el teu PIN de MySubaru\nNOTA: tots els vehicles associats a un compte han de tenir el mateix PIN", + "title": "Configuraci\u00f3 de Subaru Starlink" + }, + "user": { + "data": { + "country": "Selecciona un pa\u00eds", + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Introdueix les teves credencials de MySubaru\nNOTA: la primera configuraci\u00f3 pot tardar fins a 30 segons", + "title": "Configuraci\u00f3 de Subaru Starlink" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "update_enabled": "Activa el sondeig de vehicle" + }, + "description": "Quan estigui activat, el sondeig de vehicle enviar\u00e0 una ordre al vehicle cada 2 hores per tal d'obtenir noves dades. Sense el sondeig de vehicle, les noves dades nom\u00e9s es rebr\u00e0n quan el vehicle envia autom\u00e0ticament les dades (normalment despr\u00e9s de l'aturada del motor).", + "title": "Opcions de Subaru Starlink" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/en.json b/homeassistant/components/subaru/translations/en.json new file mode 100644 index 00000000000..ea15ff00552 --- /dev/null +++ b/homeassistant/components/subaru/translations/en.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured", + "cannot_connect": "Failed to connect" + }, + "error": { + "bad_pin_format": "PIN should be 4 digits", + "cannot_connect": "Failed to connect", + "incorrect_pin": "Incorrect PIN", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "pin": { + "data": { + "pin": "PIN" + }, + "description": "Please enter your MySubaru PIN\nNOTE: All vehicles in account must have the same PIN", + "title": "Subaru Starlink Configuration" + }, + "user": { + "data": { + "country": "Select country", + "password": "Password", + "username": "Username" + }, + "description": "Please enter your MySubaru credentials\nNOTE: Initial setup may take up to 30 seconds", + "title": "Subaru Starlink Configuration" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "update_enabled": "Enable vehicle polling" + }, + "description": "When enabled, vehicle polling will send a remote command to your vehicle every 2 hours to obtain new sensor data. Without vehicle polling, new sensor data is only received when the vehicle automatically pushes data (normally after engine shutdown).", + "title": "Subaru Starlink Options" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/et.json b/homeassistant/components/subaru/translations/et.json new file mode 100644 index 00000000000..30dd690f849 --- /dev/null +++ b/homeassistant/components/subaru/translations/et.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud", + "cannot_connect": "\u00dchendamine nurjus" + }, + "error": { + "bad_pin_format": "PIN-kood peaks olema 4-kohaline", + "cannot_connect": "\u00dchendamine nurjus", + "incorrect_pin": "Vale PIN-kood", + "invalid_auth": "Vigane autentimine", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "pin": { + "data": { + "pin": "PIN" + }, + "description": "Sisesta oma MySubaru PIN-kood\n M\u00c4RKUS. K\u00f5igil kontol olevatel s\u00f5idukitel peab olema sama PIN-kood", + "title": "Subaru Starlinki konfiguratsioon" + }, + "user": { + "data": { + "country": "Vali riik", + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "Sisesta oma MySubaru mandaat\n M\u00c4RKUS. Esmane seadistamine v\u00f5ib v\u00f5tta kuni 30 sekundit", + "title": "Subaru Starlinki konfiguratsioon" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "update_enabled": "Luba s\u00f5iduki k\u00fcsitlus" + }, + "description": "Kui see on lubatud, saadetakse k\u00fcsitlus ts\u00f5idukile iga kahe tunni j\u00e4rel, et saada uusi anduriandmeid. Ilma s\u00f5iduki valimiseta saadakse uusi anduriandmeid ainult siis, kui s\u00f5iduk automaatselt andmeid edastab (tavaliselt p\u00e4rast mootori seiskamist).", + "title": "Subaru Starlinki valikud" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/pl.json b/homeassistant/components/subaru/translations/pl.json new file mode 100644 index 00000000000..99415cdeea7 --- /dev/null +++ b/homeassistant/components/subaru/translations/pl.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "error": { + "bad_pin_format": "PIN powinien sk\u0142ada\u0107 si\u0119 z 4 cyfr", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "incorrect_pin": "Nieprawid\u0142owy PIN", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "pin": { + "data": { + "pin": "PIN" + }, + "description": "Wprowad\u017a sw\u00f3j PIN dla MySubaru\nUWAGA: Wszystkie pojazdy na koncie musz\u0105 mie\u0107 ten sam kod PIN", + "title": "Konfiguracja Subaru Starlink" + }, + "user": { + "data": { + "country": "Wybierz kraj", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce MySubaru\nUWAGA: Pocz\u0105tkowa konfiguracja mo\u017ce zaj\u0105\u0107 do 30 sekund", + "title": "Konfiguracja Subaru Starlink" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "update_enabled": "W\u0142\u0105cz odpytywanie pojazdu" + }, + "description": "Po w\u0142\u0105czeniu, odpytywanie pojazdu b\u0119dzie co 2 godziny wysy\u0142a\u0107 zdalne polecenie do pojazdu w celu uzyskania nowych danych z czujnika. Bez odpytywania pojazdu, nowe dane z czujnika s\u0105 odbierane tylko wtedy, gdy pojazd automatycznie przesy\u0142a dane (zwykle po wy\u0142\u0105czeniu silnika).", + "title": "Opcje Subaru Starlink" + } + } + } +} \ No newline at end of file From 29c06965375037e445d79f92dfa7d41063020f77 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sun, 21 Feb 2021 20:30:23 -0500 Subject: [PATCH 613/796] Clean up denonavr constants (#46883) --- homeassistant/components/denonavr/__init__.py | 3 +-- homeassistant/components/denonavr/config_flow.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/denonavr/__init__.py b/homeassistant/components/denonavr/__init__.py index 89c6413d146..3946a0d6171 100644 --- a/homeassistant/components/denonavr/__init__.py +++ b/homeassistant/components/denonavr/__init__.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from homeassistant import config_entries, core -from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST +from homeassistant.const import ATTR_COMMAND, ATTR_ENTITY_ID, CONF_HOST from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.dispatcher import dispatcher_send @@ -24,7 +24,6 @@ from .receiver import ConnectDenonAVR CONF_RECEIVER = "receiver" UNDO_UPDATE_LISTENER = "undo_update_listener" SERVICE_GET_COMMAND = "get_command" -ATTR_COMMAND = "command" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/denonavr/config_flow.py b/homeassistant/components/denonavr/config_flow.py index ec0ab4fb177..0b7c0b71847 100644 --- a/homeassistant/components/denonavr/config_flow.py +++ b/homeassistant/components/denonavr/config_flow.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp -from homeassistant.const import CONF_HOST, CONF_MAC +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_TYPE from homeassistant.core import callback from homeassistant.helpers.device_registry import format_mac @@ -25,7 +25,6 @@ IGNORED_MODELS = ["HEOS 1", "HEOS 3", "HEOS 5", "HEOS 7"] CONF_SHOW_ALL_SOURCES = "show_all_sources" CONF_ZONE2 = "zone2" CONF_ZONE3 = "zone3" -CONF_TYPE = "type" CONF_MODEL = "model" CONF_MANUFACTURER = "manufacturer" CONF_SERIAL_NUMBER = "serial_number" From 8330940996ee150b85cb90947fc980043176f856 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sun, 21 Feb 2021 20:31:09 -0500 Subject: [PATCH 614/796] Clean up acer_projector constants (#46880) --- homeassistant/components/acer_projector/switch.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/acer_projector/switch.py b/homeassistant/components/acer_projector/switch.py index f947f3fe0c0..101f7cbd615 100644 --- a/homeassistant/components/acer_projector/switch.py +++ b/homeassistant/components/acer_projector/switch.py @@ -9,6 +9,7 @@ from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity from homeassistant.const import ( CONF_FILENAME, CONF_NAME, + CONF_TIMEOUT, STATE_OFF, STATE_ON, STATE_UNKNOWN, @@ -17,7 +18,6 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_TIMEOUT = "timeout" CONF_WRITE_TIMEOUT = "write_timeout" DEFAULT_NAME = "Acer Projector" @@ -74,7 +74,6 @@ class AcerSwitch(SwitchEntity): def __init__(self, serial_port, name, timeout, write_timeout, **kwargs): """Init of the Acer projector.""" - self.ser = serial.Serial( port=serial_port, timeout=timeout, write_timeout=write_timeout, **kwargs ) @@ -90,7 +89,6 @@ class AcerSwitch(SwitchEntity): def _write_read(self, msg): """Write to the projector and read the return.""" - ret = "" # Sometimes the projector won't answer for no reason or the projector # was disconnected during runtime. From 12c4db076c50f424549400a9690f4d3b429e20eb Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Sun, 21 Feb 2021 19:40:23 -0800 Subject: [PATCH 615/796] Add more sensors to SmartTub integration (#46839) --- homeassistant/components/smarttub/sensor.py | 36 +++++++++++++++++---- tests/components/smarttub/conftest.py | 18 +++++++++++ tests/components/smarttub/test_sensor.py | 24 ++++++++++++-- 3 files changed, 69 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/smarttub/sensor.py b/homeassistant/components/smarttub/sensor.py index 320f288f36a..402fb87373f 100644 --- a/homeassistant/components/smarttub/sensor.py +++ b/homeassistant/components/smarttub/sensor.py @@ -8,23 +8,45 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, entry, async_add_entities): - """Set up climate entity for the thermostat in the tub.""" + """Set up sensor entities for the sensors in the tub.""" controller = hass.data[DOMAIN][entry.entry_id][SMARTTUB_CONTROLLER] - entities = [SmartTubState(controller.coordinator, spa) for spa in controller.spas] + entities = [] + for spa in controller.spas: + entities.extend( + [ + SmartTubSensor(controller.coordinator, spa, "State", "state"), + SmartTubSensor( + controller.coordinator, spa, "Flow Switch", "flowSwitch" + ), + SmartTubSensor(controller.coordinator, spa, "Ozone", "ozone"), + SmartTubSensor( + controller.coordinator, spa, "Blowout Cycle", "blowoutCycle" + ), + SmartTubSensor( + controller.coordinator, spa, "Cleanup Cycle", "cleanupCycle" + ), + ] + ) async_add_entities(entities) -class SmartTubState(SmartTubEntity): - """The state of the spa.""" +class SmartTubSensor(SmartTubEntity): + """Generic and base class for SmartTub sensors.""" - def __init__(self, coordinator, spa): + def __init__(self, coordinator, spa, sensor_name, spa_status_key): """Initialize the entity.""" - super().__init__(coordinator, spa, "state") + super().__init__(coordinator, spa, sensor_name) + self._spa_status_key = spa_status_key + + @property + def _state(self): + """Retrieve the underlying state from the spa.""" + return self.get_spa_status(self._spa_status_key) @property def state(self) -> str: """Return the current state of the sensor.""" - return self.get_spa_status("state").lower() + return self._state.lower() diff --git a/tests/components/smarttub/conftest.py b/tests/components/smarttub/conftest.py index d1bd7c377e3..efba121822d 100644 --- a/tests/components/smarttub/conftest.py +++ b/tests/components/smarttub/conftest.py @@ -47,6 +47,24 @@ def mock_spa(): "water": {"temperature": 38}, "heater": "ON", "state": "NORMAL", + "primaryFiltration": { + "cycle": 1, + "duration": 4, + "lastUpdated": "2021-01-20T11:38:57.014Z", + "mode": "NORMAL", + "startHour": 2, + "status": "INACTIVE", + }, + "secondaryFiltration": { + "lastUpdated": "2020-07-09T19:39:52.961Z", + "mode": "AWAY", + "status": "INACTIVE", + }, + "flowSwitch": "OPEN", + "ozone": "OFF", + "uv": "OFF", + "blowoutCycle": "INACTIVE", + "cleanupCycle": "INACTIVE", } return mock_spa diff --git a/tests/components/smarttub/test_sensor.py b/tests/components/smarttub/test_sensor.py index 7d62440295e..8551ac3cccb 100644 --- a/tests/components/smarttub/test_sensor.py +++ b/tests/components/smarttub/test_sensor.py @@ -3,8 +3,8 @@ from . import trigger_update -async def test_state_update(spa, setup_entry, hass, smarttub_api): - """Test the state entity.""" +async def test_sensors(spa, setup_entry, hass, smarttub_api): + """Test the sensors.""" entity_id = f"sensor.{spa.brand}_{spa.model}_state" state = hass.states.get(entity_id) @@ -16,3 +16,23 @@ async def test_state_update(spa, setup_entry, hass, smarttub_api): state = hass.states.get(entity_id) assert state is not None assert state.state == "bad" + + entity_id = f"sensor.{spa.brand}_{spa.model}_flow_switch" + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "open" + + entity_id = f"sensor.{spa.brand}_{spa.model}_ozone" + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "off" + + entity_id = f"sensor.{spa.brand}_{spa.model}_blowout_cycle" + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "inactive" + + entity_id = f"sensor.{spa.brand}_{spa.model}_cleanup_cycle" + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "inactive" From 0e44d612253b2b4178fedb88c8c3e38d61176392 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Mon, 22 Feb 2021 04:08:00 +0000 Subject: [PATCH 616/796] Add weather platform to template domain (#45031) Co-authored-by: J. Nick Koston --- homeassistant/components/template/const.py | 1 + homeassistant/components/template/weather.py | 220 +++++++++++++++++++ tests/components/template/test_weather.py | 56 +++++ 3 files changed, 277 insertions(+) create mode 100644 homeassistant/components/template/weather.py create mode 100644 tests/components/template/test_weather.py diff --git a/homeassistant/components/template/const.py b/homeassistant/components/template/const.py index cf1ec8bc1c3..5b38f19eaeb 100644 --- a/homeassistant/components/template/const.py +++ b/homeassistant/components/template/const.py @@ -18,4 +18,5 @@ PLATFORMS = [ "sensor", "switch", "vacuum", + "weather", ] diff --git a/homeassistant/components/template/weather.py b/homeassistant/components/template/weather.py new file mode 100644 index 00000000000..560bd5639ba --- /dev/null +++ b/homeassistant/components/template/weather.py @@ -0,0 +1,220 @@ +"""Template platform that aggregates meteorological data.""" +import voluptuous as vol + +from homeassistant.components.weather import ( + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_EXCEPTIONAL, + ATTR_CONDITION_FOG, + ATTR_CONDITION_HAIL, + ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_CONDITION_WINDY, + ATTR_CONDITION_WINDY_VARIANT, + ENTITY_ID_FORMAT, + WeatherEntity, +) +from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.reload import async_setup_reload_service + +from .const import DOMAIN, PLATFORMS +from .template_entity import TemplateEntity + +CONDITION_CLASSES = { + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_FOG, + ATTR_CONDITION_HAIL, + ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_CONDITION_WINDY, + ATTR_CONDITION_WINDY_VARIANT, + ATTR_CONDITION_EXCEPTIONAL, +} + +CONF_WEATHER = "weather" +CONF_TEMPERATURE_TEMPLATE = "temperature_template" +CONF_HUMIDITY_TEMPLATE = "humidity_template" +CONF_CONDITION_TEMPLATE = "condition_template" +CONF_PRESSURE_TEMPLATE = "pressure_template" +CONF_WIND_SPEED_TEMPLATE = "wind_speed_template" +CONF_FORECAST_TEMPLATE = "forecast_template" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_CONDITION_TEMPLATE): cv.template, + vol.Required(CONF_TEMPERATURE_TEMPLATE): cv.template, + vol.Required(CONF_HUMIDITY_TEMPLATE): cv.template, + vol.Optional(CONF_PRESSURE_TEMPLATE): cv.template, + vol.Optional(CONF_WIND_SPEED_TEMPLATE): cv.template, + vol.Optional(CONF_FORECAST_TEMPLATE): cv.template, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Template weather.""" + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + + name = config[CONF_NAME] + condition_template = config[CONF_CONDITION_TEMPLATE] + temperature_template = config[CONF_TEMPERATURE_TEMPLATE] + humidity_template = config[CONF_HUMIDITY_TEMPLATE] + pressure_template = config.get(CONF_PRESSURE_TEMPLATE) + wind_speed_template = config.get(CONF_WIND_SPEED_TEMPLATE) + forecast_template = config.get(CONF_FORECAST_TEMPLATE) + unique_id = config.get(CONF_UNIQUE_ID) + + async_add_entities( + [ + WeatherTemplate( + hass, + name, + condition_template, + temperature_template, + humidity_template, + pressure_template, + wind_speed_template, + forecast_template, + unique_id, + ) + ] + ) + + +class WeatherTemplate(TemplateEntity, WeatherEntity): + """Representation of a weather condition.""" + + def __init__( + self, + hass, + name, + condition_template, + temperature_template, + humidity_template, + pressure_template, + wind_speed_template, + forecast_template, + unique_id, + ): + """Initialize the Demo weather.""" + super().__init__() + + self._name = name + self._condition_template = condition_template + self._temperature_template = temperature_template + self._humidity_template = humidity_template + self._pressure_template = pressure_template + self._wind_speed_template = wind_speed_template + self._forecast_template = forecast_template + self._unique_id = unique_id + + self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, name, hass=hass) + + self._condition = None + self._temperature = None + self._humidity = None + self._pressure = None + self._wind_speed = None + self._forecast = [] + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def condition(self): + """Return the current condition.""" + return self._condition + + @property + def temperature(self): + """Return the temperature.""" + return self._temperature + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return self.hass.config.units.temperature_unit + + @property + def humidity(self): + """Return the humidity.""" + return self._humidity + + @property + def wind_speed(self): + """Return the wind speed.""" + return self._wind_speed + + @property + def pressure(self): + """Return the pressure.""" + return self._pressure + + @property + def forecast(self): + """Return the forecast.""" + return self._forecast + + @property + def attribution(self): + """Return the attribution.""" + return "Powered by Home Assistant" + + @property + def unique_id(self): + """Return the unique id of this light.""" + return self._unique_id + + async def async_added_to_hass(self): + """Register callbacks.""" + + if self._condition_template: + self.add_template_attribute( + "_condition", + self._condition_template, + lambda condition: condition if condition in CONDITION_CLASSES else None, + ) + if self._temperature_template: + self.add_template_attribute( + "_temperature", + self._temperature_template, + ) + if self._humidity_template: + self.add_template_attribute( + "_humidity", + self._humidity_template, + ) + if self._pressure_template: + self.add_template_attribute( + "_pressure", + self._pressure_template, + ) + if self._wind_speed_template: + self.add_template_attribute( + "_wind_speed", + self._wind_speed_template, + ) + if self._forecast_template: + self.add_template_attribute( + "_forecast", + self._forecast_template, + ) + await super().async_added_to_hass() diff --git a/tests/components/template/test_weather.py b/tests/components/template/test_weather.py new file mode 100644 index 00000000000..155c7f33a9f --- /dev/null +++ b/tests/components/template/test_weather.py @@ -0,0 +1,56 @@ +"""The tests for the Template Weather platform.""" +from homeassistant.components.weather import ( + ATTR_WEATHER_HUMIDITY, + ATTR_WEATHER_PRESSURE, + ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_WIND_SPEED, + DOMAIN, +) +from homeassistant.setup import async_setup_component + + +async def test_template_state_text(hass): + """Test the state text of a template.""" + await async_setup_component( + hass, + DOMAIN, + { + "weather": [ + {"weather": {"platform": "demo"}}, + { + "platform": "template", + "name": "test", + "condition_template": "sunny", + "forecast_template": "{{ states.weather.demo.attributes.forecast }}", + "temperature_template": "{{ states('sensor.temperature') | float }}", + "humidity_template": "{{ states('sensor.humidity') | int }}", + "pressure_template": "{{ states('sensor.pressure') }}", + "wind_speed_template": "{{ states('sensor.windspeed') }}", + }, + ] + }, + ) + await hass.async_block_till_done() + + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("sensor.temperature", 22.3) + await hass.async_block_till_done() + hass.states.async_set("sensor.humidity", 60) + await hass.async_block_till_done() + hass.states.async_set("sensor.pressure", 1000) + await hass.async_block_till_done() + hass.states.async_set("sensor.windspeed", 20) + await hass.async_block_till_done() + + state = hass.states.get("weather.test") + assert state is not None + + assert state.state == "sunny" + + data = state.attributes + assert data.get(ATTR_WEATHER_TEMPERATURE) == 22.3 + assert data.get(ATTR_WEATHER_HUMIDITY) == 60 + assert data.get(ATTR_WEATHER_PRESSURE) == 1000 + assert data.get(ATTR_WEATHER_WIND_SPEED) == 20 From d32dbc4cdd6090af3aead56470d6db6c26672b05 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Sun, 21 Feb 2021 20:46:54 -0800 Subject: [PATCH 617/796] Add support for SmartTub heat modes (#46876) --- homeassistant/components/smarttub/climate.py | 43 ++++++++++++++++---- tests/components/smarttub/conftest.py | 1 + tests/components/smarttub/test_climate.py | 26 +++++++++++- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/smarttub/climate.py b/homeassistant/components/smarttub/climate.py index 02d627d383e..ee6afc80fb1 100644 --- a/homeassistant/components/smarttub/climate.py +++ b/homeassistant/components/smarttub/climate.py @@ -6,6 +6,9 @@ from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, HVAC_MODE_HEAT, + PRESET_ECO, + PRESET_NONE, + SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS @@ -16,6 +19,8 @@ from .entity import SmartTubEntity _LOGGER = logging.getLogger(__name__) +PRESET_DAY = "day" + async def async_setup_entry(hass, entry, async_add_entities): """Set up climate entity for the thermostat in the tub.""" @@ -32,6 +37,19 @@ async def async_setup_entry(hass, entry, async_add_entities): class SmartTubThermostat(SmartTubEntity, ClimateEntity): """The target water temperature for the spa.""" + PRESET_MODES = { + "AUTO": PRESET_NONE, + "ECO": PRESET_ECO, + "DAY": PRESET_DAY, + } + + HEAT_MODES = {v: k for k, v in PRESET_MODES.items()} + + HVAC_ACTIONS = { + "OFF": CURRENT_HVAC_IDLE, + "ON": CURRENT_HVAC_HEAT, + } + def __init__(self, coordinator, spa): """Initialize the entity.""" super().__init__(coordinator, spa, "thermostat") @@ -44,12 +62,7 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity): @property def hvac_action(self): """Return the current running hvac operation.""" - heater_status = self.get_spa_status("heater") - if heater_status == "ON": - return CURRENT_HVAC_HEAT - if heater_status == "OFF": - return CURRENT_HVAC_IDLE - return None + return self.HVAC_ACTIONS.get(self.get_spa_status("heater")) @property def hvac_modes(self): @@ -92,7 +105,17 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity): Only target temperature is supported. """ - return SUPPORT_TARGET_TEMPERATURE + return SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE + + @property + def preset_mode(self): + """Return the current preset mode.""" + return self.PRESET_MODES[self.get_spa_status("heatMode")] + + @property + def preset_modes(self): + """Return the available preset modes.""" + return list(self.PRESET_MODES.values()) @property def current_temperature(self): @@ -109,3 +132,9 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity): temperature = kwargs[ATTR_TEMPERATURE] await self.spa.set_temperature(temperature) await self.coordinator.async_refresh() + + async def async_set_preset_mode(self, preset_mode: str): + """Activate the specified preset mode.""" + heat_mode = self.HEAT_MODES[preset_mode] + await self.spa.set_heat_mode(heat_mode) + await self.coordinator.async_refresh() diff --git a/tests/components/smarttub/conftest.py b/tests/components/smarttub/conftest.py index efba121822d..265afcfc24c 100644 --- a/tests/components/smarttub/conftest.py +++ b/tests/components/smarttub/conftest.py @@ -46,6 +46,7 @@ def mock_spa(): "setTemperature": 39, "water": {"temperature": 38}, "heater": "ON", + "heatMode": "AUTO", "state": "NORMAL", "primaryFiltration": { "cycle": 1, diff --git a/tests/components/smarttub/test_climate.py b/tests/components/smarttub/test_climate.py index 69fb642aab4..118264183e8 100644 --- a/tests/components/smarttub/test_climate.py +++ b/tests/components/smarttub/test_climate.py @@ -9,12 +9,18 @@ from homeassistant.components.climate.const import ( ATTR_HVAC_MODES, ATTR_MAX_TEMP, ATTR_MIN_TEMP, + ATTR_PRESET_MODE, + ATTR_PRESET_MODES, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, DOMAIN as CLIMATE_DOMAIN, HVAC_MODE_HEAT, + PRESET_ECO, + PRESET_NONE, SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, SERVICE_SET_TEMPERATURE, + SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.components.smarttub.const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP @@ -44,11 +50,15 @@ async def test_thermostat_update(spa, setup_entry, hass): assert set(state.attributes[ATTR_HVAC_MODES]) == {HVAC_MODE_HEAT} assert state.state == HVAC_MODE_HEAT - assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_TARGET_TEMPERATURE + assert ( + state.attributes[ATTR_SUPPORTED_FEATURES] + == SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE + ) assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 38 assert state.attributes[ATTR_TEMPERATURE] == 39 assert state.attributes[ATTR_MAX_TEMP] == DEFAULT_MAX_TEMP assert state.attributes[ATTR_MIN_TEMP] == DEFAULT_MIN_TEMP + assert state.attributes[ATTR_PRESET_MODES] == ["none", "eco", "day"] await hass.services.async_call( CLIMATE_DOMAIN, @@ -66,6 +76,20 @@ async def test_thermostat_update(spa, setup_entry, hass): ) # does nothing + assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_NONE + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_ECO}, + blocking=True, + ) + spa.set_heat_mode.assert_called_with("ECO") + + spa.get_status.return_value["heatMode"] = "ECO" + await trigger_update(hass) + state = hass.states.get(entity_id) + assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_ECO + spa.get_status.side_effect = smarttub.APIError await trigger_update(hass) # should not fail From a8be5be37666d5a7b7cee27247c36aadc14dcfb2 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Sun, 21 Feb 2021 20:48:27 -0800 Subject: [PATCH 618/796] Add switch platform and pump entity to SmartTub (#46842) --- homeassistant/components/smarttub/__init__.py | 2 +- homeassistant/components/smarttub/const.py | 1 + .../components/smarttub/controller.py | 9 +- .../components/smarttub/manifest.json | 2 +- homeassistant/components/smarttub/switch.py | 82 +++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/smarttub/conftest.py | 23 +++++- tests/components/smarttub/test_sensor.py | 2 +- tests/components/smarttub/test_switch.py | 38 +++++++++ 10 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/smarttub/switch.py create mode 100644 tests/components/smarttub/test_switch.py diff --git a/homeassistant/components/smarttub/__init__.py b/homeassistant/components/smarttub/__init__.py index 2b80c92510f..e8fc9989d38 100644 --- a/homeassistant/components/smarttub/__init__.py +++ b/homeassistant/components/smarttub/__init__.py @@ -7,7 +7,7 @@ from .controller import SmartTubController _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["climate", "sensor"] +PLATFORMS = ["climate", "sensor", "switch"] async def async_setup(hass, config): diff --git a/homeassistant/components/smarttub/const.py b/homeassistant/components/smarttub/const.py index 99e7f21f86d..8292c5ef826 100644 --- a/homeassistant/components/smarttub/const.py +++ b/homeassistant/components/smarttub/const.py @@ -9,6 +9,7 @@ SMARTTUB_CONTROLLER = "smarttub_controller" SCAN_INTERVAL = 60 POLLING_TIMEOUT = 10 +API_TIMEOUT = 5 DEFAULT_MIN_TEMP = 18.5 DEFAULT_MAX_TEMP = 40 diff --git a/homeassistant/components/smarttub/controller.py b/homeassistant/components/smarttub/controller.py index ad40c94fbed..feb13066b44 100644 --- a/homeassistant/components/smarttub/controller.py +++ b/homeassistant/components/smarttub/controller.py @@ -86,7 +86,14 @@ class SmartTubController: return data async def _get_spa_data(self, spa): - return {"status": await spa.get_status()} + status, pumps = await asyncio.gather( + spa.get_status(), + spa.get_pumps(), + ) + return { + "status": status, + "pumps": {pump.id: pump for pump in pumps}, + } async def async_register_devices(self, entry): """Register devices with the device registry for all spas.""" diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json index 9735a3753b4..9360c59da8b 100644 --- a/homeassistant/components/smarttub/manifest.json +++ b/homeassistant/components/smarttub/manifest.json @@ -6,7 +6,7 @@ "dependencies": [], "codeowners": ["@mdz"], "requirements": [ - "python-smarttub==0.0.6" + "python-smarttub==0.0.12" ], "quality_scale": "platinum" } diff --git a/homeassistant/components/smarttub/switch.py b/homeassistant/components/smarttub/switch.py new file mode 100644 index 00000000000..7e4c83f6feb --- /dev/null +++ b/homeassistant/components/smarttub/switch.py @@ -0,0 +1,82 @@ +"""Platform for switch integration.""" +import logging + +import async_timeout +from smarttub import SpaPump + +from homeassistant.components.switch import SwitchEntity + +from .const import API_TIMEOUT, DOMAIN, SMARTTUB_CONTROLLER +from .entity import SmartTubEntity +from .helpers import get_spa_name + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up switch entities for the pumps on the tub.""" + + controller = hass.data[DOMAIN][entry.entry_id][SMARTTUB_CONTROLLER] + + entities = [ + SmartTubPump(controller.coordinator, pump) + for spa in controller.spas + for pump in await spa.get_pumps() + ] + + async_add_entities(entities) + + +class SmartTubPump(SmartTubEntity, SwitchEntity): + """A pump on a spa.""" + + def __init__(self, coordinator, pump: SpaPump): + """Initialize the entity.""" + super().__init__(coordinator, pump.spa, "pump") + self.pump_id = pump.id + self.pump_type = pump.type + + @property + def pump(self) -> SpaPump: + """Return the underlying SpaPump object for this entity.""" + return self.coordinator.data[self.spa.id]["pumps"][self.pump_id] + + @property + def unique_id(self) -> str: + """Return a unique ID for this pump entity.""" + return f"{super().unique_id}-{self.pump_id}" + + @property + def name(self) -> str: + """Return a name for this pump entity.""" + spa_name = get_spa_name(self.spa) + if self.pump_type == SpaPump.PumpType.CIRCULATION: + return f"{spa_name} Circulation Pump" + if self.pump_type == SpaPump.PumpType.JET: + return f"{spa_name} Jet {self.pump_id}" + return f"{spa_name} pump {self.pump_id}" + + @property + def is_on(self) -> bool: + """Return True if the pump is on.""" + return self.pump.state != SpaPump.PumpState.OFF + + async def async_turn_on(self, **kwargs) -> None: + """Turn the pump on.""" + + # the API only supports toggling + if not self.is_on: + await self.async_toggle() + + async def async_turn_off(self, **kwargs) -> None: + """Turn the pump off.""" + + # the API only supports toggling + if self.is_on: + await self.async_toggle() + + async def async_toggle(self, **kwargs) -> None: + """Toggle the pump on or off.""" + async with async_timeout.timeout(API_TIMEOUT): + await self.pump.toggle() + await self.coordinator.async_request_refresh() diff --git a/requirements_all.txt b/requirements_all.txt index 9ceb668bc58..d8738946c19 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1817,7 +1817,7 @@ python-qbittorrent==0.4.2 python-ripple-api==0.0.3 # homeassistant.components.smarttub -python-smarttub==0.0.6 +python-smarttub==0.0.12 # homeassistant.components.sochain python-sochain-api==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d37ee25cbbd..27f7475328e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -942,7 +942,7 @@ python-nest==4.1.0 python-openzwave-mqtt[mqtt-client]==1.4.0 # homeassistant.components.smarttub -python-smarttub==0.0.6 +python-smarttub==0.0.12 # homeassistant.components.songpal python-songpal==0.12 diff --git a/tests/components/smarttub/conftest.py b/tests/components/smarttub/conftest.py index 265afcfc24c..fe1ca465f07 100644 --- a/tests/components/smarttub/conftest.py +++ b/tests/components/smarttub/conftest.py @@ -36,7 +36,7 @@ async def setup_component(hass): @pytest.fixture(name="spa") def mock_spa(): - """Mock a SmartTub.Spa.""" + """Mock a smarttub.Spa.""" mock_spa = create_autospec(smarttub.Spa, instance=True) mock_spa.id = "mockspa1" @@ -67,6 +67,27 @@ def mock_spa(): "blowoutCycle": "INACTIVE", "cleanupCycle": "INACTIVE", } + + mock_circulation_pump = create_autospec(smarttub.SpaPump, instance=True) + mock_circulation_pump.id = "CP" + mock_circulation_pump.spa = mock_spa + mock_circulation_pump.state = smarttub.SpaPump.PumpState.OFF + mock_circulation_pump.type = smarttub.SpaPump.PumpType.CIRCULATION + + mock_jet_off = create_autospec(smarttub.SpaPump, instance=True) + mock_jet_off.id = "P1" + mock_jet_off.spa = mock_spa + mock_jet_off.state = smarttub.SpaPump.PumpState.OFF + mock_jet_off.type = smarttub.SpaPump.PumpType.JET + + mock_jet_on = create_autospec(smarttub.SpaPump, instance=True) + mock_jet_on.id = "P2" + mock_jet_on.spa = mock_spa + mock_jet_on.state = smarttub.SpaPump.PumpState.HIGH + mock_jet_on.type = smarttub.SpaPump.PumpType.JET + + mock_spa.get_pumps.return_value = [mock_circulation_pump, mock_jet_off, mock_jet_on] + return mock_spa diff --git a/tests/components/smarttub/test_sensor.py b/tests/components/smarttub/test_sensor.py index 8551ac3cccb..8e0cbf64abc 100644 --- a/tests/components/smarttub/test_sensor.py +++ b/tests/components/smarttub/test_sensor.py @@ -3,7 +3,7 @@ from . import trigger_update -async def test_sensors(spa, setup_entry, hass, smarttub_api): +async def test_sensors(spa, setup_entry, hass): """Test the sensors.""" entity_id = f"sensor.{spa.brand}_{spa.model}_state" diff --git a/tests/components/smarttub/test_switch.py b/tests/components/smarttub/test_switch.py new file mode 100644 index 00000000000..8750bf79747 --- /dev/null +++ b/tests/components/smarttub/test_switch.py @@ -0,0 +1,38 @@ +"""Test the SmartTub switch platform.""" + +from smarttub import SpaPump + + +async def test_pumps(spa, setup_entry, hass): + """Test pump entities.""" + + for pump in spa.get_pumps.return_value: + if pump.type == SpaPump.PumpType.CIRCULATION: + entity_id = f"switch.{spa.brand}_{spa.model}_circulation_pump" + elif pump.type == SpaPump.PumpType.JET: + entity_id = f"switch.{spa.brand}_{spa.model}_jet_{pump.id.lower()}" + else: + raise NotImplementedError("Unknown pump type") + + state = hass.states.get(entity_id) + assert state is not None + if pump.state == SpaPump.PumpState.OFF: + assert state.state == "off" + + await hass.services.async_call( + "switch", + "turn_on", + {"entity_id": entity_id}, + blocking=True, + ) + pump.toggle.assert_called() + else: + assert state.state == "on" + + await hass.services.async_call( + "switch", + "turn_off", + {"entity_id": entity_id}, + blocking=True, + ) + pump.toggle.assert_called() From b6b1e725c75e641d80d20dd6f02f34e8a7906e06 Mon Sep 17 00:00:00 2001 From: Jonathan Keslin Date: Sun, 21 Feb 2021 21:16:13 -0800 Subject: [PATCH 619/796] Add support for VeSync dimmer switches (#44713) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + homeassistant/components/vesync/__init__.py | 20 ++++- homeassistant/components/vesync/common.py | 7 +- homeassistant/components/vesync/const.py | 1 + homeassistant/components/vesync/light.py | 85 +++++++++++++++++++++ 5 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/vesync/light.py diff --git a/.coveragerc b/.coveragerc index 2b71ba546cc..d66f4032f74 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1042,6 +1042,7 @@ omit = homeassistant/components/vesync/common.py homeassistant/components/vesync/const.py homeassistant/components/vesync/fan.py + homeassistant/components/vesync/light.py homeassistant/components/vesync/switch.py homeassistant/components/viaggiatreno/sensor.py homeassistant/components/vicare/* diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 94a0d5c2f25..24bd0f000df 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -18,11 +18,12 @@ from .const import ( VS_DISCOVERY, VS_DISPATCHERS, VS_FANS, + VS_LIGHTS, VS_MANAGER, VS_SWITCHES, ) -PLATFORMS = ["switch", "fan"] +PLATFORMS = ["switch", "fan", "light"] _LOGGER = logging.getLogger(__name__) @@ -85,6 +86,7 @@ async def async_setup_entry(hass, config_entry): switches = hass.data[DOMAIN][VS_SWITCHES] = [] fans = hass.data[DOMAIN][VS_FANS] = [] + lights = hass.data[DOMAIN][VS_LIGHTS] = [] hass.data[DOMAIN][VS_DISPATCHERS] = [] @@ -96,15 +98,21 @@ async def async_setup_entry(hass, config_entry): fans.extend(device_dict[VS_FANS]) hass.async_create_task(forward_setup(config_entry, "fan")) + if device_dict[VS_LIGHTS]: + lights.extend(device_dict[VS_LIGHTS]) + hass.async_create_task(forward_setup(config_entry, "light")) + async def async_new_device_discovery(service): """Discover if new devices should be added.""" manager = hass.data[DOMAIN][VS_MANAGER] switches = hass.data[DOMAIN][VS_SWITCHES] fans = hass.data[DOMAIN][VS_FANS] + lights = hass.data[DOMAIN][VS_LIGHTS] dev_dict = await async_process_devices(hass, manager) switch_devs = dev_dict.get(VS_SWITCHES, []) fan_devs = dev_dict.get(VS_FANS, []) + light_devs = dev_dict.get(VS_LIGHTS, []) switch_set = set(switch_devs) new_switches = list(switch_set.difference(switches)) @@ -126,6 +134,16 @@ async def async_setup_entry(hass, config_entry): fans.extend(new_fans) hass.async_create_task(forward_setup(config_entry, "fan")) + light_set = set(light_devs) + new_lights = list(light_set.difference(lights)) + if new_lights and lights: + lights.extend(new_lights) + async_dispatcher_send(hass, VS_DISCOVERY.format(VS_LIGHTS), new_lights) + return + if new_lights and not lights: + lights.extend(new_lights) + hass.async_create_task(forward_setup(config_entry, "light")) + hass.services.async_register( DOMAIN, SERVICE_UPDATE_DEVS, async_new_device_discovery ) diff --git a/homeassistant/components/vesync/common.py b/homeassistant/components/vesync/common.py index 42e3516f085..240a5e48287 100644 --- a/homeassistant/components/vesync/common.py +++ b/homeassistant/components/vesync/common.py @@ -3,7 +3,7 @@ import logging from homeassistant.helpers.entity import ToggleEntity -from .const import VS_FANS, VS_SWITCHES +from .const import VS_FANS, VS_LIGHTS, VS_SWITCHES _LOGGER = logging.getLogger(__name__) @@ -13,6 +13,7 @@ async def async_process_devices(hass, manager): devices = {} devices[VS_SWITCHES] = [] devices[VS_FANS] = [] + devices[VS_LIGHTS] = [] await hass.async_add_executor_job(manager.update) @@ -28,7 +29,9 @@ async def async_process_devices(hass, manager): for switch in manager.switches: if not switch.is_dimmable(): devices[VS_SWITCHES].append(switch) - _LOGGER.info("%d VeSync standard switches found", len(manager.switches)) + else: + devices[VS_LIGHTS].append(switch) + _LOGGER.info("%d VeSync switches found", len(manager.switches)) return devices diff --git a/homeassistant/components/vesync/const.py b/homeassistant/components/vesync/const.py index 9923ab94ecf..5d9dfc8aa5d 100644 --- a/homeassistant/components/vesync/const.py +++ b/homeassistant/components/vesync/const.py @@ -7,4 +7,5 @@ SERVICE_UPDATE_DEVS = "update_devices" VS_SWITCHES = "switches" VS_FANS = "fans" +VS_LIGHTS = "lights" VS_MANAGER = "manager" diff --git a/homeassistant/components/vesync/light.py b/homeassistant/components/vesync/light.py new file mode 100644 index 00000000000..53dfdc5f0a9 --- /dev/null +++ b/homeassistant/components/vesync/light.py @@ -0,0 +1,85 @@ +"""Support for VeSync dimmers.""" +import logging + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + SUPPORT_BRIGHTNESS, + LightEntity, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .common import VeSyncDevice +from .const import DOMAIN, VS_DISCOVERY, VS_DISPATCHERS, VS_LIGHTS + +_LOGGER = logging.getLogger(__name__) + +DEV_TYPE_TO_HA = { + "ESD16": "light", + "ESWD16": "light", +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up lights.""" + + async def async_discover(devices): + """Add new devices to platform.""" + _async_setup_entities(devices, async_add_entities) + + disp = async_dispatcher_connect( + hass, VS_DISCOVERY.format(VS_LIGHTS), async_discover + ) + hass.data[DOMAIN][VS_DISPATCHERS].append(disp) + + _async_setup_entities(hass.data[DOMAIN][VS_LIGHTS], async_add_entities) + return True + + +@callback +def _async_setup_entities(devices, async_add_entities): + """Check if device is online and add entity.""" + dev_list = [] + for dev in devices: + if DEV_TYPE_TO_HA.get(dev.device_type) == "light": + dev_list.append(VeSyncDimmerHA(dev)) + else: + _LOGGER.debug( + "%s - Unknown device type - %s", dev.device_name, dev.device_type + ) + continue + + async_add_entities(dev_list, update_before_add=True) + + +class VeSyncDimmerHA(VeSyncDevice, LightEntity): + """Representation of a VeSync dimmer.""" + + def __init__(self, dimmer): + """Initialize the VeSync dimmer device.""" + super().__init__(dimmer) + self.dimmer = dimmer + + def turn_on(self, **kwargs): + """Turn the device on.""" + if ATTR_BRIGHTNESS in kwargs: + # get brightness from HA data + brightness = int(kwargs[ATTR_BRIGHTNESS]) + # convert to percent that vesync api expects + brightness = round((brightness / 255) * 100) + # clamp to 1-100 + brightness = max(1, min(brightness, 100)) + self.dimmer.set_brightness(brightness) + # Avoid turning device back on if this is just a brightness adjustment + if not self.is_on: + self.device.turn_on() + + @property + def supported_features(self): + """Get supported features for this entity.""" + return SUPPORT_BRIGHTNESS + + @property + def brightness(self): + """Get dimmer brightness.""" + return round((int(self.dimmer.brightness) / 100) * 255) From b1a24c8bbbd93e28a5beb33da1784516166158e2 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 22 Feb 2021 06:34:45 +0100 Subject: [PATCH 620/796] Log the name of automations with condition errors (#46854) --- homeassistant/components/automation/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 7e07f35be45..7f006d929b1 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -568,7 +568,7 @@ async def _async_process_config( ) if CONF_CONDITION in config_block: - cond_func = await _async_process_if(hass, config, config_block) + cond_func = await _async_process_if(hass, name, config, config_block) if cond_func is None: continue @@ -606,7 +606,7 @@ async def _async_process_config( return blueprints_used -async def _async_process_if(hass, config, p_config): +async def _async_process_if(hass, name, config, p_config): """Process if checks.""" if_configs = p_config[CONF_CONDITION] @@ -634,7 +634,8 @@ async def _async_process_if(hass, config, p_config): if errors: LOGGER.warning( - "Error evaluating condition:\n%s", + "Error evaluating condition in '%s':\n%s", + name, ConditionErrorContainer("condition", errors=errors), ) return False From 5d8390fd9b6a33441f99104cda60d6b2efbb1427 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Sun, 21 Feb 2021 21:36:50 -0800 Subject: [PATCH 621/796] Add support for SmartTub filtration cycles (#46868) --- homeassistant/components/smarttub/climate.py | 16 +++-- homeassistant/components/smarttub/entity.py | 18 ++--- .../components/smarttub/manifest.json | 2 +- homeassistant/components/smarttub/sensor.py | 72 +++++++++++++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/smarttub/conftest.py | 52 +++++++------- tests/components/smarttub/test_climate.py | 6 +- tests/components/smarttub/test_sensor.py | 18 ++++- 9 files changed, 129 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/smarttub/climate.py b/homeassistant/components/smarttub/climate.py index ee6afc80fb1..66c03a22e1f 100644 --- a/homeassistant/components/smarttub/climate.py +++ b/homeassistant/components/smarttub/climate.py @@ -1,6 +1,8 @@ """Platform for climate integration.""" import logging +from smarttub import Spa + from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, @@ -38,9 +40,9 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity): """The target water temperature for the spa.""" PRESET_MODES = { - "AUTO": PRESET_NONE, - "ECO": PRESET_ECO, - "DAY": PRESET_DAY, + Spa.HeatMode.AUTO: PRESET_NONE, + Spa.HeatMode.ECONOMY: PRESET_ECO, + Spa.HeatMode.DAY: PRESET_DAY, } HEAT_MODES = {v: k for k, v in PRESET_MODES.items()} @@ -62,7 +64,7 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity): @property def hvac_action(self): """Return the current running hvac operation.""" - return self.HVAC_ACTIONS.get(self.get_spa_status("heater")) + return self.HVAC_ACTIONS.get(self.spa_status.heater) @property def hvac_modes(self): @@ -110,7 +112,7 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity): @property def preset_mode(self): """Return the current preset mode.""" - return self.PRESET_MODES[self.get_spa_status("heatMode")] + return self.PRESET_MODES[self.spa_status.heat_mode] @property def preset_modes(self): @@ -120,12 +122,12 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity): @property def current_temperature(self): """Return the current water temperature.""" - return self.get_spa_status("water.temperature") + return self.spa_status.water.temperature @property def target_temperature(self): """Return the target water temperature.""" - return self.get_spa_status("setTemperature") + return self.spa_status.set_temperature async def async_set_temperature(self, **kwargs): """Set new target temperature.""" diff --git a/homeassistant/components/smarttub/entity.py b/homeassistant/components/smarttub/entity.py index 0e84c92e3e1..eab60b4162c 100644 --- a/homeassistant/components/smarttub/entity.py +++ b/homeassistant/components/smarttub/entity.py @@ -52,18 +52,8 @@ class SmartTubEntity(CoordinatorEntity): spa_name = get_spa_name(self.spa) return f"{spa_name} {self._entity_type}" - def get_spa_status(self, path): - """Retrieve a value from the data returned by Spa.get_status(). + @property + def spa_status(self) -> smarttub.SpaState: + """Retrieve the result of Spa.get_status().""" - Nested keys can be specified by a dotted path, e.g. - status['foo']['bar'] is 'foo.bar'. - """ - - status = self.coordinator.data[self.spa.id].get("status") - if status is None: - return None - - for key in path.split("."): - status = status[key] - - return status + return self.coordinator.data[self.spa.id].get("status") diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json index 9360c59da8b..292ce81b4fb 100644 --- a/homeassistant/components/smarttub/manifest.json +++ b/homeassistant/components/smarttub/manifest.json @@ -6,7 +6,7 @@ "dependencies": [], "codeowners": ["@mdz"], "requirements": [ - "python-smarttub==0.0.12" + "python-smarttub==0.0.17" ], "quality_scale": "platinum" } diff --git a/homeassistant/components/smarttub/sensor.py b/homeassistant/components/smarttub/sensor.py index 402fb87373f..54921596fb2 100644 --- a/homeassistant/components/smarttub/sensor.py +++ b/homeassistant/components/smarttub/sensor.py @@ -1,4 +1,5 @@ """Platform for sensor integration.""" +from enum import Enum import logging from .const import DOMAIN, SMARTTUB_CONTROLLER @@ -6,6 +7,11 @@ from .entity import SmartTubEntity _LOGGER = logging.getLogger(__name__) +ATTR_DURATION = "duration" +ATTR_LAST_UPDATED = "last_updated" +ATTR_MODE = "mode" +ATTR_START_HOUR = "start_hour" + async def async_setup_entry(hass, entry, async_add_entities): """Set up sensor entities for the sensors in the tub.""" @@ -18,15 +24,17 @@ async def async_setup_entry(hass, entry, async_add_entities): [ SmartTubSensor(controller.coordinator, spa, "State", "state"), SmartTubSensor( - controller.coordinator, spa, "Flow Switch", "flowSwitch" + controller.coordinator, spa, "Flow Switch", "flow_switch" ), SmartTubSensor(controller.coordinator, spa, "Ozone", "ozone"), SmartTubSensor( - controller.coordinator, spa, "Blowout Cycle", "blowoutCycle" + controller.coordinator, spa, "Blowout Cycle", "blowout_cycle" ), SmartTubSensor( - controller.coordinator, spa, "Cleanup Cycle", "cleanupCycle" + controller.coordinator, spa, "Cleanup Cycle", "cleanup_cycle" ), + SmartTubPrimaryFiltrationCycle(controller.coordinator, spa), + SmartTubSecondaryFiltrationCycle(controller.coordinator, spa), ] ) @@ -36,17 +44,69 @@ async def async_setup_entry(hass, entry, async_add_entities): class SmartTubSensor(SmartTubEntity): """Generic and base class for SmartTub sensors.""" - def __init__(self, coordinator, spa, sensor_name, spa_status_key): + def __init__(self, coordinator, spa, sensor_name, attr_name): """Initialize the entity.""" super().__init__(coordinator, spa, sensor_name) - self._spa_status_key = spa_status_key + self._attr_name = attr_name @property def _state(self): """Retrieve the underlying state from the spa.""" - return self.get_spa_status(self._spa_status_key) + return getattr(self.spa_status, self._attr_name) @property def state(self) -> str: """Return the current state of the sensor.""" + if isinstance(self._state, Enum): + return self._state.name.lower() return self._state.lower() + + +class SmartTubPrimaryFiltrationCycle(SmartTubSensor): + """The primary filtration cycle.""" + + def __init__(self, coordinator, spa): + """Initialize the entity.""" + super().__init__( + coordinator, spa, "primary filtration cycle", "primary_filtration" + ) + + @property + def state(self) -> str: + """Return the current state of the sensor.""" + return self._state.status.name.lower() + + @property + def device_state_attributes(self): + """Return the state attributes.""" + state = self._state + return { + ATTR_DURATION: state.duration, + ATTR_LAST_UPDATED: state.last_updated.isoformat(), + ATTR_MODE: state.mode.name.lower(), + ATTR_START_HOUR: state.start_hour, + } + + +class SmartTubSecondaryFiltrationCycle(SmartTubSensor): + """The secondary filtration cycle.""" + + def __init__(self, coordinator, spa): + """Initialize the entity.""" + super().__init__( + coordinator, spa, "Secondary Filtration Cycle", "secondary_filtration" + ) + + @property + def state(self) -> str: + """Return the current state of the sensor.""" + return self._state.status.name.lower() + + @property + def device_state_attributes(self): + """Return the state attributes.""" + state = self._state + return { + ATTR_LAST_UPDATED: state.last_updated.isoformat(), + ATTR_MODE: state.mode.name.lower(), + } diff --git a/requirements_all.txt b/requirements_all.txt index d8738946c19..0d8203279b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1817,7 +1817,7 @@ python-qbittorrent==0.4.2 python-ripple-api==0.0.3 # homeassistant.components.smarttub -python-smarttub==0.0.12 +python-smarttub==0.0.17 # homeassistant.components.sochain python-sochain-api==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27f7475328e..99a0351ee37 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -942,7 +942,7 @@ python-nest==4.1.0 python-openzwave-mqtt[mqtt-client]==1.4.0 # homeassistant.components.smarttub -python-smarttub==0.0.12 +python-smarttub==0.0.17 # homeassistant.components.songpal python-songpal==0.12 diff --git a/tests/components/smarttub/conftest.py b/tests/components/smarttub/conftest.py index fe1ca465f07..ad962ba0474 100644 --- a/tests/components/smarttub/conftest.py +++ b/tests/components/smarttub/conftest.py @@ -42,32 +42,34 @@ def mock_spa(): mock_spa.id = "mockspa1" mock_spa.brand = "mockbrand1" mock_spa.model = "mockmodel1" - mock_spa.get_status.return_value = { - "setTemperature": 39, - "water": {"temperature": 38}, - "heater": "ON", - "heatMode": "AUTO", - "state": "NORMAL", - "primaryFiltration": { - "cycle": 1, - "duration": 4, - "lastUpdated": "2021-01-20T11:38:57.014Z", - "mode": "NORMAL", - "startHour": 2, - "status": "INACTIVE", + mock_spa.get_status.return_value = smarttub.SpaState( + mock_spa, + **{ + "setTemperature": 39, + "water": {"temperature": 38}, + "heater": "ON", + "heatMode": "AUTO", + "state": "NORMAL", + "primaryFiltration": { + "cycle": 1, + "duration": 4, + "lastUpdated": "2021-01-20T11:38:57.014Z", + "mode": "NORMAL", + "startHour": 2, + "status": "INACTIVE", + }, + "secondaryFiltration": { + "lastUpdated": "2020-07-09T19:39:52.961Z", + "mode": "AWAY", + "status": "INACTIVE", + }, + "flowSwitch": "OPEN", + "ozone": "OFF", + "uv": "OFF", + "blowoutCycle": "INACTIVE", + "cleanupCycle": "INACTIVE", }, - "secondaryFiltration": { - "lastUpdated": "2020-07-09T19:39:52.961Z", - "mode": "AWAY", - "status": "INACTIVE", - }, - "flowSwitch": "OPEN", - "ozone": "OFF", - "uv": "OFF", - "blowoutCycle": "INACTIVE", - "cleanupCycle": "INACTIVE", - } - + ) mock_circulation_pump = create_autospec(smarttub.SpaPump, instance=True) mock_circulation_pump.id = "CP" mock_circulation_pump.spa = mock_spa diff --git a/tests/components/smarttub/test_climate.py b/tests/components/smarttub/test_climate.py index 118264183e8..a034a4ce17e 100644 --- a/tests/components/smarttub/test_climate.py +++ b/tests/components/smarttub/test_climate.py @@ -42,7 +42,7 @@ async def test_thermostat_update(spa, setup_entry, hass): assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT - spa.get_status.return_value["heater"] = "OFF" + spa.get_status.return_value.heater = "OFF" await trigger_update(hass) state = hass.states.get(entity_id) @@ -83,9 +83,9 @@ async def test_thermostat_update(spa, setup_entry, hass): {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_ECO}, blocking=True, ) - spa.set_heat_mode.assert_called_with("ECO") + spa.set_heat_mode.assert_called_with(smarttub.Spa.HeatMode.ECONOMY) - spa.get_status.return_value["heatMode"] = "ECO" + spa.get_status.return_value.heat_mode = smarttub.Spa.HeatMode.ECONOMY await trigger_update(hass) state = hass.states.get(entity_id) assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_ECO diff --git a/tests/components/smarttub/test_sensor.py b/tests/components/smarttub/test_sensor.py index 8e0cbf64abc..7ef3062894a 100644 --- a/tests/components/smarttub/test_sensor.py +++ b/tests/components/smarttub/test_sensor.py @@ -11,7 +11,7 @@ async def test_sensors(spa, setup_entry, hass): assert state is not None assert state.state == "normal" - spa.get_status.return_value["state"] = "BAD" + spa.get_status.return_value.state = "BAD" await trigger_update(hass) state = hass.states.get(entity_id) assert state is not None @@ -36,3 +36,19 @@ async def test_sensors(spa, setup_entry, hass): state = hass.states.get(entity_id) assert state is not None assert state.state == "inactive" + + entity_id = f"sensor.{spa.brand}_{spa.model}_primary_filtration_cycle" + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "inactive" + assert state.attributes["duration"] == 4 + assert state.attributes["last_updated"] is not None + assert state.attributes["mode"] == "normal" + assert state.attributes["start_hour"] == 2 + + entity_id = f"sensor.{spa.brand}_{spa.model}_secondary_filtration_cycle" + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "inactive" + assert state.attributes["last_updated"] is not None + assert state.attributes["mode"] == "away" From 1a27af43cc48ece781dc1be9a0abf9e92d0c0782 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 22 Feb 2021 06:38:17 +0100 Subject: [PATCH 622/796] Add KNX service exposure_register (#45257) --- homeassistant/components/knx/__init__.py | 192 +++++++-------------- homeassistant/components/knx/expose.py | 152 ++++++++++++++++ homeassistant/components/knx/services.yaml | 66 ++++++- 3 files changed, 282 insertions(+), 128 deletions(-) create mode 100644 homeassistant/components/knx/expose.py diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index e4280e6bddc..8716f03838c 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -5,7 +5,6 @@ import logging import voluptuous as vol from xknx import XKNX from xknx.core.telegram_queue import TelegramQueue -from xknx.devices import DateTime, ExposeSensor from xknx.dpt import DPTArray, DPTBase, DPTBinary from xknx.exceptions import XKNXException from xknx.io import ( @@ -18,26 +17,21 @@ from xknx.telegram import AddressFilter, GroupAddress, Telegram from xknx.telegram.apci import GroupValueRead, GroupValueResponse, GroupValueWrite from homeassistant.const import ( - CONF_ENTITY_ID, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, SERVICE_RELOAD, - STATE_OFF, - STATE_ON, - STATE_UNAVAILABLE, - STATE_UNKNOWN, ) -from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import async_get_platforms -from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.reload import async_integration_yaml_config from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ServiceCallType from .const import DOMAIN, SupportedPlatforms +from .expose import create_knx_exposure from .factory import create_knx_device from .schema import ( BinarySensorSchema, @@ -75,6 +69,7 @@ SERVICE_KNX_ATTR_PAYLOAD = "payload" SERVICE_KNX_ATTR_TYPE = "type" SERVICE_KNX_ATTR_REMOVE = "remove" SERVICE_KNX_EVENT_REGISTER = "event_register" +SERVICE_KNX_EXPOSURE_REGISTER = "exposure_register" SERVICE_KNX_READ = "read" CONFIG_SCHEMA = vol.Schema( @@ -183,12 +178,27 @@ SERVICE_KNX_EVENT_REGISTER_SCHEMA = vol.Schema( } ) +SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA = vol.Any( + ExposeSchema.SCHEMA.extend( + { + vol.Optional(SERVICE_KNX_ATTR_REMOVE, default=False): cv.boolean, + } + ), + vol.Schema( + # for removing only `address` is required + { + vol.Required(SERVICE_KNX_ATTR_ADDRESS): cv.string, + vol.Required(SERVICE_KNX_ATTR_REMOVE): vol.All(cv.boolean, True), + }, + extra=vol.ALLOW_EXTRA, + ), +) + async def async_setup(hass, config): """Set up the KNX component.""" try: hass.data[DOMAIN] = KNXModule(hass, config) - hass.data[DOMAIN].async_create_exposures() await hass.data[DOMAIN].start() except XKNXException as ex: _LOGGER.warning("Could not connect to KNX interface: %s", ex) @@ -196,6 +206,12 @@ async def async_setup(hass, config): f"Could not connect to KNX interface:
{ex}", title="KNX" ) + if CONF_KNX_EXPOSE in config[DOMAIN]: + for expose_config in config[DOMAIN][CONF_KNX_EXPOSE]: + hass.data[DOMAIN].exposures.append( + create_knx_exposure(hass, hass.data[DOMAIN].xknx, expose_config) + ) + for platform in SupportedPlatforms: if platform.value in config[DOMAIN]: for device_config in config[DOMAIN][platform.value]: @@ -235,6 +251,14 @@ async def async_setup(hass, config): schema=SERVICE_KNX_EVENT_REGISTER_SCHEMA, ) + async_register_admin_service( + hass, + DOMAIN, + SERVICE_KNX_EXPOSURE_REGISTER, + hass.data[DOMAIN].service_exposure_register_modify, + schema=SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA, + ) + async def reload_service_handler(service_call: ServiceCallType) -> None: """Remove all KNX components and load new ones from config.""" @@ -269,6 +293,7 @@ class KNXModule: self.config = config self.connected = False self.exposures = [] + self.service_exposures = {} self.init_xknx() self._knx_event_callback: TelegramQueue.Callback = self.register_callback() @@ -340,34 +365,6 @@ class KNXModule: auto_reconnect=True, ) - @callback - def async_create_exposures(self): - """Create exposures.""" - if CONF_KNX_EXPOSE not in self.config[DOMAIN]: - return - for to_expose in self.config[DOMAIN][CONF_KNX_EXPOSE]: - expose_type = to_expose.get(ExposeSchema.CONF_KNX_EXPOSE_TYPE) - entity_id = to_expose.get(CONF_ENTITY_ID) - attribute = to_expose.get(ExposeSchema.CONF_KNX_EXPOSE_ATTRIBUTE) - default = to_expose.get(ExposeSchema.CONF_KNX_EXPOSE_DEFAULT) - address = to_expose.get(ExposeSchema.CONF_KNX_EXPOSE_ADDRESS) - if expose_type.lower() in ["time", "date", "datetime"]: - exposure = KNXExposeTime(self.xknx, expose_type, address) - exposure.async_register() - self.exposures.append(exposure) - else: - exposure = KNXExposeSensor( - self.hass, - self.xknx, - expose_type, - entity_id, - attribute, - default, - address, - ) - exposure.async_register() - self.exposures.append(exposure) - async def telegram_received_cb(self, telegram): """Call invoked after a KNX telegram was received.""" data = None @@ -417,6 +414,37 @@ class KNXModule: group_address, ) + async def service_exposure_register_modify(self, call): + """Service for adding or removing an exposure to KNX bus.""" + group_address = call.data.get(SERVICE_KNX_ATTR_ADDRESS) + + if call.data.get(SERVICE_KNX_ATTR_REMOVE): + try: + removed_exposure = self.service_exposures.pop(group_address) + except KeyError as err: + raise HomeAssistantError( + f"Could not find exposure for '{group_address}' to remove." + ) from err + else: + removed_exposure.shutdown() + return + + if group_address in self.service_exposures: + replaced_exposure = self.service_exposures.pop(group_address) + _LOGGER.warning( + "Service exposure_register replacing already registered exposure for '%s' - %s", + group_address, + replaced_exposure.device.name, + ) + replaced_exposure.shutdown() + exposure = create_knx_exposure(self.hass, self.xknx, call.data) + self.service_exposures[group_address] = exposure + _LOGGER.debug( + "Service exposure_register registered exposure for '%s' - %s", + group_address, + exposure.device.name, + ) + async def service_send_to_knx_bus(self, call): """Service for sending an arbitrary KNX message to the KNX bus.""" attr_payload = call.data.get(SERVICE_KNX_ATTR_PAYLOAD) @@ -448,93 +476,3 @@ class KNXModule: payload=GroupValueRead(), ) await self.xknx.telegrams.put(telegram) - - -class KNXExposeTime: - """Object to Expose Time/Date object to KNX bus.""" - - def __init__(self, xknx: XKNX, expose_type: str, address: str): - """Initialize of Expose class.""" - self.xknx = xknx - self.expose_type = expose_type - self.address = address - self.device = None - - @callback - def async_register(self): - """Register listener.""" - self.device = DateTime( - self.xknx, - name=self.expose_type.capitalize(), - broadcast_type=self.expose_type.upper(), - localtime=True, - group_address=self.address, - ) - - -class KNXExposeSensor: - """Object to Expose Home Assistant entity to KNX bus.""" - - def __init__(self, hass, xknx, expose_type, entity_id, attribute, default, address): - """Initialize of Expose class.""" - self.hass = hass - self.xknx = xknx - self.type = expose_type - self.entity_id = entity_id - self.expose_attribute = attribute - self.expose_default = default - self.address = address - self.device = None - - @callback - def async_register(self): - """Register listener.""" - if self.expose_attribute is not None: - _name = self.entity_id + "__" + self.expose_attribute - else: - _name = self.entity_id - self.device = ExposeSensor( - self.xknx, - name=_name, - group_address=self.address, - value_type=self.type, - ) - async_track_state_change_event( - self.hass, [self.entity_id], self._async_entity_changed - ) - - async def _async_entity_changed(self, event): - """Handle entity change.""" - new_state = event.data.get("new_state") - if new_state is None: - return - if new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE): - return - - if self.expose_attribute is not None: - new_attribute = new_state.attributes.get(self.expose_attribute) - old_state = event.data.get("old_state") - - if old_state is not None: - old_attribute = old_state.attributes.get(self.expose_attribute) - if old_attribute == new_attribute: - # don't send same value sequentially - return - await self._async_set_knx_value(new_attribute) - else: - await self._async_set_knx_value(new_state.state) - - async def _async_set_knx_value(self, value): - """Set new value on xknx ExposeSensor.""" - if value is None: - if self.expose_default is None: - return - value = self.expose_default - - if self.type == "binary": - if value == STATE_ON: - value = True - elif value == STATE_OFF: - value = False - - await self.device.set(value) diff --git a/homeassistant/components/knx/expose.py b/homeassistant/components/knx/expose.py new file mode 100644 index 00000000000..93abd7d7b43 --- /dev/null +++ b/homeassistant/components/knx/expose.py @@ -0,0 +1,152 @@ +"""Exposures to KNX bus.""" +from typing import Union + +from xknx import XKNX +from xknx.devices import DateTime, ExposeSensor + +from homeassistant.const import ( + CONF_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.typing import ConfigType + +from .schema import ExposeSchema + + +@callback +def create_knx_exposure( + hass: HomeAssistant, xknx: XKNX, config: ConfigType +) -> Union["KNXExposeSensor", "KNXExposeTime"]: + """Create exposures from config.""" + expose_type = config.get(ExposeSchema.CONF_KNX_EXPOSE_TYPE) + entity_id = config.get(CONF_ENTITY_ID) + attribute = config.get(ExposeSchema.CONF_KNX_EXPOSE_ATTRIBUTE) + default = config.get(ExposeSchema.CONF_KNX_EXPOSE_DEFAULT) + address = config.get(ExposeSchema.CONF_KNX_EXPOSE_ADDRESS) + + exposure: Union["KNXExposeSensor", "KNXExposeTime"] + if expose_type.lower() in ["time", "date", "datetime"]: + exposure = KNXExposeTime(xknx, expose_type, address) + else: + exposure = KNXExposeSensor( + hass, + xknx, + expose_type, + entity_id, + attribute, + default, + address, + ) + exposure.async_register() + return exposure + + +class KNXExposeSensor: + """Object to Expose Home Assistant entity to KNX bus.""" + + def __init__(self, hass, xknx, expose_type, entity_id, attribute, default, address): + """Initialize of Expose class.""" + self.hass = hass + self.xknx = xknx + self.type = expose_type + self.entity_id = entity_id + self.expose_attribute = attribute + self.expose_default = default + self.address = address + self.device = None + self._remove_listener = None + + @callback + def async_register(self): + """Register listener.""" + if self.expose_attribute is not None: + _name = self.entity_id + "__" + self.expose_attribute + else: + _name = self.entity_id + self.device = ExposeSensor( + self.xknx, + name=_name, + group_address=self.address, + value_type=self.type, + ) + self._remove_listener = async_track_state_change_event( + self.hass, [self.entity_id], self._async_entity_changed + ) + + @callback + def shutdown(self) -> None: + """Prepare for deletion.""" + if self._remove_listener is not None: + self._remove_listener() + if self.device is not None: + self.device.shutdown() + + async def _async_entity_changed(self, event): + """Handle entity change.""" + new_state = event.data.get("new_state") + if new_state is None: + return + if new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE): + return + + if self.expose_attribute is None: + await self._async_set_knx_value(new_state.state) + return + + new_attribute = new_state.attributes.get(self.expose_attribute) + old_state = event.data.get("old_state") + + if old_state is not None: + old_attribute = old_state.attributes.get(self.expose_attribute) + if old_attribute == new_attribute: + # don't send same value sequentially + return + await self._async_set_knx_value(new_attribute) + + async def _async_set_knx_value(self, value): + """Set new value on xknx ExposeSensor.""" + if value is None: + if self.expose_default is None: + return + value = self.expose_default + + if self.type == "binary": + if value == STATE_ON: + value = True + elif value == STATE_OFF: + value = False + + await self.device.set(value) + + +class KNXExposeTime: + """Object to Expose Time/Date object to KNX bus.""" + + def __init__(self, xknx: XKNX, expose_type: str, address: str): + """Initialize of Expose class.""" + self.xknx = xknx + self.expose_type = expose_type + self.address = address + self.device = None + + @callback + def async_register(self): + """Register listener.""" + self.device = DateTime( + self.xknx, + name=self.expose_type.capitalize(), + broadcast_type=self.expose_type.upper(), + localtime=True, + group_address=self.address, + ) + + @callback + def shutdown(self): + """Prepare for deletion.""" + if self.device is not None: + self.device.shutdown() diff --git a/homeassistant/components/knx/services.yaml b/homeassistant/components/knx/services.yaml index aa946459cfd..ef74acc49b1 100644 --- a/homeassistant/components/knx/services.yaml +++ b/homeassistant/components/knx/services.yaml @@ -2,25 +2,89 @@ send: description: "Send arbitrary data directly to the KNX bus." fields: address: + name: "Group address" description: "Group address(es) to write to." + required: true example: "1/1/0" + selector: + text: payload: + name: "Payload" description: "Payload to send to the bus. Integers are treated as DPT 1/2/3 payloads. For DPTs > 6 bits send a list. Each value represents 1 octet (0-255). Pad with 0 to DPT byte length." + required: true example: "[0, 4]" type: + name: "Value type" description: "Optional. If set, the payload will not be sent as raw bytes, but encoded as given DPT. Knx sensor types are valid values (see https://www.home-assistant.io/integrations/sensor.knx)." + required: false example: "temperature" + selector: + text: read: description: "Send GroupValueRead requests to the KNX bus. Response can be used from `knx_event` and will be processed in KNX entities." fields: address: + name: "Group address" description: "Group address(es) to send read request to. Lists will read multiple group addresses." + required: true example: "1/1/0" + selector: + text: event_register: description: "Add or remove single group address to knx_event filter for triggering `knx_event`s. Only addresses added with this service can be removed." fields: address: + name: "Group address" description: "Group address that shall be added or removed." + required: true example: "1/1/0" remove: - description: "Optional. If `True` the group address will be removed. Defaults to `False`." + name: "Remove event registration" + description: "Optional. If `True` the group address will be removed." + required: false + default: false + selector: + boolean: +exposure_register: + description: "Add or remove exposures to KNX bus. Only exposures added with this service can be removed." + fields: + address: + name: "Group address" + description: "Group address state or attribute updates will be sent to. GroupValueRead requests will be answered. Per address only one exposure can be registered." + required: true + example: "1/1/0" + selector: + text: + type: + name: "Value type" + description: "Telegrams will be encoded as given DPT. 'binary' and all Knx sensor types are valid values (see https://www.home-assistant.io/integrations/sensor.knx)" + required: true + example: "percentU8" + selector: + text: + entity_id: + name: "Entity" + description: "Entity id whose state or attribute shall be exposed." + required: true + example: "light.kitchen" + selector: + entity: + attribute: + name: "Entity attribute" + description: "Optional. Attribute of the entity that shall be sent to the KNX bus. If not set the state will be sent. Eg. for a light the state is eigther “on” or “off” - with attribute you can expose its “brightness”." + required: false + example: "brightness" + default: + name: "Default value" + description: "Optional. Default value to send to the bus if the state or attribute value is None. Eg. a light with state “off” has no brightness attribute so a default value of 0 could be used. If not set (or None) no value would be sent to the bus and a GroupReadRequest to the address would return the last known value." + required: false + example: "0" + remove: + name: "Remove exposure" + description: "Optional. If `True` the exposure will be removed. Only `address` is required for removal." + required: false + default: false + selector: + boolean: +reload: + description: "Reload KNX configuration." From 5cd022a683bb3b081a17b14d483fa1cd355df2e7 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Sun, 21 Feb 2021 22:09:21 -0800 Subject: [PATCH 623/796] Add light platform to SmartTub (#46886) Co-authored-by: J. Nick Koston --- homeassistant/components/smarttub/__init__.py | 2 +- homeassistant/components/smarttub/const.py | 7 + .../components/smarttub/controller.py | 4 +- homeassistant/components/smarttub/light.py | 141 ++++++++++++++++++ tests/components/smarttub/conftest.py | 14 ++ tests/components/smarttub/test_light.py | 38 +++++ 6 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/smarttub/light.py create mode 100644 tests/components/smarttub/test_light.py diff --git a/homeassistant/components/smarttub/__init__.py b/homeassistant/components/smarttub/__init__.py index e8fc9989d38..8467c208076 100644 --- a/homeassistant/components/smarttub/__init__.py +++ b/homeassistant/components/smarttub/__init__.py @@ -7,7 +7,7 @@ from .controller import SmartTubController _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["climate", "sensor", "switch"] +PLATFORMS = ["climate", "light", "sensor", "switch"] async def async_setup(hass, config): diff --git a/homeassistant/components/smarttub/const.py b/homeassistant/components/smarttub/const.py index 8292c5ef826..0b97926cc43 100644 --- a/homeassistant/components/smarttub/const.py +++ b/homeassistant/components/smarttub/const.py @@ -13,3 +13,10 @@ API_TIMEOUT = 5 DEFAULT_MIN_TEMP = 18.5 DEFAULT_MAX_TEMP = 40 + +# the device doesn't remember any state for the light, so we have to choose a +# mode (smarttub.SpaLight.LightMode) when turning it on. There is no white +# mode. +DEFAULT_LIGHT_EFFECT = "purple" +# default to 50% brightness +DEFAULT_LIGHT_BRIGHTNESS = 128 diff --git a/homeassistant/components/smarttub/controller.py b/homeassistant/components/smarttub/controller.py index feb13066b44..bf8de2f4e2e 100644 --- a/homeassistant/components/smarttub/controller.py +++ b/homeassistant/components/smarttub/controller.py @@ -86,13 +86,15 @@ class SmartTubController: return data async def _get_spa_data(self, spa): - status, pumps = await asyncio.gather( + status, pumps, lights = await asyncio.gather( spa.get_status(), spa.get_pumps(), + spa.get_lights(), ) return { "status": status, "pumps": {pump.id: pump for pump in pumps}, + "lights": {light.zone: light for light in lights}, } async def async_register_devices(self, entry): diff --git a/homeassistant/components/smarttub/light.py b/homeassistant/components/smarttub/light.py new file mode 100644 index 00000000000..a4ada7c3024 --- /dev/null +++ b/homeassistant/components/smarttub/light.py @@ -0,0 +1,141 @@ +"""Platform for light integration.""" +import logging + +from smarttub import SpaLight + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_EFFECT, + EFFECT_COLORLOOP, + SUPPORT_BRIGHTNESS, + SUPPORT_EFFECT, + LightEntity, +) + +from .const import ( + DEFAULT_LIGHT_BRIGHTNESS, + DEFAULT_LIGHT_EFFECT, + DOMAIN, + SMARTTUB_CONTROLLER, +) +from .entity import SmartTubEntity +from .helpers import get_spa_name + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up entities for any lights in the tub.""" + + controller = hass.data[DOMAIN][entry.entry_id][SMARTTUB_CONTROLLER] + + entities = [ + SmartTubLight(controller.coordinator, light) + for spa in controller.spas + for light in await spa.get_lights() + ] + + async_add_entities(entities) + + +class SmartTubLight(SmartTubEntity, LightEntity): + """A light on a spa.""" + + def __init__(self, coordinator, light): + """Initialize the entity.""" + super().__init__(coordinator, light.spa, "light") + self.light_zone = light.zone + + @property + def light(self) -> SpaLight: + """Return the underlying SpaLight object for this entity.""" + return self.coordinator.data[self.spa.id]["lights"][self.light_zone] + + @property + def unique_id(self) -> str: + """Return a unique ID for this light entity.""" + return f"{super().unique_id}-{self.light_zone}" + + @property + def name(self) -> str: + """Return a name for this light entity.""" + spa_name = get_spa_name(self.spa) + return f"{spa_name} Light {self.light_zone}" + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + + # SmartTub intensity is 0..100 + return self._smarttub_to_hass_brightness(self.light.intensity) + + @staticmethod + def _smarttub_to_hass_brightness(intensity): + if intensity in (0, 1): + return 0 + return round(intensity * 255 / 100) + + @staticmethod + def _hass_to_smarttub_brightness(brightness): + return round(brightness * 100 / 255) + + @property + def is_on(self): + """Return true if the light is on.""" + return self.light.mode != SpaLight.LightMode.OFF + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS | SUPPORT_EFFECT + + @property + def effect(self): + """Return the current effect.""" + mode = self.light.mode.name.lower() + if mode in self.effect_list: + return mode + return None + + @property + def effect_list(self): + """Return the list of supported effects.""" + effects = [ + effect + for effect in map(self._light_mode_to_effect, SpaLight.LightMode) + if effect is not None + ] + + return effects + + @staticmethod + def _light_mode_to_effect(light_mode: SpaLight.LightMode): + if light_mode == SpaLight.LightMode.OFF: + return None + if light_mode == SpaLight.LightMode.HIGH_SPEED_COLOR_WHEEL: + return EFFECT_COLORLOOP + + return light_mode.name.lower() + + @staticmethod + def _effect_to_light_mode(effect): + if effect == EFFECT_COLORLOOP: + return SpaLight.LightMode.HIGH_SPEED_COLOR_WHEEL + + return SpaLight.LightMode[effect.upper()] + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + + mode = self._effect_to_light_mode(kwargs.get(ATTR_EFFECT, DEFAULT_LIGHT_EFFECT)) + intensity = self._hass_to_smarttub_brightness( + kwargs.get(ATTR_BRIGHTNESS, DEFAULT_LIGHT_BRIGHTNESS) + ) + + await self.light.set_mode(mode, intensity) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + await self.light.set_mode(self.light.LightMode.OFF, 0) + await self.coordinator.async_request_refresh() diff --git a/tests/components/smarttub/conftest.py b/tests/components/smarttub/conftest.py index ad962ba0474..79e5d06d3b3 100644 --- a/tests/components/smarttub/conftest.py +++ b/tests/components/smarttub/conftest.py @@ -90,6 +90,20 @@ def mock_spa(): mock_spa.get_pumps.return_value = [mock_circulation_pump, mock_jet_off, mock_jet_on] + mock_light_off = create_autospec(smarttub.SpaLight, instance=True) + mock_light_off.spa = mock_spa + mock_light_off.zone = 1 + mock_light_off.intensity = 0 + mock_light_off.mode = smarttub.SpaLight.LightMode.OFF + + mock_light_on = create_autospec(smarttub.SpaLight, instance=True) + mock_light_on.spa = mock_spa + mock_light_on.zone = 2 + mock_light_on.intensity = 50 + mock_light_on.mode = smarttub.SpaLight.LightMode.PURPLE + + mock_spa.get_lights.return_value = [mock_light_off, mock_light_on] + return mock_spa diff --git a/tests/components/smarttub/test_light.py b/tests/components/smarttub/test_light.py new file mode 100644 index 00000000000..5e9d9459eab --- /dev/null +++ b/tests/components/smarttub/test_light.py @@ -0,0 +1,38 @@ +"""Test the SmartTub light platform.""" + +from smarttub import SpaLight + + +async def test_light(spa, setup_entry, hass): + """Test light entity.""" + + for light in spa.get_lights.return_value: + entity_id = f"light.{spa.brand}_{spa.model}_light_{light.zone}" + state = hass.states.get(entity_id) + assert state is not None + if light.mode == SpaLight.LightMode.OFF: + assert state.state == "off" + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": entity_id}, + blocking=True, + ) + light.set_mode.assert_called() + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": entity_id, "brightness": 255}, + blocking=True, + ) + light.set_mode.assert_called_with(SpaLight.LightMode.PURPLE, 100) + + else: + assert state.state == "on" + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": entity_id}, + blocking=True, + ) From 5c29adea3de3d74abc4166f45dc4cec904152135 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Mon, 22 Feb 2021 06:12:50 +0000 Subject: [PATCH 624/796] Add KMTronic Integration (#41682) Co-authored-by: J. Nick Koston --- CODEOWNERS | 1 + homeassistant/components/kmtronic/__init__.py | 104 ++++++++++++ .../components/kmtronic/config_flow.py | 74 +++++++++ homeassistant/components/kmtronic/const.py | 16 ++ .../components/kmtronic/manifest.json | 8 + .../components/kmtronic/strings.json | 21 +++ homeassistant/components/kmtronic/switch.py | 67 ++++++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/kmtronic/__init__.py | 1 + tests/components/kmtronic/test_config_flow.py | 145 +++++++++++++++++ tests/components/kmtronic/test_switch.py | 150 ++++++++++++++++++ 13 files changed, 594 insertions(+) create mode 100644 homeassistant/components/kmtronic/__init__.py create mode 100644 homeassistant/components/kmtronic/config_flow.py create mode 100644 homeassistant/components/kmtronic/const.py create mode 100644 homeassistant/components/kmtronic/manifest.json create mode 100644 homeassistant/components/kmtronic/strings.json create mode 100644 homeassistant/components/kmtronic/switch.py create mode 100644 tests/components/kmtronic/__init__.py create mode 100644 tests/components/kmtronic/test_config_flow.py create mode 100644 tests/components/kmtronic/test_switch.py diff --git a/CODEOWNERS b/CODEOWNERS index 788f3636143..a9d4ce63209 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -243,6 +243,7 @@ homeassistant/components/keba/* @dannerph homeassistant/components/keenetic_ndms2/* @foxel homeassistant/components/kef/* @basnijholt homeassistant/components/keyboard_remote/* @bendavid +homeassistant/components/kmtronic/* @dgomes homeassistant/components/knx/* @Julius2342 @farmio @marvin-w homeassistant/components/kodi/* @OnFreund @cgtobi homeassistant/components/konnected/* @heythisisnate @kit-klein diff --git a/homeassistant/components/kmtronic/__init__.py b/homeassistant/components/kmtronic/__init__.py new file mode 100644 index 00000000000..b55ab9e1c9c --- /dev/null +++ b/homeassistant/components/kmtronic/__init__.py @@ -0,0 +1,104 @@ +"""The kmtronic integration.""" +import asyncio +from datetime import timedelta +import logging + +import aiohttp +import async_timeout +from pykmtronic.auth import Auth +from pykmtronic.hub import KMTronicHubAPI +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry, ConfigEntryNotReady +from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + CONF_HOSTNAME, + CONF_PASSWORD, + CONF_USERNAME, + DATA_COORDINATOR, + DATA_HOST, + DATA_HUB, + DOMAIN, + MANUFACTURER, +) + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + +PLATFORMS = ["switch"] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the kmtronic component.""" + hass.data[DOMAIN] = {} + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up kmtronic from a config entry.""" + + session = aiohttp_client.async_get_clientsession(hass) + auth = Auth( + session, + f"http://{entry.data[CONF_HOSTNAME]}", + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + ) + hub = KMTronicHubAPI(auth) + + async def async_update_data(): + try: + async with async_timeout.timeout(10): + await hub.async_update_relays() + except aiohttp.client_exceptions.ClientResponseError as err: + raise UpdateFailed(f"Wrong credentials: {err}") from err + except ( + asyncio.TimeoutError, + aiohttp.client_exceptions.ClientConnectorError, + ) as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"{MANUFACTURER} {hub.name}", + update_method=async_update_data, + update_interval=timedelta(seconds=30), + ) + await coordinator.async_refresh() + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + hass.data[DOMAIN][entry.entry_id] = { + DATA_HUB: hub, + DATA_HOST: entry.data[DATA_HOST], + DATA_COORDINATOR: coordinator, + } + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """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/kmtronic/config_flow.py b/homeassistant/components/kmtronic/config_flow.py new file mode 100644 index 00000000000..376bb64c34c --- /dev/null +++ b/homeassistant/components/kmtronic/config_flow.py @@ -0,0 +1,74 @@ +"""Config flow for kmtronic integration.""" +import logging + +import aiohttp +from pykmtronic.auth import Auth +from pykmtronic.hub import KMTronicHubAPI +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.helpers import aiohttp_client + +from .const import CONF_HOSTNAME, CONF_PASSWORD, CONF_USERNAME +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema({CONF_HOSTNAME: str, CONF_USERNAME: str, CONF_PASSWORD: str}) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect.""" + + session = aiohttp_client.async_get_clientsession(hass) + auth = Auth( + session, + f"http://{data[CONF_HOSTNAME]}", + data[CONF_USERNAME], + data[CONF_PASSWORD], + ) + hub = KMTronicHubAPI(auth) + + try: + await hub.async_get_status() + except aiohttp.client_exceptions.ClientResponseError as err: + raise InvalidAuth from err + except aiohttp.client_exceptions.ClientConnectorError as err: + raise CannotConnect from err + + return data + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for kmtronic.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + + return self.async_create_entry(title=info["host"], data=user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/kmtronic/const.py b/homeassistant/components/kmtronic/const.py new file mode 100644 index 00000000000..58553217799 --- /dev/null +++ b/homeassistant/components/kmtronic/const.py @@ -0,0 +1,16 @@ +"""Constants for the kmtronic integration.""" + +DOMAIN = "kmtronic" + +CONF_HOSTNAME = "host" +CONF_USERNAME = "username" +CONF_PASSWORD = "password" + +DATA_HUB = "hub" +DATA_HOST = "host" +DATA_COORDINATOR = "coordinator" + +MANUFACTURER = "KMtronic" +ATTR_MANUFACTURER = "manufacturer" +ATTR_IDENTIFIERS = "identifiers" +ATTR_NAME = "name" diff --git a/homeassistant/components/kmtronic/manifest.json b/homeassistant/components/kmtronic/manifest.json new file mode 100644 index 00000000000..27e9f953eb7 --- /dev/null +++ b/homeassistant/components/kmtronic/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "kmtronic", + "name": "KMtronic", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/kmtronic", + "requirements": ["pykmtronic==0.0.3"], + "codeowners": ["@dgomes"] +} diff --git a/homeassistant/components/kmtronic/strings.json b/homeassistant/components/kmtronic/strings.json new file mode 100644 index 00000000000..7becb830d91 --- /dev/null +++ b/homeassistant/components/kmtronic/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/kmtronic/switch.py b/homeassistant/components/kmtronic/switch.py new file mode 100644 index 00000000000..5970ec20cb8 --- /dev/null +++ b/homeassistant/components/kmtronic/switch.py @@ -0,0 +1,67 @@ +"""KMtronic Switch integration.""" + +from homeassistant.components.switch import SwitchEntity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DATA_COORDINATOR, DATA_HOST, DATA_HUB, DOMAIN + + +async def async_setup_entry(hass, entry, async_add_entities): + """Config entry example.""" + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + hub = hass.data[DOMAIN][entry.entry_id][DATA_HUB] + host = hass.data[DOMAIN][entry.entry_id][DATA_HOST] + await hub.async_get_relays() + + async_add_entities( + [ + KMtronicSwitch(coordinator, host, relay, entry.unique_id) + for relay in hub.relays + ] + ) + + +class KMtronicSwitch(CoordinatorEntity, SwitchEntity): + """KMtronic Switch Entity.""" + + def __init__(self, coordinator, host, relay, config_entry_id): + """Pass coordinator to CoordinatorEntity.""" + super().__init__(coordinator) + self._host = host + self._relay = relay + self._config_entry_id = config_entry_id + + @property + def available(self) -> bool: + """Return whether the entity is available.""" + return self.coordinator.last_update_success + + @property + def name(self) -> str: + """Return the name of the entity.""" + return f"Relay{self._relay.id}" + + @property + def unique_id(self) -> str: + """Return the unique ID of the entity.""" + return f"{self._config_entry_id}_relay{self._relay.id}" + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return True + + @property + def is_on(self): + """Return entity state.""" + return self._relay.is_on + + async def async_turn_on(self, **kwargs) -> None: + """Turn the switch on.""" + await self._relay.turn_on() + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs) -> None: + """Turn the switch off.""" + await self._relay.turn_off() + self.async_write_ha_state() diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 7e17a839068..8e8949e5788 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -115,6 +115,7 @@ FLOWS = [ "izone", "juicenet", "keenetic_ndms2", + "kmtronic", "kodi", "konnected", "kulersky", diff --git a/requirements_all.txt b/requirements_all.txt index 0d8203279b5..05ed5e7aa92 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1473,6 +1473,9 @@ pyitachip2ir==0.0.7 # homeassistant.components.kira pykira==0.1.1 +# homeassistant.components.kmtronic +pykmtronic==0.0.3 + # homeassistant.components.kodi pykodi==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 99a0351ee37..46c91feeb16 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -775,6 +775,9 @@ pyisy==2.1.0 # homeassistant.components.kira pykira==0.1.1 +# homeassistant.components.kmtronic +pykmtronic==0.0.3 + # homeassistant.components.kodi pykodi==0.2.1 diff --git a/tests/components/kmtronic/__init__.py b/tests/components/kmtronic/__init__.py new file mode 100644 index 00000000000..2f089d6495f --- /dev/null +++ b/tests/components/kmtronic/__init__.py @@ -0,0 +1 @@ +"""Tests for the kmtronic integration.""" diff --git a/tests/components/kmtronic/test_config_flow.py b/tests/components/kmtronic/test_config_flow.py new file mode 100644 index 00000000000..ebbbf626451 --- /dev/null +++ b/tests/components/kmtronic/test_config_flow.py @@ -0,0 +1,145 @@ +"""Test the kmtronic config flow.""" +from unittest.mock import Mock, patch + +from aiohttp import ClientConnectorError, ClientResponseError + +from homeassistant import config_entries, setup +from homeassistant.components.kmtronic.const import DOMAIN +from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED + +from tests.common import MockConfigEntry + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.kmtronic.config_flow.KMTronicHubAPI.async_get_status", + return_value=[Mock()], + ), patch( + "homeassistant.components.kmtronic.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.kmtronic.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "1.1.1.1" + assert result2["data"] == { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.kmtronic.config_flow.KMTronicHubAPI.async_get_status", + side_effect=ClientResponseError(None, None, status=401), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.kmtronic.config_flow.KMTronicHubAPI.async_get_status", + side_effect=ClientConnectorError(None, Mock()), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_unknown_error(hass): + """Test we handle unknown errors.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.kmtronic.config_flow.KMTronicHubAPI.async_get_status", + side_effect=Exception(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +async def test_unload_config_entry(hass, aioclient_mock): + """Test entry unloading.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={"host": "1.1.1.1", "username": "admin", "password": "admin"}, + ) + config_entry.add_to_hass(hass) + + aioclient_mock.get( + "http://1.1.1.1/status.xml", + text="00", + ) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + config_entries = hass.config_entries.async_entries(DOMAIN) + assert len(config_entries) == 1 + assert config_entries[0] is config_entry + assert config_entry.state == ENTRY_STATE_LOADED + + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ENTRY_STATE_NOT_LOADED diff --git a/tests/components/kmtronic/test_switch.py b/tests/components/kmtronic/test_switch.py new file mode 100644 index 00000000000..5eec3537176 --- /dev/null +++ b/tests/components/kmtronic/test_switch.py @@ -0,0 +1,150 @@ +"""The tests for the KMtronic switch platform.""" +import asyncio +from datetime import timedelta + +from homeassistant.components.kmtronic.const import DOMAIN +from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_relay_on_off(hass, aioclient_mock): + """Tests the relay turns on correctly.""" + + aioclient_mock.get( + "http://1.1.1.1/status.xml", + text="00", + ) + + MockConfigEntry( + domain=DOMAIN, data={"host": "1.1.1.1", "username": "foo", "password": "bar"} + ).add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + # Mocks the response for turning a relay1 on + aioclient_mock.get( + "http://1.1.1.1/FF0101", + text="", + ) + + state = hass.states.get("switch.relay1") + assert state.state == "off" + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": "switch.relay1"}, blocking=True + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.relay1") + assert state.state == "on" + + # Mocks the response for turning a relay1 off + aioclient_mock.get( + "http://1.1.1.1/FF0100", + text="", + ) + + await hass.services.async_call( + "switch", "turn_off", {"entity_id": "switch.relay1"}, blocking=True + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.relay1") + assert state.state == "off" + + +async def test_update(hass, aioclient_mock): + """Tests switch refreshes status periodically.""" + now = dt_util.utcnow() + future = now + timedelta(minutes=10) + + aioclient_mock.get( + "http://1.1.1.1/status.xml", + text="00", + ) + + MockConfigEntry( + domain=DOMAIN, data={"host": "1.1.1.1", "username": "foo", "password": "bar"} + ).add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + + await hass.async_block_till_done() + state = hass.states.get("switch.relay1") + assert state.state == "off" + + aioclient_mock.clear_requests() + aioclient_mock.get( + "http://1.1.1.1/status.xml", + text="11", + ) + async_fire_time_changed(hass, future) + + await hass.async_block_till_done() + state = hass.states.get("switch.relay1") + assert state.state == "on" + + +async def test_config_entry_not_ready(hass, aioclient_mock): + """Tests configuration entry not ready.""" + + aioclient_mock.get( + "http://1.1.1.1/status.xml", + exc=asyncio.TimeoutError(), + ) + + config_entry = MockConfigEntry( + domain=DOMAIN, data={"host": "1.1.1.1", "username": "foo", "password": "bar"} + ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_failed_update(hass, aioclient_mock): + """Tests coordinator update fails.""" + now = dt_util.utcnow() + future = now + timedelta(minutes=10) + + aioclient_mock.get( + "http://1.1.1.1/status.xml", + text="00", + ) + + MockConfigEntry( + domain=DOMAIN, data={"host": "1.1.1.1", "username": "foo", "password": "bar"} + ).add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + + await hass.async_block_till_done() + state = hass.states.get("switch.relay1") + assert state.state == "off" + + aioclient_mock.clear_requests() + aioclient_mock.get( + "http://1.1.1.1/status.xml", + text="401 Unauthorized: Password required", + status=401, + ) + async_fire_time_changed(hass, future) + + await hass.async_block_till_done() + state = hass.states.get("switch.relay1") + assert state.state == STATE_UNAVAILABLE + + future += timedelta(minutes=10) + aioclient_mock.clear_requests() + aioclient_mock.get( + "http://1.1.1.1/status.xml", + exc=asyncio.TimeoutError(), + ) + async_fire_time_changed(hass, future) + + await hass.async_block_till_done() + state = hass.states.get("switch.relay1") + assert state.state == STATE_UNAVAILABLE From b2b476596b673911b8ecb840ec40e4967dff78b8 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Sun, 21 Feb 2021 22:25:01 -0800 Subject: [PATCH 625/796] Add UV sensor to SmartTub (#46888) --- homeassistant/components/smarttub/sensor.py | 1 + tests/components/smarttub/test_sensor.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/components/smarttub/sensor.py b/homeassistant/components/smarttub/sensor.py index 54921596fb2..4619d07dbe0 100644 --- a/homeassistant/components/smarttub/sensor.py +++ b/homeassistant/components/smarttub/sensor.py @@ -27,6 +27,7 @@ async def async_setup_entry(hass, entry, async_add_entities): controller.coordinator, spa, "Flow Switch", "flow_switch" ), SmartTubSensor(controller.coordinator, spa, "Ozone", "ozone"), + SmartTubSensor(controller.coordinator, spa, "UV", "uv"), SmartTubSensor( controller.coordinator, spa, "Blowout Cycle", "blowout_cycle" ), diff --git a/tests/components/smarttub/test_sensor.py b/tests/components/smarttub/test_sensor.py index 7ef3062894a..5b0163daf26 100644 --- a/tests/components/smarttub/test_sensor.py +++ b/tests/components/smarttub/test_sensor.py @@ -27,6 +27,11 @@ async def test_sensors(spa, setup_entry, hass): assert state is not None assert state.state == "off" + entity_id = f"sensor.{spa.brand}_{spa.model}_uv" + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "off" + entity_id = f"sensor.{spa.brand}_{spa.model}_blowout_cycle" state = hass.states.get(entity_id) assert state is not None From 75b37b4c2a0bdf68076ecc9eb556c9066e472e9f Mon Sep 17 00:00:00 2001 From: Jan-Willem Mulder Date: Mon, 22 Feb 2021 07:53:58 +0100 Subject: [PATCH 626/796] Expose locked attribute in deCONZ climate platform (#46814) --- homeassistant/components/deconz/climate.py | 5 ++++- homeassistant/components/deconz/const.py | 1 + tests/components/deconz/test_climate.py | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 98e3864e191..44111fbbb1e 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -26,7 +26,7 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import ATTR_OFFSET, ATTR_VALVE, NEW_SENSOR +from .const import ATTR_LOCKED, ATTR_OFFSET, ATTR_VALVE, NEW_SENSOR from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry @@ -254,4 +254,7 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): if self._device.valve is not None: attr[ATTR_VALVE] = self._device.valve + if self._device.locked is not None: + attr[ATTR_LOCKED] = self._device.locked + return attr diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index cbad37b1b87..67effa4e81b 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -46,6 +46,7 @@ NEW_SCENE = "scenes" NEW_SENSOR = "sensors" ATTR_DARK = "dark" +ATTR_LOCKED = "locked" ATTR_OFFSET = "offset" ATTR_ON = "on" ATTR_VALVE = "valve" diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 0a37debade3..5577a2d0414 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -92,7 +92,7 @@ async def test_simple_climate_device(hass, aioclient_mock): "battery": 59, "displayflipped": None, "heatsetpoint": 2100, - "locked": None, + "locked": True, "mountingmode": None, "offset": 0, "on": True, @@ -132,6 +132,7 @@ async def test_simple_climate_device(hass, aioclient_mock): ] assert climate_thermostat.attributes["current_temperature"] == 21.0 assert climate_thermostat.attributes["temperature"] == 21.0 + assert climate_thermostat.attributes["locked"] is True assert hass.states.get("sensor.thermostat_battery_level").state == "59" # Event signals thermostat configured off From d61d39de08737a0bb0c7935c5cfa286cf8b694e8 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 22 Feb 2021 08:11:59 +0100 Subject: [PATCH 627/796] Handle ConditionError with multiple entity_id for state/numeric_state (#46855) --- homeassistant/exceptions.py | 2 +- homeassistant/helpers/condition.py | 46 +++++++++++++++++++++++------- tests/helpers/test_condition.py | 46 ++++++++++++++++-------------- 3 files changed, 61 insertions(+), 33 deletions(-) diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 0ac231fd314..84ba2cfa348 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -85,7 +85,7 @@ class ConditionErrorIndex(ConditionError): @attr.s class ConditionErrorContainer(ConditionError): - """Condition error with index.""" + """Condition error with subconditions.""" # List of ConditionErrors that this error wraps errors: Sequence[ConditionError] = attr.ib() diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index c20755a1780..40087650141 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -342,12 +342,25 @@ def async_numeric_state_from_config( if value_template is not None: value_template.hass = hass - return all( - async_numeric_state( - hass, entity_id, below, above, value_template, variables, attribute - ) - for entity_id in entity_ids - ) + errors = [] + for index, entity_id in enumerate(entity_ids): + try: + if not async_numeric_state( + hass, entity_id, below, above, value_template, variables, attribute + ): + return False + except ConditionError as ex: + errors.append( + ConditionErrorIndex( + "numeric_state", index=index, total=len(entity_ids), error=ex + ) + ) + + # Raise the errors if no check was false + if errors: + raise ConditionErrorContainer("numeric_state", errors=errors) + + return True return if_numeric_state @@ -429,10 +442,23 @@ def state_from_config( def if_state(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: """Test if condition.""" - return all( - state(hass, entity_id, req_states, for_period, attribute) - for entity_id in entity_ids - ) + errors = [] + for index, entity_id in enumerate(entity_ids): + try: + if not state(hass, entity_id, req_states, for_period, attribute): + return False + except ConditionError as ex: + errors.append( + ConditionErrorIndex( + "state", index=index, total=len(entity_ids), error=ex + ) + ) + + # Raise the errors if no check was false + if errors: + raise ConditionErrorContainer("state", errors=errors) + + return True return if_state diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 2c35a3c8b15..5074b6e70c4 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -390,17 +390,18 @@ async def test_state_raises(hass): with pytest.raises(ConditionError, match="no entity"): condition.state(hass, entity=None, req_state="missing") - # Unknown entity_id - with pytest.raises(ConditionError, match="unknown entity"): - test = await condition.async_from_config( - hass, - { - "condition": "state", - "entity_id": "sensor.door_unknown", - "state": "open", - }, - ) - + # Unknown entities + test = await condition.async_from_config( + hass, + { + "condition": "state", + "entity_id": ["sensor.door_unknown", "sensor.window_unknown"], + "state": "open", + }, + ) + with pytest.raises(ConditionError, match="unknown entity.*door"): + test(hass) + with pytest.raises(ConditionError, match="unknown entity.*window"): test(hass) # Unknown attribute @@ -632,17 +633,18 @@ async def test_state_using_input_entities(hass): async def test_numeric_state_raises(hass): """Test that numeric_state raises ConditionError on errors.""" - # Unknown entity_id - with pytest.raises(ConditionError, match="unknown entity"): - test = await condition.async_from_config( - hass, - { - "condition": "numeric_state", - "entity_id": "sensor.temperature_unknown", - "above": 0, - }, - ) - + # Unknown entities + test = await condition.async_from_config( + hass, + { + "condition": "numeric_state", + "entity_id": ["sensor.temperature_unknown", "sensor.humidity_unknown"], + "above": 0, + }, + ) + with pytest.raises(ConditionError, match="unknown entity.*temperature"): + test(hass) + with pytest.raises(ConditionError, match="unknown entity.*humidity"): test(hass) # Unknown attribute From e5aef45bd74dcf46142fef8fe870b32e96e993e6 Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Mon, 22 Feb 2021 01:39:10 -0800 Subject: [PATCH 628/796] Add usercode support to totalconnect (#39199) * Add test for invalid usercode * Add usercodes to totalconnect. * Update existing tests for usercodes * Fix tests * Add test for invalid usercode * Add usercodes to totalconnect. * Update existing tests for usercodes * Fix tests * Remove YAML support * Fix conflict * Bump to total_connect_client 0.56 * Change Exception to HomeAssistantError * Fix config_flow.py * Simplify async_setup since no yaml * Remove import from config flow and tests * Add reauth and test for it. Various other fixes. * Fix pylint in __init__ * Show config yaml as deprecated * separate config_flow and init tests * Assert ENTRY_STATE_SETUP_ERROR in init test * Add test for reauth flow * Fix reauth and tests * Fix strings * Restore username and usercode with new passord * Correct the integration name * Update tests/components/totalconnect/test_config_flow.py Co-authored-by: Martin Hjelmare * Update tests/components/totalconnect/test_init.py Co-authored-by: Martin Hjelmare * Update .coveragerc * Add test for invalid auth during reauth * Bump total-connect-client to 0.57 * Fix .coveragerc * More tests for usercodes * Fix usercode test * Reload config entry on reauth Co-authored-by: Martin Hjelmare --- .coveragerc | 5 +- .../components/totalconnect/__init__.py | 65 +++++--- .../components/totalconnect/config_flow.py | 133 +++++++++++++-- .../components/totalconnect/const.py | 5 + .../components/totalconnect/manifest.json | 5 +- .../components/totalconnect/strings.json | 17 +- .../totalconnect/translations/en.json | 17 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/totalconnect/common.py | 38 ++++- .../totalconnect/test_alarm_control_panel.py | 52 +++++- .../totalconnect/test_config_flow.py | 154 ++++++++++++------ tests/components/totalconnect/test_init.py | 29 ++++ 13 files changed, 421 insertions(+), 103 deletions(-) create mode 100644 tests/components/totalconnect/test_init.py diff --git a/.coveragerc b/.coveragerc index d66f4032f74..899577f2acf 100644 --- a/.coveragerc +++ b/.coveragerc @@ -978,7 +978,10 @@ omit = homeassistant/components/toon/sensor.py homeassistant/components/toon/switch.py homeassistant/components/torque/sensor.py - homeassistant/components/totalconnect/* + homeassistant/components/totalconnect/__init__.py + homeassistant/components/totalconnect/alarm_control_panel.py + homeassistant/components/totalconnect/binary_sensor.py + homeassistant/components/totalconnect/const.py homeassistant/components/touchline/climate.py homeassistant/components/tplink/common.py homeassistant/components/tplink/switch.py diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index cf3f059cfb9..179d60b794a 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -5,60 +5,79 @@ import logging from total_connect_client import TotalConnectClient import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from .const import DOMAIN +from .const import CONF_USERCODES, DOMAIN _LOGGER = logging.getLogger(__name__) PLATFORMS = ["alarm_control_panel", "binary_sensor"] CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) async def async_setup(hass: HomeAssistant, config: dict): """Set up by configuration file.""" - if DOMAIN not in config: - return True - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config[DOMAIN], - ) - ) + hass.data.setdefault(DOMAIN, {}) return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up upon config entry in user interface.""" - hass.data.setdefault(DOMAIN, {}) - conf = entry.data username = conf[CONF_USERNAME] password = conf[CONF_PASSWORD] + if CONF_USERCODES not in conf: + _LOGGER.warning("No usercodes in TotalConnect configuration") + # should only happen for those who used UI before we added usercodes + await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + }, + data=conf, + ) + return False + + temp_codes = conf[CONF_USERCODES] + usercodes = {} + for code in temp_codes: + usercodes[int(code)] = temp_codes[code] + client = await hass.async_add_executor_job( - TotalConnectClient.TotalConnectClient, username, password + TotalConnectClient.TotalConnectClient, username, password, usercodes ) if not client.is_valid_credentials(): _LOGGER.error("TotalConnect authentication failed") + await hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + }, + data=conf, + ) + ) + return False hass.data[DOMAIN][entry.entry_id] = client diff --git a/homeassistant/components/totalconnect/config_flow.py b/homeassistant/components/totalconnect/config_flow.py index 2608a3c812c..27fa4203a42 100644 --- a/homeassistant/components/totalconnect/config_flow.py +++ b/homeassistant/components/totalconnect/config_flow.py @@ -5,7 +5,11 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import DOMAIN # pylint: disable=unused-import +from .const import CONF_USERCODES, DOMAIN # pylint: disable=unused-import + +CONF_LOCATION = "location" + +PASSWORD_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -13,6 +17,13 @@ class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self): + """Initialize the config flow.""" + self.username = None + self.password = None + self.usercodes = {} + self.client = None + async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" errors = {} @@ -25,14 +36,16 @@ class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(username) self._abort_if_unique_id_configured() - valid = await self.is_valid(username, password) + client = await self.hass.async_add_executor_job( + TotalConnectClient.TotalConnectClient, username, password, None + ) - if valid: - # authentication success / valid - return self.async_create_entry( - title="Total Connect", - data={CONF_USERNAME: username, CONF_PASSWORD: password}, - ) + if client.is_valid_credentials(): + # username/password valid so show user locations + self.username = username + self.password = password + self.client = client + return await self.async_step_locations() # authentication failed / invalid errors["base"] = "invalid_auth" @@ -44,13 +57,101 @@ class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=data_schema, errors=errors ) - async def async_step_import(self, user_input): - """Import a config entry.""" - return await self.async_step_user(user_input) + async def async_step_locations(self, user_entry=None): + """Handle the user locations and associated usercodes.""" + errors = {} + if user_entry is not None: + for location_id in self.usercodes: + if self.usercodes[location_id] is None: + valid = await self.hass.async_add_executor_job( + self.client.locations[location_id].set_usercode, + user_entry[CONF_LOCATION], + ) + if valid: + self.usercodes[location_id] = user_entry[CONF_LOCATION] + else: + errors[CONF_LOCATION] = "usercode" + break - async def is_valid(self, username="", password=""): - """Return true if the given username and password are valid.""" - client = await self.hass.async_add_executor_job( - TotalConnectClient.TotalConnectClient, username, password + complete = True + for location_id in self.usercodes: + if self.usercodes[location_id] is None: + complete = False + + if not errors and complete: + return self.async_create_entry( + title="Total Connect", + data={ + CONF_USERNAME: self.username, + CONF_PASSWORD: self.password, + CONF_USERCODES: self.usercodes, + }, + ) + else: + for location_id in self.client.locations: + self.usercodes[location_id] = None + + # show the next location that needs a usercode + location_codes = {} + for location_id in self.usercodes: + if self.usercodes[location_id] is None: + location_codes[ + vol.Required( + CONF_LOCATION, + default=location_id, + ) + ] = str + break + + data_schema = vol.Schema(location_codes) + return self.async_show_form( + step_id="locations", + data_schema=data_schema, + errors=errors, + description_placeholders={"base": "description"}, ) - return client.is_valid_credentials() + + async def async_step_reauth(self, config): + """Perform reauth upon an authentication error or no usercode.""" + self.username = config[CONF_USERNAME] + self.usercodes = config[CONF_USERCODES] + + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Dialog that informs the user that reauth is required.""" + errors = {} + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=PASSWORD_DATA_SCHEMA, + ) + + client = await self.hass.async_add_executor_job( + TotalConnectClient.TotalConnectClient, + self.username, + user_input[CONF_PASSWORD], + self.usercodes, + ) + + if not client.is_valid_credentials(): + errors["base"] = "invalid_auth" + return self.async_show_form( + step_id="reauth_confirm", + errors=errors, + data_schema=PASSWORD_DATA_SCHEMA, + ) + + existing_entry = await self.async_set_unique_id(self.username) + new_entry = { + CONF_USERNAME: self.username, + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_USERCODES: self.usercodes, + } + self.hass.config_entries.async_update_entry(existing_entry, data=new_entry) + + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) + + return self.async_abort(reason="reauth_successful") diff --git a/homeassistant/components/totalconnect/const.py b/homeassistant/components/totalconnect/const.py index 6c19bf0a217..22ecd14281f 100644 --- a/homeassistant/components/totalconnect/const.py +++ b/homeassistant/components/totalconnect/const.py @@ -1,3 +1,8 @@ """TotalConnect constants.""" DOMAIN = "totalconnect" +CONF_USERCODES = "usercodes" +CONF_LOCATION = "location" + +# Most TotalConnect alarms will work passing '-1' as usercode +DEFAULT_USERCODE = "-1" diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index 4ec632f4577..8a42ca99f03 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -1,8 +1,9 @@ { "domain": "totalconnect", - "name": "Honeywell Total Connect Alarm", + "name": "Total Connect", "documentation": "https://www.home-assistant.io/integrations/totalconnect", - "requirements": ["total_connect_client==0.55"], + "requirements": ["total_connect_client==0.57"], + "dependencies": [], "codeowners": ["@austinmroczek"], "config_flow": true } diff --git a/homeassistant/components/totalconnect/strings.json b/homeassistant/components/totalconnect/strings.json index 7b306554b7b..41b0bf4648b 100644 --- a/homeassistant/components/totalconnect/strings.json +++ b/homeassistant/components/totalconnect/strings.json @@ -7,13 +7,26 @@ "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "locations": { + "title": "Location Usercodes", + "description": "Enter the usercode for this user at this location", + "data": { + "location": "Location" + } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "Total Connect needs to re-authenticate your account" } }, "error": { - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "usercode": "Usercode not valid for this user at this location" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } } diff --git a/homeassistant/components/totalconnect/translations/en.json b/homeassistant/components/totalconnect/translations/en.json index f02a3eadf9c..5071e623701 100644 --- a/homeassistant/components/totalconnect/translations/en.json +++ b/homeassistant/components/totalconnect/translations/en.json @@ -1,12 +1,25 @@ { "config": { "abort": { - "already_configured": "Account is already configured" + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { - "invalid_auth": "Invalid authentication" + "invalid_auth": "Invalid authentication", + "usercode": "Usercode not valid for this user at this location" }, "step": { + "locations": { + "data": { + "location": "Location" + }, + "description": "Enter the usercode for this user at this location", + "title": "Location Usercodes" + }, + "reauth_confirm": { + "description": "Total Connect needs to re-authenticate your account", + "title": "Reauthenticate Integration" + }, "user": { "data": { "password": "Password", diff --git a/requirements_all.txt b/requirements_all.txt index 05ed5e7aa92..5d704b7522a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2221,7 +2221,7 @@ todoist-python==8.0.0 toonapi==0.2.0 # homeassistant.components.totalconnect -total_connect_client==0.55 +total_connect_client==0.57 # homeassistant.components.tplink_lte tp-connected==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 46c91feeb16..957161dca12 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1133,7 +1133,7 @@ teslajsonpy==0.11.5 toonapi==0.2.0 # homeassistant.components.totalconnect -total_connect_client==0.55 +total_connect_client==0.57 # homeassistant.components.transmission transmissionrpc==0.11 diff --git a/tests/components/totalconnect/common.py b/tests/components/totalconnect/common.py index 17fa244f9b2..d4285c07425 100644 --- a/tests/components/totalconnect/common.py +++ b/tests/components/totalconnect/common.py @@ -3,7 +3,7 @@ from unittest.mock import patch from total_connect_client import TotalConnectClient -from homeassistant.components.totalconnect import DOMAIN +from homeassistant.components.totalconnect.const import CONF_USERCODES, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component @@ -29,13 +29,19 @@ USER = { } RESPONSE_AUTHENTICATE = { - "ResultCode": 0, + "ResultCode": TotalConnectClient.TotalConnectClient.SUCCESS, "SessionID": 1, "Locations": LOCATIONS, "ModuleFlags": MODULE_FLAGS, "UserInfo": USER, } +RESPONSE_AUTHENTICATE_FAILED = { + "ResultCode": TotalConnectClient.TotalConnectClient.BAD_USER_OR_PASSWORD, + "ResultData": "test bad authentication", +} + + PARTITION_DISARMED = { "PartitionID": "1", "ArmingState": TotalConnectClient.TotalConnectLocation.DISARMED, @@ -101,6 +107,32 @@ RESPONSE_DISARM_FAILURE = { "ResultCode": TotalConnectClient.TotalConnectClient.COMMAND_FAILED, "ResultData": "Command Failed", } +RESPONSE_USER_CODE_INVALID = { + "ResultCode": TotalConnectClient.TotalConnectClient.USER_CODE_INVALID, + "ResultData": "testing user code invalid", +} +RESPONSE_SUCCESS = {"ResultCode": TotalConnectClient.TotalConnectClient.SUCCESS} + +USERNAME = "username@me.com" +PASSWORD = "password" +USERCODES = {123456: "7890"} +CONFIG_DATA = { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_USERCODES: USERCODES, +} +CONFIG_DATA_NO_USERCODES = {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + + +USERNAME = "username@me.com" +PASSWORD = "password" +USERCODES = {123456: "7890"} +CONFIG_DATA = { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_USERCODES: USERCODES, +} +CONFIG_DATA_NO_USERCODES = {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} async def setup_platform(hass, platform): @@ -108,7 +140,7 @@ async def setup_platform(hass, platform): # first set up a config entry and add it to hass mock_entry = MockConfigEntry( domain=DOMAIN, - data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}, + data=CONFIG_DATA, ) mock_entry.add_to_hass(hass) diff --git a/tests/components/totalconnect/test_alarm_control_panel.py b/tests/components/totalconnect/test_alarm_control_panel.py index bc90c1aae2a..ba929c0bc54 100644 --- a/tests/components/totalconnect/test_alarm_control_panel.py +++ b/tests/components/totalconnect/test_alarm_control_panel.py @@ -14,6 +14,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, ) +from homeassistant.exceptions import HomeAssistantError from .common import ( RESPONSE_ARM_FAILURE, @@ -23,6 +24,7 @@ from .common import ( RESPONSE_DISARM_FAILURE, RESPONSE_DISARM_SUCCESS, RESPONSE_DISARMED, + RESPONSE_USER_CODE_INVALID, setup_platform, ) @@ -72,12 +74,31 @@ async def test_arm_home_failure(hass): await setup_platform(hass, ALARM_DOMAIN) assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state - with pytest.raises(Exception) as e: + with pytest.raises(HomeAssistantError) as err: await hass.services.async_call( ALARM_DOMAIN, SERVICE_ALARM_ARM_HOME, DATA, blocking=True ) await hass.async_block_till_done() - assert f"{e.value}" == "TotalConnect failed to arm home test." + assert f"{err.value}" == "TotalConnect failed to arm home test." + assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state + + +async def test_arm_home_invalid_usercode(hass): + """Test arm home method with invalid usercode.""" + responses = [RESPONSE_DISARMED, RESPONSE_USER_CODE_INVALID, RESPONSE_DISARMED] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state + + with pytest.raises(HomeAssistantError) as err: + await hass.services.async_call( + ALARM_DOMAIN, SERVICE_ALARM_ARM_HOME, DATA, blocking=True + ) + await hass.async_block_till_done() + assert f"{err.value}" == "TotalConnect failed to arm home test." assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state @@ -108,12 +129,12 @@ async def test_arm_away_failure(hass): await setup_platform(hass, ALARM_DOMAIN) assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state - with pytest.raises(Exception) as e: + with pytest.raises(HomeAssistantError) as err: await hass.services.async_call( ALARM_DOMAIN, SERVICE_ALARM_ARM_AWAY, DATA, blocking=True ) await hass.async_block_till_done() - assert f"{e.value}" == "TotalConnect failed to arm away test." + assert f"{err.value}" == "TotalConnect failed to arm away test." assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state @@ -144,10 +165,29 @@ async def test_disarm_failure(hass): await setup_platform(hass, ALARM_DOMAIN) assert STATE_ALARM_ARMED_AWAY == hass.states.get(ENTITY_ID).state - with pytest.raises(Exception) as e: + with pytest.raises(HomeAssistantError) as err: await hass.services.async_call( ALARM_DOMAIN, SERVICE_ALARM_DISARM, DATA, blocking=True ) await hass.async_block_till_done() - assert f"{e.value}" == "TotalConnect failed to disarm test." + assert f"{err.value}" == "TotalConnect failed to disarm test." + assert STATE_ALARM_ARMED_AWAY == hass.states.get(ENTITY_ID).state + + +async def test_disarm_invalid_usercode(hass): + """Test disarm method failure.""" + responses = [RESPONSE_ARMED_AWAY, RESPONSE_USER_CODE_INVALID, RESPONSE_ARMED_AWAY] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + assert STATE_ALARM_ARMED_AWAY == hass.states.get(ENTITY_ID).state + + with pytest.raises(HomeAssistantError) as err: + await hass.services.async_call( + ALARM_DOMAIN, SERVICE_ALARM_DISARM, DATA, blocking=True + ) + await hass.async_block_till_done() + assert f"{err.value}" == "TotalConnect failed to disarm test." assert STATE_ALARM_ARMED_AWAY == hass.states.get(ENTITY_ID).state diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py index a1aa8780cfb..5d1723a835e 100644 --- a/tests/components/totalconnect/test_config_flow.py +++ b/tests/components/totalconnect/test_config_flow.py @@ -1,78 +1,97 @@ -"""Tests for the iCloud config flow.""" +"""Tests for the TotalConnect config flow.""" from unittest.mock import patch from homeassistant import data_entry_flow -from homeassistant.components.totalconnect.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.components.totalconnect.const import CONF_LOCATION, DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_PASSWORD + +from .common import ( + CONFIG_DATA, + CONFIG_DATA_NO_USERCODES, + RESPONSE_AUTHENTICATE, + RESPONSE_DISARMED, + RESPONSE_SUCCESS, + RESPONSE_USER_CODE_INVALID, + USERNAME, +) from tests.common import MockConfigEntry -USERNAME = "username@me.com" -PASSWORD = "password" - async def test_user(hass): - """Test user config.""" - # no data provided so show the form + """Test user step.""" + # user starts with no data entered, so show the user form result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DOMAIN, + context={"source": SOURCE_USER}, + data=None, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" - # now data is provided, so check if login is correct and create the entry - with patch( - "homeassistant.components.totalconnect.config_flow.TotalConnectClient.TotalConnectClient" - ) as client_mock: - client_mock.return_value.is_valid_credentials.return_value = True + +async def test_user_show_locations(hass): + """Test user locations form.""" + # user/pass provided, so check if valid then ask for usercodes on locations form + responses = [ + RESPONSE_AUTHENTICATE, + RESPONSE_DISARMED, + RESPONSE_USER_CODE_INVALID, + RESPONSE_SUCCESS, + ] + + with patch("zeep.Client", autospec=True), patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ) as mock_request, patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.get_zone_details", + return_value=True, + ), patch( + "homeassistant.components.totalconnect.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + data=CONFIG_DATA_NO_USERCODES, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + # first it should show the locations form + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "locations" + # client should have sent two requests, authenticate and get status + assert mock_request.call_count == 2 - -async def test_import(hass): - """Test import step with good username and password.""" - with patch( - "homeassistant.components.totalconnect.config_flow.TotalConnectClient.TotalConnectClient" - ) as client_mock: - client_mock.return_value.is_valid_credentials.return_value = True - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + # user enters an invalid usercode + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_LOCATION: "bad"}, ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "locations" + # client should have sent 3rd request to validate usercode + assert mock_request.call_count == 3 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + # user enters a valid usercode + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={CONF_LOCATION: "7890"}, + ) + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + # client should have sent another request to validate usercode + assert mock_request.call_count == 4 async def test_abort_if_already_setup(hass): """Test abort if the account is already setup.""" MockConfigEntry( domain=DOMAIN, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + data=CONFIG_DATA, unique_id=USERNAME, ).add_to_hass(hass) - # Should fail, same USERNAME (import) - with patch( - "homeassistant.components.totalconnect.config_flow.TotalConnectClient.TotalConnectClient" - ) as client_mock: - client_mock.return_value.is_valid_credentials.return_value = True - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - # Should fail, same USERNAME (flow) with patch( "homeassistant.components.totalconnect.config_flow.TotalConnectClient.TotalConnectClient" @@ -81,7 +100,7 @@ async def test_abort_if_already_setup(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + data=CONFIG_DATA, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -97,8 +116,51 @@ async def test_login_failed(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + data=CONFIG_DATA, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "invalid_auth"} + + +async def test_reauth(hass): + """Test reauth.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=CONFIG_DATA, + unique_id=USERNAME, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=entry.data + ) + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.totalconnect.config_flow.TotalConnectClient.TotalConnectClient" + ) as client_mock: + # first test with an invalid password + client_mock.return_value.is_valid_credentials.return_value = False + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PASSWORD: "password"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": "invalid_auth"} + + # now test with the password valid + client_mock.return_value.is_valid_credentials.return_value = True + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PASSWORD: "password"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + + assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/totalconnect/test_init.py b/tests/components/totalconnect/test_init.py new file mode 100644 index 00000000000..b8024dbe70d --- /dev/null +++ b/tests/components/totalconnect/test_init.py @@ -0,0 +1,29 @@ +"""Tests for the TotalConnect init process.""" +from unittest.mock import patch + +from homeassistant.components.totalconnect.const import DOMAIN +from homeassistant.config_entries import ENTRY_STATE_SETUP_ERROR +from homeassistant.setup import async_setup_component + +from .common import CONFIG_DATA + +from tests.common import MockConfigEntry + + +async def test_reauth_started(hass): + """Test that reauth is started when we have login errors.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + data=CONFIG_DATA, + ) + mock_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient", + autospec=True, + ) as mock_client: + mock_client.return_value.is_valid_credentials.return_value = False + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + assert mock_entry.state == ENTRY_STATE_SETUP_ERROR From 23c2bd4e6943ee813973dee753e2dfe3aa118e52 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 22 Feb 2021 12:44:40 +0100 Subject: [PATCH 629/796] Upgrade mypy to 0.812 (#46898) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 4683c927085..12f215f177a 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ codecov==2.1.10 coverage==5.4 jsonpickle==1.4.1 mock-open==1.4.0 -mypy==0.800 +mypy==0.812 pre-commit==2.10.1 pylint==2.6.0 astroid==2.4.2 From 338c07a56b3a9064b2a44a4834d969e2b1865209 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 22 Feb 2021 13:01:02 +0100 Subject: [PATCH 630/796] Add Xiaomi Miio vacuum config flow (#46669) --- .../components/xiaomi_miio/__init__.py | 9 +- .../components/xiaomi_miio/config_flow.py | 68 +++-- homeassistant/components/xiaomi_miio/const.py | 4 + .../components/xiaomi_miio/device.py | 8 +- .../components/xiaomi_miio/strings.json | 2 +- .../xiaomi_miio/translations/en.json | 21 +- .../components/xiaomi_miio/vacuum.py | 263 +++++++++--------- .../xiaomi_miio/test_config_flow.py | 60 ++++ tests/components/xiaomi_miio/test_vacuum.py | 32 ++- 9 files changed, 278 insertions(+), 189 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 273fc53da5a..a8b32a31576 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -17,6 +17,7 @@ from .const import ( DOMAIN, KEY_COORDINATOR, MODELS_SWITCH, + MODELS_VACUUM, ) from .gateway import ConnectXiaomiGateway @@ -24,6 +25,7 @@ _LOGGER = logging.getLogger(__name__) GATEWAY_PLATFORMS = ["alarm_control_panel", "sensor", "light"] SWITCH_PLATFORMS = ["switch"] +VACUUM_PLATFORMS = ["vacuum"] async def async_setup(hass: core.HomeAssistant, config: dict): @@ -117,9 +119,14 @@ async def async_setup_device_entry( model = entry.data[CONF_MODEL] # Identify platforms to setup + platforms = [] if model in MODELS_SWITCH: platforms = SWITCH_PLATFORMS - else: + for vacuum_model in MODELS_VACUUM: + if model.startswith(vacuum_model): + platforms = VACUUM_PLATFORMS + + if not platforms: return False for component in platforms: diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 2a1532eaf9b..d7e2198f72f 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -15,8 +15,9 @@ from .const import ( CONF_MAC, CONF_MODEL, DOMAIN, + MODELS_ALL, + MODELS_ALL_DEVICES, MODELS_GATEWAY, - MODELS_SWITCH, ) from .device import ConnectXiaomiDevice @@ -29,6 +30,7 @@ DEVICE_SETTINGS = { vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)), } DEVICE_CONFIG = vol.Schema({vol.Required(CONF_HOST): str}).extend(DEVICE_SETTINGS) +DEVICE_MODEL_CONFIG = {vol.Optional(CONF_MODEL): vol.In(MODELS_ALL)} class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -40,6 +42,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize.""" self.host = None + self.mac = None async def async_step_import(self, conf: dict): """Import a configuration from config.yaml.""" @@ -53,15 +56,15 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle zeroconf discovery.""" name = discovery_info.get("name") self.host = discovery_info.get("host") - mac_address = discovery_info.get("properties", {}).get("mac") + self.mac = discovery_info.get("properties", {}).get("mac") - if not name or not self.host or not mac_address: + if not name or not self.host or not self.mac: return self.async_abort(reason="not_xiaomi_miio") # Check which device is discovered. for gateway_model in MODELS_GATEWAY: if name.startswith(gateway_model.replace(".", "-")): - unique_id = format_mac(mac_address) + unique_id = format_mac(self.mac) await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured({CONF_HOST: self.host}) @@ -70,9 +73,9 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_step_device() - for switch_model in MODELS_SWITCH: - if name.startswith(switch_model.replace(".", "-")): - unique_id = format_mac(mac_address) + for device_model in MODELS_ALL_DEVICES: + if name.startswith(device_model.replace(".", "-")): + unique_id = format_mac(self.mac) await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured({CONF_HOST: self.host}) @@ -95,6 +98,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: token = user_input[CONF_TOKEN] + model = user_input.get(CONF_MODEL) if user_input.get(CONF_HOST): self.host = user_input[CONF_HOST] @@ -103,12 +107,17 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await connect_device_class.async_connect_device(self.host, token) device_info = connect_device_class.device_info - if device_info is not None: + if model is None and device_info is not None: + model = device_info.model + + if model is not None: + if self.mac is None and device_info is not None: + self.mac = format_mac(device_info.mac_address) + # Setup Gateways for gateway_model in MODELS_GATEWAY: - if device_info.model.startswith(gateway_model): - mac = format_mac(device_info.mac_address) - unique_id = mac + if model.startswith(gateway_model): + unique_id = self.mac await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() return self.async_create_entry( @@ -117,29 +126,29 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_FLOW_TYPE: CONF_GATEWAY, CONF_HOST: self.host, CONF_TOKEN: token, - CONF_MODEL: device_info.model, - CONF_MAC: mac, + CONF_MODEL: model, + CONF_MAC: self.mac, }, ) # Setup all other Miio Devices name = user_input.get(CONF_NAME, DEFAULT_DEVICE_NAME) - if device_info.model in MODELS_SWITCH: - mac = format_mac(device_info.mac_address) - unique_id = mac - await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured() - return self.async_create_entry( - title=name, - data={ - CONF_FLOW_TYPE: CONF_DEVICE, - CONF_HOST: self.host, - CONF_TOKEN: token, - CONF_MODEL: device_info.model, - CONF_MAC: mac, - }, - ) + for device_model in MODELS_ALL_DEVICES: + if model.startswith(device_model): + unique_id = self.mac + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=name, + data={ + CONF_FLOW_TYPE: CONF_DEVICE, + CONF_HOST: self.host, + CONF_TOKEN: token, + CONF_MODEL: model, + CONF_MAC: self.mac, + }, + ) errors["base"] = "unknown_device" else: errors["base"] = "cannot_connect" @@ -149,4 +158,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): else: schema = DEVICE_CONFIG + if errors: + schema = schema.extend(DEVICE_MODEL_CONFIG) + return self.async_show_form(step_id="device", data_schema=schema, errors=errors) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index c0ddb698340..d6c39146f6a 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -23,6 +23,10 @@ MODELS_SWITCH = [ "chuangmi.plug.hmi206", "lumi.acpartner.v3", ] +MODELS_VACUUM = ["roborock.vacuum"] + +MODELS_ALL_DEVICES = MODELS_SWITCH + MODELS_VACUUM +MODELS_ALL = MODELS_ALL_DEVICES + MODELS_GATEWAY # Fan Services SERVICE_SET_BUZZER_ON = "fan_set_buzzer_on" diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index 48bedbf0cc8..cb91726ecad 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -78,10 +78,14 @@ class XiaomiMiioEntity(Entity): @property def device_info(self): """Return the device info.""" - return { - "connections": {(dr.CONNECTION_NETWORK_MAC, self._mac)}, + device_info = { "identifiers": {(DOMAIN, self._device_id)}, "manufacturer": "Xiaomi", "name": self._name, "model": self._model, } + + if self._mac is not None: + device_info["connections"] = {(dr.CONNECTION_NETWORK_MAC, self._mac)} + + return device_info diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json index 1ab0c6f51c6..90710baebca 100644 --- a/homeassistant/components/xiaomi_miio/strings.json +++ b/homeassistant/components/xiaomi_miio/strings.json @@ -8,7 +8,7 @@ "data": { "host": "[%key:common::config_flow::data::ip%]", "token": "[%key:common::config_flow::data::api_token%]", - "name": "Name of the device" + "model": "Device model (Optional)" } } }, diff --git a/homeassistant/components/xiaomi_miio/translations/en.json b/homeassistant/components/xiaomi_miio/translations/en.json index fe95af5e06c..37a8ce06eba 100644 --- a/homeassistant/components/xiaomi_miio/translations/en.json +++ b/homeassistant/components/xiaomi_miio/translations/en.json @@ -6,7 +6,6 @@ }, "error": { "cannot_connect": "Failed to connect", - "no_device_selected": "No device selected, please select one device.", "unknown_device": "The device model is not known, not able to setup the device using config flow." }, "flow_title": "Xiaomi Miio: {name}", @@ -14,27 +13,11 @@ "device": { "data": { "host": "IP Address", - "name": "Name of the device", - "token": "API Token" + "token": "API Token", + "model": "Device model (Optional)" }, "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.", "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" - }, - "gateway": { - "data": { - "host": "IP Address", - "name": "Name of the Gateway", - "token": "API Token" - }, - "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.", - "title": "Connect to a Xiaomi Gateway" - }, - "user": { - "data": { - "gateway": "Connect to a Xiaomi Gateway" - }, - "description": "Select to which device you want to connect.", - "title": "Xiaomi Miio" } } } diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index ab76d14a69a..7bdbfca7bc9 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -26,11 +26,15 @@ from homeassistant.components.vacuum import ( SUPPORT_STOP, StateVacuumEntity, ) +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.util.dt import as_utc from .const import ( + CONF_DEVICE, + CONF_FLOW_TYPE, + DOMAIN, SERVICE_CLEAN_SEGMENT, SERVICE_CLEAN_ZONE, SERVICE_GOTO, @@ -39,11 +43,11 @@ from .const import ( SERVICE_START_REMOTE_CONTROL, SERVICE_STOP_REMOTE_CONTROL, ) +from .device import XiaomiMiioEntity _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Xiaomi Vacuum cleaner" -DATA_KEY = "vacuum.xiaomi_miio" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -116,110 +120,124 @@ STATE_CODE_TO_STATE = { async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Xiaomi vacuum cleaner robot platform.""" - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {} - - host = config[CONF_HOST] - token = config[CONF_TOKEN] - name = config[CONF_NAME] - - # Create handler - _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) - vacuum = Vacuum(host, token) - - mirobo = MiroboVacuum(name, vacuum) - hass.data[DATA_KEY][host] = mirobo - - async_add_entities([mirobo], update_before_add=True) - - platform = entity_platform.current_platform.get() - - platform.async_register_entity_service( - SERVICE_START_REMOTE_CONTROL, - {}, - MiroboVacuum.async_remote_control_start.__name__, + """Import Miio configuration from YAML.""" + _LOGGER.warning( + "Loading Xiaomi Miio Vacuum via platform setup is deprecated. Please remove it from your configuration." ) - - platform.async_register_entity_service( - SERVICE_STOP_REMOTE_CONTROL, - {}, - MiroboVacuum.async_remote_control_stop.__name__, - ) - - platform.async_register_entity_service( - SERVICE_MOVE_REMOTE_CONTROL, - { - vol.Optional(ATTR_RC_VELOCITY): vol.All( - vol.Coerce(float), vol.Clamp(min=-0.29, max=0.29) - ), - vol.Optional(ATTR_RC_ROTATION): vol.All( - vol.Coerce(int), vol.Clamp(min=-179, max=179) - ), - vol.Optional(ATTR_RC_DURATION): cv.positive_int, - }, - MiroboVacuum.async_remote_control_move.__name__, - ) - - platform.async_register_entity_service( - SERVICE_MOVE_REMOTE_CONTROL_STEP, - { - vol.Optional(ATTR_RC_VELOCITY): vol.All( - vol.Coerce(float), vol.Clamp(min=-0.29, max=0.29) - ), - vol.Optional(ATTR_RC_ROTATION): vol.All( - vol.Coerce(int), vol.Clamp(min=-179, max=179) - ), - vol.Optional(ATTR_RC_DURATION): cv.positive_int, - }, - MiroboVacuum.async_remote_control_move_step.__name__, - ) - - platform.async_register_entity_service( - SERVICE_CLEAN_ZONE, - { - vol.Required(ATTR_ZONE_ARRAY): vol.All( - list, - [ - vol.ExactSequence( - [ - vol.Coerce(int), - vol.Coerce(int), - vol.Coerce(int), - vol.Coerce(int), - ] - ) - ], - ), - vol.Required(ATTR_ZONE_REPEATER): vol.All( - vol.Coerce(int), vol.Clamp(min=1, max=3) - ), - }, - MiroboVacuum.async_clean_zone.__name__, - ) - - platform.async_register_entity_service( - SERVICE_GOTO, - { - vol.Required("x_coord"): vol.Coerce(int), - vol.Required("y_coord"): vol.Coerce(int), - }, - MiroboVacuum.async_goto.__name__, - ) - platform.async_register_entity_service( - SERVICE_CLEAN_SEGMENT, - {vol.Required("segments"): vol.Any(vol.Coerce(int), [vol.Coerce(int)])}, - MiroboVacuum.async_clean_segment.__name__, + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) ) -class MiroboVacuum(StateVacuumEntity): +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Xiaomi vacuum cleaner robot from a config entry.""" + entities = [] + + if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: + host = config_entry.data[CONF_HOST] + token = config_entry.data[CONF_TOKEN] + name = config_entry.title + unique_id = config_entry.unique_id + + # Create handler + _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) + vacuum = Vacuum(host, token) + + mirobo = MiroboVacuum(name, vacuum, config_entry, unique_id) + entities.append(mirobo) + + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_START_REMOTE_CONTROL, + {}, + MiroboVacuum.async_remote_control_start.__name__, + ) + + platform.async_register_entity_service( + SERVICE_STOP_REMOTE_CONTROL, + {}, + MiroboVacuum.async_remote_control_stop.__name__, + ) + + platform.async_register_entity_service( + SERVICE_MOVE_REMOTE_CONTROL, + { + vol.Optional(ATTR_RC_VELOCITY): vol.All( + vol.Coerce(float), vol.Clamp(min=-0.29, max=0.29) + ), + vol.Optional(ATTR_RC_ROTATION): vol.All( + vol.Coerce(int), vol.Clamp(min=-179, max=179) + ), + vol.Optional(ATTR_RC_DURATION): cv.positive_int, + }, + MiroboVacuum.async_remote_control_move.__name__, + ) + + platform.async_register_entity_service( + SERVICE_MOVE_REMOTE_CONTROL_STEP, + { + vol.Optional(ATTR_RC_VELOCITY): vol.All( + vol.Coerce(float), vol.Clamp(min=-0.29, max=0.29) + ), + vol.Optional(ATTR_RC_ROTATION): vol.All( + vol.Coerce(int), vol.Clamp(min=-179, max=179) + ), + vol.Optional(ATTR_RC_DURATION): cv.positive_int, + }, + MiroboVacuum.async_remote_control_move_step.__name__, + ) + + platform.async_register_entity_service( + SERVICE_CLEAN_ZONE, + { + vol.Required(ATTR_ZONE_ARRAY): vol.All( + list, + [ + vol.ExactSequence( + [ + vol.Coerce(int), + vol.Coerce(int), + vol.Coerce(int), + vol.Coerce(int), + ] + ) + ], + ), + vol.Required(ATTR_ZONE_REPEATER): vol.All( + vol.Coerce(int), vol.Clamp(min=1, max=3) + ), + }, + MiroboVacuum.async_clean_zone.__name__, + ) + + platform.async_register_entity_service( + SERVICE_GOTO, + { + vol.Required("x_coord"): vol.Coerce(int), + vol.Required("y_coord"): vol.Coerce(int), + }, + MiroboVacuum.async_goto.__name__, + ) + platform.async_register_entity_service( + SERVICE_CLEAN_SEGMENT, + {vol.Required("segments"): vol.Any(vol.Coerce(int), [vol.Coerce(int)])}, + MiroboVacuum.async_clean_segment.__name__, + ) + + async_add_entities(entities, update_before_add=True) + + +class MiroboVacuum(XiaomiMiioEntity, StateVacuumEntity): """Representation of a Xiaomi Vacuum cleaner robot.""" - def __init__(self, name, vacuum): + def __init__(self, name, device, entry, unique_id): """Initialize the Xiaomi vacuum cleaner robot handler.""" - self._name = name - self._vacuum = vacuum + super().__init__(name, device, entry, unique_id) self.vacuum_state = None self._available = False @@ -233,11 +251,6 @@ class MiroboVacuum(StateVacuumEntity): self._timers = None - @property - def name(self): - """Return the name of the device.""" - return self._name - @property def state(self): """Return the status of the vacuum cleaner.""" @@ -364,16 +377,16 @@ class MiroboVacuum(StateVacuumEntity): async def async_start(self): """Start or resume the cleaning task.""" await self._try_command( - "Unable to start the vacuum: %s", self._vacuum.resume_or_start + "Unable to start the vacuum: %s", self._device.resume_or_start ) async def async_pause(self): """Pause the cleaning task.""" - await self._try_command("Unable to set start/pause: %s", self._vacuum.pause) + await self._try_command("Unable to set start/pause: %s", self._device.pause) async def async_stop(self, **kwargs): """Stop the vacuum cleaner.""" - await self._try_command("Unable to stop: %s", self._vacuum.stop) + await self._try_command("Unable to stop: %s", self._device.stop) async def async_set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" @@ -390,28 +403,28 @@ class MiroboVacuum(StateVacuumEntity): ) return await self._try_command( - "Unable to set fan speed: %s", self._vacuum.set_fan_speed, fan_speed + "Unable to set fan speed: %s", self._device.set_fan_speed, fan_speed ) async def async_return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" - await self._try_command("Unable to return home: %s", self._vacuum.home) + await self._try_command("Unable to return home: %s", self._device.home) async def async_clean_spot(self, **kwargs): """Perform a spot clean-up.""" await self._try_command( - "Unable to start the vacuum for a spot clean-up: %s", self._vacuum.spot + "Unable to start the vacuum for a spot clean-up: %s", self._device.spot ) async def async_locate(self, **kwargs): """Locate the vacuum cleaner.""" - await self._try_command("Unable to locate the botvac: %s", self._vacuum.find) + await self._try_command("Unable to locate the botvac: %s", self._device.find) async def async_send_command(self, command, params=None, **kwargs): """Send raw command.""" await self._try_command( "Unable to send command to the vacuum: %s", - self._vacuum.raw_command, + self._device.raw_command, command, params, ) @@ -419,13 +432,13 @@ class MiroboVacuum(StateVacuumEntity): async def async_remote_control_start(self): """Start remote control mode.""" await self._try_command( - "Unable to start remote control the vacuum: %s", self._vacuum.manual_start + "Unable to start remote control the vacuum: %s", self._device.manual_start ) async def async_remote_control_stop(self): """Stop remote control mode.""" await self._try_command( - "Unable to stop remote control the vacuum: %s", self._vacuum.manual_stop + "Unable to stop remote control the vacuum: %s", self._device.manual_stop ) async def async_remote_control_move( @@ -434,7 +447,7 @@ class MiroboVacuum(StateVacuumEntity): """Move vacuum with remote control mode.""" await self._try_command( "Unable to move with remote control the vacuum: %s", - self._vacuum.manual_control, + self._device.manual_control, velocity=velocity, rotation=rotation, duration=duration, @@ -446,7 +459,7 @@ class MiroboVacuum(StateVacuumEntity): """Move vacuum one step with remote control mode.""" await self._try_command( "Unable to remote control the vacuum: %s", - self._vacuum.manual_control_once, + self._device.manual_control_once, velocity=velocity, rotation=rotation, duration=duration, @@ -456,7 +469,7 @@ class MiroboVacuum(StateVacuumEntity): """Goto the specified coordinates.""" await self._try_command( "Unable to send the vacuum cleaner to the specified coordinates: %s", - self._vacuum.goto, + self._device.goto, x_coord=x_coord, y_coord=y_coord, ) @@ -468,23 +481,23 @@ class MiroboVacuum(StateVacuumEntity): await self._try_command( "Unable to start cleaning of the specified segments: %s", - self._vacuum.segment_clean, + self._device.segment_clean, segments=segments, ) def update(self): """Fetch state from the device.""" try: - state = self._vacuum.status() + state = self._device.status() self.vacuum_state = state - self._fan_speeds = self._vacuum.fan_speed_presets() + self._fan_speeds = self._device.fan_speed_presets() self._fan_speeds_reverse = {v: k for k, v in self._fan_speeds.items()} - self.consumable_state = self._vacuum.consumable_status() - self.clean_history = self._vacuum.clean_history() - self.last_clean = self._vacuum.last_clean_details() - self.dnd_state = self._vacuum.dnd_status() + self.consumable_state = self._device.consumable_status() + self.clean_history = self._device.clean_history() + self.last_clean = self._device.last_clean_details() + self.dnd_state = self._device.dnd_status() self._available = True except (OSError, DeviceException) as exc: @@ -494,7 +507,7 @@ class MiroboVacuum(StateVacuumEntity): # Fetch timers separately, see #38285 try: - self._timers = self._vacuum.timer() + self._timers = self._device.timer() except DeviceException as exc: _LOGGER.debug( "Unable to fetch timers, this may happen on some devices: %s", exc @@ -507,6 +520,6 @@ class MiroboVacuum(StateVacuumEntity): _zone.append(repeats) _LOGGER.debug("Zone with repeats: %s", zone) try: - await self.hass.async_add_executor_job(self._vacuum.zoned_clean, zone) + await self.hass.async_add_executor_job(self._device.zoned_clean, zone) except (OSError, DeviceException) as exc: _LOGGER.error("Unable to send zoned_clean command to the vacuum: %s", exc) diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 220c51034f1..f4f7b5e2b46 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -257,6 +257,53 @@ async def test_import_flow_success(hass): } +async def test_config_flow_step_device_manual_model_succes(hass): + """Test config flow, device connection error, manual model.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "device" + assert result["errors"] == {} + + with patch( + "homeassistant.components.xiaomi_miio.device.Device.info", + side_effect=DeviceException({}), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "device" + assert result["errors"] == {"base": "cannot_connect"} + + overwrite_model = const.MODELS_VACUUM[0] + + with patch( + "homeassistant.components.xiaomi_miio.device.Device.info", + side_effect=DeviceException({}), + ), patch( + "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_TOKEN: TEST_TOKEN, const.CONF_MODEL: overwrite_model}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == DEFAULT_DEVICE_NAME + assert result["data"] == { + const.CONF_FLOW_TYPE: const.CONF_DEVICE, + CONF_HOST: TEST_HOST, + CONF_TOKEN: TEST_TOKEN, + const.CONF_MODEL: overwrite_model, + const.CONF_MAC: None, + } + + async def config_flow_device_success(hass, model_to_test): """Test a successful config flow for a device (base class).""" result = await hass.config_entries.flow.async_init( @@ -342,3 +389,16 @@ async def test_zeroconf_plug_success(hass): test_plug_model = const.MODELS_SWITCH[0] test_zeroconf_name = const.MODELS_SWITCH[0].replace(".", "-") await zeroconf_device_success(hass, test_zeroconf_name, test_plug_model) + + +async def test_config_flow_vacuum_success(hass): + """Test a successful config flow for a vacuum.""" + test_vacuum_model = const.MODELS_VACUUM[0] + await config_flow_device_success(hass, test_vacuum_model) + + +async def test_zeroconf_vacuum_success(hass): + """Test a successful zeroconf discovery of a vacuum.""" + test_vacuum_model = const.MODELS_VACUUM[0] + test_zeroconf_name = const.MODELS_VACUUM[0].replace(".", "-") + await zeroconf_device_success(hass, test_zeroconf_name, test_vacuum_model) diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index b1a3c08b84b..23e5d8884b3 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -22,6 +22,7 @@ from homeassistant.components.vacuum import ( STATE_CLEANING, STATE_ERROR, ) +from homeassistant.components.xiaomi_miio import const from homeassistant.components.xiaomi_miio.const import DOMAIN as XIAOMI_DOMAIN from homeassistant.components.xiaomi_miio.vacuum import ( ATTR_CLEANED_AREA, @@ -38,7 +39,6 @@ from homeassistant.components.xiaomi_miio.vacuum import ( ATTR_SIDE_BRUSH_LEFT, ATTR_TIMERS, CONF_HOST, - CONF_NAME, CONF_TOKEN, SERVICE_CLEAN_SEGMENT, SERVICE_CLEAN_ZONE, @@ -51,12 +51,14 @@ from homeassistant.components.xiaomi_miio.vacuum import ( from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, - CONF_PLATFORM, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.setup import async_setup_component + +from .test_config_flow import TEST_MAC + +from tests.common import MockConfigEntry PLATFORM = "xiaomi_miio" @@ -521,17 +523,21 @@ async def setup_component(hass, entity_name): """Set up vacuum component.""" entity_id = f"{DOMAIN}.{entity_name}" - await async_setup_component( - hass, - DOMAIN, - { - DOMAIN: { - CONF_PLATFORM: PLATFORM, - CONF_HOST: "192.168.1.100", - CONF_NAME: entity_name, - CONF_TOKEN: "12345678901234567890123456789012", - } + config_entry = MockConfigEntry( + domain=XIAOMI_DOMAIN, + unique_id="123456", + title=entity_name, + data={ + const.CONF_FLOW_TYPE: const.CONF_DEVICE, + CONF_HOST: "192.168.1.100", + CONF_TOKEN: "12345678901234567890123456789012", + const.CONF_MODEL: const.MODELS_VACUUM[0], + const.CONF_MAC: TEST_MAC, }, ) + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + return entity_id From 36b56586def1e2ae0e63b68f58966adea3989fe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Hansl=C3=ADk?= Date: Mon, 22 Feb 2021 13:58:32 +0100 Subject: [PATCH 631/796] Bump samsungtvws from 1.4.0 to 1.6.0 (#46878) --- homeassistant/components/samsungtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 5584d2dd452..08dc4d0c049 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/samsungtv", "requirements": [ "samsungctl[websocket]==0.7.1", - "samsungtvws==1.4.0" + "samsungtvws==1.6.0" ], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 5d704b7522a..b98cbde46bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1998,7 +1998,7 @@ rxv==0.6.0 samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv -samsungtvws==1.4.0 +samsungtvws==1.6.0 # homeassistant.components.satel_integra satel_integra==0.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 957161dca12..b1beff34323 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1029,7 +1029,7 @@ rxv==0.6.0 samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv -samsungtvws==1.4.0 +samsungtvws==1.6.0 # homeassistant.components.dhcp scapy==2.4.4 From 82a9dc620cc20692e5b5c84381be38084f89ad75 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 22 Feb 2021 14:31:22 +0100 Subject: [PATCH 632/796] Add device_class to Shelly cover domain (#46894) Fix author --- homeassistant/components/shelly/cover.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/shelly/cover.py b/homeassistant/components/shelly/cover.py index 6caa7d5132c..0438e5fe6b7 100644 --- a/homeassistant/components/shelly/cover.py +++ b/homeassistant/components/shelly/cover.py @@ -3,6 +3,7 @@ from aioshelly import Block from homeassistant.components.cover import ( ATTR_POSITION, + DEVICE_CLASS_SHUTTER, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, @@ -75,6 +76,11 @@ class ShellyCover(ShellyBlockEntity, CoverEntity): """Flag supported features.""" return self._supported_features + @property + def device_class(self) -> str: + """Return the class of the device.""" + return DEVICE_CLASS_SHUTTER + async def async_close_cover(self, **kwargs): """Close cover.""" self.control_result = await self.block.set_state(go="close") From 603191702fcfcd51c257567565d6990f3a6930ed Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 22 Feb 2021 14:43:29 +0100 Subject: [PATCH 633/796] Cleanup of possibily confusing comment in esphome (#46903) --- homeassistant/components/esphome/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 23b4044fc9e..6ce411b5169 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -52,10 +52,7 @@ CONFIG_SCHEMA = vol.Schema({}, extra=vol.ALLOW_EXTRA) async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: - """Stub to allow setting up this component. - - Configuration through YAML is not supported at this time. - """ + """Stub to allow setting up this component.""" return True From 81d011efc533cc516ad57dfb873f654bd8f1f31b Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Mon, 22 Feb 2021 06:10:00 -0800 Subject: [PATCH 634/796] Add binary sensor to SmartTub for online status (#46889) --- homeassistant/components/smarttub/__init__.py | 2 +- .../components/smarttub/binary_sensor.py | 40 +++++++++++++++++++ homeassistant/components/smarttub/entity.py | 16 +++++++- homeassistant/components/smarttub/sensor.py | 16 ++------ tests/components/smarttub/conftest.py | 1 + .../components/smarttub/test_binary_sensor.py | 13 ++++++ 6 files changed, 72 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/smarttub/binary_sensor.py create mode 100644 tests/components/smarttub/test_binary_sensor.py diff --git a/homeassistant/components/smarttub/__init__.py b/homeassistant/components/smarttub/__init__.py index 8467c208076..457af4b7bc0 100644 --- a/homeassistant/components/smarttub/__init__.py +++ b/homeassistant/components/smarttub/__init__.py @@ -7,7 +7,7 @@ from .controller import SmartTubController _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["climate", "light", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "climate", "light", "sensor", "switch"] async def async_setup(hass, config): diff --git a/homeassistant/components/smarttub/binary_sensor.py b/homeassistant/components/smarttub/binary_sensor.py new file mode 100644 index 00000000000..52dbfd71a37 --- /dev/null +++ b/homeassistant/components/smarttub/binary_sensor.py @@ -0,0 +1,40 @@ +"""Platform for binary sensor integration.""" +import logging + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + BinarySensorEntity, +) + +from .const import DOMAIN, SMARTTUB_CONTROLLER +from .entity import SmartTubSensorBase + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up binary sensor entities for the binary sensors in the tub.""" + + controller = hass.data[DOMAIN][entry.entry_id][SMARTTUB_CONTROLLER] + + entities = [SmartTubOnline(controller.coordinator, spa) for spa in controller.spas] + + async_add_entities(entities) + + +class SmartTubOnline(SmartTubSensorBase, BinarySensorEntity): + """A binary sensor indicating whether the spa is currently online (connected to the cloud).""" + + def __init__(self, coordinator, spa): + """Initialize the entity.""" + super().__init__(coordinator, spa, "Online", "online") + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + return self._state is True + + @property + def device_class(self) -> str: + """Return the device class for this entity.""" + return DEVICE_CLASS_CONNECTIVITY diff --git a/homeassistant/components/smarttub/entity.py b/homeassistant/components/smarttub/entity.py index eab60b4162c..8be956a2b70 100644 --- a/homeassistant/components/smarttub/entity.py +++ b/homeassistant/components/smarttub/entity.py @@ -13,8 +13,6 @@ from .helpers import get_spa_name _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["climate"] - class SmartTubEntity(CoordinatorEntity): """Base class for SmartTub entities.""" @@ -57,3 +55,17 @@ class SmartTubEntity(CoordinatorEntity): """Retrieve the result of Spa.get_status().""" return self.coordinator.data[self.spa.id].get("status") + + +class SmartTubSensorBase(SmartTubEntity): + """Base class for SmartTub sensors.""" + + def __init__(self, coordinator, spa, sensor_name, attr_name): + """Initialize the entity.""" + super().__init__(coordinator, spa, sensor_name) + self._attr_name = attr_name + + @property + def _state(self): + """Retrieve the underlying state from the spa.""" + return getattr(self.spa_status, self._attr_name) diff --git a/homeassistant/components/smarttub/sensor.py b/homeassistant/components/smarttub/sensor.py index 4619d07dbe0..be3d60c0241 100644 --- a/homeassistant/components/smarttub/sensor.py +++ b/homeassistant/components/smarttub/sensor.py @@ -3,7 +3,7 @@ from enum import Enum import logging from .const import DOMAIN, SMARTTUB_CONTROLLER -from .entity import SmartTubEntity +from .entity import SmartTubSensorBase _LOGGER = logging.getLogger(__name__) @@ -42,18 +42,8 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities) -class SmartTubSensor(SmartTubEntity): - """Generic and base class for SmartTub sensors.""" - - def __init__(self, coordinator, spa, sensor_name, attr_name): - """Initialize the entity.""" - super().__init__(coordinator, spa, sensor_name) - self._attr_name = attr_name - - @property - def _state(self): - """Retrieve the underlying state from the spa.""" - return getattr(self.spa_status, self._attr_name) +class SmartTubSensor(SmartTubSensorBase): + """Generic class for SmartTub status sensors.""" @property def state(self) -> str: diff --git a/tests/components/smarttub/conftest.py b/tests/components/smarttub/conftest.py index 79e5d06d3b3..b7c90b5ad3e 100644 --- a/tests/components/smarttub/conftest.py +++ b/tests/components/smarttub/conftest.py @@ -48,6 +48,7 @@ def mock_spa(): "setTemperature": 39, "water": {"temperature": 38}, "heater": "ON", + "online": True, "heatMode": "AUTO", "state": "NORMAL", "primaryFiltration": { diff --git a/tests/components/smarttub/test_binary_sensor.py b/tests/components/smarttub/test_binary_sensor.py new file mode 100644 index 00000000000..b2624369e96 --- /dev/null +++ b/tests/components/smarttub/test_binary_sensor.py @@ -0,0 +1,13 @@ +"""Test the SmartTub binary sensor platform.""" + +from homeassistant.components.binary_sensor import DEVICE_CLASS_CONNECTIVITY, STATE_ON + + +async def test_binary_sensors(spa, setup_entry, hass): + """Test the binary sensors.""" + + entity_id = f"binary_sensor.{spa.brand}_{spa.model}_online" + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_ON + assert state.attributes.get("device_class") == DEVICE_CLASS_CONNECTIVITY From 75e04f3a713b6fab3ff4839cd1006280302b7bdf Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 22 Feb 2021 09:28:08 -0500 Subject: [PATCH 635/796] Clean up constants (#46885) --- homeassistant/components/air_quality/__init__.py | 6 ++++-- homeassistant/components/channels/media_player.py | 5 +---- homeassistant/components/comfoconnect/sensor.py | 2 +- homeassistant/components/facebox/image_processing.py | 2 +- homeassistant/components/hassio/const.py | 1 - homeassistant/components/hassio/discovery.py | 11 ++--------- .../components/homematicip_cloud/generic_entity.py | 2 +- homeassistant/components/netatmo/const.py | 1 - homeassistant/components/netatmo/webhook.py | 2 +- homeassistant/components/nightscout/const.py | 1 - homeassistant/components/nightscout/sensor.py | 3 ++- homeassistant/components/rachio/switch.py | 3 +-- homeassistant/components/reddit/sensor.py | 2 +- homeassistant/components/traccar/__init__.py | 8 ++++++-- homeassistant/components/traccar/const.py | 1 - .../components/usgs_earthquakes_feed/geo_location.py | 2 +- homeassistant/components/watson_tts/tts.py | 1 - homeassistant/components/zwave_js/__init__.py | 3 +-- homeassistant/components/zwave_js/const.py | 1 - tests/components/dynalite/common.py | 2 +- tests/components/nightscout/test_sensor.py | 3 +-- 21 files changed, 25 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/air_quality/__init__.py b/homeassistant/components/air_quality/__init__.py index 48423d08e69..52c9208854a 100644 --- a/homeassistant/components/air_quality/__init__.py +++ b/homeassistant/components/air_quality/__init__.py @@ -2,7 +2,10 @@ from datetime import timedelta import logging -from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, +) from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -13,7 +16,6 @@ from homeassistant.helpers.entity_component import EntityComponent _LOGGER = logging.getLogger(__name__) ATTR_AQI = "air_quality_index" -ATTR_ATTRIBUTION = "attribution" ATTR_CO2 = "carbon_dioxide" ATTR_CO = "carbon_monoxide" ATTR_N2O = "nitrogen_oxide" diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py index 481cdd7ecad..5376dc3fe97 100644 --- a/homeassistant/components/channels/media_player.py +++ b/homeassistant/components/channels/media_player.py @@ -18,6 +18,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, ) from homeassistant.const import ( + ATTR_SECONDS, CONF_HOST, CONF_NAME, CONF_PORT, @@ -53,10 +54,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -# Service call validation schemas -ATTR_SECONDS = "seconds" - - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Channels platform.""" device = ChannelsPlayer(config[CONF_NAME], config[CONF_HOST], config[CONF_PORT]) diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index 53075beecaf..660228b0b8d 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -29,6 +29,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_ID, CONF_RESOURCES, DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, @@ -72,7 +73,6 @@ ATTR_SUPPLY_TEMPERATURE = "supply_temperature" _LOGGER = logging.getLogger(__name__) ATTR_ICON = "icon" -ATTR_ID = "id" ATTR_LABEL = "label" ATTR_MULTIPLIER = "multiplier" ATTR_UNIT = "unit" diff --git a/homeassistant/components/facebox/image_processing.py b/homeassistant/components/facebox/image_processing.py index ee6e4d8a6fa..6a460ac305b 100644 --- a/homeassistant/components/facebox/image_processing.py +++ b/homeassistant/components/facebox/image_processing.py @@ -15,6 +15,7 @@ from homeassistant.components.image_processing import ( ) from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_ID, ATTR_NAME, CONF_IP_ADDRESS, CONF_PASSWORD, @@ -34,7 +35,6 @@ _LOGGER = logging.getLogger(__name__) ATTR_BOUNDING_BOX = "bounding_box" ATTR_CLASSIFIER = "classifier" ATTR_IMAGE_ID = "image_id" -ATTR_ID = "id" ATTR_MATCHED = "matched" FACEBOX_NAME = "name" CLASSIFIER = "facebox" diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 00893f83401..0cb1649dfc5 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -16,7 +16,6 @@ ATTR_INPUT = "input" ATTR_NAME = "name" ATTR_PANELS = "panels" ATTR_PASSWORD = "password" -ATTR_SERVICE = "service" ATTR_SNAPSHOT = "snapshot" ATTR_TITLE = "title" ATTR_USERNAME = "username" diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index f3337254f1a..cda05eccbec 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -6,17 +6,10 @@ from aiohttp import web from aiohttp.web_exceptions import HTTPServiceUnavailable from homeassistant.components.http import HomeAssistantView -from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.const import ATTR_SERVICE, EVENT_HOMEASSISTANT_START from homeassistant.core import callback -from .const import ( - ATTR_ADDON, - ATTR_CONFIG, - ATTR_DISCOVERY, - ATTR_NAME, - ATTR_SERVICE, - ATTR_UUID, -) +from .const import ATTR_ADDON, ATTR_CONFIG, ATTR_DISCOVERY, ATTR_NAME, ATTR_UUID from .handler import HassioAPIError _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index 65e5ade7d1d..a1e13658d20 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -5,6 +5,7 @@ from typing import Any, Dict, Optional from homematicip.aio.device import AsyncDevice from homematicip.aio.group import AsyncGroup +from homeassistant.const import ATTR_ID from homeassistant.core import callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import Entity @@ -19,7 +20,6 @@ ATTR_LOW_BATTERY = "low_battery" ATTR_CONFIG_PENDING = "config_pending" ATTR_CONNECTION_TYPE = "connection_type" ATTR_DUTY_CYCLE_REACHED = "duty_cycle_reached" -ATTR_ID = "id" ATTR_IS_GROUP = "is_group" # RSSI HAP -> Device ATTR_RSSI_DEVICE = "rssi_device" diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index ed1c5f0a880..4c3650ef121 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -56,7 +56,6 @@ DEFAULT_PERSON = "Unknown" DEFAULT_DISCOVERY = True DEFAULT_WEBHOOKS = False -ATTR_ID = "id" ATTR_PSEUDO = "pseudo" ATTR_NAME = "name" ATTR_EVENT_TYPE = "event_type" diff --git a/homeassistant/components/netatmo/webhook.py b/homeassistant/components/netatmo/webhook.py index 582fce8985c..309451fd982 100644 --- a/homeassistant/components/netatmo/webhook.py +++ b/homeassistant/components/netatmo/webhook.py @@ -1,13 +1,13 @@ """The Netatmo integration.""" import logging +from homeassistant.const import ATTR_ID from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( ATTR_EVENT_TYPE, ATTR_FACE_URL, - ATTR_ID, ATTR_IS_KNOWN, ATTR_NAME, ATTR_PERSONS, diff --git a/homeassistant/components/nightscout/const.py b/homeassistant/components/nightscout/const.py index 4bb96a94c29..7e47f7ff49d 100644 --- a/homeassistant/components/nightscout/const.py +++ b/homeassistant/components/nightscout/const.py @@ -3,6 +3,5 @@ DOMAIN = "nightscout" ATTR_DEVICE = "device" -ATTR_DATE = "date" ATTR_DELTA = "delta" ATTR_DIRECTION = "direction" diff --git a/homeassistant/components/nightscout/sensor.py b/homeassistant/components/nightscout/sensor.py index f4ff14d7b2a..efa625577d9 100644 --- a/homeassistant/components/nightscout/sensor.py +++ b/homeassistant/components/nightscout/sensor.py @@ -8,10 +8,11 @@ from aiohttp import ClientError from py_nightscout import Api as NightscoutAPI from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_DATE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from .const import ATTR_DATE, ATTR_DELTA, ATTR_DEVICE, ATTR_DIRECTION, DOMAIN +from .const import ATTR_DELTA, ATTR_DEVICE, ATTR_DIRECTION, DOMAIN SCAN_INTERVAL = timedelta(minutes=1) diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 8009d79b224..44a17acaecf 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -6,7 +6,7 @@ import logging import voluptuous as vol from homeassistant.components.switch import SwitchEntity -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, ATTR_ID from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform @@ -67,7 +67,6 @@ from .webhooks import ( _LOGGER = logging.getLogger(__name__) ATTR_DURATION = "duration" -ATTR_ID = "id" ATTR_PERCENT = "percent" ATTR_SCHEDULE_SUMMARY = "Summary" ATTR_SCHEDULE_ENABLED = "Enabled" diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py index 0fe4e87f863..7a04fb6a8ae 100644 --- a/homeassistant/components/reddit/sensor.py +++ b/homeassistant/components/reddit/sensor.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( + ATTR_ID, CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_MAXIMUM, @@ -21,7 +22,6 @@ _LOGGER = logging.getLogger(__name__) CONF_SORT_BY = "sort_by" CONF_SUBREDDITS = "subreddits" -ATTR_ID = "id" ATTR_BODY = "body" ATTR_COMMENTS_NUMBER = "comms_num" ATTR_CREATED = "created" diff --git a/homeassistant/components/traccar/__init__.py b/homeassistant/components/traccar/__init__.py index c19a9cdd27e..cc598a9851b 100644 --- a/homeassistant/components/traccar/__init__.py +++ b/homeassistant/components/traccar/__init__.py @@ -3,7 +3,12 @@ from aiohttp import web import voluptuous as vol from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER -from homeassistant.const import CONF_WEBHOOK_ID, HTTP_OK, HTTP_UNPROCESSABLE_ENTITY +from homeassistant.const import ( + ATTR_ID, + CONF_WEBHOOK_ID, + HTTP_OK, + HTTP_UNPROCESSABLE_ENTITY, +) from homeassistant.helpers import config_entry_flow import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -13,7 +18,6 @@ from .const import ( ATTR_ALTITUDE, ATTR_BATTERY, ATTR_BEARING, - ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_SPEED, diff --git a/homeassistant/components/traccar/const.py b/homeassistant/components/traccar/const.py index 56c0ab5ba1d..06dd368b6a3 100644 --- a/homeassistant/components/traccar/const.py +++ b/homeassistant/components/traccar/const.py @@ -12,7 +12,6 @@ ATTR_BATTERY = "batt" ATTR_BEARING = "bearing" ATTR_CATEGORY = "category" ATTR_GEOFENCE = "geofence" -ATTR_ID = "id" ATTR_LATITUDE = "lat" ATTR_LONGITUDE = "lon" ATTR_MOTION = "motion" diff --git a/homeassistant/components/usgs_earthquakes_feed/geo_location.py b/homeassistant/components/usgs_earthquakes_feed/geo_location.py index 2b149fcac26..9fd98de42df 100644 --- a/homeassistant/components/usgs_earthquakes_feed/geo_location.py +++ b/homeassistant/components/usgs_earthquakes_feed/geo_location.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent from homeassistant.const import ( ATTR_ATTRIBUTION, + ATTR_TIME, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, @@ -30,7 +31,6 @@ ATTR_EXTERNAL_ID = "external_id" ATTR_MAGNITUDE = "magnitude" ATTR_PLACE = "place" ATTR_STATUS = "status" -ATTR_TIME = "time" ATTR_TYPE = "type" ATTR_UPDATED = "updated" diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index 9b2af2ea7fe..ad989ec39fc 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -8,7 +8,6 @@ import homeassistant.helpers.config_validation as cv CONF_URL = "watson_url" CONF_APIKEY = "watson_apikey" -ATTR_CREDENTIALS = "credentials" DEFAULT_URL = "https://stream.watsonplatform.net/text-to-speech/api" diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 9c8c245e910..f6edb2f4596 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -12,7 +12,7 @@ from zwave_js_server.model.value import ValueNotification from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ATTR_DOMAIN, CONF_URL, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry @@ -24,7 +24,6 @@ from .const import ( ATTR_COMMAND_CLASS, ATTR_COMMAND_CLASS_NAME, ATTR_DEVICE_ID, - ATTR_DOMAIN, ATTR_ENDPOINT, ATTR_HOME_ID, ATTR_LABEL, diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index dc2ffaeaa20..9905eba0693 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -28,7 +28,6 @@ ATTR_VALUE = "value" ATTR_COMMAND_CLASS = "command_class" ATTR_COMMAND_CLASS_NAME = "command_class_name" ATTR_TYPE = "type" -ATTR_DOMAIN = "domain" ATTR_DEVICE_ID = "device_id" ATTR_PROPERTY_NAME = "property_name" ATTR_PROPERTY_KEY_NAME = "property_key_name" diff --git a/tests/components/dynalite/common.py b/tests/components/dynalite/common.py index 48ec378689e..072e222c194 100644 --- a/tests/components/dynalite/common.py +++ b/tests/components/dynalite/common.py @@ -2,11 +2,11 @@ from unittest.mock import AsyncMock, Mock, call, patch from homeassistant.components import dynalite +from homeassistant.const import ATTR_SERVICE from homeassistant.helpers import entity_registry from tests.common import MockConfigEntry -ATTR_SERVICE = "service" ATTR_METHOD = "method" ATTR_ARGS = "args" diff --git a/tests/components/nightscout/test_sensor.py b/tests/components/nightscout/test_sensor.py index 3df98a2595a..5e73c75d93c 100644 --- a/tests/components/nightscout/test_sensor.py +++ b/tests/components/nightscout/test_sensor.py @@ -1,12 +1,11 @@ """The sensor tests for the Nightscout platform.""" from homeassistant.components.nightscout.const import ( - ATTR_DATE, ATTR_DELTA, ATTR_DEVICE, ATTR_DIRECTION, ) -from homeassistant.const import ATTR_ICON, STATE_UNAVAILABLE +from homeassistant.const import ATTR_DATE, ATTR_ICON, STATE_UNAVAILABLE from tests.components.nightscout import ( GLUCOSE_READINGS, From c8ffac20b9b717b704c3c8a9d80e6fdc6b088a22 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 22 Feb 2021 16:26:46 +0100 Subject: [PATCH 636/796] Add name to services (#46905) --- homeassistant/components/light/services.yaml | 13 ++++++++++--- homeassistant/helpers/service.py | 6 ++++-- script/hassfest/services.py | 1 + 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 2161ed3f81d..4f5d74cbbbb 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -1,7 +1,10 @@ # Describes the format for available light services turn_on: - description: Turn a light on + name: Turn on lights + description: > + Turn on one or more lights and adjust properties of the light, even when + they are turned on already. target: fields: transition: @@ -311,7 +314,8 @@ turn_on: text: turn_off: - description: Turn a light off + name: Turn off lights + description: Turns off one or more lights. target: fields: transition: @@ -340,7 +344,10 @@ turn_off: - short toggle: - description: Toggles a light + name: Toggle lights + description: > + Toggles one or more lights, from on to off, or, off to on, based on their + current state. target: fields: transition: diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 01fb76ec1bc..a55ba8a84af 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -449,6 +449,7 @@ async def async_get_all_descriptions( # positives for things like scripts, that register as a service description = { + "name": yaml_description.get("name", ""), "description": yaml_description.get("description", ""), "fields": yaml_description.get("fields", {}), } @@ -472,8 +473,9 @@ def async_set_service_schema( hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {}) description = { - "description": schema.get("description") or "", - "fields": schema.get("fields") or {}, + "name": schema.get("name", ""), + "description": schema.get("description", ""), + "fields": schema.get("fields", {}), } hass.data[SERVICE_DESCRIPTION_CACHE][f"{domain}.{service}"] = description diff --git a/script/hassfest/services.py b/script/hassfest/services.py index 9037b0bb45e..62e9b2f88a1 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -37,6 +37,7 @@ FIELD_SCHEMA = vol.Schema( SERVICE_SCHEMA = vol.Schema( { vol.Required("description"): str, + vol.Optional("name"): str, vol.Optional("target"): vol.Any( selector.TargetSelector.CONFIG_SCHEMA, None # pylint: disable=no-member ), From 692942b39999c9cf071ecb4447f1609c270884b4 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 22 Feb 2021 18:50:01 +0100 Subject: [PATCH 637/796] Add service names to Netatmo services (#46909) --- homeassistant/components/netatmo/services.yaml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml index 11f83830dff..7005d26c326 100644 --- a/homeassistant/components/netatmo/services.yaml +++ b/homeassistant/components/netatmo/services.yaml @@ -1,6 +1,7 @@ # Describes the format for available Netatmo services set_camera_light: - description: Set the camera light mode + name: Set camera light mode + description: Sets the light mode for a Netatmo Outdoor camera light. target: entity: integration: netatmo @@ -19,7 +20,8 @@ set_camera_light: - "auto" set_schedule: - description: Set the heating schedule + name: Set heating schedule + description: Set the heating schedule for Netatmo climate device. The schedule name must match a schedule configured at Netatmo. target: entity: integration: netatmo @@ -33,7 +35,8 @@ set_schedule: text: set_persons_home: - description: Set a list of persons as at home. Person's name must match a name known by the Welcome Camera + name: Set persons at home + description: Set a list of persons as at home. Person's name must match a name known by the Netatmo Indoor (Welcome) Camera. target: entity: integration: netatmo @@ -47,7 +50,8 @@ set_persons_home: text: set_person_away: - description: Set a person away. If no person is set the home will be marked as empty. Person's name must match a name known by the Welcome Camera + name: Set person away + description: Set a person as away. If no person is set the home will be marked as empty. Person's name must match a name known by the Netatmo Indoor (Welcome) Camera. target: entity: integration: netatmo @@ -60,7 +64,9 @@ set_person_away: text: register_webhook: - description: Register webhook + name: Register webhook + description: Register the webhook to the Netatmo backend. unregister_webhook: - description: Unregister webhook + name: Unregister webhook + description: Unregister the webhook from the Netatmo backend. From 6e10b39d67e36e45d43846f9d1246476f99a22d0 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 22 Feb 2021 12:54:06 -0500 Subject: [PATCH 638/796] add name and target filter to zwave_js lock services.yaml (#46914) --- homeassistant/components/zwave_js/services.yaml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml index edb1f8b1ba4..dd66a0c7f29 100644 --- a/homeassistant/components/zwave_js/services.yaml +++ b/homeassistant/components/zwave_js/services.yaml @@ -1,8 +1,12 @@ # Describes the format for available Z-Wave services clear_lock_usercode: - description: Clear a usercode from lock + name: Clear a usercode from a lock + description: Clear a usercode from a lock target: + entity: + domain: lock + integration: zwave_js fields: code_slot: name: Code slot @@ -13,8 +17,12 @@ clear_lock_usercode: text: set_lock_usercode: - description: Set a usercode to lock + name: Set a usercode on a lock + description: Set a usercode on a lock target: + entity: + domain: lock + integration: zwave_js fields: code_slot: name: Code slot From 5907129b25a3193d157994fdde3d45525e076588 Mon Sep 17 00:00:00 2001 From: Michal Knizek Date: Mon, 22 Feb 2021 19:30:23 +0100 Subject: [PATCH 639/796] Increase tado API polling interval to 5 minutes (#46915) Polling interval of 15 seconds causes high load on tado servers and does not provide enough value to warrant it. tado plans to introduce a rate limit to prevent such misuse of the API, therefore the polling interval needs to be increased to make sure the integration works well in the future. --- homeassistant/components/tado/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index e88fb4c60b8..c7fb180e6d8 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -33,8 +33,8 @@ _LOGGER = logging.getLogger(__name__) TADO_COMPONENTS = ["binary_sensor", "sensor", "climate", "water_heater"] -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) -SCAN_INTERVAL = timedelta(seconds=15) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=4) +SCAN_INTERVAL = timedelta(minutes=5) CONFIG_SCHEMA = cv.deprecated(DOMAIN) From 668574c48f99041ec56d626cc856b167f39dedb1 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 22 Feb 2021 13:31:18 -0500 Subject: [PATCH 640/796] Add name and target filter to vizio entity service (#46916) * Add name and target filter to vizio entity service * Update homeassistant/components/vizio/services.yaml Co-authored-by: Franck Nijhof * add selectors * Update homeassistant/components/vizio/services.yaml Co-authored-by: Franck Nijhof * Update homeassistant/components/vizio/services.yaml Co-authored-by: Franck Nijhof * Update homeassistant/components/vizio/services.yaml Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/vizio/services.yaml | 28 +++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/vizio/services.yaml b/homeassistant/components/vizio/services.yaml index 50bde6cab78..a2981fa32ad 100644 --- a/homeassistant/components/vizio/services.yaml +++ b/homeassistant/components/vizio/services.yaml @@ -1,15 +1,29 @@ update_setting: - description: Update the value of a setting on a particular Vizio media player device. + name: Update a Vizio media player setting + description: Update the value of a setting on a Vizio media player device. + target: + entity: + integration: vizio + domain: media_player fields: - entity_id: - description: Name of an entity to send command to. - example: "media_player.vizio_smartcast" setting_type: - description: The type of setting to be changed. Available types are listed in the `setting_types` property. + name: Setting type + description: The type of setting to be changed. Available types are listed in the 'setting_types' property. + required: true example: "audio" + selector: + text: setting_name: - description: The name of the setting to be changed. Available settings for a given setting_type are listed in the `_settings` property. + name: Setting name + description: The name of the setting to be changed. Available settings for a given setting_type are listed in the '_settings' property. + required: true example: "eq" + selector: + text: new_value: - description: The new value for the setting + name: New value + description: The new value for the setting. + required: true example: "Music" + selector: + text: From e70d896e1bfe56cfd6aa90cb85200e022ec92474 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Mon, 22 Feb 2021 11:53:57 -0700 Subject: [PATCH 641/796] Add litterrobot integration (#45886) --- CODEOWNERS | 1 + .../components/litterrobot/__init__.py | 54 ++++++++ .../components/litterrobot/config_flow.py | 51 +++++++ homeassistant/components/litterrobot/const.py | 2 + homeassistant/components/litterrobot/hub.py | 122 +++++++++++++++++ .../components/litterrobot/manifest.json | 8 ++ .../components/litterrobot/strings.json | 20 +++ .../litterrobot/translations/en.json | 20 +++ .../components/litterrobot/vacuum.py | 127 ++++++++++++++++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/litterrobot/__init__.py | 1 + tests/components/litterrobot/common.py | 24 ++++ tests/components/litterrobot/conftest.py | 35 +++++ .../litterrobot/test_config_flow.py | 92 +++++++++++++ tests/components/litterrobot/test_init.py | 20 +++ tests/components/litterrobot/test_vacuum.py | 92 +++++++++++++ 18 files changed, 676 insertions(+) create mode 100644 homeassistant/components/litterrobot/__init__.py create mode 100644 homeassistant/components/litterrobot/config_flow.py create mode 100644 homeassistant/components/litterrobot/const.py create mode 100644 homeassistant/components/litterrobot/hub.py create mode 100644 homeassistant/components/litterrobot/manifest.json create mode 100644 homeassistant/components/litterrobot/strings.json create mode 100644 homeassistant/components/litterrobot/translations/en.json create mode 100644 homeassistant/components/litterrobot/vacuum.py create mode 100644 tests/components/litterrobot/__init__.py create mode 100644 tests/components/litterrobot/common.py create mode 100644 tests/components/litterrobot/conftest.py create mode 100644 tests/components/litterrobot/test_config_flow.py create mode 100644 tests/components/litterrobot/test_init.py create mode 100644 tests/components/litterrobot/test_vacuum.py diff --git a/CODEOWNERS b/CODEOWNERS index a9d4ce63209..b20b489ac6d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -253,6 +253,7 @@ homeassistant/components/launch_library/* @ludeeus homeassistant/components/lcn/* @alengwenus homeassistant/components/life360/* @pnbruckner homeassistant/components/linux_battery/* @fabaff +homeassistant/components/litterrobot/* @natekspencer homeassistant/components/local_ip/* @issacg homeassistant/components/logger/* @home-assistant/core homeassistant/components/logi_circle/* @evanjd diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py new file mode 100644 index 00000000000..bf43d5c465e --- /dev/null +++ b/homeassistant/components/litterrobot/__init__.py @@ -0,0 +1,54 @@ +"""The Litter-Robot integration.""" +import asyncio + +from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import DOMAIN +from .hub import LitterRobotHub + +PLATFORMS = ["vacuum"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Litter-Robot component.""" + hass.data.setdefault(DOMAIN, {}) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Litter-Robot from a config entry.""" + hub = hass.data[DOMAIN][entry.entry_id] = LitterRobotHub(hass, entry.data) + try: + await hub.login(load_robots=True) + except LitterRobotLoginException: + return False + except LitterRobotException as ex: + raise ConfigEntryNotReady from ex + + 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/litterrobot/config_flow.py b/homeassistant/components/litterrobot/config_flow.py new file mode 100644 index 00000000000..d6c92d8dad6 --- /dev/null +++ b/homeassistant/components/litterrobot/config_flow.py @@ -0,0 +1,51 @@ +"""Config flow for Litter-Robot integration.""" +import logging + +from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from .const import DOMAIN # pylint:disable=unused-import +from .hub import LitterRobotHub + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Litter-Robot.""" + + 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: + for entry in self._async_current_entries(): + if entry.data[CONF_USERNAME] == user_input[CONF_USERNAME]: + return self.async_abort(reason="already_configured") + + hub = LitterRobotHub(self.hass, user_input) + try: + await hub.login() + return self.async_create_entry( + title=user_input[CONF_USERNAME], data=user_input + ) + except LitterRobotLoginException: + errors["base"] = "invalid_auth" + except LitterRobotException: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/litterrobot/const.py b/homeassistant/components/litterrobot/const.py new file mode 100644 index 00000000000..5ac889d9b73 --- /dev/null +++ b/homeassistant/components/litterrobot/const.py @@ -0,0 +1,2 @@ +"""Constants for the Litter-Robot integration.""" +DOMAIN = "litterrobot" diff --git a/homeassistant/components/litterrobot/hub.py b/homeassistant/components/litterrobot/hub.py new file mode 100644 index 00000000000..0d0559140c7 --- /dev/null +++ b/homeassistant/components/litterrobot/hub.py @@ -0,0 +1,122 @@ +"""A wrapper 'hub' for the Litter-Robot API and base entity for common attributes.""" +from datetime import time, timedelta +import logging +from types import MethodType +from typing import Any, Optional + +from pylitterbot import Account, Robot +from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException + +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) +import homeassistant.util.dt as dt_util + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +REFRESH_WAIT_TIME = 12 +UPDATE_INTERVAL = 10 + + +class LitterRobotHub: + """A Litter-Robot hub wrapper class.""" + + def __init__(self, hass: HomeAssistant, data: dict): + """Initialize the Litter-Robot hub.""" + self._data = data + self.account = None + self.logged_in = False + + async def _async_update_data(): + """Update all device states from the Litter-Robot API.""" + await self.account.refresh_robots() + return True + + self.coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=DOMAIN, + update_method=_async_update_data, + update_interval=timedelta(seconds=UPDATE_INTERVAL), + ) + + async def login(self, load_robots: bool = False): + """Login to Litter-Robot.""" + self.logged_in = False + self.account = Account() + try: + await self.account.connect( + username=self._data[CONF_USERNAME], + password=self._data[CONF_PASSWORD], + load_robots=load_robots, + ) + self.logged_in = True + return self.logged_in + except LitterRobotLoginException as ex: + _LOGGER.error("Invalid credentials") + raise ex + except LitterRobotException as ex: + _LOGGER.error("Unable to connect to Litter-Robot API") + raise ex + + +class LitterRobotEntity(CoordinatorEntity): + """Generic Litter-Robot entity representing common data and methods.""" + + def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub): + """Pass coordinator to CoordinatorEntity.""" + super().__init__(hub.coordinator) + self.robot = robot + self.entity_type = entity_type if entity_type else "" + self.hub = hub + + @property + def name(self): + """Return the name of this entity.""" + return f"{self.robot.name} {self.entity_type}" + + @property + def unique_id(self): + """Return a unique ID.""" + return f"{self.robot.serial}-{self.entity_type}" + + @property + def device_info(self): + """Return the device information for a Litter-Robot.""" + model = "Litter-Robot 3 Connect" + if not self.robot.serial.startswith("LR3C"): + model = "Other Litter-Robot Connected Device" + return { + "identifiers": {(DOMAIN, self.robot.serial)}, + "name": self.robot.name, + "manufacturer": "Litter-Robot", + "model": model, + } + + async def perform_action_and_refresh(self, action: MethodType, *args: Any): + """Perform an action and initiates a refresh of the robot data after a few seconds.""" + await action(*args) + async_call_later( + self.hass, REFRESH_WAIT_TIME, self.hub.coordinator.async_request_refresh + ) + + @staticmethod + def parse_time_at_default_timezone(time_str: str) -> Optional[time]: + """Parse a time string and add default timezone.""" + parsed_time = dt_util.parse_time(time_str) + + if parsed_time is None: + return None + + return time( + hour=parsed_time.hour, + minute=parsed_time.minute, + second=parsed_time.second, + tzinfo=dt_util.DEFAULT_TIME_ZONE, + ) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json new file mode 100644 index 00000000000..1c6ac7274bf --- /dev/null +++ b/homeassistant/components/litterrobot/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "litterrobot", + "name": "Litter-Robot", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/litterrobot", + "requirements": ["pylitterbot==2021.2.5"], + "codeowners": ["@natekspencer"] +} diff --git a/homeassistant/components/litterrobot/strings.json b/homeassistant/components/litterrobot/strings.json new file mode 100644 index 00000000000..96dc8b371d1 --- /dev/null +++ b/homeassistant/components/litterrobot/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/litterrobot/translations/en.json b/homeassistant/components/litterrobot/translations/en.json new file mode 100644 index 00000000000..b3fc76ae458 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username" + } + } + } + } +} diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py new file mode 100644 index 00000000000..a57c1ffead5 --- /dev/null +++ b/homeassistant/components/litterrobot/vacuum.py @@ -0,0 +1,127 @@ +"""Support for Litter-Robot "Vacuum".""" +from pylitterbot import Robot + +from homeassistant.components.vacuum import ( + STATE_CLEANING, + STATE_DOCKED, + STATE_ERROR, + SUPPORT_SEND_COMMAND, + SUPPORT_START, + SUPPORT_STATE, + SUPPORT_STATUS, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + VacuumEntity, +) +from homeassistant.const import STATE_OFF + +from .const import DOMAIN +from .hub import LitterRobotEntity + +SUPPORT_LITTERROBOT = ( + SUPPORT_SEND_COMMAND + | SUPPORT_START + | SUPPORT_STATE + | SUPPORT_STATUS + | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON +) +TYPE_LITTER_BOX = "Litter Box" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Litter-Robot cleaner using config entry.""" + hub = hass.data[DOMAIN][config_entry.entry_id] + + entities = [] + for robot in hub.account.robots: + entities.append(LitterRobotCleaner(robot, TYPE_LITTER_BOX, hub)) + + if entities: + async_add_entities(entities, True) + + +class LitterRobotCleaner(LitterRobotEntity, VacuumEntity): + """Litter-Robot "Vacuum" Cleaner.""" + + @property + def supported_features(self): + """Flag cleaner robot features that are supported.""" + return SUPPORT_LITTERROBOT + + @property + def state(self): + """Return the state of the cleaner.""" + switcher = { + Robot.UnitStatus.CCP: STATE_CLEANING, + Robot.UnitStatus.EC: STATE_CLEANING, + Robot.UnitStatus.CCC: STATE_DOCKED, + Robot.UnitStatus.CST: STATE_DOCKED, + Robot.UnitStatus.DF1: STATE_DOCKED, + Robot.UnitStatus.DF2: STATE_DOCKED, + Robot.UnitStatus.RDY: STATE_DOCKED, + Robot.UnitStatus.OFF: STATE_OFF, + } + + return switcher.get(self.robot.unit_status, STATE_ERROR) + + @property + def error(self): + """Return the error associated with the current state, if any.""" + return self.robot.unit_status.value + + @property + def status(self): + """Return the status of the cleaner.""" + return f"{self.robot.unit_status.value}{' (Sleeping)' if self.robot.is_sleeping else ''}" + + async def async_turn_on(self, **kwargs): + """Turn the cleaner on, starting a clean cycle.""" + await self.perform_action_and_refresh(self.robot.set_power_status, True) + + async def async_turn_off(self, **kwargs): + """Turn the unit off, stopping any cleaning in progress as is.""" + await self.perform_action_and_refresh(self.robot.set_power_status, False) + + async def async_start(self): + """Start a clean cycle.""" + await self.perform_action_and_refresh(self.robot.start_cleaning) + + async def async_send_command(self, command, params=None, **kwargs): + """Send command. + + Available commands: + - reset_waste_drawer + * params: none + - set_sleep_mode + * params: + - enabled: bool + - sleep_time: str (optional) + + """ + if command == "reset_waste_drawer": + # Normally we need to request a refresh of data after a command is sent. + # However, the API for resetting the waste drawer returns a refreshed + # data set for the robot. Thus, we only need to tell hass to update the + # state of devices associated with this robot. + await self.robot.reset_waste_drawer() + self.hub.coordinator.async_set_updated_data(True) + elif command == "set_sleep_mode": + await self.perform_action_and_refresh( + self.robot.set_sleep_mode, + params.get("enabled"), + self.parse_time_at_default_timezone(params.get("sleep_time")), + ) + else: + raise NotImplementedError() + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + return { + "clean_cycle_wait_time_minutes": self.robot.clean_cycle_wait_time_minutes, + "is_sleeping": self.robot.is_sleeping, + "power_status": self.robot.power_status, + "unit_status_code": self.robot.unit_status.name, + "last_seen": self.robot.last_seen, + } diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 8e8949e5788..36c22262ef4 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -121,6 +121,7 @@ FLOWS = [ "kulersky", "life360", "lifx", + "litterrobot", "local_ip", "locative", "logi_circle", diff --git a/requirements_all.txt b/requirements_all.txt index b98cbde46bc..55ea1510b59 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1503,6 +1503,9 @@ pylibrespot-java==0.1.0 # homeassistant.components.litejet pylitejet==0.1 +# homeassistant.components.litterrobot +pylitterbot==2021.2.5 + # homeassistant.components.loopenergy pyloopenergy==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b1beff34323..b590a8c50b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -793,6 +793,9 @@ pylibrespot-java==0.1.0 # homeassistant.components.litejet pylitejet==0.1 +# homeassistant.components.litterrobot +pylitterbot==2021.2.5 + # homeassistant.components.lutron_caseta pylutron-caseta==0.9.0 diff --git a/tests/components/litterrobot/__init__.py b/tests/components/litterrobot/__init__.py new file mode 100644 index 00000000000..a7267365100 --- /dev/null +++ b/tests/components/litterrobot/__init__.py @@ -0,0 +1 @@ +"""Tests for the Litter-Robot Component.""" diff --git a/tests/components/litterrobot/common.py b/tests/components/litterrobot/common.py new file mode 100644 index 00000000000..ed893a3a756 --- /dev/null +++ b/tests/components/litterrobot/common.py @@ -0,0 +1,24 @@ +"""Common utils for Litter-Robot tests.""" +from homeassistant.components.litterrobot import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +BASE_PATH = "homeassistant.components.litterrobot" +CONFIG = {DOMAIN: {CONF_USERNAME: "user@example.com", CONF_PASSWORD: "password"}} + +ROBOT_NAME = "Test" +ROBOT_SERIAL = "LR3C012345" +ROBOT_DATA = { + "powerStatus": "AC", + "lastSeen": "2021-02-01T15:30:00.000000", + "cleanCycleWaitTimeMinutes": "7", + "unitStatus": "RDY", + "litterRobotNickname": ROBOT_NAME, + "cycleCount": "15", + "panelLockActive": "0", + "cyclesAfterDrawerFull": "0", + "litterRobotSerial": ROBOT_SERIAL, + "cycleCapacity": "30", + "litterRobotId": "a0123b4567cd8e", + "nightLightActive": "1", + "sleepModeActive": "112:50:19", +} diff --git a/tests/components/litterrobot/conftest.py b/tests/components/litterrobot/conftest.py new file mode 100644 index 00000000000..2f967d266bc --- /dev/null +++ b/tests/components/litterrobot/conftest.py @@ -0,0 +1,35 @@ +"""Configure pytest for Litter-Robot tests.""" +from unittest.mock import AsyncMock, MagicMock + +from pylitterbot import Robot +import pytest + +from homeassistant.components import litterrobot +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .common import ROBOT_DATA + + +def create_mock_robot(hass): + """Create a mock Litter-Robot device.""" + robot = Robot(data=ROBOT_DATA) + robot.start_cleaning = AsyncMock() + robot.set_power_status = AsyncMock() + robot.reset_waste_drawer = AsyncMock() + robot.set_sleep_mode = AsyncMock() + return robot + + +@pytest.fixture() +def mock_hub(hass): + """Mock a Litter-Robot hub.""" + hub = MagicMock( + hass=hass, + account=MagicMock(), + logged_in=True, + coordinator=MagicMock(spec=DataUpdateCoordinator), + spec=litterrobot.LitterRobotHub, + ) + hub.coordinator.last_update_success = True + hub.account.robots = [create_mock_robot(hass)] + return hub diff --git a/tests/components/litterrobot/test_config_flow.py b/tests/components/litterrobot/test_config_flow.py new file mode 100644 index 00000000000..fd88595d37e --- /dev/null +++ b/tests/components/litterrobot/test_config_flow.py @@ -0,0 +1,92 @@ +"""Test the Litter-Robot config flow.""" +from unittest.mock import patch + +from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException + +from homeassistant import config_entries, setup + +from .common import CONF_USERNAME, CONFIG, DOMAIN + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.litterrobot.config_flow.LitterRobotHub.login", + return_value=True, + ), patch( + "homeassistant.components.litterrobot.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.litterrobot.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG[DOMAIN] + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == CONFIG[DOMAIN][CONF_USERNAME] + assert result2["data"] == CONFIG[DOMAIN] + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.litterrobot.config_flow.LitterRobotHub.login", + side_effect=LitterRobotLoginException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG[DOMAIN] + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.litterrobot.config_flow.LitterRobotHub.login", + side_effect=LitterRobotException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG[DOMAIN] + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_unknown_error(hass): + """Test we handle unknown error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.litterrobot.config_flow.LitterRobotHub.login", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG[DOMAIN] + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/litterrobot/test_init.py b/tests/components/litterrobot/test_init.py new file mode 100644 index 00000000000..1d0ed075cc7 --- /dev/null +++ b/tests/components/litterrobot/test_init.py @@ -0,0 +1,20 @@ +"""Test Litter-Robot setup process.""" +from homeassistant.components import litterrobot +from homeassistant.setup import async_setup_component + +from .common import CONFIG + +from tests.common import MockConfigEntry + + +async def test_unload_entry(hass): + """Test being able to unload an entry.""" + entry = MockConfigEntry( + domain=litterrobot.DOMAIN, + data=CONFIG[litterrobot.DOMAIN], + ) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, litterrobot.DOMAIN, {}) is True + assert await litterrobot.async_unload_entry(hass, entry) + assert hass.data[litterrobot.DOMAIN] == {} diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py new file mode 100644 index 00000000000..b47eff64e13 --- /dev/null +++ b/tests/components/litterrobot/test_vacuum.py @@ -0,0 +1,92 @@ +"""Test the Litter-Robot vacuum entity.""" +from datetime import timedelta +from unittest.mock import patch + +import pytest + +from homeassistant.components import litterrobot +from homeassistant.components.litterrobot.hub import REFRESH_WAIT_TIME +from homeassistant.components.vacuum import ( + ATTR_PARAMS, + DOMAIN as PLATFORM_DOMAIN, + SERVICE_SEND_COMMAND, + SERVICE_START, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_DOCKED, +) +from homeassistant.const import ATTR_COMMAND, ATTR_ENTITY_ID +from homeassistant.util.dt import utcnow + +from .common import CONFIG + +from tests.common import MockConfigEntry, async_fire_time_changed + +ENTITY_ID = "vacuum.test_litter_box" + + +async def setup_hub(hass, mock_hub): + """Load the Litter-Robot vacuum platform with the provided hub.""" + hass.config.components.add(litterrobot.DOMAIN) + entry = MockConfigEntry( + domain=litterrobot.DOMAIN, + data=CONFIG[litterrobot.DOMAIN], + ) + + with patch.dict(hass.data, {litterrobot.DOMAIN: {entry.entry_id: mock_hub}}): + await hass.config_entries.async_forward_entry_setup(entry, PLATFORM_DOMAIN) + await hass.async_block_till_done() + + +async def test_vacuum(hass, mock_hub): + """Tests the vacuum entity was set up.""" + await setup_hub(hass, mock_hub) + + vacuum = hass.states.get(ENTITY_ID) + assert vacuum is not None + assert vacuum.state == STATE_DOCKED + assert vacuum.attributes["is_sleeping"] is False + + +@pytest.mark.parametrize( + "service,command,extra", + [ + (SERVICE_START, "start_cleaning", None), + (SERVICE_TURN_OFF, "set_power_status", None), + (SERVICE_TURN_ON, "set_power_status", None), + ( + SERVICE_SEND_COMMAND, + "reset_waste_drawer", + {ATTR_COMMAND: "reset_waste_drawer"}, + ), + ( + SERVICE_SEND_COMMAND, + "set_sleep_mode", + { + ATTR_COMMAND: "set_sleep_mode", + ATTR_PARAMS: {"enabled": True, "sleep_time": "22:30"}, + }, + ), + ], +) +async def test_commands(hass, mock_hub, service, command, extra): + """Test sending commands to the vacuum.""" + await setup_hub(hass, mock_hub) + + vacuum = hass.states.get(ENTITY_ID) + assert vacuum is not None + assert vacuum.state == STATE_DOCKED + + data = {ATTR_ENTITY_ID: ENTITY_ID} + if extra: + data.update(extra) + + await hass.services.async_call( + PLATFORM_DOMAIN, + service, + data, + blocking=True, + ) + future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME) + async_fire_time_changed(hass, future) + getattr(mock_hub.account.robots[0], command).assert_called_once() From fb32c2e3a8a7ea245916545573f1d15b86898f3a Mon Sep 17 00:00:00 2001 From: kpine Date: Mon, 22 Feb 2021 10:56:23 -0800 Subject: [PATCH 642/796] Test zwave_js GE 12730 fan controller device-specific discovery (#46840) * Add test for GE 12730 fan controller device-specific discovery * Adjust Co-authored-by: Martin Hjelmare --- tests/components/zwave_js/conftest.py | 14 + tests/components/zwave_js/test_discovery.py | 13 +- .../fixtures/zwave_js/fan_ge_12730_state.json | 434 ++++++++++++++++++ 3 files changed, 460 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/zwave_js/fan_ge_12730_state.json diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 02b11970376..2e856bde362 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -170,6 +170,12 @@ def iblinds_v2_state_fixture(): return json.loads(load_fixture("zwave_js/cover_iblinds_v2_state.json")) +@pytest.fixture(name="ge_12730_state", scope="session") +def ge_12730_state_fixture(): + """Load the GE 12730 node state fixture data.""" + return json.loads(load_fixture("zwave_js/fan_ge_12730_state.json")) + + @pytest.fixture(name="client") def mock_client_fixture(controller_state, version_state): """Mock a client.""" @@ -373,3 +379,11 @@ def iblinds_cover_fixture(client, iblinds_v2_state): node = Node(client, iblinds_v2_state) client.driver.controller.nodes[node.node_id] = node return node + + +@pytest.fixture(name="ge_12730") +def ge_12730_fixture(client, ge_12730_state): + """Mock a GE 12730 fan controller node.""" + node = Node(client, ge_12730_state) + client.driver.controller.nodes[node.node_id] = node + return node diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index f7d26f07d21..8f3dbce8dca 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -3,7 +3,6 @@ async def test_iblinds_v2(hass, client, iblinds_v2, integration): """Test that an iBlinds v2.0 multilevel switch value is discovered as a cover.""" - node = iblinds_v2 assert node.device_class.specific == "Unused" @@ -12,3 +11,15 @@ async def test_iblinds_v2(hass, client, iblinds_v2, integration): state = hass.states.get("cover.window_blind_controller") assert state + + +async def test_ge_12730(hass, client, ge_12730, integration): + """Test GE 12730 Fan Controller v2.0 multilevel switch is discovered as a fan.""" + node = ge_12730 + assert node.device_class.specific == "Multilevel Power Switch" + + state = hass.states.get("light.in_wall_smart_fan_control") + assert not state + + state = hass.states.get("fan.in_wall_smart_fan_control") + assert state diff --git a/tests/fixtures/zwave_js/fan_ge_12730_state.json b/tests/fixtures/zwave_js/fan_ge_12730_state.json new file mode 100644 index 00000000000..692cc75fe99 --- /dev/null +++ b/tests/fixtures/zwave_js/fan_ge_12730_state.json @@ -0,0 +1,434 @@ +{ + "nodeId": 24, + "index": 0, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Routing Slave", + "generic": "Multilevel Switch", + "specific": "Multilevel Power Switch", + "mandatorySupportedCCs": [ + "Basic", + "Multilevel Switch", + "All Switch" + ], + "mandatoryControlCCs": [] + }, + "isListening": true, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 99, + "productId": 12340, + "productType": 18756, + "firmwareVersion": "3.10", + "deviceConfig": { + "manufacturerId": 99, + "manufacturer": "GE/Jasco", + "label": "12730 / ZW4002", + "description": "In-Wall Smart Fan Control", + "devices": [ + { + "productType": "0x4944", + "productId": "0x3034" + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + } + }, + "label": "12730 / ZW4002", + "neighbors": [ + 1, + 12 + ], + "interviewAttempts": 1, + "interviewStage": 7, + "endpoints": [ + { + "nodeId": 24, + "index": 0 + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 99, + "label": "Target value" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Transition duration" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 99, + "label": "Current value" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + } + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + } + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "LED Light", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 2, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "LED on when light off", + "1": "LED on when light on", + "2": "LED always off" + }, + "label": "LED Light", + "description": "Sets when the LED on the switch is lit.", + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Invert Switch", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "No", + "1": "Yes" + }, + "label": "Invert Switch", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "Dim Rate Steps (Z-Wave Command)", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 99, + "default": 1, + "format": 1, + "allowManualEntry": true, + "label": "Dim Rate Steps (Z-Wave Command)", + "description": "Number of steps or levels", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyName": "Dim Rate Timing (Z-Wave)", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 1, + "max": 255, + "default": 3, + "format": 1, + "allowManualEntry": true, + "label": "Dim Rate Timing (Z-Wave)", + "description": "Timing of steps or levels", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 9, + "propertyName": "Dim Rate Steps (Manual)", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 99, + "default": 1, + "format": 1, + "allowManualEntry": true, + "label": "Dim Rate Steps (Manual)", + "description": "Number of steps or levels", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "Dim Rate Timing (Manual)", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 1, + "max": 255, + "default": 3, + "format": 1, + "allowManualEntry": true, + "label": "Dim Rate Timing (Manual)", + "description": "Timing of steps", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 11, + "propertyName": "Dim Rate Steps (All-On/All-Off)", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 99, + "default": 1, + "format": 1, + "allowManualEntry": true, + "label": "Dim Rate Steps (All-On/All-Off)", + "description": "Number of steps or levels", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 12, + "propertyName": "Dim Rate Timing (All-On/All-Off)", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 1, + "max": 255, + "default": 3, + "format": 1, + "allowManualEntry": true, + "label": "Dim Rate Timing (All-On/All-Off)", + "description": "Timing of steps or levels", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 99 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 18756 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 12340 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 6 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "3.67" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "3.10" + ] + } + ] +} From f0c7aff2483f68112d625ca4ee627733261cd5f3 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Mon, 22 Feb 2021 15:12:00 -0500 Subject: [PATCH 643/796] Clean up Mitemp_bt constants (#46881) * Use core constants for acer_projector * Use core constants for mitemp_bt * remove acer changes --- homeassistant/components/mitemp_bt/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mitemp_bt/sensor.py b/homeassistant/components/mitemp_bt/sensor.py index 6b64c88c1ce..244a0c410d5 100644 --- a/homeassistant/components/mitemp_bt/sensor.py +++ b/homeassistant/components/mitemp_bt/sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_MAC, CONF_MONITORED_CONDITIONS, CONF_NAME, + CONF_TIMEOUT, DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, @@ -34,7 +35,6 @@ CONF_ADAPTER = "adapter" CONF_CACHE = "cache_value" CONF_MEDIAN = "median" CONF_RETRIES = "retries" -CONF_TIMEOUT = "timeout" DEFAULT_ADAPTER = "hci0" DEFAULT_UPDATE_INTERVAL = 300 From 8ac9faef3b7959fa44beae109c4758029260f9f0 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 22 Feb 2021 21:36:38 +0100 Subject: [PATCH 644/796] Description tweaks for automation services (#46926) --- homeassistant/components/automation/services.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/automation/services.yaml b/homeassistant/components/automation/services.yaml index d8380914ce6..95f2057f5ee 100644 --- a/homeassistant/components/automation/services.yaml +++ b/homeassistant/components/automation/services.yaml @@ -16,16 +16,16 @@ turn_off: boolean: toggle: - description: Toggle an automation + description: Toggle (enable / disable) an automation target: trigger: - description: Trigger the action of an automation + description: Trigger the actions of an automation target: fields: skip_condition: name: Skip conditions - description: Whether or not the condition will be skipped. + description: Whether or not the conditions will be skipped. default: true example: true selector: From be33336d96887455d9bcef768cf9d9aea1f1558f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 22 Feb 2021 22:48:47 +0100 Subject: [PATCH 645/796] Update frontend to 20210222.0 (#46928) --- 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 7faac6c99cb..ea46e0b2e07 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210208.0" + "home-assistant-frontend==20210222.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5f2aa859e26..fa5862b5c28 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.41.0 -home-assistant-frontend==20210208.0 +home-assistant-frontend==20210222.0 httpx==0.16.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 55ea1510b59..081881b2505 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -763,7 +763,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210208.0 +home-assistant-frontend==20210222.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b590a8c50b3..376eef402e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -412,7 +412,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210208.0 +home-assistant-frontend==20210222.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 04e07d8b2c3f2d65351d5606c7d7f770f59338a6 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Mon, 22 Feb 2021 17:03:38 -0500 Subject: [PATCH 646/796] Add get_config_parameters websocket command to zwave_js (#46463) * Add get_configuration_values websocket command to zwave_js * Tweak return value * Review comments and cleanup returned values * Update test * Rename to get_config_parameters * Add get_configuration_values websocket command to zwave_js * Rename to get_config_parameters * fix test * fix tests #2 * Add readable to metadata Co-authored-by: Raman Gupta <7243222+raman325@users.noreply.github.com> --- homeassistant/components/zwave_js/api.py | 43 ++++++++++++++++++++++++ tests/components/zwave_js/test_api.py | 18 ++++++++++ 2 files changed, 61 insertions(+) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index bed2166a4da..fb9282b4763 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -31,6 +31,7 @@ def async_register_api(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, websocket_stop_inclusion) websocket_api.async_register_command(hass, websocket_remove_node) websocket_api.async_register_command(hass, websocket_stop_exclusion) + websocket_api.async_register_command(hass, websocket_get_config_parameters) hass.http.register_view(DumpView) # type: ignore @@ -263,6 +264,48 @@ async def websocket_remove_node( ) +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/get_config_parameters", + vol.Required(ENTRY_ID): str, + vol.Required(NODE_ID): int, + } +) +@callback +def websocket_get_config_parameters( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: + """Get a list of configuration parameterss for a Z-Wave node.""" + entry_id = msg[ENTRY_ID] + node_id = msg[NODE_ID] + client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + node = client.driver.controller.nodes[node_id] + values = node.get_configuration_values() + result = {} + for value_id, zwave_value in values.items(): + metadata = zwave_value.metadata + result[value_id] = { + "property": zwave_value.property_, + "configuration_value_type": zwave_value.configuration_value_type.value, + "metadata": { + "description": metadata.description, + "label": metadata.label, + "type": metadata.type, + "min": metadata.min, + "max": metadata.max, + "unit": metadata.unit, + "writeable": metadata.writeable, + "readable": metadata.readable, + }, + "value": zwave_value.value, + } + connection.send_result( + msg[ID], + result, + ) + + class DumpView(HomeAssistantView): """View to dump the state of the Z-Wave JS server.""" diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index a36743421c9..7689f7140f4 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -41,6 +41,24 @@ async def test_websocket_api(hass, integration, multisensor_6, hass_ws_client): assert not result["is_secure"] assert result["status"] == 1 + # Test getting configuration parameter values + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/get_config_parameters", + ENTRY_ID: entry.entry_id, + NODE_ID: node.node_id, + } + ) + msg = await ws_client.receive_json() + result = msg["result"] + + assert len(result) == 61 + key = "52-112-0-2-00-00" + assert result[key]["property"] == 2 + assert result[key]["metadata"]["type"] == "number" + assert result[key]["configuration_value_type"] == "enumerated" + async def test_add_node( hass, integration, client, hass_ws_client, nortek_thermostat_added_event From 9d7c64ec1ae56df9dde005f87a4a620dbcccff7d Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 22 Feb 2021 17:42:12 -0500 Subject: [PATCH 647/796] Add missing required=true to code slot field in zwave_js.set_lock_usercode service (#46931) --- homeassistant/components/zwave_js/services.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml index dd66a0c7f29..d2f1c75b64e 100644 --- a/homeassistant/components/zwave_js/services.yaml +++ b/homeassistant/components/zwave_js/services.yaml @@ -27,6 +27,7 @@ set_lock_usercode: code_slot: name: Code slot description: Code slot to set the code. + required: true example: 1 selector: text: From 20ccec9aab7f7a42bfe0a10697adab52e34166ea Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 22 Feb 2021 18:35:19 -0500 Subject: [PATCH 648/796] Add zwave_js/get_log_config and zwave_js/update_log_config WS API commands (#46601) * Add zwave_js.update_log_config service * fix comment * reduce lines * move update_log_config from service to ws API call * fix docstring * Add zwave_js/get_log_config WS API command * resolve stale comments * remove transports since it will be removed from upstream PR * add support to update all log config parameters since they could be useful outside of the UI for advanced users * fix comment * switch to lambda instead of single line validator * fix rebase * re-add ATTR_DOMAIN --- homeassistant/components/zwave_js/api.py | 90 ++++++++++++ tests/components/zwave_js/test_api.py | 172 ++++++++++++++++++++++- 2 files changed, 261 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index fb9282b4763..a6cd8c50a76 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -1,26 +1,40 @@ """Websocket API for Z-Wave JS.""" +import dataclasses import json +from typing import Dict from aiohttp import hdrs, web, web_exceptions import voluptuous as vol from zwave_js_server import dump +from zwave_js_server.const import LogLevel +from zwave_js_server.model.log_config import LogConfig from homeassistant.components import websocket_api from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DATA_CLIENT, DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY +# general API constants ID = "id" ENTRY_ID = "entry_id" NODE_ID = "node_id" TYPE = "type" +# constants for log config commands +CONFIG = "config" +LEVEL = "level" +LOG_TO_FILE = "log_to_file" +FILENAME = "filename" +ENABLED = "enabled" +FORCE_CONSOLE = "force_console" + @callback def async_register_api(hass: HomeAssistant) -> None: @@ -32,6 +46,8 @@ def async_register_api(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, websocket_remove_node) websocket_api.async_register_command(hass, websocket_stop_exclusion) websocket_api.async_register_command(hass, websocket_get_config_parameters) + websocket_api.async_register_command(hass, websocket_update_log_config) + websocket_api.async_register_command(hass, websocket_get_log_config) hass.http.register_view(DumpView) # type: ignore @@ -306,6 +322,80 @@ def websocket_get_config_parameters( ) +def convert_log_level_to_enum(value: str) -> LogLevel: + """Convert log level string to LogLevel enum.""" + return LogLevel[value.upper()] + + +def filename_is_present_if_logging_to_file(obj: Dict) -> Dict: + """Validate that filename is provided if log_to_file is True.""" + if obj.get(LOG_TO_FILE, False) and FILENAME not in obj: + raise vol.Invalid("`filename` must be provided if logging to file") + return obj + + +@websocket_api.require_admin # type: ignore +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/update_log_config", + vol.Required(ENTRY_ID): str, + vol.Required(CONFIG): vol.All( + vol.Schema( + { + vol.Optional(ENABLED): cv.boolean, + vol.Optional(LEVEL): vol.All( + cv.string, + vol.Lower, + vol.In([log_level.name.lower() for log_level in LogLevel]), + lambda val: LogLevel[val.upper()], + ), + vol.Optional(LOG_TO_FILE): cv.boolean, + vol.Optional(FILENAME): cv.string, + vol.Optional(FORCE_CONSOLE): cv.boolean, + } + ), + cv.has_at_least_one_key( + ENABLED, FILENAME, FORCE_CONSOLE, LEVEL, LOG_TO_FILE + ), + filename_is_present_if_logging_to_file, + ), + }, +) +async def websocket_update_log_config( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: + """Update the driver log config.""" + entry_id = msg[ENTRY_ID] + client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + result = await client.driver.async_update_log_config(LogConfig(**msg[CONFIG])) + connection.send_result( + msg[ID], + result, + ) + + +@websocket_api.require_admin # type: ignore +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/get_log_config", + vol.Required(ENTRY_ID): str, + }, +) +async def websocket_get_log_config( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: + """Cancel removing a node from the Z-Wave network.""" + entry_id = msg[ENTRY_ID] + client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + result = await client.driver.async_get_log_config() + connection.send_result( + msg[ID], + dataclasses.asdict(result), + ) + + class DumpView(HomeAssistantView): """View to dump the state of the Z-Wave JS server.""" diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 7689f7140f4..29a86d63c83 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -2,9 +2,21 @@ import json from unittest.mock import patch +from zwave_js_server.const import LogLevel from zwave_js_server.event import Event -from homeassistant.components.zwave_js.api import ENTRY_ID, ID, NODE_ID, TYPE +from homeassistant.components.zwave_js.api import ( + CONFIG, + ENABLED, + ENTRY_ID, + FILENAME, + FORCE_CONSOLE, + ID, + LEVEL, + LOG_TO_FILE, + NODE_ID, + TYPE, +) from homeassistant.components.zwave_js.const import DOMAIN from homeassistant.helpers.device_registry import async_get_registry @@ -191,3 +203,161 @@ async def test_dump_view_invalid_entry_id(integration, hass_client): client = await hass_client() resp = await client.get("/api/zwave_js/dump/INVALID") assert resp.status == 400 + + +async def test_update_log_config(hass, client, integration, hass_ws_client): + """Test that the update_log_config WS API call works and that schema validation works.""" + entry = integration + ws_client = await hass_ws_client(hass) + + # Test we can set log level + client.async_send_command.return_value = {"success": True} + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/update_log_config", + ENTRY_ID: entry.entry_id, + CONFIG: {LEVEL: "Error"}, + } + ) + msg = await ws_client.receive_json() + assert msg["result"] + assert msg["success"] + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "update_log_config" + assert args["config"] == {"level": 0} + + client.async_send_command.reset_mock() + + # Test we can set logToFile to True + client.async_send_command.return_value = {"success": True} + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/update_log_config", + ENTRY_ID: entry.entry_id, + CONFIG: {LOG_TO_FILE: True, FILENAME: "/test"}, + } + ) + msg = await ws_client.receive_json() + assert msg["result"] + assert msg["success"] + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "update_log_config" + assert args["config"] == {"logToFile": True, "filename": "/test"} + + client.async_send_command.reset_mock() + + # Test all parameters + client.async_send_command.return_value = {"success": True} + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/update_log_config", + ENTRY_ID: entry.entry_id, + CONFIG: { + LEVEL: "Error", + LOG_TO_FILE: True, + FILENAME: "/test", + FORCE_CONSOLE: True, + ENABLED: True, + }, + } + ) + msg = await ws_client.receive_json() + assert msg["result"] + assert msg["success"] + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "update_log_config" + assert args["config"] == { + "level": 0, + "logToFile": True, + "filename": "/test", + "forceConsole": True, + "enabled": True, + } + + client.async_send_command.reset_mock() + + # Test error when setting unrecognized log level + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/update_log_config", + ENTRY_ID: entry.entry_id, + CONFIG: {LEVEL: "bad_log_level"}, + } + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert "error" in msg and "value must be one of" in msg["error"]["message"] + + # Test error without service data + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/update_log_config", + ENTRY_ID: entry.entry_id, + CONFIG: {}, + } + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert "error" in msg and "must contain at least one of" in msg["error"]["message"] + + # Test error if we set logToFile to True without providing filename + await ws_client.send_json( + { + ID: 6, + TYPE: "zwave_js/update_log_config", + ENTRY_ID: entry.entry_id, + CONFIG: {LOG_TO_FILE: True}, + } + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert ( + "error" in msg + and "must be provided if logging to file" in msg["error"]["message"] + ) + + +async def test_get_log_config(hass, client, integration, hass_ws_client): + """Test that the get_log_config WS API call works.""" + entry = integration + ws_client = await hass_ws_client(hass) + + # Test we can get log configuration + client.async_send_command.return_value = { + "success": True, + "config": { + "enabled": True, + "level": 0, + "logToFile": False, + "filename": "/test.txt", + "forceConsole": False, + }, + } + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/get_log_config", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + assert msg["result"] + assert msg["success"] + + log_config = msg["result"] + assert log_config["enabled"] + assert log_config["level"] == LogLevel.ERROR + assert log_config["log_to_file"] is False + assert log_config["filename"] == "/test.txt" + assert log_config["force_console"] is False From 580d25c622accb47a7387974c8579e3e9ee53cf0 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 23 Feb 2021 00:05:06 +0000 Subject: [PATCH 649/796] [ci skip] Translation update --- .../components/abode/translations/ru.json | 2 +- .../components/airnow/translations/ru.json | 2 +- .../components/apple_tv/translations/ru.json | 2 +- .../components/august/translations/ru.json | 2 +- .../components/axis/translations/ru.json | 2 +- .../azure_devops/translations/ru.json | 2 +- .../components/blink/translations/ru.json | 2 +- .../bmw_connected_drive/translations/ru.json | 2 +- .../components/bond/translations/ru.json | 2 +- .../cloudflare/translations/ru.json | 2 +- .../components/control4/translations/ru.json | 2 +- .../components/daikin/translations/ru.json | 2 +- .../devolo_home_control/translations/ru.json | 2 +- .../components/dexcom/translations/ru.json | 2 +- .../components/doorbird/translations/ru.json | 2 +- .../components/econet/translations/ru.json | 4 +- .../components/elkm1/translations/ru.json | 2 +- .../components/esphome/translations/ru.json | 2 +- .../fireservicerota/translations/ru.json | 2 +- .../flick_electric/translations/ru.json | 2 +- .../components/flo/translations/ru.json | 2 +- .../components/flume/translations/ru.json | 2 +- .../components/foscam/translations/ru.json | 2 +- .../components/fritzbox/translations/ru.json | 2 +- .../fritzbox_callmonitor/translations/ru.json | 2 +- .../garmin_connect/translations/ru.json | 2 +- .../components/gogogate2/translations/ru.json | 2 +- .../components/habitica/translations/ru.json | 2 +- .../components/hlk_sw16/translations/ru.json | 2 +- .../huawei_lte/translations/ru.json | 2 +- .../huisbaasje/translations/ru.json | 4 +- .../hvv_departures/translations/ru.json | 2 +- .../components/icloud/translations/ru.json | 2 +- .../components/isy994/translations/ru.json | 2 +- .../components/juicenet/translations/ru.json | 2 +- .../components/kmtronic/translations/ca.json | 21 +++++++++ .../components/kmtronic/translations/en.json | 21 +++++++++ .../components/kmtronic/translations/et.json | 21 +++++++++ .../components/kmtronic/translations/no.json | 21 +++++++++ .../components/kodi/translations/ru.json | 4 +- .../components/life360/translations/ru.json | 4 +- .../litterrobot/translations/ca.json | 20 +++++++++ .../litterrobot/translations/en.json | 2 +- .../litterrobot/translations/et.json | 20 +++++++++ .../logi_circle/translations/ru.json | 2 +- .../components/mazda/translations/ru.json | 2 +- .../components/melcloud/translations/ru.json | 2 +- .../components/mikrotik/translations/ru.json | 2 +- .../components/myq/translations/ru.json | 2 +- .../components/mysensors/translations/ru.json | 4 +- .../components/neato/translations/ru.json | 4 +- .../components/nexia/translations/ru.json | 2 +- .../nightscout/translations/ru.json | 2 +- .../components/notion/translations/ru.json | 2 +- .../components/nuheat/translations/ru.json | 2 +- .../components/nuki/translations/ru.json | 2 +- .../components/omnilogic/translations/ru.json | 2 +- .../ovo_energy/translations/ru.json | 2 +- .../components/plugwise/translations/ru.json | 2 +- .../components/poolsense/translations/ru.json | 2 +- .../components/powerwall/translations/ru.json | 2 +- .../components/rachio/translations/ru.json | 2 +- .../rainmachine/translations/ru.json | 2 +- .../components/ring/translations/ru.json | 2 +- .../components/risco/translations/ru.json | 2 +- .../translations/no.json | 21 +++++++++ .../translations/ru.json | 21 +++++++++ .../components/roon/translations/ru.json | 2 +- .../ruckus_unleashed/translations/ru.json | 2 +- .../components/sense/translations/ru.json | 2 +- .../components/sharkiq/translations/ru.json | 2 +- .../components/shelly/translations/ru.json | 2 +- .../simplisafe/translations/ru.json | 2 +- .../smart_meter_texas/translations/ru.json | 2 +- .../components/smarthab/translations/ru.json | 2 +- .../components/smarttub/translations/ru.json | 2 +- .../somfy_mylink/translations/ru.json | 2 +- .../components/sonarr/translations/ru.json | 2 +- .../components/spider/translations/ru.json | 2 +- .../squeezebox/translations/ru.json | 2 +- .../srp_energy/translations/ru.json | 2 +- .../components/subaru/translations/no.json | 44 +++++++++++++++++++ .../components/subaru/translations/ru.json | 44 +++++++++++++++++++ .../synology_dsm/translations/ru.json | 2 +- .../components/tado/translations/ru.json | 2 +- .../tellduslive/translations/ru.json | 2 +- .../components/tesla/translations/ru.json | 2 +- .../components/tile/translations/ru.json | 2 +- .../totalconnect/translations/ca.json | 17 ++++++- .../totalconnect/translations/et.json | 17 ++++++- .../totalconnect/translations/ru.json | 2 +- .../transmission/translations/ru.json | 2 +- .../components/tuya/translations/ru.json | 4 +- .../components/unifi/translations/ru.json | 2 +- .../components/upcloud/translations/ru.json | 2 +- .../components/vesync/translations/ru.json | 2 +- .../components/vilfo/translations/ru.json | 2 +- .../components/wolflink/translations/ru.json | 2 +- .../xiaomi_miio/translations/ca.json | 1 + .../xiaomi_miio/translations/en.json | 22 +++++++++- .../xiaomi_miio/translations/et.json | 1 + .../zoneminder/translations/ru.json | 4 +- 102 files changed, 401 insertions(+), 101 deletions(-) create mode 100644 homeassistant/components/kmtronic/translations/ca.json create mode 100644 homeassistant/components/kmtronic/translations/en.json create mode 100644 homeassistant/components/kmtronic/translations/et.json create mode 100644 homeassistant/components/kmtronic/translations/no.json create mode 100644 homeassistant/components/litterrobot/translations/ca.json create mode 100644 homeassistant/components/litterrobot/translations/et.json create mode 100644 homeassistant/components/rituals_perfume_genie/translations/no.json create mode 100644 homeassistant/components/rituals_perfume_genie/translations/ru.json create mode 100644 homeassistant/components/subaru/translations/no.json create mode 100644 homeassistant/components/subaru/translations/ru.json diff --git a/homeassistant/components/abode/translations/ru.json b/homeassistant/components/abode/translations/ru.json index 04efaa6e519..f3804a840ab 100644 --- a/homeassistant/components/abode/translations/ru.json +++ b/homeassistant/components/abode/translations/ru.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "invalid_mfa_code": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u0434 MFA." }, "step": { diff --git a/homeassistant/components/airnow/translations/ru.json b/homeassistant/components/airnow/translations/ru.json index 650633cc816..9667accb7c4 100644 --- a/homeassistant/components/airnow/translations/ru.json +++ b/homeassistant/components/airnow/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "invalid_location": "\u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u043e\u0432 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/apple_tv/translations/ru.json b/homeassistant/components/apple_tv/translations/ru.json index e3f5804cebe..4ad9b9f52c7 100644 --- a/homeassistant/components/apple_tv/translations/ru.json +++ b/homeassistant/components/apple_tv/translations/ru.json @@ -11,7 +11,7 @@ }, "error": { "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.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\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.", "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." diff --git a/homeassistant/components/august/translations/ru.json b/homeassistant/components/august/translations/ru.json index 9ea0b531bf8..97dba8fc758 100644 --- a/homeassistant/components/august/translations/ru.json +++ b/homeassistant/components/august/translations/ru.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/axis/translations/ru.json b/homeassistant/components/axis/translations/ru.json index 6d979dc9de0..1bf3e369b65 100644 --- a/homeassistant/components/axis/translations/ru.json +++ b/homeassistant/components/axis/translations/ru.json @@ -9,7 +9,7 @@ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "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.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "flow_title": "{name} ({host})", "step": { diff --git a/homeassistant/components/azure_devops/translations/ru.json b/homeassistant/components/azure_devops/translations/ru.json index 84e0fc93b46..4e59af2dd11 100644 --- a/homeassistant/components/azure_devops/translations/ru.json +++ b/homeassistant/components/azure_devops/translations/ru.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "project_error": "\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 \u043f\u0440\u043e\u0435\u043a\u0442\u0435." }, "flow_title": "Azure DevOps: {project_url}", diff --git a/homeassistant/components/blink/translations/ru.json b/homeassistant/components/blink/translations/ru.json index 0e55fa716b9..0835ab5ac0a 100644 --- a/homeassistant/components/blink/translations/ru.json +++ b/homeassistant/components/blink/translations/ru.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/bmw_connected_drive/translations/ru.json b/homeassistant/components/bmw_connected_drive/translations/ru.json index 0840affcef4..9ac76bbea9e 100644 --- a/homeassistant/components/bmw_connected_drive/translations/ru.json +++ b/homeassistant/components/bmw_connected_drive/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/bond/translations/ru.json b/homeassistant/components/bond/translations/ru.json index 493b8e141ce..e6c4067d8ac 100644 --- a/homeassistant/components/bond/translations/ru.json +++ b/homeassistant/components/bond/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "old_firmware": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u0443\u0441\u0442\u0430\u0440\u0435\u043b\u0430 \u0438 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/cloudflare/translations/ru.json b/homeassistant/components/cloudflare/translations/ru.json index fa4819d8c83..7c397faa37e 100644 --- a/homeassistant/components/cloudflare/translations/ru.json +++ b/homeassistant/components/cloudflare/translations/ru.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "invalid_zone": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0437\u043e\u043d\u0430" }, "flow_title": "Cloudflare: {name}", diff --git a/homeassistant/components/control4/translations/ru.json b/homeassistant/components/control4/translations/ru.json index 4f51641992b..3882f03cb32 100644 --- a/homeassistant/components/control4/translations/ru.json +++ b/homeassistant/components/control4/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/daikin/translations/ru.json b/homeassistant/components/daikin/translations/ru.json index df7d9fb07dc..7365bb0e7bb 100644 --- a/homeassistant/components/daikin/translations/ru.json +++ b/homeassistant/components/daikin/translations/ru.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/devolo_home_control/translations/ru.json b/homeassistant/components/devolo_home_control/translations/ru.json index d4cf639ffd5..b2e82f1355b 100644 --- a/homeassistant/components/devolo_home_control/translations/ru.json +++ b/homeassistant/components/devolo_home_control/translations/ru.json @@ -4,7 +4,7 @@ "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, "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." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/dexcom/translations/ru.json b/homeassistant/components/dexcom/translations/ru.json index 5b6b3ab24b1..aa90d6d998d 100644 --- a/homeassistant/components/dexcom/translations/ru.json +++ b/homeassistant/components/dexcom/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/doorbird/translations/ru.json b/homeassistant/components/doorbird/translations/ru.json index 274b88a8b47..5e376ee56d3 100644 --- a/homeassistant/components/doorbird/translations/ru.json +++ b/homeassistant/components/doorbird/translations/ru.json @@ -7,7 +7,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "flow_title": "DoorBird {name} ({host})", diff --git a/homeassistant/components/econet/translations/ru.json b/homeassistant/components/econet/translations/ru.json index 109ded8db99..1b0d79ac396 100644 --- a/homeassistant/components/econet/translations/ru.json +++ b/homeassistant/components/econet/translations/ru.json @@ -3,11 +3,11 @@ "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.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/elkm1/translations/ru.json b/homeassistant/components/elkm1/translations/ru.json index 3c84b98b0ca..48a950f1cca 100644 --- a/homeassistant/components/elkm1/translations/ru.json +++ b/homeassistant/components/elkm1/translations/ru.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/esphome/translations/ru.json b/homeassistant/components/esphome/translations/ru.json index bcbe9148854..4277a057a86 100644 --- a/homeassistant/components/esphome/translations/ru.json +++ b/homeassistant/components/esphome/translations/ru.json @@ -6,7 +6,7 @@ }, "error": { "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a ESP. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 YAML-\u0444\u0430\u0439\u043b \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 'api:'.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "resolve_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0430\u0434\u0440\u0435\u0441 ESP. \u0415\u0441\u043b\u0438 \u044d\u0442\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f, \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips." }, "flow_title": "ESPHome: {name}", diff --git a/homeassistant/components/fireservicerota/translations/ru.json b/homeassistant/components/fireservicerota/translations/ru.json index 2c90bd53ca9..3955172e02d 100644 --- a/homeassistant/components/fireservicerota/translations/ru.json +++ b/homeassistant/components/fireservicerota/translations/ru.json @@ -8,7 +8,7 @@ "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." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { "reauth": { diff --git a/homeassistant/components/flick_electric/translations/ru.json b/homeassistant/components/flick_electric/translations/ru.json index c97bb9133cc..bcabe2f2157 100644 --- a/homeassistant/components/flick_electric/translations/ru.json +++ b/homeassistant/components/flick_electric/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/flo/translations/ru.json b/homeassistant/components/flo/translations/ru.json index 6f71ee41376..9e0db9fcf94 100644 --- a/homeassistant/components/flo/translations/ru.json +++ b/homeassistant/components/flo/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/flume/translations/ru.json b/homeassistant/components/flume/translations/ru.json index f35579c2dee..e4be913abcd 100644 --- a/homeassistant/components/flume/translations/ru.json +++ b/homeassistant/components/flume/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/foscam/translations/ru.json b/homeassistant/components/foscam/translations/ru.json index 01e0494a07e..f78f64af69a 100644 --- a/homeassistant/components/foscam/translations/ru.json +++ b/homeassistant/components/foscam/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "invalid_response": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043e\u0442\u0432\u0435\u0442 \u043e\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/fritzbox/translations/ru.json b/homeassistant/components/fritzbox/translations/ru.json index 50146b490ba..8cd77671bd8 100644 --- a/homeassistant/components/fritzbox/translations/ru.json +++ b/homeassistant/components/fritzbox/translations/ru.json @@ -8,7 +8,7 @@ "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": { - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "flow_title": "AVM FRITZ!Box: {name}", "step": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/ru.json b/homeassistant/components/fritzbox_callmonitor/translations/ru.json index 3eb432532c4..f1bcb18a2f6 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/ru.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/ru.json @@ -6,7 +6,7 @@ "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." }, "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." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "flow_title": "AVM FRITZ!Box call monitor: {name}", "step": { diff --git a/homeassistant/components/garmin_connect/translations/ru.json b/homeassistant/components/garmin_connect/translations/ru.json index 69fa96c2a5e..49dd5c5b3bc 100644 --- a/homeassistant/components/garmin_connect/translations/ru.json +++ b/homeassistant/components/garmin_connect/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "too_many_requests": "\u0421\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/gogogate2/translations/ru.json b/homeassistant/components/gogogate2/translations/ru.json index 0c8f14f65f4..43e9f7a1b2f 100644 --- a/homeassistant/components/gogogate2/translations/ru.json +++ b/homeassistant/components/gogogate2/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/habitica/translations/ru.json b/homeassistant/components/habitica/translations/ru.json index b3e81a34997..4899cd1e43b 100644 --- a/homeassistant/components/habitica/translations/ru.json +++ b/homeassistant/components/habitica/translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/hlk_sw16/translations/ru.json b/homeassistant/components/hlk_sw16/translations/ru.json index 6f71ee41376..9e0db9fcf94 100644 --- a/homeassistant/components/hlk_sw16/translations/ru.json +++ b/homeassistant/components/hlk_sw16/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/huawei_lte/translations/ru.json b/homeassistant/components/huawei_lte/translations/ru.json index 88457457796..c2ec20fb259 100644 --- a/homeassistant/components/huawei_lte/translations/ru.json +++ b/homeassistant/components/huawei_lte/translations/ru.json @@ -9,7 +9,7 @@ "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", "incorrect_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c.", "incorrect_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "invalid_url": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.", "login_attempts_exceeded": "\u041f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u043e \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u043f\u044b\u0442\u043e\u043a \u0432\u0445\u043e\u0434\u0430, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", "response_error": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", diff --git a/homeassistant/components/huisbaasje/translations/ru.json b/homeassistant/components/huisbaasje/translations/ru.json index ada9aed539a..a598320115d 100644 --- a/homeassistant/components/huisbaasje/translations/ru.json +++ b/homeassistant/components/huisbaasje/translations/ru.json @@ -5,8 +5,8 @@ }, "error": { "connection_exception": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", - "unauthenticated_exception": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unauthenticated_exception": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/hvv_departures/translations/ru.json b/homeassistant/components/hvv_departures/translations/ru.json index ff5819a562d..6ae27715033 100644 --- a/homeassistant/components/hvv_departures/translations/ru.json +++ b/homeassistant/components/hvv_departures/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "no_results": "\u041d\u0435\u0442 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u043e\u0432. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0435\u0439 / \u0430\u0434\u0440\u0435\u0441\u043e\u043c." }, "step": { diff --git a/homeassistant/components/icloud/translations/ru.json b/homeassistant/components/icloud/translations/ru.json index 797637e1010..bdd6fe776ad 100644 --- a/homeassistant/components/icloud/translations/ru.json +++ b/homeassistant/components/icloud/translations/ru.json @@ -6,7 +6,7 @@ "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": { - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "send_verification_code": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f.", "validate_verification_code": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437." }, diff --git a/homeassistant/components/isy994/translations/ru.json b/homeassistant/components/isy994/translations/ru.json index c0a658423c6..cbf88574e1e 100644 --- a/homeassistant/components/isy994/translations/ru.json +++ b/homeassistant/components/isy994/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "invalid_host": "URL-\u0430\u0434\u0440\u0435\u0441 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'address[:port]' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: 'http://192.168.10.100:80').", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/juicenet/translations/ru.json b/homeassistant/components/juicenet/translations/ru.json index 2fec7d485c4..d582e6f1703 100644 --- a/homeassistant/components/juicenet/translations/ru.json +++ b/homeassistant/components/juicenet/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/kmtronic/translations/ca.json b/homeassistant/components/kmtronic/translations/ca.json new file mode 100644 index 00000000000..df8218bab3e --- /dev/null +++ b/homeassistant/components/kmtronic/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/en.json b/homeassistant/components/kmtronic/translations/en.json new file mode 100644 index 00000000000..f15fe84c3ed --- /dev/null +++ b/homeassistant/components/kmtronic/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/et.json b/homeassistant/components/kmtronic/translations/et.json new file mode 100644 index 00000000000..0c1715b4932 --- /dev/null +++ b/homeassistant/components/kmtronic/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/no.json b/homeassistant/components/kmtronic/translations/no.json new file mode 100644 index 00000000000..249711bb912 --- /dev/null +++ b/homeassistant/components/kmtronic/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "password": "Passord", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/ru.json b/homeassistant/components/kodi/translations/ru.json index 312008c9b62..50742417f28 100644 --- a/homeassistant/components/kodi/translations/ru.json +++ b/homeassistant/components/kodi/translations/ru.json @@ -3,13 +3,13 @@ "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.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "no_uuid": "\u0423 \u044d\u0442\u043e\u0433\u043e \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0430 Kodi \u043d\u0435\u0442 \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430. \u0421\u043a\u043e\u0440\u0435\u0435 \u0432\u0441\u0435\u0433\u043e, \u044d\u0442\u043e \u0441\u0432\u044f\u0437\u0430\u043d\u043e \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0441\u0442\u0430\u0440\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0435\u0439 Kodi (17.x \u0438\u043b\u0438 \u043d\u0438\u0436\u0435). \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u0439\u0442\u0438 \u043d\u0430 \u0431\u043e\u043b\u0435\u0435 \u043d\u043e\u0432\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e Kodi.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "flow_title": "Kodi: {name}", diff --git a/homeassistant/components/life360/translations/ru.json b/homeassistant/components/life360/translations/ru.json index 2de2f63dbd6..5b5934fbb42 100644 --- a/homeassistant/components/life360/translations/ru.json +++ b/homeassistant/components/life360/translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "create_entry": { @@ -9,7 +9,7 @@ }, "error": { "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/litterrobot/translations/ca.json b/homeassistant/components/litterrobot/translations/ca.json new file mode 100644 index 00000000000..9677f944330 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/en.json b/homeassistant/components/litterrobot/translations/en.json index b3fc76ae458..cb0e7bed7ea 100644 --- a/homeassistant/components/litterrobot/translations/en.json +++ b/homeassistant/components/litterrobot/translations/en.json @@ -17,4 +17,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/et.json b/homeassistant/components/litterrobot/translations/et.json new file mode 100644 index 00000000000..ce02ca14929 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/translations/ru.json b/homeassistant/components/logi_circle/translations/ru.json index 2a7ccc4f374..8da20b60c39 100644 --- a/homeassistant/components/logi_circle/translations/ru.json +++ b/homeassistant/components/logi_circle/translations/ru.json @@ -9,7 +9,7 @@ "error": { "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.", "follow_link": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 \u0438 \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e, \u043f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043d\u0430\u0436\u0430\u0442\u044c \"\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c\".", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { "auth": { diff --git a/homeassistant/components/mazda/translations/ru.json b/homeassistant/components/mazda/translations/ru.json index e41babd499d..be3f861d406 100644 --- a/homeassistant/components/mazda/translations/ru.json +++ b/homeassistant/components/mazda/translations/ru.json @@ -7,7 +7,7 @@ "error": { "account_locked": "\u0410\u043a\u043a\u0430\u0443\u043d\u0442 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d. \u041f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/melcloud/translations/ru.json b/homeassistant/components/melcloud/translations/ru.json index e904ea4e8b7..5c5081cb0c6 100644 --- a/homeassistant/components/melcloud/translations/ru.json +++ b/homeassistant/components/melcloud/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/mikrotik/translations/ru.json b/homeassistant/components/mikrotik/translations/ru.json index 868ed49b5c4..21391f12b1c 100644 --- a/homeassistant/components/mikrotik/translations/ru.json +++ b/homeassistant/components/mikrotik/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." }, "step": { diff --git a/homeassistant/components/myq/translations/ru.json b/homeassistant/components/myq/translations/ru.json index daa3148beef..c3b113f148f 100644 --- a/homeassistant/components/myq/translations/ru.json +++ b/homeassistant/components/myq/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/mysensors/translations/ru.json b/homeassistant/components/mysensors/translations/ru.json index e78685e3f6b..62679709017 100644 --- a/homeassistant/components/mysensors/translations/ru.json +++ b/homeassistant/components/mysensors/translations/ru.json @@ -5,7 +5,7 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "duplicate_persistence_file": "\u042d\u0442\u043e\u0442 \u0444\u0430\u0439\u043b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0439 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", "duplicate_topic": "\u042d\u0442\u043e\u0442 \u0442\u043e\u043f\u0438\u043a \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "invalid_device": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e.", "invalid_ip": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441.", "invalid_persistence_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0439.", @@ -24,7 +24,7 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "duplicate_persistence_file": "\u042d\u0442\u043e\u0442 \u0444\u0430\u0439\u043b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0439 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", "duplicate_topic": "\u042d\u0442\u043e\u0442 \u0442\u043e\u043f\u0438\u043a \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "invalid_device": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e.", "invalid_ip": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441.", "invalid_persistence_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0439.", diff --git a/homeassistant/components/neato/translations/ru.json b/homeassistant/components/neato/translations/ru.json index 30ea15c60c3..ea1be16d7ac 100644 --- a/homeassistant/components/neato/translations/ru.json +++ b/homeassistant/components/neato/translations/ru.json @@ -3,7 +3,7 @@ "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.", "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.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\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." @@ -12,7 +12,7 @@ "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.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/nexia/translations/ru.json b/homeassistant/components/nexia/translations/ru.json index a19d16e3a7e..f1c7b5b8ced 100644 --- a/homeassistant/components/nexia/translations/ru.json +++ b/homeassistant/components/nexia/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/nightscout/translations/ru.json b/homeassistant/components/nightscout/translations/ru.json index 738c4dfa9a3..c7688973c1b 100644 --- a/homeassistant/components/nightscout/translations/ru.json +++ b/homeassistant/components/nightscout/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "flow_title": "Nightscout", diff --git a/homeassistant/components/notion/translations/ru.json b/homeassistant/components/notion/translations/ru.json index 678eff742b5..737539424b0 100644 --- a/homeassistant/components/notion/translations/ru.json +++ b/homeassistant/components/notion/translations/ru.json @@ -4,7 +4,7 @@ "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, "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.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "no_devices": "\u041d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e." }, "step": { diff --git a/homeassistant/components/nuheat/translations/ru.json b/homeassistant/components/nuheat/translations/ru.json index 09e74c0e4cb..099f6c3f1fc 100644 --- a/homeassistant/components/nuheat/translations/ru.json +++ b/homeassistant/components/nuheat/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "invalid_thermostat": "\u0421\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u0430 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/nuki/translations/ru.json b/homeassistant/components/nuki/translations/ru.json index bad9f35c076..a7fe1c61f5b 100644 --- a/homeassistant/components/nuki/translations/ru.json +++ b/homeassistant/components/nuki/translations/ru.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/omnilogic/translations/ru.json b/homeassistant/components/omnilogic/translations/ru.json index 9040654e58c..828f0530830 100644 --- a/homeassistant/components/omnilogic/translations/ru.json +++ b/homeassistant/components/omnilogic/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/ovo_energy/translations/ru.json b/homeassistant/components/ovo_energy/translations/ru.json index dd422bac01f..47a94f6a24a 100644 --- a/homeassistant/components/ovo_energy/translations/ru.json +++ b/homeassistant/components/ovo_energy/translations/ru.json @@ -3,7 +3,7 @@ "error": { "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "flow_title": "OVO Energy: {username}", "step": { diff --git a/homeassistant/components/plugwise/translations/ru.json b/homeassistant/components/plugwise/translations/ru.json index 8a59d492e66..9df460e8919 100644 --- a/homeassistant/components/plugwise/translations/ru.json +++ b/homeassistant/components/plugwise/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "flow_title": "Smile: {name}", diff --git a/homeassistant/components/poolsense/translations/ru.json b/homeassistant/components/poolsense/translations/ru.json index 3687b75a6f7..09c94368cda 100644 --- a/homeassistant/components/poolsense/translations/ru.json +++ b/homeassistant/components/poolsense/translations/ru.json @@ -4,7 +4,7 @@ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." }, "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." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/powerwall/translations/ru.json b/homeassistant/components/powerwall/translations/ru.json index 2d8246cc14f..f79b62c2c78 100644 --- a/homeassistant/components/powerwall/translations/ru.json +++ b/homeassistant/components/powerwall/translations/ru.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", "wrong_version": "\u0412\u0430\u0448 powerwall \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0432\u0435\u0440\u0441\u0438\u044e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u043e\u0433\u043e \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u0441\u043e\u043e\u0431\u0449\u0438\u0442\u0435 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0435, \u0447\u0442\u043e\u0431\u044b \u0435\u0435 \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0440\u0435\u0448\u0438\u0442\u044c." }, diff --git a/homeassistant/components/rachio/translations/ru.json b/homeassistant/components/rachio/translations/ru.json index 53cd98387fa..52248b8d686 100644 --- a/homeassistant/components/rachio/translations/ru.json +++ b/homeassistant/components/rachio/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/rainmachine/translations/ru.json b/homeassistant/components/rainmachine/translations/ru.json index 08ce690d22f..8502b66aff7 100644 --- a/homeassistant/components/rainmachine/translations/ru.json +++ b/homeassistant/components/rainmachine/translations/ru.json @@ -4,7 +4,7 @@ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." }, "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." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/ring/translations/ru.json b/homeassistant/components/ring/translations/ru.json index fb8c22c39af..636d83f2e02 100644 --- a/homeassistant/components/ring/translations/ru.json +++ b/homeassistant/components/ring/translations/ru.json @@ -4,7 +4,7 @@ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." }, "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.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/risco/translations/ru.json b/homeassistant/components/risco/translations/ru.json index 3fd1fd567f9..a507bb84e53 100644 --- a/homeassistant/components/risco/translations/ru.json +++ b/homeassistant/components/risco/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/rituals_perfume_genie/translations/no.json b/homeassistant/components/rituals_perfume_genie/translations/no.json new file mode 100644 index 00000000000..2ffc9bc91af --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "email": "E-post", + "password": "Passord" + }, + "title": "Koble til Rituals-kontoen din" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rituals_perfume_genie/translations/ru.json b/homeassistant/components/rituals_perfume_genie/translations/ru.json new file mode 100644 index 00000000000..afbf1da0e46 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Rituals" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/ru.json b/homeassistant/components/roon/translations/ru.json index 187151affe2..c01006d6269 100644 --- a/homeassistant/components/roon/translations/ru.json +++ b/homeassistant/components/roon/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "duplicate_entry": "\u042d\u0442\u043e\u0442 \u0445\u043e\u0441\u0442 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/ruckus_unleashed/translations/ru.json b/homeassistant/components/ruckus_unleashed/translations/ru.json index 6f71ee41376..9e0db9fcf94 100644 --- a/homeassistant/components/ruckus_unleashed/translations/ru.json +++ b/homeassistant/components/ruckus_unleashed/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/sense/translations/ru.json b/homeassistant/components/sense/translations/ru.json index 74be3049a75..0bb299e2208 100644 --- a/homeassistant/components/sense/translations/ru.json +++ b/homeassistant/components/sense/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/sharkiq/translations/ru.json b/homeassistant/components/sharkiq/translations/ru.json index 80af08a8958..60ce7d454d6 100644 --- a/homeassistant/components/sharkiq/translations/ru.json +++ b/homeassistant/components/sharkiq/translations/ru.json @@ -8,7 +8,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/shelly/translations/ru.json b/homeassistant/components/shelly/translations/ru.json index ad5a288cf91..a570cb7f9fb 100644 --- a/homeassistant/components/shelly/translations/ru.json +++ b/homeassistant/components/shelly/translations/ru.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "flow_title": "{name}", diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json index 94b0e6a0975..abe0542c926 100644 --- a/homeassistant/components/simplisafe/translations/ru.json +++ b/homeassistant/components/simplisafe/translations/ru.json @@ -6,7 +6,7 @@ }, "error": { "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "still_awaiting_mfa": "\u041e\u0436\u0438\u0434\u0430\u043d\u0438\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u043f\u043e \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u0435.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/smart_meter_texas/translations/ru.json b/homeassistant/components/smart_meter_texas/translations/ru.json index 9fe75df9c3f..3f4677a050e 100644 --- a/homeassistant/components/smart_meter_texas/translations/ru.json +++ b/homeassistant/components/smart_meter_texas/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/smarthab/translations/ru.json b/homeassistant/components/smarthab/translations/ru.json index cea090f51d2..45e3698034f 100644 --- a/homeassistant/components/smarthab/translations/ru.json +++ b/homeassistant/components/smarthab/translations/ru.json @@ -1,7 +1,7 @@ { "config": { "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.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "service": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a SmartHab. \u0421\u0435\u0440\u0432\u0438\u0441 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/smarttub/translations/ru.json b/homeassistant/components/smarttub/translations/ru.json index 67e055a32c5..44f27877d93 100644 --- a/homeassistant/components/smarttub/translations/ru.json +++ b/homeassistant/components/smarttub/translations/ru.json @@ -5,7 +5,7 @@ "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": { - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/somfy_mylink/translations/ru.json b/homeassistant/components/somfy_mylink/translations/ru.json index e4cc7b71712..7c981664335 100644 --- a/homeassistant/components/somfy_mylink/translations/ru.json +++ b/homeassistant/components/somfy_mylink/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "flow_title": "Somfy MyLink {mac} ({ip})", diff --git a/homeassistant/components/sonarr/translations/ru.json b/homeassistant/components/sonarr/translations/ru.json index 1b6345d4563..75d23cd3ec0 100644 --- a/homeassistant/components/sonarr/translations/ru.json +++ b/homeassistant/components/sonarr/translations/ru.json @@ -7,7 +7,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "flow_title": "Sonarr: {name}", "step": { diff --git a/homeassistant/components/spider/translations/ru.json b/homeassistant/components/spider/translations/ru.json index 983f2b94361..1b1a175cce5 100644 --- a/homeassistant/components/spider/translations/ru.json +++ b/homeassistant/components/spider/translations/ru.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "error": { - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/squeezebox/translations/ru.json b/homeassistant/components/squeezebox/translations/ru.json index 789fff313d8..fb07471d116 100644 --- a/homeassistant/components/squeezebox/translations/ru.json +++ b/homeassistant/components/squeezebox/translations/ru.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "no_server_found": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/srp_energy/translations/ru.json b/homeassistant/components/srp_energy/translations/ru.json index 3fcbace37df..125f3a5addc 100644 --- a/homeassistant/components/srp_energy/translations/ru.json +++ b/homeassistant/components/srp_energy/translations/ru.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_account": "ID \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c 9-\u0437\u043d\u0430\u0447\u043d\u044b\u043c \u0447\u0438\u0441\u043b\u043e\u043c.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/subaru/translations/no.json b/homeassistant/components/subaru/translations/no.json new file mode 100644 index 00000000000..f1a263d5cb4 --- /dev/null +++ b/homeassistant/components/subaru/translations/no.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes" + }, + "error": { + "bad_pin_format": "PIN-koden skal best\u00e5 av fire sifre", + "cannot_connect": "Tilkobling mislyktes", + "incorrect_pin": "Feil PIN", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "pin": { + "data": { + "pin": "PIN" + }, + "description": "Vennligst skriv inn MySubaru PIN-koden\n MERKNAD: Alle kj\u00f8ret\u00f8yer som er kontoen m\u00e5 ha samme PIN-kode", + "title": "Subaru Starlink-konfigurasjon" + }, + "user": { + "data": { + "country": "Velg land", + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Vennligst skriv inn MySubaru-legitimasjonen din\n MERK: F\u00f8rste oppsett kan ta opptil 30 sekunder", + "title": "Subaru Starlink-konfigurasjon" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "update_enabled": "Aktiver polling av kj\u00f8ret\u00f8y" + }, + "description": "N\u00e5r dette er aktivert, sender polling av kj\u00f8ret\u00f8y en fjernkommando til kj\u00f8ret\u00f8yet annenhver time for \u00e5 skaffe nye sensordata. Uten kj\u00f8ret\u00f8yoppm\u00e5ling mottas nye sensordata bare n\u00e5r kj\u00f8ret\u00f8yet automatisk skyver data (normalt etter motorstans).", + "title": "Subaru Starlink Alternativer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/ru.json b/homeassistant/components/subaru/translations/ru.json new file mode 100644 index 00000000000..7e3fbce6e38 --- /dev/null +++ b/homeassistant/components/subaru/translations/ru.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "error": { + "bad_pin_format": "PIN-\u043a\u043e\u0434 \u0434\u043e\u043b\u0436\u0435\u043d \u0441\u043e\u0441\u0442\u043e\u044f\u0442\u044c \u0438\u0437 4 \u0446\u0438\u0444\u0440.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "incorrect_pin": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "pin": { + "data": { + "pin": "PIN-\u043a\u043e\u0434" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434 MySubaru.\n\u0412\u0441\u0435 \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0438\u043b\u0438 \u0432 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0434\u043e\u043b\u0436\u043d\u044b \u0438\u043c\u0435\u0442\u044c \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u044b\u0439 PIN-\u043a\u043e\u0434.", + "title": "Subaru Starlink" + }, + "user": { + "data": { + "country": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0442\u0440\u0430\u043d\u0443", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 MySubaru.\n\u041f\u0435\u0440\u0432\u043e\u043d\u0430\u0447\u0430\u043b\u044c\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u043e\u0436\u0435\u0442 \u0437\u0430\u043d\u044f\u0442\u044c \u0434\u043e 30 \u0441\u0435\u043a\u0443\u043d\u0434.", + "title": "Subaru Starlink" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "update_enabled": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u043f\u0440\u043e\u0441 \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0438\u043b\u0435\u0439" + }, + "description": "\u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d, Home Assistant \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u043a\u043e\u043c\u0430\u043d\u0434\u0443 \u043d\u0430 \u0412\u0430\u0448 \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0438\u043b\u044c \u043a\u0430\u0436\u0434\u044b\u0435 2 \u0447\u0430\u0441\u0430 \u0434\u043b\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u043d\u043e\u0432\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445. \u0411\u0435\u0437 \u043e\u043f\u0440\u043e\u0441\u0430 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u043e\u0433\u043e \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430 \u0434\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432 \u0431\u0443\u0434\u0443\u0442 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u0433\u0434\u0430 \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0438\u043b\u044c \u0441\u0430\u043c\u043e\u0441\u0442\u043e\u044f\u0442\u0435\u043b\u044c\u043d\u043e \u0438\u043d\u0438\u0446\u0438\u0438\u0440\u0443\u0435\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0443 \u0434\u0430\u043d\u043d\u044b\u0445 (\u043e\u0431\u044b\u0447\u043d\u043e \u043f\u043e\u0441\u043b\u0435 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0434\u0432\u0438\u0433\u0430\u0442\u0435\u043b\u044f).", + "title": "Subaru Starlink" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/ru.json b/homeassistant/components/synology_dsm/translations/ru.json index ed3c2eea0a8..8c48b8c3fc7 100644 --- a/homeassistant/components/synology_dsm/translations/ru.json +++ b/homeassistant/components/synology_dsm/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "missing_data": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0435: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u0443\u044e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e.", "otp_failed": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u0441 \u043d\u043e\u0432\u044b\u043c \u043f\u0430\u0440\u043e\u043b\u0435\u043c.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." diff --git a/homeassistant/components/tado/translations/ru.json b/homeassistant/components/tado/translations/ru.json index 8ffb14edc0e..75c83e8582b 100644 --- a/homeassistant/components/tado/translations/ru.json +++ b/homeassistant/components/tado/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "no_homes": "\u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0434\u043e\u043c\u043e\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/tellduslive/translations/ru.json b/homeassistant/components/tellduslive/translations/ru.json index 0fc0c2f449f..95a16fa205f 100644 --- a/homeassistant/components/tellduslive/translations/ru.json +++ b/homeassistant/components/tellduslive/translations/ru.json @@ -8,7 +8,7 @@ "unknown_authorize_url_generation": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438." }, "error": { - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { "auth": { diff --git a/homeassistant/components/tesla/translations/ru.json b/homeassistant/components/tesla/translations/ru.json index 7429b8ffa53..d62a2e1f168 100644 --- a/homeassistant/components/tesla/translations/ru.json +++ b/homeassistant/components/tesla/translations/ru.json @@ -7,7 +7,7 @@ "error": { "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/tile/translations/ru.json b/homeassistant/components/tile/translations/ru.json index 62d0b10857c..f42a4d631b0 100644 --- a/homeassistant/components/tile/translations/ru.json +++ b/homeassistant/components/tile/translations/ru.json @@ -4,7 +4,7 @@ "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, "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." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/totalconnect/translations/ca.json b/homeassistant/components/totalconnect/translations/ca.json index 25dafcf7d21..ce055082a21 100644 --- a/homeassistant/components/totalconnect/translations/ca.json +++ b/homeassistant/components/totalconnect/translations/ca.json @@ -1,12 +1,25 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat" + "already_configured": "El compte ja ha estat configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "usercode": "El codi d'usuari no \u00e9s v\u00e0lid per a aquest usuari en aquesta ubicaci\u00f3" }, "step": { + "locations": { + "data": { + "location": "Ubicaci\u00f3" + }, + "description": "Introdueix el codi d'usuari de l'usuari en aquesta ubicaci\u00f3", + "title": "Codis d'usuari d'ubicaci\u00f3" + }, + "reauth_confirm": { + "description": "Total Connect ha de tornar a autenticar-se amb el teu compte", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "password": "Contrasenya", diff --git a/homeassistant/components/totalconnect/translations/et.json b/homeassistant/components/totalconnect/translations/et.json index 2940b7a9e65..3f1a15fe139 100644 --- a/homeassistant/components/totalconnect/translations/et.json +++ b/homeassistant/components/totalconnect/translations/et.json @@ -1,12 +1,25 @@ { "config": { "abort": { - "already_configured": "Konto on juba seadistatud" + "already_configured": "Konto on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { - "invalid_auth": "Tuvastamise viga" + "invalid_auth": "Tuvastamise viga", + "usercode": "Kasutajakood ei sobi selle kasutaja jaoks selles asukohas" }, "step": { + "locations": { + "data": { + "location": "Asukoht" + }, + "description": "Sisesta selle kasutaja kood selles asukohas", + "title": "Asukoha kasutajakoodid" + }, + "reauth_confirm": { + "description": "Total Connect peab konto uuesti autentima", + "title": "Taastuvasta sidumine" + }, "user": { "data": { "password": "Salas\u00f5na", diff --git a/homeassistant/components/totalconnect/translations/ru.json b/homeassistant/components/totalconnect/translations/ru.json index c5221b5e4ca..054f207b2b0 100644 --- a/homeassistant/components/totalconnect/translations/ru.json +++ b/homeassistant/components/totalconnect/translations/ru.json @@ -4,7 +4,7 @@ "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, "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." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/transmission/translations/ru.json b/homeassistant/components/transmission/translations/ru.json index d1fbd592f0f..6b326bc123c 100644 --- a/homeassistant/components/transmission/translations/ru.json +++ b/homeassistant/components/transmission/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." }, "step": { diff --git a/homeassistant/components/tuya/translations/ru.json b/homeassistant/components/tuya/translations/ru.json index 5d887710230..4babc23f2ec 100644 --- a/homeassistant/components/tuya/translations/ru.json +++ b/homeassistant/components/tuya/translations/ru.json @@ -2,11 +2,11 @@ "config": { "abort": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\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." }, "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." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "flow_title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Tuya", "step": { diff --git a/homeassistant/components/unifi/translations/ru.json b/homeassistant/components/unifi/translations/ru.json index df2150c98ec..5810204db41 100644 --- a/homeassistant/components/unifi/translations/ru.json +++ b/homeassistant/components/unifi/translations/ru.json @@ -6,7 +6,7 @@ "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": { - "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "faulty_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "service_unavailable": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "unknown_client_mac": "\u041d\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043d\u0430 \u044d\u0442\u043e\u043c MAC-\u0430\u0434\u0440\u0435\u0441\u0435." }, diff --git a/homeassistant/components/upcloud/translations/ru.json b/homeassistant/components/upcloud/translations/ru.json index ced4097a7e2..c64a69965b2 100644 --- a/homeassistant/components/upcloud/translations/ru.json +++ b/homeassistant/components/upcloud/translations/ru.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/vesync/translations/ru.json b/homeassistant/components/vesync/translations/ru.json index fd6132565f6..b3ac09685be 100644 --- a/homeassistant/components/vesync/translations/ru.json +++ b/homeassistant/components/vesync/translations/ru.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "error": { - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/vilfo/translations/ru.json b/homeassistant/components/vilfo/translations/ru.json index 8e61be90400..62ec2fe5dae 100644 --- a/homeassistant/components/vilfo/translations/ru.json +++ b/homeassistant/components/vilfo/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/wolflink/translations/ru.json b/homeassistant/components/wolflink/translations/ru.json index 841f7b26030..0b105ad922a 100644 --- a/homeassistant/components/wolflink/translations/ru.json +++ b/homeassistant/components/wolflink/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/xiaomi_miio/translations/ca.json b/homeassistant/components/xiaomi_miio/translations/ca.json index 3c666fe4ef1..170d14fc6dc 100644 --- a/homeassistant/components/xiaomi_miio/translations/ca.json +++ b/homeassistant/components/xiaomi_miio/translations/ca.json @@ -14,6 +14,7 @@ "device": { "data": { "host": "Adre\u00e7a IP", + "model": "Model del dispositiu (opcional)", "name": "Nom del dispositiu", "token": "Token d'API" }, diff --git a/homeassistant/components/xiaomi_miio/translations/en.json b/homeassistant/components/xiaomi_miio/translations/en.json index 37a8ce06eba..951ae546b56 100644 --- a/homeassistant/components/xiaomi_miio/translations/en.json +++ b/homeassistant/components/xiaomi_miio/translations/en.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "Failed to connect", + "no_device_selected": "No device selected, please select one device.", "unknown_device": "The device model is not known, not able to setup the device using config flow." }, "flow_title": "Xiaomi Miio: {name}", @@ -13,11 +14,28 @@ "device": { "data": { "host": "IP Address", - "token": "API Token", - "model": "Device model (Optional)" + "model": "Device model (Optional)", + "name": "Name of the device", + "token": "API Token" }, "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.", "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + }, + "gateway": { + "data": { + "host": "IP Address", + "name": "Name of the Gateway", + "token": "API Token" + }, + "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.", + "title": "Connect to a Xiaomi Gateway" + }, + "user": { + "data": { + "gateway": "Connect to a Xiaomi Gateway" + }, + "description": "Select to which device you want to connect.", + "title": "Xiaomi Miio" } } } diff --git a/homeassistant/components/xiaomi_miio/translations/et.json b/homeassistant/components/xiaomi_miio/translations/et.json index a5975ab05dc..a290f80ad31 100644 --- a/homeassistant/components/xiaomi_miio/translations/et.json +++ b/homeassistant/components/xiaomi_miio/translations/et.json @@ -14,6 +14,7 @@ "device": { "data": { "host": "IP-aadress", + "model": "Seadme mudel (valikuline)", "name": "Seadme nimi", "token": "API v\u00f5ti" }, diff --git a/homeassistant/components/zoneminder/translations/ru.json b/homeassistant/components/zoneminder/translations/ru.json index d599e767f64..bee720ee09a 100644 --- a/homeassistant/components/zoneminder/translations/ru.json +++ b/homeassistant/components/zoneminder/translations/ru.json @@ -4,7 +4,7 @@ "auth_fail": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 ZoneMinder.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "create_entry": { "default": "\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u0441\u0435\u0440\u0432\u0435\u0440 ZoneMinder." @@ -13,7 +13,7 @@ "auth_fail": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 ZoneMinder.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "flow_title": "ZoneMinder", "step": { From 1cecf229b94f282b39e046bb07900736179b8b2e Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 22 Feb 2021 20:34:47 -0500 Subject: [PATCH 650/796] Add zwave_js set_config_parameter WS API command (#46910) * add WS API command * handle error scenario better * fixes and remove duplicate catch * make elif statement more compact * fix conflict * switch to str(err) --- homeassistant/components/zwave_js/api.py | 60 ++++++++++- tests/components/zwave_js/test_api.py | 123 +++++++++++++++++++++++ 2 files changed, 182 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index a6cd8c50a76..71994f8b00b 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -7,11 +7,18 @@ from aiohttp import hdrs, web, web_exceptions import voluptuous as vol from zwave_js_server import dump from zwave_js_server.const import LogLevel +from zwave_js_server.exceptions import InvalidNewValue, NotFoundError, SetValueFailed from zwave_js_server.model.log_config import LogConfig +from zwave_js_server.util.node import async_set_config_parameter from homeassistant.components import websocket_api from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.websocket_api.connection import ActiveConnection +from homeassistant.components.websocket_api.const import ( + ERR_NOT_FOUND, + ERR_NOT_SUPPORTED, + ERR_UNKNOWN_ERROR, +) from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv @@ -26,6 +33,9 @@ ID = "id" ENTRY_ID = "entry_id" NODE_ID = "node_id" TYPE = "type" +PROPERTY = "property" +PROPERTY_KEY = "property_key" +VALUE = "value" # constants for log config commands CONFIG = "config" @@ -45,9 +55,10 @@ def async_register_api(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, websocket_stop_inclusion) websocket_api.async_register_command(hass, websocket_remove_node) websocket_api.async_register_command(hass, websocket_stop_exclusion) - websocket_api.async_register_command(hass, websocket_get_config_parameters) websocket_api.async_register_command(hass, websocket_update_log_config) websocket_api.async_register_command(hass, websocket_get_log_config) + websocket_api.async_register_command(hass, websocket_get_config_parameters) + websocket_api.async_register_command(hass, websocket_set_config_parameter) hass.http.register_view(DumpView) # type: ignore @@ -280,6 +291,53 @@ async def websocket_remove_node( ) +@websocket_api.require_admin # type:ignore +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/set_config_parameter", + vol.Required(ENTRY_ID): str, + vol.Required(NODE_ID): int, + vol.Required(PROPERTY): int, + vol.Optional(PROPERTY_KEY): int, + vol.Required(VALUE): int, + } +) +async def websocket_set_config_parameter( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: + """Set a config parameter value for a Z-Wave node.""" + entry_id = msg[ENTRY_ID] + node_id = msg[NODE_ID] + property_ = msg[PROPERTY] + property_key = msg.get(PROPERTY_KEY) + value = msg[VALUE] + client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + node = client.driver.controller.nodes[node_id] + try: + result = await async_set_config_parameter( + node, value, property_, property_key=property_key + ) + except (InvalidNewValue, NotFoundError, NotImplementedError, SetValueFailed) as err: + code = ERR_UNKNOWN_ERROR + if isinstance(err, NotFoundError): + code = ERR_NOT_FOUND + elif isinstance(err, (InvalidNewValue, NotImplementedError)): + code = ERR_NOT_SUPPORTED + + connection.send_error( + msg[ID], + code, + str(err), + ) + return + + connection.send_result( + msg[ID], + str(result), + ) + + @websocket_api.require_admin @websocket_api.websocket_command( { diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 29a86d63c83..403a73a6767 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -4,6 +4,7 @@ from unittest.mock import patch from zwave_js_server.const import LogLevel from zwave_js_server.event import Event +from zwave_js_server.exceptions import InvalidNewValue, NotFoundError, SetValueFailed from homeassistant.components.zwave_js.api import ( CONFIG, @@ -15,7 +16,10 @@ from homeassistant.components.zwave_js.api import ( LEVEL, LOG_TO_FILE, NODE_ID, + PROPERTY, + PROPERTY_KEY, TYPE, + VALUE, ) from homeassistant.components.zwave_js.const import DOMAIN from homeassistant.helpers.device_registry import async_get_registry @@ -186,6 +190,125 @@ async def test_remove_node( assert device is None +async def test_set_config_parameter( + hass, client, hass_ws_client, multisensor_6, integration +): + """Test the set_config_parameter service.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = {"success": True} + + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/set_config_parameter", + ENTRY_ID: entry.entry_id, + NODE_ID: 52, + PROPERTY: 102, + PROPERTY_KEY: 1, + VALUE: 1, + } + ) + + msg = await ws_client.receive_json() + assert msg["result"] + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 52 + assert args["valueId"] == { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyName": "Group 2: Send battery reports", + "propertyKey": 1, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": True, + "label": "Group 2: Send battery reports", + "description": "Include battery information in periodic reports to Group 2", + "isFromConfig": True, + }, + "value": 0, + } + assert args["value"] == 1 + + client.async_send_command.reset_mock() + + with patch( + "homeassistant.components.zwave_js.api.async_set_config_parameter", + ) as set_param_mock: + set_param_mock.side_effect = InvalidNewValue("test") + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/set_config_parameter", + ENTRY_ID: entry.entry_id, + NODE_ID: 52, + PROPERTY: 102, + PROPERTY_KEY: 1, + VALUE: 1, + } + ) + + msg = await ws_client.receive_json() + + assert len(client.async_send_command.call_args_list) == 0 + assert not msg["success"] + assert msg["error"]["code"] == "not_supported" + assert msg["error"]["message"] == "test" + + set_param_mock.side_effect = NotFoundError("test") + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/set_config_parameter", + ENTRY_ID: entry.entry_id, + NODE_ID: 52, + PROPERTY: 102, + PROPERTY_KEY: 1, + VALUE: 1, + } + ) + + msg = await ws_client.receive_json() + + assert len(client.async_send_command.call_args_list) == 0 + assert not msg["success"] + assert msg["error"]["code"] == "not_found" + assert msg["error"]["message"] == "test" + + set_param_mock.side_effect = SetValueFailed("test") + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/set_config_parameter", + ENTRY_ID: entry.entry_id, + NODE_ID: 52, + PROPERTY: 102, + PROPERTY_KEY: 1, + VALUE: 1, + } + ) + + msg = await ws_client.receive_json() + + assert len(client.async_send_command.call_args_list) == 0 + assert not msg["success"] + assert msg["error"]["code"] == "unknown_error" + assert msg["error"]["message"] == "test" + + async def test_dump_view(integration, hass_client): """Test the HTTP dump view.""" client = await hass_client() From f005c686301560411d854c1803097a70aeb63084 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 23 Feb 2021 10:37:19 +0800 Subject: [PATCH 651/796] Restore stream recorder functionality and add discontinuity support (#46772) * Add discontinuity support to stream recorder * Use same container options for both StreamOutputs * Fix pts adjuster * Remove redundant/incorrect duplicate hls segment check * Use same StreamBuffer across outputs * Remove keepalive check for recorder * Set output video timescale explicitly * Disable avoid_negative_ts --- homeassistant/components/stream/__init__.py | 2 +- homeassistant/components/stream/const.py | 4 + homeassistant/components/stream/core.py | 30 ++----- homeassistant/components/stream/hls.py | 34 ++------ homeassistant/components/stream/recorder.py | 88 +++++++++++---------- homeassistant/components/stream/worker.py | 64 ++++++++------- tests/components/stream/test_recorder.py | 15 +++- 7 files changed, 107 insertions(+), 130 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 0027152dbd6..2d115c6978d 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -136,7 +136,7 @@ class Stream: @callback def idle_callback(): - if not self.keepalive and fmt in self._outputs: + if (not self.keepalive or fmt == "recorder") and fmt in self._outputs: self.remove_provider(self._outputs[fmt]) self.check_idle() diff --git a/homeassistant/components/stream/const.py b/homeassistant/components/stream/const.py index 41df806d020..a2557286cf1 100644 --- a/homeassistant/components/stream/const.py +++ b/homeassistant/components/stream/const.py @@ -6,6 +6,10 @@ ATTR_STREAMS = "streams" OUTPUT_FORMATS = ["hls"] +SEGMENT_CONTAINER_FORMAT = "mp4" # format for segments +RECORDER_CONTAINER_FORMAT = "mp4" # format for recorder output +AUDIO_CODECS = {"aac", "mp3"} + FORMAT_CONTENT_TYPE = {"hls": "application/vnd.apple.mpegurl"} OUTPUT_IDLE_TIMEOUT = 300 # Idle timeout due to inactivity diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index eba6a069698..17d4516344a 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.event import async_call_later from homeassistant.util.decorator import Registry -from .const import ATTR_STREAMS, DOMAIN, MAX_SEGMENTS +from .const import ATTR_STREAMS, DOMAIN PROVIDERS = Registry() @@ -83,13 +83,15 @@ class IdleTimer: class StreamOutput: """Represents a stream output.""" - def __init__(self, hass: HomeAssistant, idle_timer: IdleTimer) -> None: + def __init__( + self, hass: HomeAssistant, idle_timer: IdleTimer, deque_maxlen: int = None + ) -> None: """Initialize a stream output.""" self._hass = hass self._idle_timer = idle_timer self._cursor = None self._event = asyncio.Event() - self._segments = deque(maxlen=MAX_SEGMENTS) + self._segments = deque(maxlen=deque_maxlen) @property def name(self) -> str: @@ -101,26 +103,6 @@ class StreamOutput: """Return True if the output is idle.""" return self._idle_timer.idle - @property - def format(self) -> str: - """Return container format.""" - return None - - @property - def audio_codecs(self) -> str: - """Return desired audio codecs.""" - return None - - @property - def video_codecs(self) -> tuple: - """Return desired video codecs.""" - return None - - @property - def container_options(self) -> Callable[[int], dict]: - """Return Callable which takes a sequence number and returns container options.""" - return None - @property def segments(self) -> List[int]: """Return current sequence from segments.""" @@ -177,7 +159,7 @@ class StreamOutput: """Handle cleanup.""" self._event.set() self._idle_timer.clear() - self._segments = deque(maxlen=MAX_SEGMENTS) + self._segments = deque(maxlen=self._segments.maxlen) class StreamView(HomeAssistantView): diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index 28d6a300ae7..b2600977971 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -1,13 +1,12 @@ """Provide functionality to stream HLS.""" import io -from typing import Callable from aiohttp import web from homeassistant.core import callback -from .const import FORMAT_CONTENT_TYPE, NUM_PLAYLIST_SEGMENTS -from .core import PROVIDERS, StreamOutput, StreamView +from .const import FORMAT_CONTENT_TYPE, MAX_SEGMENTS, NUM_PLAYLIST_SEGMENTS +from .core import PROVIDERS, HomeAssistant, IdleTimer, StreamOutput, StreamView from .fmp4utils import get_codec_string, get_init, get_m4s @@ -159,32 +158,11 @@ class HlsSegmentView(StreamView): class HlsStreamOutput(StreamOutput): """Represents HLS Output formats.""" + def __init__(self, hass: HomeAssistant, idle_timer: IdleTimer) -> None: + """Initialize recorder output.""" + super().__init__(hass, idle_timer, deque_maxlen=MAX_SEGMENTS) + @property def name(self) -> str: """Return provider name.""" return "hls" - - @property - def format(self) -> str: - """Return container format.""" - return "mp4" - - @property - def audio_codecs(self) -> str: - """Return desired audio codecs.""" - return {"aac", "mp3"} - - @property - def video_codecs(self) -> tuple: - """Return desired video codecs.""" - return {"hevc", "h264"} - - @property - def container_options(self) -> Callable[[int], dict]: - """Return Callable which takes a sequence number and returns container options.""" - return lambda sequence: { - # Removed skip_sidx - see https://github.com/home-assistant/core/pull/39970 - "movflags": "frag_custom+empty_moov+default_base_moof+frag_discont", - "avoid_negative_ts": "make_non_negative", - "fragment_index": str(sequence), - } diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index 0b77d0ba630..0344e220647 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -2,12 +2,13 @@ import logging import os import threading -from typing import List +from typing import Deque, List import av from homeassistant.core import HomeAssistant, callback +from .const import RECORDER_CONTAINER_FORMAT, SEGMENT_CONTAINER_FORMAT from .core import PROVIDERS, IdleTimer, Segment, StreamOutput _LOGGER = logging.getLogger(__name__) @@ -18,28 +19,20 @@ def async_setup_recorder(hass): """Only here so Provider Registry works.""" -def recorder_save_worker(file_out: str, segments: List[Segment], container_format: str): +def recorder_save_worker(file_out: str, segments: Deque[Segment]): """Handle saving stream.""" if not os.path.exists(os.path.dirname(file_out)): os.makedirs(os.path.dirname(file_out), exist_ok=True) - first_pts = {"video": None, "audio": None} - output = av.open(file_out, "w", format=container_format) + pts_adjuster = {"video": None, "audio": None} + output = None output_v = None output_a = None - # Get first_pts values from first segment - if len(segments) > 0: - segment = segments[0] - source = av.open(segment.segment, "r", format=container_format) - source_v = source.streams.video[0] - first_pts["video"] = source_v.start_time - if len(source.streams.audio) > 0: - source_a = source.streams.audio[0] - first_pts["audio"] = int( - source_v.start_time * source_v.time_base / source_a.time_base - ) - source.close() + last_stream_id = None + # The running duration of processed segments. Note that this is in av.time_base + # units which seem to be defined inversely to how stream time_bases are defined + running_duration = 0 last_sequence = float("-inf") for segment in segments: @@ -50,26 +43,54 @@ def recorder_save_worker(file_out: str, segments: List[Segment], container_forma last_sequence = segment.sequence # Open segment - source = av.open(segment.segment, "r", format=container_format) + source = av.open(segment.segment, "r", format=SEGMENT_CONTAINER_FORMAT) source_v = source.streams.video[0] - # Add output streams + source_a = source.streams.audio[0] if len(source.streams.audio) > 0 else None + + # Create output on first segment + if not output: + output = av.open( + file_out, + "w", + format=RECORDER_CONTAINER_FORMAT, + container_options={ + "video_track_timescale": str(int(1 / source_v.time_base)) + }, + ) + + # Add output streams if necessary if not output_v: output_v = output.add_stream(template=source_v) context = output_v.codec_context context.flags |= "GLOBAL_HEADER" - if not output_a and len(source.streams.audio) > 0: - source_a = source.streams.audio[0] + if source_a and not output_a: output_a = output.add_stream(template=source_a) + # Recalculate pts adjustments on first segment and on any discontinuity + # We are assuming time base is the same across all discontinuities + if last_stream_id != segment.stream_id: + last_stream_id = segment.stream_id + pts_adjuster["video"] = int( + (running_duration - source.start_time) + / (av.time_base * source_v.time_base) + ) + if source_a: + pts_adjuster["audio"] = int( + (running_duration - source.start_time) + / (av.time_base * source_a.time_base) + ) + # Remux video for packet in source.demux(): if packet.dts is None: continue - packet.pts -= first_pts[packet.stream.type] - packet.dts -= first_pts[packet.stream.type] + packet.pts += pts_adjuster[packet.stream.type] + packet.dts += pts_adjuster[packet.stream.type] packet.stream = output_v if packet.stream.type == "video" else output_a output.mux(packet) + running_duration += source.duration - source.start_time + source.close() output.close() @@ -83,33 +104,15 @@ class RecorderOutput(StreamOutput): """Initialize recorder output.""" super().__init__(hass, idle_timer) self.video_path = None - self._segments = [] @property def name(self) -> str: """Return provider name.""" return "recorder" - @property - def format(self) -> str: - """Return container format.""" - return "mp4" - - @property - def audio_codecs(self) -> str: - """Return desired audio codec.""" - return {"aac", "mp3"} - - @property - def video_codecs(self) -> tuple: - """Return desired video codecs.""" - return {"hevc", "h264"} - def prepend(self, segments: List[Segment]) -> None: """Prepend segments to existing list.""" - own_segments = self.segments - segments = [s for s in segments if s.sequence not in own_segments] - self._segments = segments + self._segments + self._segments.extendleft(reversed(segments)) def cleanup(self): """Write recording and clean up.""" @@ -117,9 +120,8 @@ class RecorderOutput(StreamOutput): thread = threading.Thread( name="recorder_save_worker", target=recorder_save_worker, - args=(self.video_path, self._segments, self.format), + args=(self.video_path, self._segments), ) thread.start() super().cleanup() - self._segments = [] diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 61d4f5db17a..d5760877c43 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -6,10 +6,12 @@ import logging import av from .const import ( + AUDIO_CODECS, MAX_MISSING_DTS, MAX_TIMESTAMP_GAP, MIN_SEGMENT_DURATION, PACKETS_TO_WAIT_FOR_AUDIO, + SEGMENT_CONTAINER_FORMAT, STREAM_TIMEOUT, ) from .core import Segment, StreamBuffer @@ -17,19 +19,20 @@ from .core import Segment, StreamBuffer _LOGGER = logging.getLogger(__name__) -def create_stream_buffer(stream_output, video_stream, audio_stream, sequence): +def create_stream_buffer(video_stream, audio_stream, sequence): """Create a new StreamBuffer.""" segment = io.BytesIO() - container_options = ( - stream_output.container_options(sequence) - if stream_output.container_options - else {} - ) + container_options = { + # Removed skip_sidx - see https://github.com/home-assistant/core/pull/39970 + "movflags": "frag_custom+empty_moov+default_base_moof+frag_discont", + "avoid_negative_ts": "disabled", + "fragment_index": str(sequence), + } output = av.open( segment, mode="w", - format=stream_output.format, + format=SEGMENT_CONTAINER_FORMAT, container_options={ "video_track_timescale": str(int(1 / video_stream.time_base)), **container_options, @@ -38,7 +41,7 @@ def create_stream_buffer(stream_output, video_stream, audio_stream, sequence): vstream = output.add_stream(template=video_stream) # Check if audio is requested astream = None - if audio_stream and audio_stream.name in stream_output.audio_codecs: + if audio_stream and audio_stream.name in AUDIO_CODECS: astream = output.add_stream(template=audio_stream) return StreamBuffer(segment, output, vstream, astream) @@ -52,10 +55,11 @@ class SegmentBuffer: self._video_stream = None self._audio_stream = None self._outputs_callback = outputs_callback - # tuple of StreamOutput, StreamBuffer + # Each element is a StreamOutput self._outputs = [] self._sequence = 0 self._segment_start_pts = None + self._stream_buffer = None def set_streams(self, video_stream, audio_stream): """Initialize output buffer with streams from container.""" @@ -70,14 +74,10 @@ class SegmentBuffer: # Fetch the latest StreamOutputs, which may have changed since the # worker started. - self._outputs = [] - for stream_output in self._outputs_callback().values(): - if self._video_stream.name not in stream_output.video_codecs: - continue - buffer = create_stream_buffer( - stream_output, self._video_stream, self._audio_stream, self._sequence - ) - self._outputs.append((buffer, stream_output)) + self._outputs = self._outputs_callback().values() + self._stream_buffer = create_stream_buffer( + self._video_stream, self._audio_stream, self._sequence + ) def mux_packet(self, packet): """Mux a packet to the appropriate StreamBuffers.""" @@ -93,22 +93,21 @@ class SegmentBuffer: self.reset(packet.pts) # Mux the packet - for (buffer, _) in self._outputs: - if packet.stream == self._video_stream: - packet.stream = buffer.vstream - elif packet.stream == self._audio_stream: - packet.stream = buffer.astream - else: - continue - buffer.output.mux(packet) + if packet.stream == self._video_stream: + packet.stream = self._stream_buffer.vstream + self._stream_buffer.output.mux(packet) + elif packet.stream == self._audio_stream: + packet.stream = self._stream_buffer.astream + self._stream_buffer.output.mux(packet) def flush(self, duration): """Create a segment from the buffered packets and write to output.""" - for (buffer, stream_output) in self._outputs: - buffer.output.close() - stream_output.put( - Segment(self._sequence, buffer.segment, duration, self._stream_id) - ) + self._stream_buffer.output.close() + segment = Segment( + self._sequence, self._stream_buffer.segment, duration, self._stream_id + ) + for stream_output in self._outputs: + stream_output.put(segment) def discontinuity(self): """Mark the stream as having been restarted.""" @@ -118,9 +117,8 @@ class SegmentBuffer: self._stream_id += 1 def close(self): - """Close all StreamBuffers.""" - for (buffer, _) in self._outputs: - buffer.output.close() + """Close stream buffer.""" + self._stream_buffer.output.close() def stream_worker(source, options, segment_buffer, quit_event): diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index 9d418c360b1..199020097bd 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -174,7 +174,20 @@ async def test_recorder_save(tmpdir): filename = f"{tmpdir}/test.mp4" # Run - recorder_save_worker(filename, [Segment(1, source, 4)], "mp4") + recorder_save_worker(filename, [Segment(1, source, 4)]) + + # Assert + assert os.path.exists(filename) + + +async def test_recorder_discontinuity(tmpdir): + """Test recorder save across a discontinuity.""" + # Setup + source = generate_h264_video() + filename = f"{tmpdir}/test.mp4" + + # Run + recorder_save_worker(filename, [Segment(1, source, 4, 0), Segment(2, source, 4, 1)]) # Assert assert os.path.exists(filename) From fb2a100f5e366dc154ccf1285085a684972b5047 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Feb 2021 17:27:09 -1000 Subject: [PATCH 652/796] Add suggested area to tado (#46932) I realized tado shows what we internally call `zone_name` as the room name in the app. --- homeassistant/components/tado/entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tado/entity.py b/homeassistant/components/tado/entity.py index e9fefe2848b..34473a45c98 100644 --- a/homeassistant/components/tado/entity.py +++ b/homeassistant/components/tado/entity.py @@ -50,6 +50,7 @@ class TadoZoneEntity(Entity): "name": self.zone_name, "manufacturer": DEFAULT_NAME, "model": TADO_ZONE, + "suggested_area": self.zone_name, } @property From bd87047ff25600bea1309e0bbea1e7411afaf679 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Feb 2021 17:27:21 -1000 Subject: [PATCH 653/796] Update tasmota to use new fan entity model (#45877) --- homeassistant/components/tasmota/fan.py | 60 ++++++++++++++----------- tests/components/tasmota/test_fan.py | 37 ++++++++++++++- 2 files changed, 69 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/tasmota/fan.py b/homeassistant/components/tasmota/fan.py index 1d7aa9f38cb..77b4532c001 100644 --- a/homeassistant/components/tasmota/fan.py +++ b/homeassistant/components/tasmota/fan.py @@ -1,24 +1,27 @@ """Support for Tasmota fans.""" +from typing import Optional + from hatasmota import const as tasmota_const from homeassistant.components import fan from homeassistant.components.fan import FanEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.percentage import ( + ordered_list_item_to_percentage, + percentage_to_ordered_list_item, +) from .const import DATA_REMOVE_DISCOVER_COMPONENT from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate -HA_TO_TASMOTA_SPEED_MAP = { - fan.SPEED_OFF: tasmota_const.FAN_SPEED_OFF, - fan.SPEED_LOW: tasmota_const.FAN_SPEED_LOW, - fan.SPEED_MEDIUM: tasmota_const.FAN_SPEED_MEDIUM, - fan.SPEED_HIGH: tasmota_const.FAN_SPEED_HIGH, -} - -TASMOTA_TO_HA_SPEED_MAP = {v: k for k, v in HA_TO_TASMOTA_SPEED_MAP.items()} +ORDERED_NAMED_FAN_SPEEDS = [ + tasmota_const.FAN_SPEED_LOW, + tasmota_const.FAN_SPEED_MEDIUM, + tasmota_const.FAN_SPEED_HIGH, +] # off is not included async def async_setup_entry(hass, config_entry, async_add_entities): @@ -56,42 +59,45 @@ class TasmotaFan( ) @property - def speed(self): - """Return the current speed.""" - return TASMOTA_TO_HA_SPEED_MAP.get(self._state) + def speed_count(self) -> Optional[int]: + """Return the number of speeds the fan supports.""" + return len(ORDERED_NAMED_FAN_SPEEDS) @property - def speed_list(self): - """Get the list of available speeds.""" - return list(HA_TO_TASMOTA_SPEED_MAP) + def percentage(self): + """Return the current speed percentage.""" + if self._state is None: + return None + if self._state == 0: + return 0 + return ordered_list_item_to_percentage(ORDERED_NAMED_FAN_SPEEDS, self._state) @property def supported_features(self): """Flag supported features.""" return fan.SUPPORT_SET_SPEED - async def async_set_speed(self, speed): + async def async_set_percentage(self, percentage): """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: + if percentage == 0: await self.async_turn_off() else: - self._tasmota_entity.set_speed(HA_TO_TASMOTA_SPEED_MAP[speed]) + tasmota_speed = percentage_to_ordered_list_item( + ORDERED_NAMED_FAN_SPEEDS, percentage + ) + self._tasmota_entity.set_speed(tasmota_speed) - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # async def async_turn_on( self, speed=None, percentage=None, preset_mode=None, **kwargs ): """Turn the fan on.""" # Tasmota does not support turning a fan on with implicit speed - await self.async_set_speed(speed or fan.SPEED_MEDIUM) + await self.async_set_percentage( + percentage + or ordered_list_item_to_percentage( + ORDERED_NAMED_FAN_SPEEDS, tasmota_const.FAN_SPEED_MEDIUM + ) + ) async def async_turn_off(self, **kwargs): """Turn the fan off.""" diff --git a/tests/components/tasmota/test_fan.py b/tests/components/tasmota/test_fan.py index 4035c877bb8..a64c5e9c5e4 100644 --- a/tests/components/tasmota/test_fan.py +++ b/tests/components/tasmota/test_fan.py @@ -52,6 +52,7 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): state = hass.states.get("fan.tasmota") assert state.state == STATE_OFF assert state.attributes["speed"] is None + assert state.attributes["percentage"] is None assert state.attributes["speed_list"] == ["off", "low", "medium", "high"] assert state.attributes["supported_features"] == fan.SUPPORT_SET_SPEED assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -60,31 +61,37 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): state = hass.states.get("fan.tasmota") assert state.state == STATE_ON assert state.attributes["speed"] == "low" + assert state.attributes["percentage"] == 33 async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"FanSpeed":2}') state = hass.states.get("fan.tasmota") assert state.state == STATE_ON assert state.attributes["speed"] == "medium" + assert state.attributes["percentage"] == 66 async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"FanSpeed":3}') state = hass.states.get("fan.tasmota") assert state.state == STATE_ON assert state.attributes["speed"] == "high" + assert state.attributes["percentage"] == 100 async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"FanSpeed":0}') state = hass.states.get("fan.tasmota") assert state.state == STATE_OFF assert state.attributes["speed"] == "off" + assert state.attributes["percentage"] == 0 async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"FanSpeed":1}') state = hass.states.get("fan.tasmota") assert state.state == STATE_ON assert state.attributes["speed"] == "low" + assert state.attributes["percentage"] == 33 async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"FanSpeed":0}') state = hass.states.get("fan.tasmota") assert state.state == STATE_OFF assert state.attributes["speed"] == "off" + assert state.attributes["percentage"] == 0 async def test_sending_mqtt_commands(hass, mqtt_mock, setup_tasmota): @@ -151,6 +158,34 @@ async def test_sending_mqtt_commands(hass, mqtt_mock, setup_tasmota): mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/FanSpeed", "3", 0, False ) + mqtt_mock.async_publish.reset_mock() + + # Set speed percentage and verify MQTT message is sent + await common.async_set_percentage(hass, "fan.tasmota", 0) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/FanSpeed", "0", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Set speed percentage and verify MQTT message is sent + await common.async_set_percentage(hass, "fan.tasmota", 15) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/FanSpeed", "1", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Set speed percentage and verify MQTT message is sent + await common.async_set_percentage(hass, "fan.tasmota", 50) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/FanSpeed", "2", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Set speed percentage and verify MQTT message is sent + await common.async_set_percentage(hass, "fan.tasmota", 90) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/FanSpeed", "3", 0, False + ) async def test_invalid_fan_speed(hass, mqtt_mock, setup_tasmota): @@ -176,7 +211,7 @@ async def test_invalid_fan_speed(hass, mqtt_mock, setup_tasmota): # 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) + assert "no_such_speed" in str(excinfo.value) mqtt_mock.async_publish.assert_not_called() From f33618d33d1337e38f58d53c5ed527b58856e9d9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Feb 2021 17:27:33 -1000 Subject: [PATCH 654/796] Add suggested area support to isy994 (#46927) --- homeassistant/components/isy994/entity.py | 2 ++ homeassistant/components/isy994/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index 95bd43facde..a484b56b145 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -100,6 +100,8 @@ class ISYEntity(Entity): f"ProductID:{node.zwave_props.product_id}" ) # Note: sw_version is not exposed by the ISY for the individual devices. + if hasattr(node, "folder") and node.folder is not None: + device_info["suggested_area"] = node.folder return device_info diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 9e22b3533d7..3769cc328db 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -2,7 +2,7 @@ "domain": "isy994", "name": "Universal Devices ISY994", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["pyisy==2.1.0"], + "requirements": ["pyisy==2.1.1"], "codeowners": ["@bdraco", "@shbatm"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 081881b2505..7966859d341 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1465,7 +1465,7 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==2.1.0 +pyisy==2.1.1 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 376eef402e4..489e365fad8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -770,7 +770,7 @@ pyipp==0.11.0 pyiqvia==0.3.1 # homeassistant.components.isy994 -pyisy==2.1.0 +pyisy==2.1.1 # homeassistant.components.kira pykira==0.1.1 From 08889a9819295bb711b48d4e8721ab66d393dd5f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Feb 2021 20:14:39 -1000 Subject: [PATCH 655/796] Fix smaty fan typing (#46941) --- homeassistant/components/smarty/fan.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smarty/fan.py b/homeassistant/components/smarty/fan.py index 20376e1d44e..481d2e56d3d 100644 --- a/homeassistant/components/smarty/fan.py +++ b/homeassistant/components/smarty/fan.py @@ -2,7 +2,6 @@ import logging import math -from typing import Optional from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity from homeassistant.core import callback @@ -65,12 +64,12 @@ class SmartyFan(FanEntity): return bool(self._smarty_fan_speed) @property - def speed_count(self) -> Optional[int]: + def speed_count(self) -> int: """Return the number of speeds the fan supports.""" return int_states_in_range(SPEED_RANGE) @property - def percentage(self) -> str: + def percentage(self) -> int: """Return speed percentage of the fan.""" if self._smarty_fan_speed == 0: return 0 From 4fdb617e22de89dcb385b7dccb918e6572f313e2 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Tue, 23 Feb 2021 03:56:44 -0500 Subject: [PATCH 656/796] Clean up constants (#46924) * Clean up constants * fix imports --- homeassistant/components/abode/__init__.py | 2 +- homeassistant/components/accuweather/const.py | 2 +- homeassistant/components/airnow/sensor.py | 2 +- homeassistant/components/awair/const.py | 2 +- homeassistant/components/brother/const.py | 3 +-- homeassistant/components/comfoconnect/sensor.py | 2 +- .../components/dlib_face_detect/image_processing.py | 3 +-- .../components/dlib_face_identify/image_processing.py | 2 +- homeassistant/components/elgato/const.py | 1 - homeassistant/components/elgato/light.py | 3 +-- .../components/fritzbox_callmonitor/config_flow.py | 4 ++-- homeassistant/components/fritzbox_callmonitor/const.py | 1 - homeassistant/components/gios/const.py | 1 - homeassistant/components/habitica/__init__.py | 9 +++++++-- homeassistant/components/habitica/const.py | 3 +-- homeassistant/components/hassio/__init__.py | 2 +- homeassistant/components/hassio/addon_panel.py | 4 ++-- homeassistant/components/hassio/const.py | 2 -- homeassistant/components/hassio/discovery.py | 4 ++-- homeassistant/components/hive/__init__.py | 1 - homeassistant/components/homematicip_cloud/services.py | 3 +-- homeassistant/components/html5/notify.py | 2 +- homeassistant/components/humidifier/__init__.py | 2 +- homeassistant/components/humidifier/const.py | 2 +- .../components/fritzbox_callmonitor/test_config_flow.py | 6 +++--- 25 files changed, 31 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 529e3ff7189..20c0624742c 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -13,6 +13,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DATE, + ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_TIME, CONF_PASSWORD, @@ -32,7 +33,6 @@ SERVICE_SETTINGS = "change_setting" SERVICE_CAPTURE_IMAGE = "capture_image" SERVICE_TRIGGER_AUTOMATION = "trigger_automation" -ATTR_DEVICE_ID = "device_id" ATTR_DEVICE_NAME = "device_name" ATTR_DEVICE_TYPE = "device_type" ATTR_EVENT_CODE = "event_code" diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index e8dbe921d77..60fdd48c8f4 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -17,6 +17,7 @@ from homeassistant.components.weather import ( ) from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_ICON, CONCENTRATION_PARTS_PER_CUBIC_METER, DEVICE_CLASS_TEMPERATURE, LENGTH_FEET, @@ -33,7 +34,6 @@ from homeassistant.const import ( ) ATTRIBUTION = "Data provided by AccuWeather" -ATTR_ICON = "icon" ATTR_FORECAST = CONF_FORECAST = "forecast" ATTR_LABEL = "label" ATTR_UNIT_IMPERIAL = "Imperial" diff --git a/homeassistant/components/airnow/sensor.py b/homeassistant/components/airnow/sensor.py index fed6def2b36..4488098701f 100644 --- a/homeassistant/components/airnow/sensor.py +++ b/homeassistant/components/airnow/sensor.py @@ -2,6 +2,7 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, + ATTR_ICON, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, ) @@ -20,7 +21,6 @@ from .const import ( ATTRIBUTION = "Data provided by AirNow" -ATTR_ICON = "icon" ATTR_LABEL = "label" ATTR_UNIT = "unit" diff --git a/homeassistant/components/awair/const.py b/homeassistant/components/awair/const.py index b262fdec572..44490b8401f 100644 --- a/homeassistant/components/awair/const.py +++ b/homeassistant/components/awair/const.py @@ -8,6 +8,7 @@ from python_awair.devices import AwairDevice from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_ICON, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, @@ -33,7 +34,6 @@ API_VOC = "volatile_organic_compounds" ATTRIBUTION = "Awair air quality sensor" -ATTR_ICON = "icon" ATTR_LABEL = "label" ATTR_UNIT = "unit" ATTR_UNIQUE_ID = "unique_id" diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index 5ae459c79aa..07843b0f3d0 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -1,5 +1,5 @@ """Constants for Brother integration.""" -from homeassistant.const import PERCENTAGE +from homeassistant.const import ATTR_ICON, PERCENTAGE ATTR_BELT_UNIT_REMAINING_LIFE = "belt_unit_remaining_life" ATTR_BLACK_DRUM_COUNTER = "black_drum_counter" @@ -20,7 +20,6 @@ ATTR_DRUM_REMAINING_PAGES = "drum_remaining_pages" ATTR_DUPLEX_COUNTER = "duplex_unit_pages_counter" ATTR_ENABLED = "enabled" ATTR_FUSER_REMAINING_LIFE = "fuser_remaining_life" -ATTR_ICON = "icon" ATTR_LABEL = "label" ATTR_LASER_REMAINING_LIFE = "laser_remaining_life" ATTR_MAGENTA_DRUM_COUNTER = "magenta_drum_counter" diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index 660228b0b8d..87fa8f4a1a6 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -29,6 +29,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_ICON, ATTR_ID, CONF_RESOURCES, DEVICE_CLASS_ENERGY, @@ -72,7 +73,6 @@ ATTR_SUPPLY_TEMPERATURE = "supply_temperature" _LOGGER = logging.getLogger(__name__) -ATTR_ICON = "icon" ATTR_LABEL = "label" ATTR_MULTIPLIER = "multiplier" ATTR_UNIT = "unit" diff --git a/homeassistant/components/dlib_face_detect/image_processing.py b/homeassistant/components/dlib_face_detect/image_processing.py index c2bec855b9b..2a5e7662d45 100644 --- a/homeassistant/components/dlib_face_detect/image_processing.py +++ b/homeassistant/components/dlib_face_detect/image_processing.py @@ -9,6 +9,7 @@ from homeassistant.components.image_processing import ( CONF_SOURCE, ImageProcessingFaceEntity, ) +from homeassistant.const import ATTR_LOCATION from homeassistant.core import split_entity_id # pylint: disable=unused-import @@ -16,8 +17,6 @@ from homeassistant.components.image_processing import ( # noqa: F401, isort:ski PLATFORM_SCHEMA, ) -ATTR_LOCATION = "location" - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dlib Face detection platform.""" diff --git a/homeassistant/components/dlib_face_identify/image_processing.py b/homeassistant/components/dlib_face_identify/image_processing.py index 32c2aa5868c..f9db607c298 100644 --- a/homeassistant/components/dlib_face_identify/image_processing.py +++ b/homeassistant/components/dlib_face_identify/image_processing.py @@ -14,12 +14,12 @@ from homeassistant.components.image_processing import ( PLATFORM_SCHEMA, ImageProcessingFaceEntity, ) +from homeassistant.const import ATTR_NAME from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -ATTR_NAME = "name" CONF_FACES = "faces" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/elgato/const.py b/homeassistant/components/elgato/const.py index 2b6caa37a8f..b2535ce0e4f 100644 --- a/homeassistant/components/elgato/const.py +++ b/homeassistant/components/elgato/const.py @@ -12,6 +12,5 @@ ATTR_MANUFACTURER = "manufacturer" ATTR_MODEL = "model" ATTR_ON = "on" ATTR_SOFTWARE_VERSION = "sw_version" -ATTR_TEMPERATURE = "temperature" CONF_SERIAL_NUMBER = "serial_number" diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index eea80e60b15..0648a4817bc 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -15,7 +15,7 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_NAME +from homeassistant.const import ATTR_NAME, ATTR_TEMPERATURE from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType @@ -25,7 +25,6 @@ from .const import ( ATTR_MODEL, ATTR_ON, ATTR_SOFTWARE_VERSION, - ATTR_TEMPERATURE, DATA_ELGATO_CLIENT, DOMAIN, ) diff --git a/homeassistant/components/fritzbox_callmonitor/config_flow.py b/homeassistant/components/fritzbox_callmonitor/config_flow.py index a08450e20a1..01a43f7c7ef 100644 --- a/homeassistant/components/fritzbox_callmonitor/config_flow.py +++ b/homeassistant/components/fritzbox_callmonitor/config_flow.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( + ATTR_NAME, CONF_HOST, CONF_NAME, CONF_PASSWORD, @@ -27,7 +28,6 @@ from .const import ( DEFAULT_USERNAME, DOMAIN, FRITZ_ACTION_GET_INFO, - FRITZ_ATTR_NAME, FRITZ_ATTR_SERIAL_NUMBER, FRITZ_SERVICE_DEVICE_INFO, SERIAL_NUMBER, @@ -119,7 +119,7 @@ class FritzBoxCallMonitorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): phonebook_info = await self.hass.async_add_executor_job( self._fritzbox_phonebook.fph.phonebook_info, phonebook_id ) - return phonebook_info[FRITZ_ATTR_NAME] + return phonebook_info[ATTR_NAME] async def _get_list_of_phonebook_names(self): """Return list of names for all available phonebooks.""" diff --git a/homeassistant/components/fritzbox_callmonitor/const.py b/homeassistant/components/fritzbox_callmonitor/const.py index a71f14401b3..6f0c87f5273 100644 --- a/homeassistant/components/fritzbox_callmonitor/const.py +++ b/homeassistant/components/fritzbox_callmonitor/const.py @@ -15,7 +15,6 @@ ICON_PHONE = "mdi:phone" ATTR_PREFIXES = "prefixes" FRITZ_ACTION_GET_INFO = "GetInfo" -FRITZ_ATTR_NAME = "name" FRITZ_ATTR_SERIAL_NUMBER = "NewSerialNumber" FRITZ_SERVICE_DEVICE_INFO = "DeviceInfo" diff --git a/homeassistant/components/gios/const.py b/homeassistant/components/gios/const.py index 117eada036b..ab354e319a8 100644 --- a/homeassistant/components/gios/const.py +++ b/homeassistant/components/gios/const.py @@ -1,7 +1,6 @@ """Constants for GIOS integration.""" from datetime import timedelta -ATTR_NAME = "name" ATTR_STATION = "station" CONF_STATION_ID = "station_id" DEFAULT_NAME = "GIOŚ" diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index ca3837ef8ca..64680a56bb3 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -7,14 +7,19 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_SENSORS, CONF_URL +from homeassistant.const import ( + ATTR_NAME, + CONF_API_KEY, + CONF_NAME, + CONF_SENSORS, + CONF_URL, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( ATTR_ARGS, - ATTR_NAME, ATTR_PATH, CONF_API_USER, DEFAULT_URL, diff --git a/homeassistant/components/habitica/const.py b/homeassistant/components/habitica/const.py index 438bcec9d94..02a46334c7a 100644 --- a/homeassistant/components/habitica/const.py +++ b/homeassistant/components/habitica/const.py @@ -1,6 +1,6 @@ """Constants for the habitica integration.""" -from homeassistant.const import CONF_NAME, CONF_PATH +from homeassistant.const import CONF_PATH CONF_API_USER = "api_user" @@ -8,7 +8,6 @@ DEFAULT_URL = "https://habitica.com" DOMAIN = "habitica" SERVICE_API_CALL = "api_call" -ATTR_NAME = CONF_NAME ATTR_PATH = CONF_PATH ATTR_ARGS = "args" EVENT_API_CALL_SUCCESS = f"{DOMAIN}_{SERVICE_API_CALL}_success" diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 4f6f0ed8348..fdeb10bcafe 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -10,6 +10,7 @@ from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components.homeassistant import SERVICE_CHECK_CONFIG import homeassistant.config as conf_util from homeassistant.const import ( + ATTR_NAME, EVENT_CORE_CONFIG_UPDATE, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, @@ -30,7 +31,6 @@ from .const import ( ATTR_FOLDERS, ATTR_HOMEASSISTANT, ATTR_INPUT, - ATTR_NAME, ATTR_PASSWORD, ATTR_SNAPSHOT, DOMAIN, diff --git a/homeassistant/components/hassio/addon_panel.py b/homeassistant/components/hassio/addon_panel.py index 9e44b961a1c..a48c8b4d05b 100644 --- a/homeassistant/components/hassio/addon_panel.py +++ b/homeassistant/components/hassio/addon_panel.py @@ -5,10 +5,10 @@ import logging from aiohttp import web from homeassistant.components.http import HomeAssistantView -from homeassistant.const import HTTP_BAD_REQUEST +from homeassistant.const import ATTR_ICON, HTTP_BAD_REQUEST from homeassistant.helpers.typing import HomeAssistantType -from .const import ATTR_ADMIN, ATTR_ENABLE, ATTR_ICON, ATTR_PANELS, ATTR_TITLE +from .const import ATTR_ADMIN, ATTR_ENABLE, ATTR_PANELS, ATTR_TITLE from .handler import HassioAPIError _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 0cb1649dfc5..a3e4451312a 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -11,9 +11,7 @@ ATTR_DISCOVERY = "discovery" ATTR_ENABLE = "enable" ATTR_FOLDERS = "folders" ATTR_HOMEASSISTANT = "homeassistant" -ATTR_ICON = "icon" ATTR_INPUT = "input" -ATTR_NAME = "name" ATTR_PANELS = "panels" ATTR_PASSWORD = "password" ATTR_SNAPSHOT = "snapshot" diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index cda05eccbec..c682e34c301 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -6,10 +6,10 @@ from aiohttp import web from aiohttp.web_exceptions import HTTPServiceUnavailable from homeassistant.components.http import HomeAssistantView -from homeassistant.const import ATTR_SERVICE, EVENT_HOMEASSISTANT_START +from homeassistant.const import ATTR_NAME, ATTR_SERVICE, EVENT_HOMEASSISTANT_START from homeassistant.core import callback -from .const import ATTR_ADDON, ATTR_CONFIG, ATTR_DISCOVERY, ATTR_NAME, ATTR_UUID +from .const import ATTR_ADDON, ATTR_CONFIG, ATTR_DISCOVERY, ATTR_UUID from .handler import HassioAPIError _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 6245db5ea7e..331ab37224f 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -23,7 +23,6 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) ATTR_AVAILABLE = "available" -ATTR_MODE = "mode" DOMAIN = "hive" DATA_HIVE = "data_hive" SERVICES = ["Heating", "HotWater", "TRV"] diff --git a/homeassistant/components/homematicip_cloud/services.py b/homeassistant/components/homematicip_cloud/services.py index d8535edda50..7c92ac5e721 100644 --- a/homeassistant/components/homematicip_cloud/services.py +++ b/homeassistant/components/homematicip_cloud/services.py @@ -9,7 +9,7 @@ from homematicip.aio.home import AsyncHome from homematicip.base.helpers import handle_config import voluptuous as vol -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import comp_entity_ids from homeassistant.helpers.service import ( @@ -29,7 +29,6 @@ ATTR_CONFIG_OUTPUT_FILE_PREFIX = "config_output_file_prefix" ATTR_CONFIG_OUTPUT_PATH = "config_output_path" ATTR_DURATION = "duration" ATTR_ENDTIME = "endtime" -ATTR_TEMPERATURE = "temperature" DEFAULT_CONFIG_FILE_PREFIX = "hmip-config" diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index c07cddb7a9c..33dd8118ee4 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -26,6 +26,7 @@ from homeassistant.components.notify import ( BaseNotificationService, ) from homeassistant.const import ( + ATTR_NAME, HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR, HTTP_UNAUTHORIZED, @@ -73,7 +74,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ATTR_SUBSCRIPTION = "subscription" ATTR_BROWSER = "browser" -ATTR_NAME = "name" ATTR_ENDPOINT = "endpoint" ATTR_KEYS = "keys" diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index fc455feb477..1763e169d50 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_MODE, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -27,7 +28,6 @@ from .const import ( ATTR_HUMIDITY, ATTR_MAX_HUMIDITY, ATTR_MIN_HUMIDITY, - ATTR_MODE, DEFAULT_MAX_HUMIDITY, DEFAULT_MIN_HUMIDITY, DEVICE_CLASS_DEHUMIDIFIER, diff --git a/homeassistant/components/humidifier/const.py b/homeassistant/components/humidifier/const.py index 82e87ae5c31..7e70c51df28 100644 --- a/homeassistant/components/humidifier/const.py +++ b/homeassistant/components/humidifier/const.py @@ -1,4 +1,5 @@ """Provides the constants needed for component.""" +from homeassistant.const import ATTR_MODE # noqa: F401 pylint: disable=unused-import MODE_NORMAL = "normal" MODE_ECO = "eco" @@ -10,7 +11,6 @@ MODE_SLEEP = "sleep" MODE_AUTO = "auto" MODE_BABY = "baby" -ATTR_MODE = "mode" ATTR_AVAILABLE_MODES = "available_modes" ATTR_HUMIDITY = "humidity" ATTR_MAX_HUMIDITY = "max_humidity" diff --git a/tests/components/fritzbox_callmonitor/test_config_flow.py b/tests/components/fritzbox_callmonitor/test_config_flow.py index 00bc1e18679..cde30b615eb 100644 --- a/tests/components/fritzbox_callmonitor/test_config_flow.py +++ b/tests/components/fritzbox_callmonitor/test_config_flow.py @@ -14,12 +14,12 @@ from homeassistant.components.fritzbox_callmonitor.const import ( CONF_PHONEBOOK, CONF_PREFIXES, DOMAIN, - FRITZ_ATTR_NAME, FRITZ_ATTR_SERIAL_NUMBER, SERIAL_NUMBER, ) from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import ( + ATTR_NAME, CONF_HOST, CONF_NAME, CONF_PASSWORD, @@ -69,8 +69,8 @@ MOCK_YAML_CONFIG = { CONF_NAME: MOCK_NAME, } MOCK_DEVICE_INFO = {FRITZ_ATTR_SERIAL_NUMBER: MOCK_SERIAL_NUMBER} -MOCK_PHONEBOOK_INFO_1 = {FRITZ_ATTR_NAME: MOCK_PHONEBOOK_NAME_1} -MOCK_PHONEBOOK_INFO_2 = {FRITZ_ATTR_NAME: MOCK_PHONEBOOK_NAME_2} +MOCK_PHONEBOOK_INFO_1 = {ATTR_NAME: MOCK_PHONEBOOK_NAME_1} +MOCK_PHONEBOOK_INFO_2 = {ATTR_NAME: MOCK_PHONEBOOK_NAME_2} MOCK_UNIQUE_ID = f"{MOCK_SERIAL_NUMBER}-{MOCK_PHONEBOOK_ID}" From 6fe72b04eb507a111324ae636b8285790fbc1e96 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 23 Feb 2021 13:24:07 +0100 Subject: [PATCH 657/796] Add zwave_js constant for add-on slug (#46950) --- homeassistant/components/zwave_js/__init__.py | 5 +++-- homeassistant/components/zwave_js/config_flow.py | 11 ++++++----- homeassistant/components/zwave_js/const.py | 2 ++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index f6edb2f4596..530a8022233 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -21,6 +21,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from .api import async_register_api from .const import ( + ADDON_SLUG, ATTR_COMMAND_CLASS, ATTR_COMMAND_CLASS_NAME, ATTR_DEVICE_ID, @@ -333,11 +334,11 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: return try: - await hass.components.hassio.async_stop_addon("core_zwave_js") + await hass.components.hassio.async_stop_addon(ADDON_SLUG) except HassioAPIError as err: LOGGER.error("Failed to stop the Z-Wave JS add-on: %s", err) return try: - await hass.components.hassio.async_uninstall_addon("core_zwave_js") + await hass.components.hassio.async_uninstall_addon(ADDON_SLUG) except HassioAPIError as err: LOGGER.error("Failed to uninstall the Z-Wave JS add-on: %s", err) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index b18e28419dd..ec74acf9886 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -14,6 +14,7 @@ from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( # pylint:disable=unused-import + ADDON_SLUG, CONF_INTEGRATION_CREATED_ADDON, CONF_USE_ADDON, DOMAIN, @@ -248,7 +249,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self._async_set_addon_config(new_addon_config) try: - await self.hass.components.hassio.async_start_addon("core_zwave_js") + await self.hass.components.hassio.async_start_addon(ADDON_SLUG) except self.hass.components.hassio.HassioAPIError as err: _LOGGER.error("Failed to start Z-Wave JS add-on: %s", err) errors["base"] = "addon_start_failed" @@ -294,7 +295,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Return and cache Z-Wave JS add-on info.""" try: addon_info: dict = await self.hass.components.hassio.async_get_addon_info( - "core_zwave_js" + ADDON_SLUG ) except self.hass.components.hassio.HassioAPIError as err: _LOGGER.error("Failed to get Z-Wave JS add-on info: %s", err) @@ -322,7 +323,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): options = {"options": config} try: await self.hass.components.hassio.async_set_addon_options( - "core_zwave_js", options + ADDON_SLUG, options ) except self.hass.components.hassio.HassioAPIError as err: _LOGGER.error("Failed to set Z-Wave JS add-on config: %s", err) @@ -331,7 +332,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_install_addon(self) -> None: """Install the Z-Wave JS add-on.""" try: - await self.hass.components.hassio.async_install_addon("core_zwave_js") + await self.hass.components.hassio.async_install_addon(ADDON_SLUG) finally: # Continue the flow after show progress when the task is done. self.hass.async_create_task( @@ -343,7 +344,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: discovery_info: dict = ( await self.hass.components.hassio.async_get_addon_discovery_info( - "core_zwave_js" + ADDON_SLUG ) ) except self.hass.components.hassio.HassioAPIError as err: diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 9905eba0693..5eb537c0d4e 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -32,3 +32,5 @@ ATTR_DEVICE_ID = "device_id" ATTR_PROPERTY_NAME = "property_name" ATTR_PROPERTY_KEY_NAME = "property_key_name" ATTR_PARAMETERS = "parameters" + +ADDON_SLUG = "core_zwave_js" From 593e7aea5a06cc724f5b12fbca70d595bcb252d4 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 23 Feb 2021 14:00:21 +0100 Subject: [PATCH 658/796] Bump accuweather to 0.1.0 (#46951) --- homeassistant/components/accuweather/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/accuweather/manifest.json b/homeassistant/components/accuweather/manifest.json index 6ccd6a4f10b..b03c0e51018 100644 --- a/homeassistant/components/accuweather/manifest.json +++ b/homeassistant/components/accuweather/manifest.json @@ -2,7 +2,7 @@ "domain": "accuweather", "name": "AccuWeather", "documentation": "https://www.home-assistant.io/integrations/accuweather/", - "requirements": ["accuweather==0.0.11"], + "requirements": ["accuweather==0.1.0"], "codeowners": ["@bieniu"], "config_flow": true, "quality_scale": "platinum" diff --git a/requirements_all.txt b/requirements_all.txt index 7966859d341..00bd7af0d74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -96,7 +96,7 @@ WazeRouteCalculator==0.12 abodepy==1.2.0 # homeassistant.components.accuweather -accuweather==0.0.11 +accuweather==0.1.0 # homeassistant.components.bmp280 adafruit-circuitpython-bmp280==3.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 489e365fad8..3a0954f41ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -42,7 +42,7 @@ WSDiscovery==2.0.0 abodepy==1.2.0 # homeassistant.components.accuweather -accuweather==0.0.11 +accuweather==0.1.0 # homeassistant.components.androidtv adb-shell[async]==0.2.1 From ea4bbd771f165f7a55014c6fe620600601a20315 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 23 Feb 2021 14:10:13 +0100 Subject: [PATCH 659/796] Add service names to previously enriched services (#46929) Co-authored-by: Tobias Sauerwein --- .../alarm_control_panel/services.yaml | 18 ++++-- homeassistant/components/alert/services.yaml | 9 ++- .../components/automation/services.yaml | 15 +++-- .../components/browser/services.yaml | 3 +- homeassistant/components/camera/services.yaml | 21 ++++--- .../components/climate/services.yaml | 27 ++++++--- .../components/color_extractor/services.yaml | 3 +- .../components/conversation/services.yaml | 3 +- .../components/counter/services.yaml | 12 ++-- homeassistant/components/cover/services.yaml | 30 ++++++---- .../components/downloader/services.yaml | 3 +- homeassistant/components/fan/services.yaml | 30 ++++++---- .../components/homeassistant/services.yaml | 12 ++-- homeassistant/components/hue/services.yaml | 3 +- .../components/input_datetime/services.yaml | 6 +- .../components/input_number/services.yaml | 11 +++- .../components/input_select/services.yaml | 19 ++++-- .../components/input_text/services.yaml | 4 +- homeassistant/components/light/services.yaml | 6 +- homeassistant/components/lock/services.yaml | 12 ++-- homeassistant/components/logger/services.yaml | 22 +++---- .../components/media_player/services.yaml | 58 +++++++++++++------ homeassistant/components/mqtt/services.yaml | 11 ++-- .../components/netatmo/services.yaml | 13 ++++- homeassistant/components/number/services.yaml | 3 +- .../components/recorder/services.yaml | 3 +- homeassistant/components/scene/services.yaml | 12 ++-- .../components/shopping_list/services.yaml | 6 +- homeassistant/components/sonos/services.yaml | 16 ++++- .../components/system_log/services.yaml | 6 +- homeassistant/components/timer/services.yaml | 10 +++- homeassistant/components/toon/services.yaml | 1 + .../components/twentemilieu/services.yaml | 1 + homeassistant/components/vacuum/services.yaml | 34 +++++++---- homeassistant/components/vizio/services.yaml | 10 +++- homeassistant/components/wled/services.yaml | 6 +- 36 files changed, 308 insertions(+), 151 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index a6151c58db0..b18f1cfb782 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -1,7 +1,8 @@ # Describes the format for available alarm control panel services alarm_disarm: - description: Send the alarm the command for disarm + name: Disarm + description: Send the alarm the command for disarm. target: fields: code: @@ -12,7 +13,8 @@ alarm_disarm: text: alarm_arm_custom_bypass: - description: Send arm custom bypass command + name: Arm with custom bypass + description: Send arm custom bypass command. target: fields: code: @@ -24,7 +26,8 @@ alarm_arm_custom_bypass: text: alarm_arm_home: - description: Send the alarm the command for arm home + name: Arm home + description: Send the alarm the command for arm home. target: fields: code: @@ -35,7 +38,8 @@ alarm_arm_home: text: alarm_arm_away: - description: Send the alarm the command for arm away + name: Arm away + description: Send the alarm the command for arm away. target: fields: code: @@ -46,7 +50,8 @@ alarm_arm_away: text: alarm_arm_night: - description: Send the alarm the command for arm night + name: Arm night + description: Send the alarm the command for arm night. target: fields: code: @@ -57,7 +62,8 @@ alarm_arm_night: text: alarm_trigger: - description: Send the alarm the command for trigger + name: Trigger + description: Send the alarm the command for trigger. target: fields: code: diff --git a/homeassistant/components/alert/services.yaml b/homeassistant/components/alert/services.yaml index c8c1d5d814a..5800d642b93 100644 --- a/homeassistant/components/alert/services.yaml +++ b/homeassistant/components/alert/services.yaml @@ -1,11 +1,14 @@ toggle: - description: Toggle alert's notifications + name: Toggle + description: Toggle alert's notifications. target: turn_off: - description: Silence alert's notifications + name: Turn off + description: Silence alert's notifications. target: turn_on: - description: Reset alert's notifications + name: Turn on + description: Reset alert's notifications. target: diff --git a/homeassistant/components/automation/services.yaml b/homeassistant/components/automation/services.yaml index 95f2057f5ee..5d399fb253e 100644 --- a/homeassistant/components/automation/services.yaml +++ b/homeassistant/components/automation/services.yaml @@ -1,10 +1,12 @@ # Describes the format for available automation services turn_on: - description: Enable an automation + name: Turn on + description: Enable an automation. target: turn_off: - description: Disable an automation + name: Turn off + description: Disable an automation. target: fields: stop_actions: @@ -16,11 +18,13 @@ turn_off: boolean: toggle: - description: Toggle (enable / disable) an automation + name: Toggle + description: Toggle (enable / disable) an automation. target: trigger: - description: Trigger the actions of an automation + name: Trigger + description: Trigger the actions of an automation. target: fields: skip_condition: @@ -32,4 +36,5 @@ trigger: boolean: reload: - description: Reload the automation configuration + name: Reload + description: Reload the automation configuration. diff --git a/homeassistant/components/browser/services.yaml b/homeassistant/components/browser/services.yaml index f6c5e7c90e1..1014e50db21 100644 --- a/homeassistant/components/browser/services.yaml +++ b/homeassistant/components/browser/services.yaml @@ -1,6 +1,7 @@ browse_url: + name: Browse description: - Open a URL in the default browser on the host machine of Home Assistant + Open a URL in the default browser on the host machine of Home Assistant. fields: url: name: URL diff --git a/homeassistant/components/camera/services.yaml b/homeassistant/components/camera/services.yaml index 3ae5b650304..3c8e99f001b 100644 --- a/homeassistant/components/camera/services.yaml +++ b/homeassistant/components/camera/services.yaml @@ -1,23 +1,28 @@ # Describes the format for available camera services turn_off: - description: Turn off camera + name: Turn off + description: Turn off camera. target: turn_on: - description: Turn on camera + name: Turn on + description: Turn on camera. target: enable_motion_detection: - description: Enable the motion detection in a camera + name: Enable motion detection + description: Enable the motion detection in a camera. target: disable_motion_detection: - description: Disable the motion detection in a camera + name: Disable motion detection + description: Disable the motion detection in a camera. target: snapshot: - description: Take a snapshot from a camera + name: Take snapshot + description: Take a snapshot from a camera. target: fields: filename: @@ -29,7 +34,8 @@ snapshot: text: play_stream: - description: Play camera stream on supported media player + name: Play stream + description: Play camera stream on supported media player. target: fields: media_player: @@ -51,7 +57,8 @@ play_stream: - "hls" record: - description: Record live camera feed + name: Record + description: Record live camera feed. target: fields: filename: diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index d333260202f..ca88896c6c2 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -1,7 +1,8 @@ # Describes the format for available climate services set_aux_heat: - description: Turn auxiliary heater on/off for climate device + name: Turn on/off auxiliary heater + description: Turn auxiliary heater on/off for climate device. target: fields: aux_heat: @@ -13,7 +14,8 @@ set_aux_heat: boolean: set_preset_mode: - description: Set preset mode for climate device + name: Set preset mode + description: Set preset mode for climate device. target: fields: preset_mode: @@ -25,7 +27,8 @@ set_preset_mode: text: set_temperature: - description: Set target temperature of climate device + name: Set temperature + description: Set target temperature of climate device. target: fields: temperature: @@ -76,7 +79,8 @@ set_temperature: - "heat" set_humidity: - description: Set target humidity of climate device + name: Set target humidity + description: Set target humidity of climate device. target: fields: humidity: @@ -93,7 +97,8 @@ set_humidity: mode: slider set_fan_mode: - description: Set fan operation for climate device + name: Set fan mode + description: Set fan operation for climate device. target: fields: fan_mode: @@ -105,7 +110,8 @@ set_fan_mode: text: set_hvac_mode: - description: Set HVAC operation mode for climate device + name: Set HVAC mode + description: Set HVAC operation mode for climate device. target: fields: hvac_mode: @@ -124,7 +130,8 @@ set_hvac_mode: - "heat" set_swing_mode: - description: Set swing operation for climate device + name: Set swing mode + description: Set swing operation for climate device. target: fields: swing_mode: @@ -136,9 +143,11 @@ set_swing_mode: text: turn_on: - description: Turn climate device on + name: Turn on + description: Turn climate device on. target: turn_off: - description: Turn climate device off + name: Turn off + description: Turn climate device off. target: diff --git a/homeassistant/components/color_extractor/services.yaml b/homeassistant/components/color_extractor/services.yaml index 671a2a2ebb9..00438dc9aa1 100644 --- a/homeassistant/components/color_extractor/services.yaml +++ b/homeassistant/components/color_extractor/services.yaml @@ -1,7 +1,8 @@ turn_on: + name: Turn on description: Set the light RGB to the predominant color found in the image provided by - URL or file path + URL or file path. target: fields: color_extract_url: diff --git a/homeassistant/components/conversation/services.yaml b/homeassistant/components/conversation/services.yaml index 5c85de7c187..edba9ffb0b9 100644 --- a/homeassistant/components/conversation/services.yaml +++ b/homeassistant/components/conversation/services.yaml @@ -1,6 +1,7 @@ # Describes the format for available component services process: - description: Launch a conversation from a transcribed text + name: Process + description: Launch a conversation from a transcribed text. fields: text: name: Text diff --git a/homeassistant/components/counter/services.yaml b/homeassistant/components/counter/services.yaml index 16010f6e2f4..4dd427c1fa1 100644 --- a/homeassistant/components/counter/services.yaml +++ b/homeassistant/components/counter/services.yaml @@ -1,19 +1,23 @@ # Describes the format for available counter services decrement: - description: Decrement a counter + name: Decrement + description: Decrement a counter. target: increment: - description: Increment a counter + name: Increment + description: Increment a counter. target: reset: - description: Reset a counter + name: Reset + description: Reset a counter. target: configure: - description: Change counter parameters + name: Configure + description: Change counter parameters. target: fields: minimum: diff --git a/homeassistant/components/cover/services.yaml b/homeassistant/components/cover/services.yaml index 173674193cb..1419a5f48ed 100644 --- a/homeassistant/components/cover/services.yaml +++ b/homeassistant/components/cover/services.yaml @@ -1,19 +1,23 @@ # Describes the format for available cover services open_cover: - description: Open all or specified cover + name: Open + description: Open all or specified cover. target: close_cover: - description: Close all or specified cover + name: Close + description: Close all or specified cover. target: toggle: - description: Toggles a cover open/closed + name: Toggle + description: Toggle a cover open/closed. target: set_cover_position: - description: Move to specific position all or specified cover + name: Set position + description: Move to specific position all or specified cover. target: fields: position: @@ -30,23 +34,28 @@ set_cover_position: mode: slider stop_cover: - description: Stop all or specified cover + name: Stop + description: Stop all or specified cover. target: open_cover_tilt: - description: Open all or specified cover tilt + name: Open tilt + description: Open all or specified cover tilt. target: close_cover_tilt: - description: Close all or specified cover tilt + name: Close tilt + description: Close all or specified cover tilt. target: toggle_cover_tilt: - description: Toggle a cover tilt open/closed + name: Toggle tilt + description: Toggle a cover tilt open/closed. target: set_cover_tilt_position: - description: Move to specific position all or specified cover tilt + name: Set tilt position + description: Move to specific position all or specified cover tilt. target: fields: tilt_position: @@ -63,5 +72,6 @@ set_cover_tilt_position: mode: slider stop_cover_tilt: - description: Stop all or specified cover + name: Stop tilt + description: Stop all or specified cover. target: diff --git a/homeassistant/components/downloader/services.yaml b/homeassistant/components/downloader/services.yaml index 5ac383fc4f6..cecb3804227 100644 --- a/homeassistant/components/downloader/services.yaml +++ b/homeassistant/components/downloader/services.yaml @@ -1,5 +1,6 @@ download_file: - description: Downloads a file to the download location + name: Download file + description: Download a file to the download location. fields: url: name: URL diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index ad513b84e8f..3c8eb2d0761 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -1,6 +1,7 @@ # Describes the format for available fan services set_speed: - description: Set fan speed + name: Set speed + description: Set fan speed. target: fields: speed: @@ -12,7 +13,8 @@ set_speed: text: set_preset_mode: - description: Set preset mode for a fan device + name: Set preset mode + description: Set preset mode for a fan device. target: fields: preset_mode: @@ -24,7 +26,8 @@ set_preset_mode: text: set_percentage: - description: Set fan speed percentage + name: Set speed percentage + description: Set fan speed percentage. target: fields: percentage: @@ -41,7 +44,8 @@ set_percentage: mode: slider turn_on: - description: Turn fan on + name: Turn on + description: Turn fan on. target: fields: speed: @@ -67,11 +71,13 @@ turn_on: text: turn_off: - description: Turn fan off + name: Turn off + description: Turn fan off. target: oscillate: - description: Oscillate the fan + name: Oscillate + description: Oscillate the fan. target: fields: oscillating: @@ -83,11 +89,13 @@ oscillate: boolean: toggle: - description: Toggle the fan on/off + name: Toggle + description: Toggle the fan on/off. target: set_direction: - description: Set the fan rotation + name: Set direction + description: Set the fan rotation. target: fields: direction: @@ -102,6 +110,7 @@ set_direction: - "reverse" increase_speed: + name: Increase speed description: Increase the speed of the fan by one speed or a percentage_step. fields: entity_id: @@ -110,7 +119,7 @@ increase_speed: percentage_step: advanced: true required: false - description: Increase speed by a percentage. Should be between 0..100. [optional] + description: Increase speed by a percentage. example: 50 selector: number: @@ -121,6 +130,7 @@ increase_speed: mode: slider decrease_speed: + name: Decrease speed description: Decrease the speed of the fan by one speed or a percentage_step. fields: entity_id: @@ -129,7 +139,7 @@ decrease_speed: percentage_step: advanced: true required: false - description: Decrease speed by a percentage. Should be between 0..100. [optional] + description: Decrease speed by a percentage. example: 50 selector: number: diff --git a/homeassistant/components/homeassistant/services.yaml b/homeassistant/components/homeassistant/services.yaml index d23bdfdba72..6bd0a0852ed 100644 --- a/homeassistant/components/homeassistant/services.yaml +++ b/homeassistant/components/homeassistant/services.yaml @@ -1,16 +1,20 @@ check_config: + name: Check configuration description: Check the Home Assistant configuration files for errors. Errors will be - displayed in the Home Assistant log + displayed in the Home Assistant log. reload_core_config: - description: Reload the core configuration + name: Reload core configuration + description: Reload the core configuration. restart: - description: Restart the Home Assistant service + name: Restart + description: Restart the Home Assistant service. set_location: - description: Update the Home Assistant location + name: Set location + description: Update the Home Assistant location. fields: latitude: name: Latitude diff --git a/homeassistant/components/hue/services.yaml b/homeassistant/components/hue/services.yaml index 80ca25007bc..07eeca6fa0f 100644 --- a/homeassistant/components/hue/services.yaml +++ b/homeassistant/components/hue/services.yaml @@ -1,7 +1,8 @@ # Describes the format for available hue services hue_activate_scene: - description: Activate a hue scene stored in the hue hub + name: Activate scene + description: Activate a hue scene stored in the hue hub. fields: group_name: name: Group diff --git a/homeassistant/components/input_datetime/services.yaml b/homeassistant/components/input_datetime/services.yaml index ef4cb9556c8..0243ca9f67d 100644 --- a/homeassistant/components/input_datetime/services.yaml +++ b/homeassistant/components/input_datetime/services.yaml @@ -1,5 +1,6 @@ set_datetime: - description: This can be used to dynamically set the date and/or time + name: Set + description: This can be used to dynamically set the date and/or time. target: fields: date: @@ -33,4 +34,5 @@ set_datetime: mode: box reload: - description: Reload the input_datetime configuration + name: Reload + description: Reload the input_datetime configuration. diff --git a/homeassistant/components/input_number/services.yaml b/homeassistant/components/input_number/services.yaml index 700a2c28144..7d388238022 100644 --- a/homeassistant/components/input_number/services.yaml +++ b/homeassistant/components/input_number/services.yaml @@ -1,13 +1,16 @@ decrement: - description: Decrement the value of an input number entity by its stepping + name: Decrement + description: Decrement the value of an input number entity by its stepping. target: increment: - description: Increment the value of an input number entity by its stepping + name: Increment + description: Increment the value of an input number entity by its stepping. target: set_value: - description: Set the value of an input number entity + name: Set + description: Set the value of an input number entity. target: fields: value: @@ -21,5 +24,7 @@ set_value: max: 9223372036854775807 step: 0.001 mode: box + reload: + name: Reload description: Reload the input_number configuration. diff --git a/homeassistant/components/input_select/services.yaml b/homeassistant/components/input_select/services.yaml index bf1b9e81033..f8fbe158aab 100644 --- a/homeassistant/components/input_select/services.yaml +++ b/homeassistant/components/input_select/services.yaml @@ -1,5 +1,6 @@ select_next: - description: Select the next options of an input select entity + name: Next + description: Select the next options of an input select entity. target: fields: cycle: @@ -11,7 +12,8 @@ select_next: boolean: select_option: - description: Select an option of an input select entity + name: Select + description: Select an option of an input select entity. target: fields: option: @@ -23,7 +25,8 @@ select_option: text: select_previous: - description: Select the previous options of an input select entity + name: Previous + description: Select the previous options of an input select entity. target: fields: cycle: @@ -35,15 +38,18 @@ select_previous: boolean: select_first: - description: Select the first option of an input select entity + name: First + description: Select the first option of an input select entity. target: select_last: - description: Select the last option of an input select entity + name: Last + description: Select the last option of an input select entity. target: set_options: - description: Set the options of an input select entity + name: Set options + description: Set the options of an input select entity. target: fields: options: @@ -55,4 +61,5 @@ set_options: object: reload: + name: Reload description: Reload the input_select configuration. diff --git a/homeassistant/components/input_text/services.yaml b/homeassistant/components/input_text/services.yaml index b5ac97f837a..5983683ec6d 100644 --- a/homeassistant/components/input_text/services.yaml +++ b/homeassistant/components/input_text/services.yaml @@ -1,5 +1,6 @@ set_value: - description: Set the value of an input text entity + name: Set + description: Set the value of an input text entity. target: fields: value: @@ -11,4 +12,5 @@ set_value: text: reload: + name: Reload description: Reload the input_text configuration. diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 4f5d74cbbbb..fe96f3a6777 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -1,7 +1,7 @@ # Describes the format for available light services turn_on: - name: Turn on lights + name: Turn on description: > Turn on one or more lights and adjust properties of the light, even when they are turned on already. @@ -314,7 +314,7 @@ turn_on: text: turn_off: - name: Turn off lights + name: Turn off description: Turns off one or more lights. target: fields: @@ -344,7 +344,7 @@ turn_off: - short toggle: - name: Toggle lights + name: Toggle description: > Toggles one or more lights, from on to off, or, off to on, based on their current state. diff --git a/homeassistant/components/lock/services.yaml b/homeassistant/components/lock/services.yaml index 22af0ab97cf..f5f6077ddc1 100644 --- a/homeassistant/components/lock/services.yaml +++ b/homeassistant/components/lock/services.yaml @@ -21,7 +21,8 @@ get_usercode: example: 1 lock: - description: Lock all or specified locks + name: Lock + description: Lock all or specified locks. target: fields: code: @@ -32,7 +33,8 @@ lock: text: open: - description: Open all or specified locks + name: Open + description: Open all or specified locks. target: fields: code: @@ -43,7 +45,8 @@ open: text: set_usercode: - description: Set a usercode to lock + name: Set usercode + description: Set a usercode to lock. fields: node_id: description: Node id of the lock. @@ -56,7 +59,8 @@ set_usercode: example: 1234 unlock: - description: Unlock all or specified locks + name: Unlock + description: Unlock all or specified locks. target: fields: code: diff --git a/homeassistant/components/logger/services.yaml b/homeassistant/components/logger/services.yaml index 4bd46b4b01e..39c0bcfdfe1 100644 --- a/homeassistant/components/logger/services.yaml +++ b/homeassistant/components/logger/services.yaml @@ -1,11 +1,10 @@ set_default_level: - description: Set the default log level for components + name: Set default level + description: Set the default log level for integrations. fields: level: name: Level - description: - "Default severity level. Possible values are debug, info, warn, warning, - error, fatal, critical" + description: Default severity level for all integrations. example: debug selector: select: @@ -18,26 +17,27 @@ set_default_level: - critical set_level: - description: Set log level for components + name: Set level + description: Set log level for integrations. fields: homeassistant.core: description: - "Example on how to change the logging level for a Home Assistant core - components. Possible values are debug, info, warn, warning, error, - fatal, critical" + "Example on how to change the logging level for a Home Assistant Core + integrations. Possible values are debug, info, warn, warning, error, + fatal, critical." example: debug homeassistant.components.mqtt: description: "Example on how to change the logging level for an Integration. Possible - values are debug, info, warn, warning, error, fatal, critical" + values are debug, info, warn, warning, error, fatal, critical." example: warning custom_components.my_integration: description: "Example on how to change the logging level for a Custom Integration. - Possible values are debug, info, warn, warning, error, fatal, critical" + Possible values are debug, info, warn, warning, error, fatal, critical." example: debug aiohttp: description: "Example on how to change the logging level for a Python module. - Possible values are debug, info, warn, warning, error, fatal, critical" + Possible values are debug, info, warn, warning, error, fatal, critical." example: error diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index f85e0658426..03084f0f1a1 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -1,27 +1,33 @@ # Describes the format for available media player services turn_on: - description: Turn a media player power on + name: Turn on + description: Turn a media player power on. target: turn_off: - description: Turn a media player power off + name: Turn off + description: Turn a media player power off. target: toggle: - description: Toggles a media player power state + name: Toggle + description: Toggles a media player power state. target: volume_up: - description: Turn a media player volume up + name: Turn up volume + description: Turn a media player volume up. target: volume_down: - description: Turn a media player volume down + name: Turn down volume + description: Turn a media player volume down. target: volume_mute: - description: Mute a media player's volume + name: Mute volume + description: Mute a media player's volume. target: fields: is_volume_muted: @@ -33,7 +39,8 @@ volume_mute: boolean: volume_set: - description: Set a media player's volume level + name: Set volume + description: Set a media player's volume level. target: fields: volume_level: @@ -49,32 +56,39 @@ volume_set: mode: slider media_play_pause: - description: Toggle media player play/pause state + name: Play/Pause + description: Toggle media player play/pause state. target: media_play: - description: Send the media player the command for play + name: Play + description: Send the media player the command for play. target: media_pause: - description: Send the media player the command for pause + name: Pause + description: Send the media player the command for pause. target: media_stop: - description: Send the media player the stop command + name: Stop + description: Send the media player the stop command. target: media_next_track: - description: Send the media player the command for next track + name: Next + description: Send the media player the command for next track. target: media_previous_track: - description: Send the media player the command for previous track + name: Previous + description: Send the media player the command for previous track. target: media_seek: + name: Seek description: - Send the media player the command to seek in current playing media + Send the media player the command to seek in current playing media. fields: seek_position: name: Position @@ -89,7 +103,8 @@ media_seek: mode: box play_media: - description: Send the media player the command for playing media + name: Play media + description: Send the media player the command for playing media. target: fields: media_content_id: @@ -111,7 +126,8 @@ play_media: text: select_source: - description: Send the media player the command to change input source + name: Select source + description: Send the media player the command to change input source. target: fields: source: @@ -123,7 +139,8 @@ select_source: text: select_sound_mode: - description: Send the media player the command to change sound mode + name: Select sound mode + description: Send the media player the command to change sound mode. target: fields: sound_mode: @@ -134,11 +151,13 @@ select_sound_mode: text: clear_playlist: - description: Send the media player the command to clear players playlist + name: Clear playlist + description: Send the media player the command to clear players playlist. target: shuffle_set: - description: Set shuffling state + name: Shuffle + description: Set shuffling state. target: fields: shuffle: @@ -150,6 +169,7 @@ shuffle_set: boolean: repeat_set: + name: Repeat description: Set repeat mode target: fields: diff --git a/homeassistant/components/mqtt/services.yaml b/homeassistant/components/mqtt/services.yaml index c6c3014362f..21d3915628a 100644 --- a/homeassistant/components/mqtt/services.yaml +++ b/homeassistant/components/mqtt/services.yaml @@ -1,7 +1,8 @@ # Describes the format for available MQTT services publish: - description: Publish a message to an MQTT topic + name: Publish + description: Publish a message to an MQTT topic. fields: topic: name: Topic @@ -49,9 +50,10 @@ publish: boolean: dump: + name: Dump description: - Dump messages on a topic selector to the 'mqtt_dump.txt' file in your config - folder + Dump messages on a topic selector to the 'mqtt_dump.txt' file in your + configuration folder. fields: topic: name: Topic @@ -73,4 +75,5 @@ dump: mode: slider reload: - description: Reload all MQTT entities from YAML + name: Reload + description: Reload all MQTT entities from YAML. diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml index 7005d26c326..06f56d084c6 100644 --- a/homeassistant/components/netatmo/services.yaml +++ b/homeassistant/components/netatmo/services.yaml @@ -21,7 +21,9 @@ set_camera_light: set_schedule: name: Set heating schedule - description: Set the heating schedule for Netatmo climate device. The schedule name must match a schedule configured at Netatmo. + description: + Set the heating schedule for Netatmo climate device. The schedule name must + match a schedule configured at Netatmo. target: entity: integration: netatmo @@ -36,7 +38,9 @@ set_schedule: set_persons_home: name: Set persons at home - description: Set a list of persons as at home. Person's name must match a name known by the Netatmo Indoor (Welcome) Camera. + description: + Set a list of persons as at home. Person's name must match a name known by + the Netatmo Indoor (Welcome) Camera. target: entity: integration: netatmo @@ -51,7 +55,10 @@ set_persons_home: set_person_away: name: Set person away - description: Set a person as away. If no person is set the home will be marked as empty. Person's name must match a name known by the Netatmo Indoor (Welcome) Camera. + description: + Set a person as away. If no person is set the home will be marked as empty. + Person's name must match a name known by the Netatmo Indoor (Welcome) + Camera. target: entity: integration: netatmo diff --git a/homeassistant/components/number/services.yaml b/homeassistant/components/number/services.yaml index 4cb0cf09829..a684fef7d5d 100644 --- a/homeassistant/components/number/services.yaml +++ b/homeassistant/components/number/services.yaml @@ -1,7 +1,8 @@ # Describes the format for available Number entity services set_value: - description: Set the value of a Number entity + name: Set + description: Set the value of a Number entity. target: fields: value: diff --git a/homeassistant/components/recorder/services.yaml b/homeassistant/components/recorder/services.yaml index cad1925080f..e3dea47f4f8 100644 --- a/homeassistant/components/recorder/services.yaml +++ b/homeassistant/components/recorder/services.yaml @@ -1,7 +1,8 @@ # Describes the format for available recorder services purge: - description: Start purge task - to clean up old data from your database + name: Purge + description: Start purge task - to clean up old data from your database. fields: keep_days: name: Days to keep diff --git a/homeassistant/components/scene/services.yaml b/homeassistant/components/scene/services.yaml index dc9e9e07ab4..9d07460379c 100644 --- a/homeassistant/components/scene/services.yaml +++ b/homeassistant/components/scene/services.yaml @@ -1,7 +1,8 @@ # Describes the format for available scene services turn_on: - description: Activate a scene + name: Activate + description: Activate a scene. target: fields: transition: @@ -19,10 +20,12 @@ turn_on: mode: slider reload: - description: Reload the scene configuration + name: Reload + description: Reload the scene configuration. apply: - description: Activate a scene with configuration + name: Apply + description: Activate a scene with configuration. fields: entities: name: Entities state @@ -50,7 +53,8 @@ apply: mode: slider create: - description: Creates a new scene + name: Create + description: Creates a new scene. fields: scene_id: name: Scene entity ID diff --git a/homeassistant/components/shopping_list/services.yaml b/homeassistant/components/shopping_list/services.yaml index 961fb867aa7..2a1e89b9786 100644 --- a/homeassistant/components/shopping_list/services.yaml +++ b/homeassistant/components/shopping_list/services.yaml @@ -1,5 +1,6 @@ add_item: - description: Adds an item to the shopping list + name: Add item + description: Add an item to the shopping list. fields: name: name: Name @@ -10,7 +11,8 @@ add_item: text: complete_item: - description: Marks an item as completed in the shopping list. + name: Complete item + description: Mark an item as completed in the shopping list. fields: name: name: Name diff --git a/homeassistant/components/sonos/services.yaml b/homeassistant/components/sonos/services.yaml index 04a46940d6a..99b430e4680 100644 --- a/homeassistant/components/sonos/services.yaml +++ b/homeassistant/components/sonos/services.yaml @@ -1,8 +1,10 @@ join: + name: Join group description: Group player together. fields: master: - description: Entity ID of the player that should become the coordinator of the group. + description: + Entity ID of the player that should become the coordinator of the group. example: "media_player.living_room_sonos" selector: entity: @@ -17,6 +19,7 @@ join: domain: media_player unjoin: + name: Unjoin group description: Unjoin the player from a group. fields: entity_id: @@ -28,6 +31,7 @@ unjoin: domain: media_player snapshot: + name: Snapshot description: Take a snapshot of the media player. fields: entity_id: @@ -42,6 +46,7 @@ snapshot: example: "true" restore: + name: Restore description: Restore a snapshot of the media player. fields: entity_id: @@ -56,6 +61,7 @@ restore: example: "true" set_sleep_timer: + name: Set timer description: Set a Sonos timer. fields: entity_id: @@ -75,7 +81,9 @@ set_sleep_timer: step: 1 unit_of_measurement: seconds mode: slider + clear_sleep_timer: + name: Clear timer description: Clear a Sonos timer. fields: entity_id: @@ -87,6 +95,7 @@ clear_sleep_timer: domain: media_player set_option: + name: Set option description: Set Sonos sound options. fields: entity_id: @@ -113,7 +122,8 @@ set_option: boolean: play_queue: - description: Starts playing the queue from the first item. + name: Play queue + description: Start playing the queue from the first item. fields: entity_id: description: Name(s) of entities that will start playing. @@ -132,6 +142,7 @@ play_queue: mode: box remove_from_queue: + name: Remove from queue description: Removes an item from the queue. fields: entity_id: @@ -151,6 +162,7 @@ remove_from_queue: mode: box update_alarm: + name: Update alarm description: Updates an alarm with new time and volume settings. fields: alarm_id: diff --git a/homeassistant/components/system_log/services.yaml b/homeassistant/components/system_log/services.yaml index e07aea9c2a1..a762c31f205 100644 --- a/homeassistant/components/system_log/services.yaml +++ b/homeassistant/components/system_log/services.yaml @@ -1,8 +1,10 @@ clear: - description: Clear all log entries + name: Clear all + description: Clear all log entries. write: - description: Write log entry + name: Write + description: Write log entry. fields: message: name: Message diff --git a/homeassistant/components/timer/services.yaml b/homeassistant/components/timer/services.yaml index fcde11cd47f..54175de3cf7 100644 --- a/homeassistant/components/timer/services.yaml +++ b/homeassistant/components/timer/services.yaml @@ -1,6 +1,7 @@ # Describes the format for available timer services start: + name: Start description: Start a timer target: fields: @@ -12,13 +13,16 @@ start: text: pause: - description: Pause a timer + name: Pause + description: Pause a timer. target: cancel: - description: Cancel a timer + name: Cancel + description: Cancel a timer. target: finish: - description: Finish a timer + name: Finish + description: Finish a timer. target: diff --git a/homeassistant/components/toon/services.yaml b/homeassistant/components/toon/services.yaml index 3e06e6d3f9f..909018f820b 100644 --- a/homeassistant/components/toon/services.yaml +++ b/homeassistant/components/toon/services.yaml @@ -1,4 +1,5 @@ update: + name: Update description: Update all entities with fresh data from Toon fields: display: diff --git a/homeassistant/components/twentemilieu/services.yaml b/homeassistant/components/twentemilieu/services.yaml index 7a6a16f33ad..6227bad1b6d 100644 --- a/homeassistant/components/twentemilieu/services.yaml +++ b/homeassistant/components/twentemilieu/services.yaml @@ -1,4 +1,5 @@ update: + name: Update description: Update all entities with fresh data from Twente Milieu fields: id: diff --git a/homeassistant/components/vacuum/services.yaml b/homeassistant/components/vacuum/services.yaml index a60ce9ee658..e0064bc475b 100644 --- a/homeassistant/components/vacuum/services.yaml +++ b/homeassistant/components/vacuum/services.yaml @@ -1,43 +1,53 @@ # Describes the format for available vacuum services turn_on: - description: Start a new cleaning task + name: Turn on + description: Start a new cleaning task. target: turn_off: - description: Stop the current cleaning task and return to home + name: Turn off + description: Stop the current cleaning task and return to home. target: stop: - description: Stop the current cleaning task + name: Stop + description: Stop the current cleaning task. target: locate: - description: Locate the vacuum cleaner robot + name: Locate + description: Locate the vacuum cleaner robot. target: start_pause: - description: Start, pause, or resume the cleaning task + name: Start/Pause + description: Start, pause, or resume the cleaning task. target: start: - description: Start or resume the cleaning task + name: Start + description: Start or resume the cleaning task. target: pause: - description: Pause the cleaning task + name: Pause + description: Pause the cleaning task. target: return_to_base: - description: Tell the vacuum cleaner to return to its dock + name: Return to base + description: Tell the vacuum cleaner to return to its dock. target: clean_spot: - description: Tell the vacuum cleaner to do a spot clean-up + name: Clean spot + description: Tell the vacuum cleaner to do a spot clean-up. target: send_command: - description: Send a raw command to the vacuum cleaner + name: Send command + description: Send a raw command to the vacuum cleaner. target: fields: command: @@ -47,7 +57,6 @@ send_command: example: "set_dnd_timer" selector: text: - params: name: Parameters description: Parameters for the command. @@ -56,7 +65,8 @@ send_command: object: set_fan_speed: - description: Set the fan speed of the vacuum cleaner + name: Set fan speed + description: Set the fan speed of the vacuum cleaner. target: fields: fan_speed: diff --git a/homeassistant/components/vizio/services.yaml b/homeassistant/components/vizio/services.yaml index a2981fa32ad..7a2ea859b7d 100644 --- a/homeassistant/components/vizio/services.yaml +++ b/homeassistant/components/vizio/services.yaml @@ -1,5 +1,5 @@ update_setting: - name: Update a Vizio media player setting + name: Update setting description: Update the value of a setting on a Vizio media player device. target: entity: @@ -8,14 +8,18 @@ update_setting: fields: setting_type: name: Setting type - description: The type of setting to be changed. Available types are listed in the 'setting_types' property. + description: + The type of setting to be changed. Available types are listed in the + 'setting_types' property. required: true example: "audio" selector: text: setting_name: name: Setting name - description: The name of the setting to be changed. Available settings for a given setting_type are listed in the '_settings' property. + description: + The name of the setting to be changed. Available settings for a given + setting_type are listed in the '_settings' property. required: true example: "eq" selector: diff --git a/homeassistant/components/wled/services.yaml b/homeassistant/components/wled/services.yaml index 827e8b5fb36..d6927610a47 100644 --- a/homeassistant/components/wled/services.yaml +++ b/homeassistant/components/wled/services.yaml @@ -1,5 +1,6 @@ effect: - description: Controls the effect settings of WLED + name: Set effect + description: Control the effect settings of WLED. target: fields: effect: @@ -44,7 +45,8 @@ effect: boolean: preset: - description: Calls a preset on the WLED device + name: Set preset + description: Set a preset for the WLED device. target: fields: preset: From afa91e886b93d35f61ec8a838ef74a2b38ec48a0 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 23 Feb 2021 14:29:57 +0100 Subject: [PATCH 660/796] Add description to tts and notify services (#46764) Co-authored-by: Paulus Schoutsen Co-authored-by: Franck Nijhof --- .../components/media_player/services.yaml | 2 +- homeassistant/components/notify/__init__.py | 31 +++++++++++++++++-- homeassistant/components/notify/services.yaml | 31 ++++++++++++++++--- homeassistant/components/tts/__init__.py | 24 +++++++++++++- homeassistant/components/tts/services.yaml | 26 ++++++++++++++-- homeassistant/helpers/service.py | 3 ++ 6 files changed, 105 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 03084f0f1a1..eaca8483be1 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -118,7 +118,7 @@ play_media: media_content_type: name: Content type description: - The type of the content to play. Must be one of image, music, tvshow, + The type of the content to play. Like image, music, tvshow, video, episode, channel or playlist. required: true example: "music" diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 1e9c7d8595a..7be66dc3c59 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -2,7 +2,7 @@ import asyncio from functools import partial import logging -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, cast import voluptuous as vol @@ -12,10 +12,12 @@ from homeassistant.core import ServiceCall from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.loader import bind_hass +from homeassistant.loader import async_get_integration, bind_hass from homeassistant.setup import async_prepare_setup_platform from homeassistant.util import slugify +from homeassistant.util.yaml import load_yaml # mypy: allow-untyped-defs, no-check-untyped-defs @@ -41,6 +43,9 @@ SERVICE_PERSISTENT_NOTIFICATION = "persistent_notification" NOTIFY_SERVICES = "notify_services" +CONF_DESCRIPTION = "description" +CONF_FIELDS = "fields" + PLATFORM_SCHEMA = vol.Schema( {vol.Required(CONF_PLATFORM): cv.string, vol.Optional(CONF_NAME): cv.string}, extra=vol.ALLOW_EXTRA, @@ -161,6 +166,13 @@ class BaseNotificationService: self._target_service_name_prefix = target_service_name_prefix self.registered_targets = {} + # Load service descriptions from notify/services.yaml + integration = await async_get_integration(hass, DOMAIN) + services_yaml = integration.file_path / "services.yaml" + self.services_dict = cast( + dict, await hass.async_add_executor_job(load_yaml, str(services_yaml)) + ) + async def async_register_services(self) -> None: """Create or update the notify services.""" assert self.hass @@ -185,6 +197,13 @@ class BaseNotificationService: self._async_notify_message_service, schema=NOTIFY_SERVICE_SCHEMA, ) + # Register the service description + service_desc = { + CONF_NAME: f"Send a notification via {target_name}", + CONF_DESCRIPTION: f"Sends a notification message using the {target_name} integration.", + CONF_FIELDS: self.services_dict[SERVICE_NOTIFY][CONF_FIELDS], + } + async_set_service_schema(self.hass, DOMAIN, target_name, service_desc) for stale_target_name in stale_targets: del self.registered_targets[stale_target_name] @@ -203,6 +222,14 @@ class BaseNotificationService: schema=NOTIFY_SERVICE_SCHEMA, ) + # Register the service description + service_desc = { + CONF_NAME: f"Send a notification with {self._service_name}", + CONF_DESCRIPTION: f"Sends a notification message using the {self._service_name} service.", + CONF_FIELDS: self.services_dict[SERVICE_NOTIFY][CONF_FIELDS], + } + async_set_service_schema(self.hass, DOMAIN, self._service_name, service_desc) + async def async_unregister_services(self) -> None: """Unregister the notify services.""" assert self.hass diff --git a/homeassistant/components/notify/services.yaml b/homeassistant/components/notify/services.yaml index 8c75c94e34a..f6918b6c09c 100644 --- a/homeassistant/components/notify/services.yaml +++ b/homeassistant/components/notify/services.yaml @@ -1,22 +1,37 @@ # Describes the format for available notification services notify: - description: Send a notification. + name: Send a notification + description: Sends a notification message to selected notify platforms. fields: message: + name: Message description: Message body of the notification. example: The garage door has been open for 10 minutes. + selector: + text: title: + name: Title description: Optional title for your notification. example: "Your Garage Door Friend" + selector: + text: target: - description: An array of targets to send the notification to. Optional depending on the platform. + description: + An array of targets to send the notification to. Optional depending on + the platform. example: platform specific data: - description: Extended information for notification. Optional depending on the platform. + name: Data + description: + Extended information for notification. Optional depending on the + platform. example: platform specific + selector: + object: persistent_notification: + name: Send a persistent notification description: Sends a notification to the visible in the front-end. fields: message: @@ -27,10 +42,16 @@ persistent_notification: example: "Your Garage Door Friend" apns_register: - description: Registers a device to receive push notifications. + name: Register APNS device + description: + Registers a device to receive push notifications via APNS (Apple Push + Notification Service). fields: push_id: - description: The device token, a 64 character hex string (256 bits). The device token is provided to you by your client app, which receives the token after registering itself with the remote notification service. + description: + The device token, a 64 character hex string (256 bits). The device token + is provided to you by your client app, which receives the token after + registering itself with the remote notification service. example: "72f2a8633655c5ce574fdc9b2b34ff8abdfc3b739b6ceb7a9ff06c1cbbf99f62" name: description: A friendly name for the device (optional). diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index ff1bf946e83..67bc933a530 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -7,7 +7,7 @@ import logging import mimetypes import os import re -from typing import Dict, Optional +from typing import Dict, Optional, cast from aiohttp import web import mutagen @@ -24,6 +24,7 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.const import ( ATTR_ENTITY_ID, + CONF_NAME, CONF_PLATFORM, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, @@ -33,8 +34,11 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.network import get_url +from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.loader import async_get_integration from homeassistant.setup import async_prepare_setup_platform +from homeassistant.util.yaml import load_yaml # mypy: allow-untyped-defs, no-check-untyped-defs @@ -55,6 +59,9 @@ CONF_LANG = "language" CONF_SERVICE_NAME = "service_name" CONF_TIME_MEMORY = "time_memory" +CONF_DESCRIPTION = "description" +CONF_FIELDS = "fields" + DEFAULT_CACHE = True DEFAULT_CACHE_DIR = "tts" DEFAULT_TIME_MEMORY = 300 @@ -127,6 +134,13 @@ async def async_setup(hass, config): hass.http.register_view(TextToSpeechView(tts)) hass.http.register_view(TextToSpeechUrlView(tts)) + # Load service descriptions from tts/services.yaml + integration = await async_get_integration(hass, DOMAIN) + services_yaml = integration.file_path / "services.yaml" + services_dict = cast( + dict, await hass.async_add_executor_job(load_yaml, str(services_yaml)) + ) + async def async_setup_platform(p_type, p_config=None, discovery_info=None): """Set up a TTS platform.""" if p_config is None: @@ -193,6 +207,14 @@ async def async_setup(hass, config): DOMAIN, service_name, async_say_handle, schema=SCHEMA_SERVICE_SAY ) + # Register the service description + service_desc = { + CONF_NAME: "Say an TTS message with {p_type}", + CONF_DESCRIPTION: f"Say something using text-to-speech on a media player with {p_type}.", + CONF_FIELDS: services_dict[SERVICE_SAY][CONF_FIELDS], + } + async_set_service_schema(hass, DOMAIN, service_name, service_desc) + setup_tasks = [ asyncio.create_task(async_setup_platform(p_type, p_config)) for p_type, p_config in config_per_platform(config, DOMAIN) diff --git a/homeassistant/components/tts/services.yaml b/homeassistant/components/tts/services.yaml index 7d1bf95572b..2b48dd39dee 100644 --- a/homeassistant/components/tts/services.yaml +++ b/homeassistant/components/tts/services.yaml @@ -1,23 +1,43 @@ # Describes the format for available TTS services say: - description: Say some things on a media player. + name: Say an TTS message + description: Say something using text-to-speech on a media player. fields: entity_id: + name: Entity description: Name(s) of media player entities. example: "media_player.floor" + required: true + selector: + entity: + domain: media_player message: + name: Message description: Text to speak on devices. example: "My name is hanna" + required: true + selector: + text: cache: + name: Cache description: Control file cache of this message. example: "true" + default: false + selector: + boolean: language: + name: Language description: Language to use for speech generation. example: "ru" + selector: + text: options: - description: A dictionary containing platform-specific options. Optional depending on the platform. + description: + A dictionary containing platform-specific options. Optional depending on + the platform. example: platform specific clear_cache: - description: Remove cache files and RAM cache. + name: Clear TTS cache + description: Remove all text-to-speech cache files and RAM cache. diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index a55ba8a84af..7983190dbe8 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -478,6 +478,9 @@ def async_set_service_schema( "fields": schema.get("fields", {}), } + if "target" in schema: + description["target"] = schema["target"] + hass.data[SERVICE_DESCRIPTION_CACHE][f"{domain}.{service}"] = description From f0f752936ba776078dce39a189319e3e658c230f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 23 Feb 2021 14:49:04 +0100 Subject: [PATCH 661/796] Update homeassistant services.yaml (#46952) Add names and missing target to turn off service --- homeassistant/components/homeassistant/services.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/homeassistant/services.yaml b/homeassistant/components/homeassistant/services.yaml index 6bd0a0852ed..38814d9f902 100644 --- a/homeassistant/components/homeassistant/services.yaml +++ b/homeassistant/components/homeassistant/services.yaml @@ -32,22 +32,29 @@ set_location: text: stop: + name: Stop description: Stop the Home Assistant service. toggle: + name: Generic toggle description: Generic service to toggle devices on/off under any domain target: entity: {} turn_on: + name: Generic turn on description: Generic service to turn devices on under any domain. target: entity: {} turn_off: + name: Generic turn off description: Generic service to turn devices off under any domain. + target: + entity: {} update_entity: + name: Update entity description: Force one or more entities to update its data target: entity: {} From 3c35b6558bf83d4ed704453d7dbd34168f441cab Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Tue, 23 Feb 2021 09:31:47 -0500 Subject: [PATCH 662/796] Return states list from zwave_js get_config_parameters websocket if available (#46954) --- homeassistant/components/zwave_js/api.py | 3 +++ tests/components/zwave_js/test_api.py | 1 + 2 files changed, 4 insertions(+) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 71994f8b00b..8358b93aae5 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -374,6 +374,9 @@ def websocket_get_config_parameters( }, "value": zwave_value.value, } + if zwave_value.metadata.states: + result[value_id]["metadata"]["states"] = zwave_value.metadata.states + connection.send_result( msg[ID], result, diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 403a73a6767..d2a6215575f 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -74,6 +74,7 @@ async def test_websocket_api(hass, integration, multisensor_6, hass_ws_client): assert result[key]["property"] == 2 assert result[key]["metadata"]["type"] == "number" assert result[key]["configuration_value_type"] == "enumerated" + assert result[key]["metadata"]["states"] async def test_add_node( From 7a7147edcf6bf3922c546bdf0f917f3fbc1dcc4c Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Tue, 23 Feb 2021 16:34:02 +0100 Subject: [PATCH 663/796] Add stop tilt support to KNX (#46947) --- homeassistant/components/knx/cover.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 86afd467be1..4c08612926b 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -13,6 +13,7 @@ from homeassistant.components.cover import ( SUPPORT_SET_POSITION, SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, + SUPPORT_STOP_TILT, CoverEntity, ) from homeassistant.core import callback @@ -64,7 +65,10 @@ class KNXCover(KnxEntity, CoverEntity): supported_features |= SUPPORT_STOP if self._device.supports_angle: supported_features |= ( - SUPPORT_SET_TILT_POSITION | SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT + SUPPORT_SET_TILT_POSITION + | SUPPORT_OPEN_TILT + | SUPPORT_CLOSE_TILT + | SUPPORT_STOP_TILT ) return supported_features @@ -139,6 +143,11 @@ class KNXCover(KnxEntity, CoverEntity): """Close the cover tilt.""" await self._device.set_short_down() + async def async_stop_cover_tilt(self, **kwargs): + """Stop the cover tilt.""" + await self._device.stop() + self.stop_auto_updater() + def start_auto_updater(self): """Start the autoupdater to update Home Assistant while cover is moving.""" if self._unsubscribe_auto_updater is None: From c94968d8114c12f1c01e97a928ab5bb4b5defcf3 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 23 Feb 2021 16:36:53 +0100 Subject: [PATCH 664/796] Catch more zwave_js errors (#46957) --- homeassistant/components/zwave_js/__init__.py | 9 +++++++- homeassistant/components/zwave_js/climate.py | 10 +++++++-- tests/components/zwave_js/test_init.py | 21 +++++++++++++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 530a8022233..836bd771923 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -219,7 +219,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, handle_ha_shutdown) ) - await driver_ready.wait() + try: + await driver_ready.wait() + except asyncio.CancelledError: + LOGGER.debug("Cancelling start platforms") + return LOGGER.info("Connection to Zwave JS Server initialized") @@ -271,6 +275,9 @@ async def client_listen( should_reload = False except BaseZwaveJSServerError as err: LOGGER.error("Failed to listen: %s", err) + except Exception as err: # pylint: disable=broad-except + # We need to guard against unknown exceptions to not crash this task. + LOGGER.exception("Unexpected exception: %s", err) # The entry needs to be reloaded since a new driver state # will be acquired on reconnect. diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index f864efe91ff..54966538aae 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -261,7 +261,10 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): if self._current_mode and self._current_mode.value is None: # guard missing value return None - temp = self._setpoint_value(self._current_mode_setpoint_enums[0]) + try: + temp = self._setpoint_value(self._current_mode_setpoint_enums[0]) + except ValueError: + return None return temp.value if temp else None @property @@ -270,7 +273,10 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): if self._current_mode and self._current_mode.value is None: # guard missing value return None - temp = self._setpoint_value(self._current_mode_setpoint_enums[1]) + try: + temp = self._setpoint_value(self._current_mode_setpoint_enums[1]) + except ValueError: + return None return temp.value if temp else None @property diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 1aad07400ad..6e41da42c8f 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -3,6 +3,7 @@ from copy import deepcopy from unittest.mock import patch import pytest +from zwave_js_server.exceptions import BaseZwaveJSServerError from zwave_js_server.model.node import Node from homeassistant.components.hassio.handler import HassioAPIError @@ -76,6 +77,26 @@ async def test_initialized_timeout(hass, client, connect_timeout): assert entry.state == ENTRY_STATE_SETUP_RETRY +@pytest.mark.parametrize("error", [BaseZwaveJSServerError("Boom"), Exception("Boom")]) +async def test_listen_failure(hass, client, error): + """Test we handle errors during client listen.""" + + async def listen(driver_ready): + """Mock the client listen method.""" + # Set the connect side effect to stop an endless loop on reload. + client.connect.side_effect = BaseZwaveJSServerError("Boom") + raise error + + client.listen.side_effect = listen + entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"}) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ENTRY_STATE_SETUP_RETRY + + async def test_on_node_added_ready( hass, multisensor_6_state, client, integration, device_registry ): From 5a3bd30e01681be1102410c37418f04475ee9fd4 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 23 Feb 2021 11:35:11 -0500 Subject: [PATCH 665/796] Add zwave_js.set_config_parameter service (#46673) * create zwave_js.set_config_value service * update docstring * PR comments * make proposed changes * handle providing a label for the new value * fix docstring * use new library function * config param endpoint is always 0 * corresponding changes from upstream PR * bug fixes and add tests * create zwave_js.set_config_value service * update docstring * PR comments * make proposed changes * handle providing a label for the new value * fix docstring * use new library function * config param endpoint is always 0 * corresponding changes from upstream PR * bug fixes and add tests * use lambda to avoid extra function * add services description file * bring back the missing selector * move helper functions to helper file for reuse * allow target selector for automation editor * formatting * fix service schema * update docstrings * raise error in service if call to set value is unsuccessful * Update homeassistant/components/zwave_js/services.yaml Co-authored-by: Franck Nijhof * Update homeassistant/components/zwave_js/services.yaml Co-authored-by: Franck Nijhof * Update homeassistant/components/zwave_js/services.yaml Co-authored-by: Franck Nijhof * Update homeassistant/components/zwave_js/services.yaml Co-authored-by: Franck Nijhof * Update homeassistant/components/zwave_js/services.yaml Co-authored-by: Franck Nijhof * Update homeassistant/components/zwave_js/services.yaml Co-authored-by: Franck Nijhof * remove extra param to vol.Optional * switch to set over list for nodes * switch to set over list for nodes Co-authored-by: Marcel van der Veldt Co-authored-by: Franck Nijhof --- homeassistant/components/zwave_js/__init__.py | 6 +- homeassistant/components/zwave_js/const.py | 7 + homeassistant/components/zwave_js/entity.py | 11 +- homeassistant/components/zwave_js/helpers.py | 100 ++++++ homeassistant/components/zwave_js/services.py | 110 +++++++ .../components/zwave_js/services.yaml | 28 ++ tests/components/zwave_js/test_init.py | 2 +- tests/components/zwave_js/test_services.py | 295 ++++++++++++++++++ 8 files changed, 548 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/zwave_js/helpers.py create mode 100644 homeassistant/components/zwave_js/services.py create mode 100644 tests/components/zwave_js/test_services.py diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 836bd771923..a70716ad421 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -43,7 +43,8 @@ from .const import ( ZWAVE_JS_EVENT, ) from .discovery import async_discover_values -from .entity import get_device_id +from .helpers import get_device_id +from .services import ZWaveServices LOGGER = logging.getLogger(__package__) CONNECT_TIMEOUT = 10 @@ -192,6 +193,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_UNSUBSCRIBE: unsubscribe_callbacks, } + services = ZWaveServices(hass) + services.async_register() + # Set up websocket API async_register_api(hass) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 5eb537c0d4e..1031a51719a 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -33,4 +33,11 @@ ATTR_PROPERTY_NAME = "property_name" ATTR_PROPERTY_KEY_NAME = "property_key_name" ATTR_PARAMETERS = "parameters" +# service constants +SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter" + +ATTR_CONFIG_PARAMETER = "parameter" +ATTR_CONFIG_PARAMETER_BITMASK = "bitmask" +ATTR_CONFIG_VALUE = "value" + ADDON_SLUG = "core_zwave_js" diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 3e81bfaeadf..3141dd0caea 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -1,30 +1,23 @@ """Generic Z-Wave Entity Class.""" import logging -from typing import List, Optional, Tuple, Union +from typing import List, Optional, Union from zwave_js_server.client import Client as ZwaveClient -from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.value import Value as ZwaveValue, get_value_id from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback from homeassistant.helpers.entity import Entity -from .const import DOMAIN from .discovery import ZwaveDiscoveryInfo +from .helpers import get_device_id LOGGER = logging.getLogger(__name__) EVENT_VALUE_UPDATED = "value updated" -@callback -def get_device_id(client: ZwaveClient, node: ZwaveNode) -> Tuple[str, str]: - """Get device registry identifier for Z-Wave node.""" - return (DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}") - - class ZWaveBaseEntity(Entity): """Generic Entity Class for a Z-Wave Device.""" diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py new file mode 100644 index 00000000000..cc00c39b747 --- /dev/null +++ b/homeassistant/components/zwave_js/helpers.py @@ -0,0 +1,100 @@ +"""Helper functions for Z-Wave JS integration.""" +from typing import List, Tuple, cast + +from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.model.node import Node as ZwaveNode + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import async_get as async_get_dev_reg +from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg + +from .const import DATA_CLIENT, DOMAIN + + +@callback +def get_device_id(client: ZwaveClient, node: ZwaveNode) -> Tuple[str, str]: + """Get device registry identifier for Z-Wave node.""" + return (DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}") + + +@callback +def get_home_and_node_id_from_device_id(device_id: Tuple[str, str]) -> List[str]: + """ + Get home ID and node ID for Z-Wave device registry entry. + + Returns [home_id, node_id] + """ + return device_id[1].split("-") + + +@callback +def async_get_node_from_device_id(hass: HomeAssistant, device_id: str) -> ZwaveNode: + """ + Get node from a device ID. + + Raises ValueError if device is invalid or node can't be found. + """ + device_entry = async_get_dev_reg(hass).async_get(device_id) + + if not device_entry: + raise ValueError("Device ID is not valid") + + # Use device config entry ID's to validate that this is a valid zwave_js device + # and to get the client + config_entry_ids = device_entry.config_entries + config_entry_id = next( + ( + config_entry_id + for config_entry_id in config_entry_ids + if cast( + ConfigEntry, + hass.config_entries.async_get_entry(config_entry_id), + ).domain + == DOMAIN + ), + None, + ) + if config_entry_id is None or config_entry_id not in hass.data[DOMAIN]: + raise ValueError("Device is not from an existing zwave_js config entry") + + client = hass.data[DOMAIN][config_entry_id][DATA_CLIENT] + + # Get node ID from device identifier, perform some validation, and then get the + # node + identifier = next( + ( + get_home_and_node_id_from_device_id(identifier) + for identifier in device_entry.identifiers + if identifier[0] == DOMAIN + ), + None, + ) + + node_id = int(identifier[1]) if identifier is not None else None + + if node_id is None or node_id not in client.driver.controller.nodes: + raise ValueError("Device node can't be found") + + return client.driver.controller.nodes[node_id] + + +@callback +def async_get_node_from_entity_id(hass: HomeAssistant, entity_id: str) -> ZwaveNode: + """ + Get node from an entity ID. + + Raises ValueError if entity is invalid. + """ + entity_entry = async_get_ent_reg(hass).async_get(entity_id) + + if not entity_entry: + raise ValueError("Entity ID is not valid") + + if entity_entry.platform != DOMAIN: + raise ValueError("Entity is not from zwave_js integration") + + # Assert for mypy, safe because we know that zwave_js entities are always + # tied to a device + assert entity_entry.device_id + return async_get_node_from_device_id(hass, entity_entry.device_id) diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py new file mode 100644 index 00000000000..da60ddab666 --- /dev/null +++ b/homeassistant/components/zwave_js/services.py @@ -0,0 +1,110 @@ +"""Methods and classes related to executing Z-Wave commands and publishing these to hass.""" + +import logging +from typing import Dict, Set, Union + +import voluptuous as vol +from zwave_js_server.model.node import Node as ZwaveNode +from zwave_js_server.util.node import async_set_config_parameter + +from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant, ServiceCall, callback +import homeassistant.helpers.config_validation as cv + +from . import const +from .helpers import async_get_node_from_device_id, async_get_node_from_entity_id + +_LOGGER = logging.getLogger(__name__) + + +def parameter_name_does_not_need_bitmask( + val: Dict[str, Union[int, str]] +) -> Dict[str, Union[int, str]]: + """Validate that if a parameter name is provided, bitmask is not as well.""" + if isinstance(val[const.ATTR_CONFIG_PARAMETER], str) and ( + val.get(const.ATTR_CONFIG_PARAMETER_BITMASK) + ): + raise vol.Invalid( + "Don't include a bitmask when a parameter name is specified", + path=[const.ATTR_CONFIG_PARAMETER, const.ATTR_CONFIG_PARAMETER_BITMASK], + ) + return val + + +# Validates that a bitmask is provided in hex form and converts it to decimal +# int equivalent since that's what the library uses +BITMASK_SCHEMA = vol.All( + cv.string, vol.Lower, vol.Match(r"^(0x)?[0-9a-f]+$"), lambda value: int(value, 16) +) + + +class ZWaveServices: + """Class that holds our services (Zwave Commands) that should be published to hass.""" + + def __init__(self, hass: HomeAssistant): + """Initialize with hass object.""" + self._hass = hass + + @callback + def async_register(self) -> None: + """Register all our services.""" + self._hass.services.async_register( + const.DOMAIN, + const.SERVICE_SET_CONFIG_PARAMETER, + self.async_set_config_parameter, + schema=vol.All( + { + vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Any( + vol.Coerce(int), cv.string + ), + vol.Optional(const.ATTR_CONFIG_PARAMETER_BITMASK): vol.Any( + vol.Coerce(int), BITMASK_SCHEMA + ), + vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( + vol.Coerce(int), cv.string + ), + }, + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + parameter_name_does_not_need_bitmask, + ), + ) + + async def async_set_config_parameter(self, service: ServiceCall) -> None: + """Set a config value on a node.""" + nodes: Set[ZwaveNode] = set() + if ATTR_ENTITY_ID in service.data: + nodes |= { + async_get_node_from_entity_id(self._hass, entity_id) + for entity_id in service.data[ATTR_ENTITY_ID] + } + if ATTR_DEVICE_ID in service.data: + nodes |= { + async_get_node_from_device_id(self._hass, device_id) + for device_id in service.data[ATTR_DEVICE_ID] + } + property_or_property_name = service.data[const.ATTR_CONFIG_PARAMETER] + property_key = service.data.get(const.ATTR_CONFIG_PARAMETER_BITMASK) + new_value = service.data[const.ATTR_CONFIG_VALUE] + + for node in nodes: + zwave_value = await async_set_config_parameter( + node, + new_value, + property_or_property_name, + property_key=property_key, + ) + + if zwave_value: + _LOGGER.info( + "Set configuration parameter %s on Node %s with value %s", + zwave_value, + node, + new_value, + ) + else: + raise ValueError( + f"Unable to set configuration parameter on Node {node} with " + f"value {new_value}" + ) diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml index d2f1c75b64e..a5e9efd7216 100644 --- a/homeassistant/components/zwave_js/services.yaml +++ b/homeassistant/components/zwave_js/services.yaml @@ -38,3 +38,31 @@ set_lock_usercode: example: 1234 selector: text: + +set_config_parameter: + name: Set a Z-Wave device configuration parameter + description: Allow for changing configuration parameters of your Z-Wave devices. + target: + entity: + integration: zwave_js + fields: + parameter: + name: Parameter + description: The (name or id of the) configuration parameter you want to configure. + example: Minimum brightness level + required: true + selector: + text: + value: + name: Value + description: The new value to set for this configuration parameter. + example: 5 + required: true + selector: + object: + bitmask: + name: Bitmask + description: Target a specific bitmask (see the documentation for more information). + advanced: true + selector: + object: diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 6e41da42c8f..6a255becf2d 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -8,7 +8,7 @@ from zwave_js_server.model.node import Node from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.zwave_js.const import DOMAIN -from homeassistant.components.zwave_js.entity import get_device_id +from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.config_entries import ( CONN_CLASS_LOCAL_PUSH, ENTRY_STATE_LOADED, diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py new file mode 100644 index 00000000000..b085d9e32fb --- /dev/null +++ b/tests/components/zwave_js/test_services.py @@ -0,0 +1,295 @@ +"""Test the Z-Wave JS services.""" +import pytest +import voluptuous as vol + +from homeassistant.components.zwave_js.const import ( + ATTR_CONFIG_PARAMETER, + ATTR_CONFIG_PARAMETER_BITMASK, + ATTR_CONFIG_VALUE, + DOMAIN, + SERVICE_SET_CONFIG_PARAMETER, +) +from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID +from homeassistant.helpers.device_registry import async_get as async_get_dev_reg +from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg + +from .common import AIR_TEMPERATURE_SENSOR + +from tests.common import MockConfigEntry + + +async def test_set_config_parameter(hass, client, multisensor_6, integration): + """Test the set_config_parameter service.""" + dev_reg = async_get_dev_reg(hass) + ent_reg = async_get_ent_reg(hass) + entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR) + + # Test setting config parameter by property and property_key + await hass.services.async_call( + DOMAIN, + SERVICE_SET_CONFIG_PARAMETER, + { + ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR, + ATTR_CONFIG_PARAMETER: 102, + ATTR_CONFIG_PARAMETER_BITMASK: 1, + ATTR_CONFIG_VALUE: 1, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 52 + assert args["valueId"] == { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyName": "Group 2: Send battery reports", + "propertyKey": 1, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": True, + "label": "Group 2: Send battery reports", + "description": "Include battery information in periodic reports to Group 2", + "isFromConfig": True, + }, + "value": 0, + } + assert args["value"] == 1 + + client.async_send_command.reset_mock() + + # Test setting parameter by property name + await hass.services.async_call( + DOMAIN, + SERVICE_SET_CONFIG_PARAMETER, + { + ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR, + ATTR_CONFIG_PARAMETER: "Group 2: Send battery reports", + ATTR_CONFIG_VALUE: 1, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 52 + assert args["valueId"] == { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyName": "Group 2: Send battery reports", + "propertyKey": 1, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": True, + "label": "Group 2: Send battery reports", + "description": "Include battery information in periodic reports to Group 2", + "isFromConfig": True, + }, + "value": 0, + } + assert args["value"] == 1 + + client.async_send_command.reset_mock() + + # Test setting parameter by property name and state label + await hass.services.async_call( + DOMAIN, + SERVICE_SET_CONFIG_PARAMETER, + { + ATTR_DEVICE_ID: entity_entry.device_id, + ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)", + ATTR_CONFIG_VALUE: "Fahrenheit", + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 52 + assert args["valueId"] == { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 41, + "propertyName": "Temperature Threshold (Unit)", + "propertyKey": 15, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "valueSize": 3, + "min": 1, + "max": 2, + "default": 1, + "format": 0, + "allowManualEntry": False, + "states": {"1": "Celsius", "2": "Fahrenheit"}, + "label": "Temperature Threshold (Unit)", + "isFromConfig": True, + }, + "value": 0, + } + assert args["value"] == 2 + + client.async_send_command.reset_mock() + + # Test setting parameter by property and bitmask + await hass.services.async_call( + DOMAIN, + SERVICE_SET_CONFIG_PARAMETER, + { + ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR, + ATTR_CONFIG_PARAMETER: 102, + ATTR_CONFIG_PARAMETER_BITMASK: "0x01", + ATTR_CONFIG_VALUE: 1, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 52 + assert args["valueId"] == { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyName": "Group 2: Send battery reports", + "propertyKey": 1, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": True, + "label": "Group 2: Send battery reports", + "description": "Include battery information in periodic reports to Group 2", + "isFromConfig": True, + }, + "value": 0, + } + assert args["value"] == 1 + + # Test that an invalid entity ID raises a ValueError + with pytest.raises(ValueError): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_CONFIG_PARAMETER, + { + ATTR_ENTITY_ID: "sensor.fake_entity", + ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)", + ATTR_CONFIG_VALUE: "Fahrenheit", + }, + blocking=True, + ) + + # Test that an invalid device ID raises a ValueError + with pytest.raises(ValueError): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_CONFIG_PARAMETER, + { + ATTR_DEVICE_ID: "fake_device_id", + ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)", + ATTR_CONFIG_VALUE: "Fahrenheit", + }, + blocking=True, + ) + + # Test that we can't include a bitmask value if parameter is a string + with pytest.raises(vol.Invalid): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_CONFIG_PARAMETER, + { + ATTR_DEVICE_ID: entity_entry.device_id, + ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)", + ATTR_CONFIG_PARAMETER_BITMASK: 1, + ATTR_CONFIG_VALUE: "Fahrenheit", + }, + blocking=True, + ) + + non_zwave_js_config_entry = MockConfigEntry(entry_id="fake_entry_id") + non_zwave_js_config_entry.add_to_hass(hass) + non_zwave_js_device = dev_reg.async_get_or_create( + config_entry_id=non_zwave_js_config_entry.entry_id, + identifiers={("test", "test")}, + ) + + # Test that a non Z-Wave JS device raises a ValueError + with pytest.raises(ValueError): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_CONFIG_PARAMETER, + { + ATTR_DEVICE_ID: non_zwave_js_device.id, + ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)", + ATTR_CONFIG_VALUE: "Fahrenheit", + }, + blocking=True, + ) + + zwave_js_device_with_invalid_node_id = dev_reg.async_get_or_create( + config_entry_id=integration.entry_id, identifiers={(DOMAIN, "500-500")} + ) + + # Test that a Z-Wave JS device with an invalid node ID raises a ValueError + with pytest.raises(ValueError): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_CONFIG_PARAMETER, + { + ATTR_DEVICE_ID: zwave_js_device_with_invalid_node_id.id, + ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)", + ATTR_CONFIG_VALUE: "Fahrenheit", + }, + blocking=True, + ) + + non_zwave_js_entity = ent_reg.async_get_or_create( + "test", + "sensor", + "test_sensor", + suggested_object_id="test_sensor", + config_entry=non_zwave_js_config_entry, + ) + + # Test that a non Z-Wave JS entity raises a ValueError + with pytest.raises(ValueError): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_CONFIG_PARAMETER, + { + ATTR_ENTITY_ID: non_zwave_js_entity.entity_id, + ATTR_CONFIG_PARAMETER: "Temperature Threshold (Unit)", + ATTR_CONFIG_VALUE: "Fahrenheit", + }, + blocking=True, + ) From ffe42e150a8f061bef86e438efd120eafb884a43 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Feb 2021 12:03:30 -0600 Subject: [PATCH 666/796] Add missing target to increase_speed/decrease_speed service (#46939) --- homeassistant/components/fan/services.yaml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index 3c8eb2d0761..f86a32823dc 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -112,10 +112,8 @@ set_direction: increase_speed: name: Increase speed description: Increase the speed of the fan by one speed or a percentage_step. + target: fields: - entity_id: - description: Name(s) of the entities to increase speed - example: "fan.living_room" percentage_step: advanced: true required: false @@ -132,10 +130,8 @@ increase_speed: decrease_speed: name: Decrease speed description: Decrease the speed of the fan by one speed or a percentage_step. + target: fields: - entity_id: - description: Name(s) of the entities to decrease speed - example: "fan.living_room" percentage_step: advanced: true required: false From 6a8b5ee51b15fa74c1f237a96811b6eea4826552 Mon Sep 17 00:00:00 2001 From: Jon Caruana Date: Tue, 23 Feb 2021 12:20:58 -0800 Subject: [PATCH 667/796] LiteJet is now configured using config_flow (#44409) Co-authored-by: J. Nick Koston --- CODEOWNERS | 1 + homeassistant/components/litejet/__init__.py | 91 +++++--- .../components/litejet/config_flow.py | 53 +++++ homeassistant/components/litejet/const.py | 8 + homeassistant/components/litejet/light.py | 43 ++-- .../components/litejet/manifest.json | 5 +- homeassistant/components/litejet/scene.py | 40 +++- homeassistant/components/litejet/strings.json | 19 ++ homeassistant/components/litejet/switch.py | 49 +++-- .../components/litejet/translations/en.json | 19 ++ homeassistant/components/litejet/trigger.py | 15 +- homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/litejet/__init__.py | 50 +++++ tests/components/litejet/conftest.py | 64 +++++- tests/components/litejet/test_config_flow.py | 77 +++++++ tests/components/litejet/test_init.py | 51 ++--- tests/components/litejet/test_light.py | 204 ++++++++---------- tests/components/litejet/test_scene.py | 65 ++---- tests/components/litejet/test_switch.py | 150 +++++-------- tests/components/litejet/test_trigger.py | 168 ++++++++------- 22 files changed, 725 insertions(+), 452 deletions(-) create mode 100644 homeassistant/components/litejet/config_flow.py create mode 100644 homeassistant/components/litejet/const.py create mode 100644 homeassistant/components/litejet/strings.json create mode 100644 homeassistant/components/litejet/translations/en.json create mode 100644 tests/components/litejet/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index b20b489ac6d..e4e2ab59615 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -253,6 +253,7 @@ homeassistant/components/launch_library/* @ludeeus homeassistant/components/lcn/* @alengwenus homeassistant/components/life360/* @pnbruckner homeassistant/components/linux_battery/* @fabaff +homeassistant/components/litejet/* @joncar homeassistant/components/litterrobot/* @natekspencer homeassistant/components/local_ip/* @issacg homeassistant/components/logger/* @home-assistant/core diff --git a/homeassistant/components/litejet/__init__.py b/homeassistant/components/litejet/__init__.py index 9977bb9bdb4..0c8f59c4127 100644 --- a/homeassistant/components/litejet/__init__.py +++ b/homeassistant/components/litejet/__init__.py @@ -1,49 +1,86 @@ """Support for the LiteJet lighting system.""" -from pylitejet import LiteJet +import asyncio +import logging + +import pylitejet +from serial import SerialException import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PORT -from homeassistant.helpers import discovery +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv -CONF_EXCLUDE_NAMES = "exclude_names" -CONF_INCLUDE_SWITCHES = "include_switches" +from .const import CONF_EXCLUDE_NAMES, CONF_INCLUDE_SWITCHES, DOMAIN, PLATFORMS -DOMAIN = "litejet" +_LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_PORT): cv.string, - vol.Optional(CONF_EXCLUDE_NAMES): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_INCLUDE_SWITCHES, default=False): cv.boolean, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_PORT): cv.string, + vol.Optional(CONF_EXCLUDE_NAMES): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(CONF_INCLUDE_SWITCHES, default=False): cv.boolean, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) def setup(hass, config): """Set up the LiteJet component.""" + if DOMAIN in config and not hass.config_entries.async_entries(DOMAIN): + # No config entry exists and configuration.yaml config exists, trigger the import flow. + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] + ) + ) + return True - url = config[DOMAIN].get(CONF_PORT) - hass.data["litejet_system"] = LiteJet(url) - hass.data["litejet_config"] = config[DOMAIN] +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up LiteJet via a config entry.""" + port = entry.data[CONF_PORT] - discovery.load_platform(hass, "light", DOMAIN, {}, config) - if config[DOMAIN].get(CONF_INCLUDE_SWITCHES): - discovery.load_platform(hass, "switch", DOMAIN, {}, config) - discovery.load_platform(hass, "scene", DOMAIN, {}, config) + try: + system = pylitejet.LiteJet(port) + except SerialException as ex: + _LOGGER.error("Error connecting to the LiteJet MCP at %s", port, exc_info=ex) + raise ConfigEntryNotReady from ex + + hass.data[DOMAIN] = system + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) return True -def is_ignored(hass, name): - """Determine if a load, switch, or scene should be ignored.""" - for prefix in hass.data["litejet_config"].get(CONF_EXCLUDE_NAMES, []): - if name.startswith(prefix): - return True - return False +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a LiteJet 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].close() + hass.data.pop(DOMAIN) + + return unload_ok diff --git a/homeassistant/components/litejet/config_flow.py b/homeassistant/components/litejet/config_flow.py new file mode 100644 index 00000000000..e1c7d8ab7b9 --- /dev/null +++ b/homeassistant/components/litejet/config_flow.py @@ -0,0 +1,53 @@ +"""Config flow for the LiteJet lighting system.""" +import logging +from typing import Any, Dict, Optional + +import pylitejet +from serial import SerialException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PORT + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class LiteJetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """LiteJet config flow.""" + + async def async_step_user( + self, user_input: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Create a LiteJet config entry based upon user input.""" + if self.hass.config_entries.async_entries(DOMAIN): + return self.async_abort(reason="single_instance_allowed") + + errors = {} + if user_input is not None: + port = user_input[CONF_PORT] + + await self.async_set_unique_id(port) + self._abort_if_unique_id_configured() + + try: + system = pylitejet.LiteJet(port) + system.close() + except SerialException: + errors[CONF_PORT] = "open_failed" + else: + return self.async_create_entry( + title=port, + data={CONF_PORT: port}, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_PORT): str}), + errors=errors, + ) + + async def async_step_import(self, import_data): + """Import litejet config from configuration.yaml.""" + return self.async_create_entry(title=import_data[CONF_PORT], data=import_data) diff --git a/homeassistant/components/litejet/const.py b/homeassistant/components/litejet/const.py new file mode 100644 index 00000000000..8e27aa3a0a7 --- /dev/null +++ b/homeassistant/components/litejet/const.py @@ -0,0 +1,8 @@ +"""LiteJet constants.""" + +DOMAIN = "litejet" + +CONF_EXCLUDE_NAMES = "exclude_names" +CONF_INCLUDE_SWITCHES = "include_switches" + +PLATFORMS = ["light", "switch", "scene"] diff --git a/homeassistant/components/litejet/light.py b/homeassistant/components/litejet/light.py index efc6830d775..27ce904cc2c 100644 --- a/homeassistant/components/litejet/light.py +++ b/homeassistant/components/litejet/light.py @@ -1,43 +1,53 @@ """Support for LiteJet lights.""" import logging -from homeassistant.components import litejet from homeassistant.components.light import ( ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, LightEntity, ) +from .const import DOMAIN + _LOGGER = logging.getLogger(__name__) ATTR_NUMBER = "number" -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up lights for the LiteJet platform.""" - litejet_ = hass.data["litejet_system"] +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up entry.""" - devices = [] - for i in litejet_.loads(): - name = litejet_.get_load_name(i) - if not litejet.is_ignored(hass, name): - devices.append(LiteJetLight(hass, litejet_, i, name)) - add_entities(devices, True) + system = hass.data[DOMAIN] + + def get_entities(system): + entities = [] + for i in system.loads(): + name = system.get_load_name(i) + entities.append(LiteJetLight(config_entry.entry_id, system, i, name)) + return entities + + async_add_entities(await hass.async_add_executor_job(get_entities, system), True) class LiteJetLight(LightEntity): """Representation of a single LiteJet light.""" - def __init__(self, hass, lj, i, name): + def __init__(self, entry_id, lj, i, name): """Initialize a LiteJet light.""" - self._hass = hass + self._entry_id = entry_id self._lj = lj self._index = i self._brightness = 0 self._name = name - lj.on_load_activated(i, self._on_load_changed) - lj.on_load_deactivated(i, self._on_load_changed) + async def async_added_to_hass(self): + """Run when this Entity has been added to HA.""" + self._lj.on_load_activated(self._index, self._on_load_changed) + self._lj.on_load_deactivated(self._index, self._on_load_changed) + + async def async_will_remove_from_hass(self): + """Entity being removed from hass.""" + self._lj.unsubscribe(self._on_load_changed) def _on_load_changed(self): """Handle state changes.""" @@ -54,6 +64,11 @@ class LiteJetLight(LightEntity): """Return the light's name.""" return self._name + @property + def unique_id(self): + """Return a unique identifier for this light.""" + return f"{self._entry_id}_{self._index}" + @property def brightness(self): """Return the light's brightness.""" diff --git a/homeassistant/components/litejet/manifest.json b/homeassistant/components/litejet/manifest.json index 1e469370b43..e23e5ac2964 100644 --- a/homeassistant/components/litejet/manifest.json +++ b/homeassistant/components/litejet/manifest.json @@ -2,6 +2,7 @@ "domain": "litejet", "name": "LiteJet", "documentation": "https://www.home-assistant.io/integrations/litejet", - "requirements": ["pylitejet==0.1"], - "codeowners": [] + "requirements": ["pylitejet==0.3.0"], + "codeowners": ["@joncar"], + "config_flow": true } diff --git a/homeassistant/components/litejet/scene.py b/homeassistant/components/litejet/scene.py index 3311b8d86a0..daadfce90dc 100644 --- a/homeassistant/components/litejet/scene.py +++ b/homeassistant/components/litejet/scene.py @@ -1,29 +1,37 @@ """Support for LiteJet scenes.""" +import logging from typing import Any -from homeassistant.components import litejet from homeassistant.components.scene import Scene +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + ATTR_NUMBER = "number" -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up scenes for the LiteJet platform.""" - litejet_ = hass.data["litejet_system"] +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up entry.""" - devices = [] - for i in litejet_.scenes(): - name = litejet_.get_scene_name(i) - if not litejet.is_ignored(hass, name): - devices.append(LiteJetScene(litejet_, i, name)) - add_entities(devices) + system = hass.data[DOMAIN] + + def get_entities(system): + entities = [] + for i in system.scenes(): + name = system.get_scene_name(i) + entities.append(LiteJetScene(config_entry.entry_id, system, i, name)) + return entities + + async_add_entities(await hass.async_add_executor_job(get_entities, system), True) class LiteJetScene(Scene): """Representation of a single LiteJet scene.""" - def __init__(self, lj, i, name): + def __init__(self, entry_id, lj, i, name): """Initialize the scene.""" + self._entry_id = entry_id self._lj = lj self._index = i self._name = name @@ -33,6 +41,11 @@ class LiteJetScene(Scene): """Return the name of the scene.""" return self._name + @property + def unique_id(self): + """Return a unique identifier for this scene.""" + return f"{self._entry_id}_{self._index}" + @property def device_state_attributes(self): """Return the device-specific state attributes.""" @@ -41,3 +54,8 @@ class LiteJetScene(Scene): def activate(self, **kwargs: Any) -> None: """Activate the scene.""" self._lj.activate_scene(self._index) + + @property + def entity_registry_enabled_default(self) -> bool: + """Scenes are only enabled by explicit user choice.""" + return False diff --git a/homeassistant/components/litejet/strings.json b/homeassistant/components/litejet/strings.json new file mode 100644 index 00000000000..79c4ed5f329 --- /dev/null +++ b/homeassistant/components/litejet/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "title": "Connect To LiteJet", + "description": "Connect the LiteJet's RS232-2 port to your computer and enter the path to the serial port device.\n\nThe LiteJet MCP must be configured for 19.2 K baud, 8 data bits, 1 stop bit, no parity, and to transmit a 'CR' after each response.", + "data": { + "port": "[%key:common::config_flow::data::port%]" + } + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + }, + "error": { + "open_failed": "Cannot open the specified serial port." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/switch.py b/homeassistant/components/litejet/switch.py index a734dc46d3e..b782a4a9d98 100644 --- a/homeassistant/components/litejet/switch.py +++ b/homeassistant/components/litejet/switch.py @@ -1,39 +1,50 @@ """Support for LiteJet switch.""" import logging -from homeassistant.components import litejet from homeassistant.components.switch import SwitchEntity +from .const import DOMAIN + ATTR_NUMBER = "number" _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the LiteJet switch platform.""" - litejet_ = hass.data["litejet_system"] +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up entry.""" - devices = [] - for i in litejet_.button_switches(): - name = litejet_.get_switch_name(i) - if not litejet.is_ignored(hass, name): - devices.append(LiteJetSwitch(hass, litejet_, i, name)) - add_entities(devices, True) + system = hass.data[DOMAIN] + + def get_entities(system): + entities = [] + for i in system.button_switches(): + name = system.get_switch_name(i) + entities.append(LiteJetSwitch(config_entry.entry_id, system, i, name)) + return entities + + async_add_entities(await hass.async_add_executor_job(get_entities, system), True) class LiteJetSwitch(SwitchEntity): """Representation of a single LiteJet switch.""" - def __init__(self, hass, lj, i, name): + def __init__(self, entry_id, lj, i, name): """Initialize a LiteJet switch.""" - self._hass = hass + self._entry_id = entry_id self._lj = lj self._index = i self._state = False self._name = name - lj.on_switch_pressed(i, self._on_switch_pressed) - lj.on_switch_released(i, self._on_switch_released) + async def async_added_to_hass(self): + """Run when this Entity has been added to HA.""" + self._lj.on_switch_pressed(self._index, self._on_switch_pressed) + self._lj.on_switch_released(self._index, self._on_switch_released) + + async def async_will_remove_from_hass(self): + """Entity being removed from hass.""" + self._lj.unsubscribe(self._on_switch_pressed) + self._lj.unsubscribe(self._on_switch_released) def _on_switch_pressed(self): _LOGGER.debug("Updating pressed for %s", self._name) @@ -50,6 +61,11 @@ class LiteJetSwitch(SwitchEntity): """Return the name of the switch.""" return self._name + @property + def unique_id(self): + """Return a unique identifier for this switch.""" + return f"{self._entry_id}_{self._index}" + @property def is_on(self): """Return if the switch is pressed.""" @@ -72,3 +88,8 @@ class LiteJetSwitch(SwitchEntity): def turn_off(self, **kwargs): """Release the switch.""" self._lj.release_switch(self._index) + + @property + def entity_registry_enabled_default(self) -> bool: + """Switches are only enabled by explicit user choice.""" + return False diff --git a/homeassistant/components/litejet/translations/en.json b/homeassistant/components/litejet/translations/en.json new file mode 100644 index 00000000000..e09b20dc9f2 --- /dev/null +++ b/homeassistant/components/litejet/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "error": { + "open_failed": "Cannot open the specified serial port." + }, + "step": { + "user": { + "data": { + "port": "Port" + }, + "description": "Connect the LiteJet's RS232-2 port to your computer and enter the path to the serial port device.\n\nThe LiteJet MCP must be configured for 19.2 K baud, 8 data bits, 1 stop bit, no parity, and to transmit a 'CR' after each response.", + "title": "Connect To LiteJet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/trigger.py b/homeassistant/components/litejet/trigger.py index 0b0117465df..71841d9c4fd 100644 --- a/homeassistant/components/litejet/trigger.py +++ b/homeassistant/components/litejet/trigger.py @@ -1,4 +1,6 @@ """Trigger an automation when a LiteJet switch is released.""" +from typing import Callable + import voluptuous as vol from homeassistant.const import CONF_PLATFORM @@ -7,7 +9,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_utc_time import homeassistant.util.dt as dt_util -# mypy: allow-untyped-defs, no-check-untyped-defs +from .const import DOMAIN CONF_NUMBER = "number" CONF_HELD_MORE_THAN = "held_more_than" @@ -33,7 +35,7 @@ async def async_attach_trigger(hass, config, action, automation_info): held_more_than = config.get(CONF_HELD_MORE_THAN) held_less_than = config.get(CONF_HELD_LESS_THAN) pressed_time = None - cancel_pressed_more_than = None + cancel_pressed_more_than: Callable = None job = HassJob(action) @callback @@ -91,12 +93,15 @@ async def async_attach_trigger(hass, config, action, automation_info): ): hass.add_job(call_action) - hass.data["litejet_system"].on_switch_pressed(number, pressed) - hass.data["litejet_system"].on_switch_released(number, released) + system = hass.data[DOMAIN] + + system.on_switch_pressed(number, pressed) + system.on_switch_released(number, released) @callback def async_remove(): """Remove all subscriptions used for this trigger.""" - return + system.unsubscribe(pressed) + system.unsubscribe(released) return async_remove diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 36c22262ef4..e8e06ed7a15 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -121,6 +121,7 @@ FLOWS = [ "kulersky", "life360", "lifx", + "litejet", "litterrobot", "local_ip", "locative", diff --git a/requirements_all.txt b/requirements_all.txt index 00bd7af0d74..5f971b4ece7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1501,7 +1501,7 @@ pylgnetcast-homeassistant==0.2.0.dev0 pylibrespot-java==0.1.0 # homeassistant.components.litejet -pylitejet==0.1 +pylitejet==0.3.0 # homeassistant.components.litterrobot pylitterbot==2021.2.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a0954f41ad..c78f2fbb96a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -791,7 +791,7 @@ pylast==4.1.0 pylibrespot-java==0.1.0 # homeassistant.components.litejet -pylitejet==0.1 +pylitejet==0.3.0 # homeassistant.components.litterrobot pylitterbot==2021.2.5 diff --git a/tests/components/litejet/__init__.py b/tests/components/litejet/__init__.py index 9a01fbe5114..13e2b547cd8 100644 --- a/tests/components/litejet/__init__.py +++ b/tests/components/litejet/__init__.py @@ -1 +1,51 @@ """Tests for the litejet component.""" +from homeassistant.components import scene, switch +from homeassistant.components.litejet import DOMAIN +from homeassistant.const import CONF_PORT + +from tests.common import MockConfigEntry + + +async def async_init_integration( + hass, use_switch=False, use_scene=False +) -> MockConfigEntry: + """Set up the LiteJet integration in Home Assistant.""" + + registry = await hass.helpers.entity_registry.async_get_registry() + + entry_data = {CONF_PORT: "/dev/mock"} + + entry = MockConfigEntry( + domain=DOMAIN, unique_id=entry_data[CONF_PORT], data=entry_data + ) + + if use_switch: + registry.async_get_or_create( + switch.DOMAIN, + DOMAIN, + f"{entry.entry_id}_1", + suggested_object_id="mock_switch_1", + disabled_by=None, + ) + registry.async_get_or_create( + switch.DOMAIN, + DOMAIN, + f"{entry.entry_id}_2", + suggested_object_id="mock_switch_2", + disabled_by=None, + ) + + if use_scene: + registry.async_get_or_create( + scene.DOMAIN, + DOMAIN, + f"{entry.entry_id}_1", + suggested_object_id="mock_scene_1", + disabled_by=None, + ) + + 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/litejet/conftest.py b/tests/components/litejet/conftest.py index 68797f96ccf..00b1eb92190 100644 --- a/tests/components/litejet/conftest.py +++ b/tests/components/litejet/conftest.py @@ -1,2 +1,62 @@ -"""litejet conftest.""" -from tests.components.light.conftest import mock_light_profiles # noqa +"""Fixtures for LiteJet testing.""" +from datetime import timedelta +from unittest.mock import patch + +import pytest + +import homeassistant.util.dt as dt_util + + +@pytest.fixture +def mock_litejet(): + """Mock LiteJet system.""" + with patch("pylitejet.LiteJet") as mock_pylitejet: + + def get_load_name(number): + return f"Mock Load #{number}" + + def get_scene_name(number): + return f"Mock Scene #{number}" + + def get_switch_name(number): + return f"Mock Switch #{number}" + + mock_lj = mock_pylitejet.return_value + + mock_lj.switch_pressed_callbacks = {} + mock_lj.switch_released_callbacks = {} + mock_lj.load_activated_callbacks = {} + mock_lj.load_deactivated_callbacks = {} + + def on_switch_pressed(number, callback): + mock_lj.switch_pressed_callbacks[number] = callback + + def on_switch_released(number, callback): + mock_lj.switch_released_callbacks[number] = callback + + def on_load_activated(number, callback): + mock_lj.load_activated_callbacks[number] = callback + + def on_load_deactivated(number, callback): + mock_lj.load_deactivated_callbacks[number] = callback + + mock_lj.on_switch_pressed.side_effect = on_switch_pressed + mock_lj.on_switch_released.side_effect = on_switch_released + mock_lj.on_load_activated.side_effect = on_load_activated + mock_lj.on_load_deactivated.side_effect = on_load_deactivated + + mock_lj.loads.return_value = range(1, 3) + mock_lj.get_load_name.side_effect = get_load_name + mock_lj.get_load_level.return_value = 0 + + mock_lj.button_switches.return_value = range(1, 3) + mock_lj.all_switches.return_value = range(1, 6) + mock_lj.get_switch_name.side_effect = get_switch_name + + mock_lj.scenes.return_value = range(1, 3) + mock_lj.get_scene_name.side_effect = get_scene_name + + mock_lj.start_time = dt_util.utcnow() + mock_lj.last_delta = timedelta(0) + + yield mock_lj diff --git a/tests/components/litejet/test_config_flow.py b/tests/components/litejet/test_config_flow.py new file mode 100644 index 00000000000..015ba1c6494 --- /dev/null +++ b/tests/components/litejet/test_config_flow.py @@ -0,0 +1,77 @@ +"""The tests for the litejet component.""" +from unittest.mock import patch + +from serial import SerialException + +from homeassistant.components.litejet.const import DOMAIN +from homeassistant.const import CONF_PORT + +from tests.common import MockConfigEntry + + +async def test_show_config_form(hass): + """Test show configuration form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + +async def test_create_entry(hass, mock_litejet): + """Test create entry from user input.""" + test_data = {CONF_PORT: "/dev/test"} + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=test_data + ) + + assert result["type"] == "create_entry" + assert result["title"] == "/dev/test" + assert result["data"] == test_data + + +async def test_flow_entry_already_exists(hass): + """Test user input when a config entry already exists.""" + first_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_PORT: "/dev/first"}, + ) + first_entry.add_to_hass(hass) + + test_data = {CONF_PORT: "/dev/test"} + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=test_data + ) + + assert result["type"] == "abort" + assert result["reason"] == "single_instance_allowed" + + +async def test_flow_open_failed(hass): + """Test user input when serial port open fails.""" + test_data = {CONF_PORT: "/dev/test"} + + with patch("pylitejet.LiteJet") as mock_pylitejet: + mock_pylitejet.side_effect = SerialException + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=test_data + ) + + assert result["type"] == "form" + assert result["errors"][CONF_PORT] == "open_failed" + + +async def test_import_step(hass): + """Test initializing via import step.""" + test_data = {CONF_PORT: "/dev/imported"} + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "import"}, data=test_data + ) + + assert result["type"] == "create_entry" + assert result["title"] == test_data[CONF_PORT] + assert result["data"] == test_data diff --git a/tests/components/litejet/test_init.py b/tests/components/litejet/test_init.py index 4aee0086cbd..63686452621 100644 --- a/tests/components/litejet/test_init.py +++ b/tests/components/litejet/test_init.py @@ -1,41 +1,30 @@ """The tests for the litejet component.""" -import unittest - from homeassistant.components import litejet +from homeassistant.components.litejet.const import DOMAIN +from homeassistant.const import CONF_PORT +from homeassistant.setup import async_setup_component -from tests.common import get_test_home_assistant +from . import async_init_integration -class TestLiteJet(unittest.TestCase): - """Test the litejet component.""" +async def test_setup_with_no_config(hass): + """Test that nothing happens.""" + assert await async_setup_component(hass, DOMAIN, {}) is True + assert DOMAIN not in hass.data - def setup_method(self, method): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.start() - self.hass.block_till_done() - def teardown_method(self, method): - """Stop everything that was started.""" - self.hass.stop() +async def test_setup_with_config_to_import(hass, mock_litejet): + """Test that import happens.""" + assert ( + await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PORT: "/dev/hello"}}) + is True + ) + assert DOMAIN in hass.data - def test_is_ignored_unspecified(self): - """Ensure it is ignored when unspecified.""" - self.hass.data["litejet_config"] = {} - assert not litejet.is_ignored(self.hass, "Test") - def test_is_ignored_empty(self): - """Ensure it is ignored when empty.""" - self.hass.data["litejet_config"] = {litejet.CONF_EXCLUDE_NAMES: []} - assert not litejet.is_ignored(self.hass, "Test") +async def test_unload_entry(hass, mock_litejet): + """Test being able to unload an entry.""" + entry = await async_init_integration(hass, use_switch=True, use_scene=True) - def test_is_ignored_normal(self): - """Test if usually ignored.""" - self.hass.data["litejet_config"] = { - litejet.CONF_EXCLUDE_NAMES: ["Test", "Other One"] - } - assert litejet.is_ignored(self.hass, "Test") - assert not litejet.is_ignored(self.hass, "Other one") - assert not litejet.is_ignored(self.hass, "Other 0ne") - assert litejet.is_ignored(self.hass, "Other One There") - assert litejet.is_ignored(self.hass, "Other One") + assert await litejet.async_unload_entry(hass, entry) + assert DOMAIN not in hass.data diff --git a/tests/components/litejet/test_light.py b/tests/components/litejet/test_light.py index e08bd5c27ac..c455d3a960e 100644 --- a/tests/components/litejet/test_light.py +++ b/tests/components/litejet/test_light.py @@ -1,14 +1,11 @@ """The tests for the litejet component.""" import logging -import unittest -from unittest import mock -from homeassistant import setup -from homeassistant.components import litejet -import homeassistant.components.light as light +from homeassistant.components import light +from homeassistant.components.light import ATTR_BRIGHTNESS +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON -from tests.common import get_test_home_assistant -from tests.components.light import common +from . import async_init_integration _LOGGER = logging.getLogger(__name__) @@ -18,144 +15,113 @@ ENTITY_OTHER_LIGHT = "light.mock_load_2" ENTITY_OTHER_LIGHT_NUMBER = 2 -class TestLiteJetLight(unittest.TestCase): - """Test the litejet component.""" +async def test_on_brightness(hass, mock_litejet): + """Test turning the light on with brightness.""" + await async_init_integration(hass) - @mock.patch("homeassistant.components.litejet.LiteJet") - def setup_method(self, method, mock_pylitejet): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.start() + assert hass.states.get(ENTITY_LIGHT).state == "off" + assert hass.states.get(ENTITY_OTHER_LIGHT).state == "off" - self.load_activated_callbacks = {} - self.load_deactivated_callbacks = {} + assert not light.is_on(hass, ENTITY_LIGHT) - def get_load_name(number): - return f"Mock Load #{number}" + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_BRIGHTNESS: 102}, + blocking=True, + ) + mock_litejet.activate_load_at.assert_called_with(ENTITY_LIGHT_NUMBER, 39, 0) - def on_load_activated(number, callback): - self.load_activated_callbacks[number] = callback - def on_load_deactivated(number, callback): - self.load_deactivated_callbacks[number] = callback +async def test_on_off(hass, mock_litejet): + """Test turning the light on and off.""" + await async_init_integration(hass) - self.mock_lj = mock_pylitejet.return_value - self.mock_lj.loads.return_value = range(1, 3) - self.mock_lj.button_switches.return_value = range(0) - self.mock_lj.all_switches.return_value = range(0) - self.mock_lj.scenes.return_value = range(0) - self.mock_lj.get_load_level.return_value = 0 - self.mock_lj.get_load_name.side_effect = get_load_name - self.mock_lj.on_load_activated.side_effect = on_load_activated - self.mock_lj.on_load_deactivated.side_effect = on_load_deactivated + assert hass.states.get(ENTITY_LIGHT).state == "off" + assert hass.states.get(ENTITY_OTHER_LIGHT).state == "off" - assert setup.setup_component( - self.hass, - litejet.DOMAIN, - {"litejet": {"port": "/dev/serial/by-id/mock-litejet"}}, - ) - self.hass.block_till_done() + assert not light.is_on(hass, ENTITY_LIGHT) - self.mock_lj.get_load_level.reset_mock() + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT}, + blocking=True, + ) + mock_litejet.activate_load.assert_called_with(ENTITY_LIGHT_NUMBER) - def light(self): - """Test for main light entity.""" - return self.hass.states.get(ENTITY_LIGHT) + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT}, + blocking=True, + ) + mock_litejet.deactivate_load.assert_called_with(ENTITY_LIGHT_NUMBER) - def other_light(self): - """Test the other light.""" - return self.hass.states.get(ENTITY_OTHER_LIGHT) - def teardown_method(self, method): - """Stop everything that was started.""" - self.hass.stop() +async def test_activated_event(hass, mock_litejet): + """Test handling an event from LiteJet.""" - def test_on_brightness(self): - """Test turning the light on with brightness.""" - assert self.light().state == "off" - assert self.other_light().state == "off" + await async_init_integration(hass) - assert not light.is_on(self.hass, ENTITY_LIGHT) + # Light 1 + mock_litejet.get_load_level.return_value = 99 + mock_litejet.get_load_level.reset_mock() + mock_litejet.load_activated_callbacks[ENTITY_LIGHT_NUMBER]() + await hass.async_block_till_done() - common.turn_on(self.hass, ENTITY_LIGHT, brightness=102) - self.hass.block_till_done() - self.mock_lj.activate_load_at.assert_called_with(ENTITY_LIGHT_NUMBER, 39, 0) + mock_litejet.get_load_level.assert_called_once_with(ENTITY_LIGHT_NUMBER) - def test_on_off(self): - """Test turning the light on and off.""" - assert self.light().state == "off" - assert self.other_light().state == "off" + assert light.is_on(hass, ENTITY_LIGHT) + assert not light.is_on(hass, ENTITY_OTHER_LIGHT) + assert hass.states.get(ENTITY_LIGHT).state == "on" + assert hass.states.get(ENTITY_OTHER_LIGHT).state == "off" + assert hass.states.get(ENTITY_LIGHT).attributes.get(ATTR_BRIGHTNESS) == 255 - assert not light.is_on(self.hass, ENTITY_LIGHT) + # Light 2 - common.turn_on(self.hass, ENTITY_LIGHT) - self.hass.block_till_done() - self.mock_lj.activate_load.assert_called_with(ENTITY_LIGHT_NUMBER) + mock_litejet.get_load_level.return_value = 40 + mock_litejet.get_load_level.reset_mock() + mock_litejet.load_activated_callbacks[ENTITY_OTHER_LIGHT_NUMBER]() + await hass.async_block_till_done() - common.turn_off(self.hass, ENTITY_LIGHT) - self.hass.block_till_done() - self.mock_lj.deactivate_load.assert_called_with(ENTITY_LIGHT_NUMBER) + mock_litejet.get_load_level.assert_called_once_with(ENTITY_OTHER_LIGHT_NUMBER) - def test_activated_event(self): - """Test handling an event from LiteJet.""" - self.mock_lj.get_load_level.return_value = 99 + assert light.is_on(hass, ENTITY_LIGHT) + assert light.is_on(hass, ENTITY_OTHER_LIGHT) + assert hass.states.get(ENTITY_LIGHT).state == "on" + assert hass.states.get(ENTITY_OTHER_LIGHT).state == "on" + assert ( + int(hass.states.get(ENTITY_OTHER_LIGHT).attributes.get(ATTR_BRIGHTNESS)) == 103 + ) - # Light 1 - _LOGGER.info(self.load_activated_callbacks[ENTITY_LIGHT_NUMBER]) - self.load_activated_callbacks[ENTITY_LIGHT_NUMBER]() - self.hass.block_till_done() +async def test_deactivated_event(hass, mock_litejet): + """Test handling an event from LiteJet.""" + await async_init_integration(hass) - self.mock_lj.get_load_level.assert_called_once_with(ENTITY_LIGHT_NUMBER) + # Initial state is on. + mock_litejet.get_load_level.return_value = 99 - assert light.is_on(self.hass, ENTITY_LIGHT) - assert not light.is_on(self.hass, ENTITY_OTHER_LIGHT) - assert self.light().state == "on" - assert self.other_light().state == "off" - assert self.light().attributes.get(light.ATTR_BRIGHTNESS) == 255 + mock_litejet.load_activated_callbacks[ENTITY_OTHER_LIGHT_NUMBER]() + await hass.async_block_till_done() - # Light 2 + assert light.is_on(hass, ENTITY_OTHER_LIGHT) - self.mock_lj.get_load_level.return_value = 40 + # Event indicates it is off now. - self.mock_lj.get_load_level.reset_mock() + mock_litejet.get_load_level.reset_mock() + mock_litejet.get_load_level.return_value = 0 - self.load_activated_callbacks[ENTITY_OTHER_LIGHT_NUMBER]() - self.hass.block_till_done() + mock_litejet.load_deactivated_callbacks[ENTITY_OTHER_LIGHT_NUMBER]() + await hass.async_block_till_done() - self.mock_lj.get_load_level.assert_called_once_with(ENTITY_OTHER_LIGHT_NUMBER) + # (Requesting the level is not strictly needed with a deactivated + # event but the implementation happens to do it. This could be + # changed to an assert_not_called in the future.) + mock_litejet.get_load_level.assert_called_with(ENTITY_OTHER_LIGHT_NUMBER) - assert light.is_on(self.hass, ENTITY_OTHER_LIGHT) - assert light.is_on(self.hass, ENTITY_LIGHT) - assert self.light().state == "on" - assert self.other_light().state == "on" - assert int(self.other_light().attributes[light.ATTR_BRIGHTNESS]) == 103 - - def test_deactivated_event(self): - """Test handling an event from LiteJet.""" - # Initial state is on. - - self.mock_lj.get_load_level.return_value = 99 - - self.load_activated_callbacks[ENTITY_OTHER_LIGHT_NUMBER]() - self.hass.block_till_done() - - assert light.is_on(self.hass, ENTITY_OTHER_LIGHT) - - # Event indicates it is off now. - - self.mock_lj.get_load_level.reset_mock() - self.mock_lj.get_load_level.return_value = 0 - - self.load_deactivated_callbacks[ENTITY_OTHER_LIGHT_NUMBER]() - self.hass.block_till_done() - - # (Requesting the level is not strictly needed with a deactivated - # event but the implementation happens to do it. This could be - # changed to an assert_not_called in the future.) - self.mock_lj.get_load_level.assert_called_with(ENTITY_OTHER_LIGHT_NUMBER) - - assert not light.is_on(self.hass, ENTITY_OTHER_LIGHT) - assert not light.is_on(self.hass, ENTITY_LIGHT) - assert self.light().state == "off" - assert self.other_light().state == "off" + assert not light.is_on(hass, ENTITY_OTHER_LIGHT) + assert not light.is_on(hass, ENTITY_LIGHT) + assert hass.states.get(ENTITY_LIGHT).state == "off" + assert hass.states.get(ENTITY_OTHER_LIGHT).state == "off" diff --git a/tests/components/litejet/test_scene.py b/tests/components/litejet/test_scene.py index fe9298cf187..5df26f8c680 100644 --- a/tests/components/litejet/test_scene.py +++ b/tests/components/litejet/test_scene.py @@ -1,12 +1,8 @@ """The tests for the litejet component.""" -import unittest -from unittest import mock +from homeassistant.components import scene +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON -from homeassistant import setup -from homeassistant.components import litejet - -from tests.common import get_test_home_assistant -from tests.components.scene import common +from . import async_init_integration ENTITY_SCENE = "scene.mock_scene_1" ENTITY_SCENE_NUMBER = 1 @@ -14,46 +10,31 @@ ENTITY_OTHER_SCENE = "scene.mock_scene_2" ENTITY_OTHER_SCENE_NUMBER = 2 -class TestLiteJetScene(unittest.TestCase): - """Test the litejet component.""" +async def test_disabled_by_default(hass, mock_litejet): + """Test the scene is disabled by default.""" + await async_init_integration(hass) - @mock.patch("homeassistant.components.litejet.LiteJet") - def setup_method(self, method, mock_pylitejet): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.start() + registry = await hass.helpers.entity_registry.async_get_registry() - def get_scene_name(number): - return f"Mock Scene #{number}" + state = hass.states.get(ENTITY_SCENE) + assert state is None - self.mock_lj = mock_pylitejet.return_value - self.mock_lj.loads.return_value = range(0) - self.mock_lj.button_switches.return_value = range(0) - self.mock_lj.all_switches.return_value = range(0) - self.mock_lj.scenes.return_value = range(1, 3) - self.mock_lj.get_scene_name.side_effect = get_scene_name + entry = registry.async_get(ENTITY_SCENE) + assert entry + assert entry.disabled + assert entry.disabled_by == "integration" - assert setup.setup_component( - self.hass, - litejet.DOMAIN, - {"litejet": {"port": "/dev/serial/by-id/mock-litejet"}}, - ) - self.hass.block_till_done() - def teardown_method(self, method): - """Stop everything that was started.""" - self.hass.stop() +async def test_activate(hass, mock_litejet): + """Test activating the scene.""" - def scene(self): - """Get the current scene.""" - return self.hass.states.get(ENTITY_SCENE) + await async_init_integration(hass, use_scene=True) - def other_scene(self): - """Get the other scene.""" - return self.hass.states.get(ENTITY_OTHER_SCENE) + state = hass.states.get(ENTITY_SCENE) + assert state is not None - def test_activate(self): - """Test activating the scene.""" - common.activate(self.hass, ENTITY_SCENE) - self.hass.block_till_done() - self.mock_lj.activate_scene.assert_called_once_with(ENTITY_SCENE_NUMBER) + await hass.services.async_call( + scene.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_SCENE}, blocking=True + ) + + mock_litejet.activate_scene.assert_called_once_with(ENTITY_SCENE_NUMBER) diff --git a/tests/components/litejet/test_switch.py b/tests/components/litejet/test_switch.py index 2f897045c92..dfcb9801093 100644 --- a/tests/components/litejet/test_switch.py +++ b/tests/components/litejet/test_switch.py @@ -1,14 +1,10 @@ """The tests for the litejet component.""" import logging -import unittest -from unittest import mock -from homeassistant import setup -from homeassistant.components import litejet -import homeassistant.components.switch as switch +from homeassistant.components import switch +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON -from tests.common import get_test_home_assistant -from tests.components.switch import common +from . import async_init_integration _LOGGER = logging.getLogger(__name__) @@ -18,117 +14,67 @@ ENTITY_OTHER_SWITCH = "switch.mock_switch_2" ENTITY_OTHER_SWITCH_NUMBER = 2 -class TestLiteJetSwitch(unittest.TestCase): - """Test the litejet component.""" +async def test_on_off(hass, mock_litejet): + """Test turning the switch on and off.""" - @mock.patch("homeassistant.components.litejet.LiteJet") - def setup_method(self, method, mock_pylitejet): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.start() + await async_init_integration(hass, use_switch=True) - self.switch_pressed_callbacks = {} - self.switch_released_callbacks = {} + assert hass.states.get(ENTITY_SWITCH).state == "off" + assert hass.states.get(ENTITY_OTHER_SWITCH).state == "off" - def get_switch_name(number): - return f"Mock Switch #{number}" + assert not switch.is_on(hass, ENTITY_SWITCH) - def on_switch_pressed(number, callback): - self.switch_pressed_callbacks[number] = callback + await hass.services.async_call( + switch.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_SWITCH}, blocking=True + ) + mock_litejet.press_switch.assert_called_with(ENTITY_SWITCH_NUMBER) - def on_switch_released(number, callback): - self.switch_released_callbacks[number] = callback + await hass.services.async_call( + switch.DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_SWITCH}, blocking=True + ) + mock_litejet.release_switch.assert_called_with(ENTITY_SWITCH_NUMBER) - self.mock_lj = mock_pylitejet.return_value - self.mock_lj.loads.return_value = range(0) - self.mock_lj.button_switches.return_value = range(1, 3) - self.mock_lj.all_switches.return_value = range(1, 6) - self.mock_lj.scenes.return_value = range(0) - self.mock_lj.get_switch_name.side_effect = get_switch_name - self.mock_lj.on_switch_pressed.side_effect = on_switch_pressed - self.mock_lj.on_switch_released.side_effect = on_switch_released - config = {"litejet": {"port": "/dev/serial/by-id/mock-litejet"}} - if method == self.test_include_switches_False: - config["litejet"]["include_switches"] = False - elif method != self.test_include_switches_unspecified: - config["litejet"]["include_switches"] = True +async def test_pressed_event(hass, mock_litejet): + """Test handling an event from LiteJet.""" - assert setup.setup_component(self.hass, litejet.DOMAIN, config) - self.hass.block_till_done() + await async_init_integration(hass, use_switch=True) - def teardown_method(self, method): - """Stop everything that was started.""" - self.hass.stop() + # Switch 1 + mock_litejet.switch_pressed_callbacks[ENTITY_SWITCH_NUMBER]() + await hass.async_block_till_done() - def switch(self): - """Return the switch state.""" - return self.hass.states.get(ENTITY_SWITCH) + assert switch.is_on(hass, ENTITY_SWITCH) + assert not switch.is_on(hass, ENTITY_OTHER_SWITCH) + assert hass.states.get(ENTITY_SWITCH).state == "on" + assert hass.states.get(ENTITY_OTHER_SWITCH).state == "off" - def other_switch(self): - """Return the other switch state.""" - return self.hass.states.get(ENTITY_OTHER_SWITCH) + # Switch 2 + mock_litejet.switch_pressed_callbacks[ENTITY_OTHER_SWITCH_NUMBER]() + await hass.async_block_till_done() - def test_include_switches_unspecified(self): - """Test that switches are ignored by default.""" - self.mock_lj.button_switches.assert_not_called() - self.mock_lj.all_switches.assert_not_called() + assert switch.is_on(hass, ENTITY_OTHER_SWITCH) + assert switch.is_on(hass, ENTITY_SWITCH) + assert hass.states.get(ENTITY_SWITCH).state == "on" + assert hass.states.get(ENTITY_OTHER_SWITCH).state == "on" - def test_include_switches_False(self): - """Test that switches can be explicitly ignored.""" - self.mock_lj.button_switches.assert_not_called() - self.mock_lj.all_switches.assert_not_called() - def test_on_off(self): - """Test turning the switch on and off.""" - assert self.switch().state == "off" - assert self.other_switch().state == "off" +async def test_released_event(hass, mock_litejet): + """Test handling an event from LiteJet.""" - assert not switch.is_on(self.hass, ENTITY_SWITCH) + await async_init_integration(hass, use_switch=True) - common.turn_on(self.hass, ENTITY_SWITCH) - self.hass.block_till_done() - self.mock_lj.press_switch.assert_called_with(ENTITY_SWITCH_NUMBER) + # Initial state is on. + mock_litejet.switch_pressed_callbacks[ENTITY_OTHER_SWITCH_NUMBER]() + await hass.async_block_till_done() - common.turn_off(self.hass, ENTITY_SWITCH) - self.hass.block_till_done() - self.mock_lj.release_switch.assert_called_with(ENTITY_SWITCH_NUMBER) + assert switch.is_on(hass, ENTITY_OTHER_SWITCH) - def test_pressed_event(self): - """Test handling an event from LiteJet.""" - # Switch 1 - _LOGGER.info(self.switch_pressed_callbacks[ENTITY_SWITCH_NUMBER]) - self.switch_pressed_callbacks[ENTITY_SWITCH_NUMBER]() - self.hass.block_till_done() + # Event indicates it is off now. + mock_litejet.switch_released_callbacks[ENTITY_OTHER_SWITCH_NUMBER]() + await hass.async_block_till_done() - assert switch.is_on(self.hass, ENTITY_SWITCH) - assert not switch.is_on(self.hass, ENTITY_OTHER_SWITCH) - assert self.switch().state == "on" - assert self.other_switch().state == "off" - - # Switch 2 - self.switch_pressed_callbacks[ENTITY_OTHER_SWITCH_NUMBER]() - self.hass.block_till_done() - - assert switch.is_on(self.hass, ENTITY_OTHER_SWITCH) - assert switch.is_on(self.hass, ENTITY_SWITCH) - assert self.other_switch().state == "on" - assert self.switch().state == "on" - - def test_released_event(self): - """Test handling an event from LiteJet.""" - # Initial state is on. - self.switch_pressed_callbacks[ENTITY_OTHER_SWITCH_NUMBER]() - self.hass.block_till_done() - - assert switch.is_on(self.hass, ENTITY_OTHER_SWITCH) - - # Event indicates it is off now. - - self.switch_released_callbacks[ENTITY_OTHER_SWITCH_NUMBER]() - self.hass.block_till_done() - - assert not switch.is_on(self.hass, ENTITY_OTHER_SWITCH) - assert not switch.is_on(self.hass, ENTITY_SWITCH) - assert self.other_switch().state == "off" - assert self.switch().state == "off" + assert not switch.is_on(hass, ENTITY_OTHER_SWITCH) + assert not switch.is_on(hass, ENTITY_SWITCH) + assert hass.states.get(ENTITY_SWITCH).state == "off" + assert hass.states.get(ENTITY_OTHER_SWITCH).state == "off" diff --git a/tests/components/litejet/test_trigger.py b/tests/components/litejet/test_trigger.py index 3cbbd474b88..216da9b54ef 100644 --- a/tests/components/litejet/test_trigger.py +++ b/tests/components/litejet/test_trigger.py @@ -2,14 +2,16 @@ from datetime import timedelta import logging from unittest import mock +from unittest.mock import patch import pytest from homeassistant import setup -from homeassistant.components import litejet import homeassistant.components.automation as automation import homeassistant.util.dt as dt_util +from . import async_init_integration + from tests.common import async_fire_time_changed, async_mock_service from tests.components.blueprint.conftest import stub_blueprint_populate # noqa @@ -27,88 +29,51 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -def get_switch_name(number): - """Get a mock switch name.""" - return f"Mock Switch #{number}" - - -@pytest.fixture -def mock_lj(hass): - """Initialize components.""" - with mock.patch("homeassistant.components.litejet.LiteJet") as mock_pylitejet: - mock_lj = mock_pylitejet.return_value - - mock_lj.switch_pressed_callbacks = {} - mock_lj.switch_released_callbacks = {} - - def on_switch_pressed(number, callback): - mock_lj.switch_pressed_callbacks[number] = callback - - def on_switch_released(number, callback): - mock_lj.switch_released_callbacks[number] = callback - - mock_lj.loads.return_value = range(0) - mock_lj.button_switches.return_value = range(1, 3) - mock_lj.all_switches.return_value = range(1, 6) - mock_lj.scenes.return_value = range(0) - mock_lj.get_switch_name.side_effect = get_switch_name - mock_lj.on_switch_pressed.side_effect = on_switch_pressed - mock_lj.on_switch_released.side_effect = on_switch_released - - config = {"litejet": {"port": "/dev/serial/by-id/mock-litejet"}} - assert hass.loop.run_until_complete( - setup.async_setup_component(hass, litejet.DOMAIN, config) - ) - - mock_lj.start_time = dt_util.utcnow() - mock_lj.last_delta = timedelta(0) - return mock_lj - - -async def simulate_press(hass, mock_lj, number): +async def simulate_press(hass, mock_litejet, number): """Test to simulate a press.""" _LOGGER.info("*** simulate press of %d", number) - callback = mock_lj.switch_pressed_callbacks.get(number) + callback = mock_litejet.switch_pressed_callbacks.get(number) with mock.patch( "homeassistant.helpers.condition.dt_util.utcnow", - return_value=mock_lj.start_time + mock_lj.last_delta, + return_value=mock_litejet.start_time + mock_litejet.last_delta, ): if callback is not None: await hass.async_add_executor_job(callback) await hass.async_block_till_done() -async def simulate_release(hass, mock_lj, number): +async def simulate_release(hass, mock_litejet, number): """Test to simulate releasing.""" _LOGGER.info("*** simulate release of %d", number) - callback = mock_lj.switch_released_callbacks.get(number) + callback = mock_litejet.switch_released_callbacks.get(number) with mock.patch( "homeassistant.helpers.condition.dt_util.utcnow", - return_value=mock_lj.start_time + mock_lj.last_delta, + return_value=mock_litejet.start_time + mock_litejet.last_delta, ): if callback is not None: await hass.async_add_executor_job(callback) await hass.async_block_till_done() -async def simulate_time(hass, mock_lj, delta): +async def simulate_time(hass, mock_litejet, delta): """Test to simulate time.""" _LOGGER.info( - "*** simulate time change by %s: %s", delta, mock_lj.start_time + delta + "*** simulate time change by %s: %s", delta, mock_litejet.start_time + delta ) - mock_lj.last_delta = delta + mock_litejet.last_delta = delta with mock.patch( "homeassistant.helpers.condition.dt_util.utcnow", - return_value=mock_lj.start_time + delta, + return_value=mock_litejet.start_time + delta, ): _LOGGER.info("now=%s", dt_util.utcnow()) - async_fire_time_changed(hass, mock_lj.start_time + delta) + async_fire_time_changed(hass, mock_litejet.start_time + delta) await hass.async_block_till_done() _LOGGER.info("done with now=%s", dt_util.utcnow()) async def setup_automation(hass, trigger): """Test setting up the automation.""" + await async_init_integration(hass, use_switch=True) assert await setup.async_setup_component( hass, automation.DOMAIN, @@ -125,19 +90,19 @@ async def setup_automation(hass, trigger): await hass.async_block_till_done() -async def test_simple(hass, calls, mock_lj): +async def test_simple(hass, calls, mock_litejet): """Test the simplest form of a LiteJet trigger.""" await setup_automation( hass, {"platform": "litejet", "number": ENTITY_OTHER_SWITCH_NUMBER} ) - await simulate_press(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER) - await simulate_release(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER) + await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) + await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 1 -async def test_held_more_than_short(hass, calls, mock_lj): +async def test_held_more_than_short(hass, calls, mock_litejet): """Test a too short hold.""" await setup_automation( hass, @@ -148,13 +113,13 @@ async def test_held_more_than_short(hass, calls, mock_lj): }, ) - await simulate_press(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER) - await simulate_time(hass, mock_lj, timedelta(seconds=0.1)) - await simulate_release(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER) + await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) + await simulate_time(hass, mock_litejet, timedelta(seconds=0.1)) + await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 -async def test_held_more_than_long(hass, calls, mock_lj): +async def test_held_more_than_long(hass, calls, mock_litejet): """Test a hold that is long enough.""" await setup_automation( hass, @@ -165,15 +130,15 @@ async def test_held_more_than_long(hass, calls, mock_lj): }, ) - await simulate_press(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER) + await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 - await simulate_time(hass, mock_lj, timedelta(seconds=0.3)) + await simulate_time(hass, mock_litejet, timedelta(seconds=0.3)) assert len(calls) == 1 - await simulate_release(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER) + await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 1 -async def test_held_less_than_short(hass, calls, mock_lj): +async def test_held_less_than_short(hass, calls, mock_litejet): """Test a hold that is short enough.""" await setup_automation( hass, @@ -184,14 +149,14 @@ async def test_held_less_than_short(hass, calls, mock_lj): }, ) - await simulate_press(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER) - await simulate_time(hass, mock_lj, timedelta(seconds=0.1)) + await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) + await simulate_time(hass, mock_litejet, timedelta(seconds=0.1)) assert len(calls) == 0 - await simulate_release(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER) + await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 1 -async def test_held_less_than_long(hass, calls, mock_lj): +async def test_held_less_than_long(hass, calls, mock_litejet): """Test a hold that is too long.""" await setup_automation( hass, @@ -202,15 +167,15 @@ async def test_held_less_than_long(hass, calls, mock_lj): }, ) - await simulate_press(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER) + await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 - await simulate_time(hass, mock_lj, timedelta(seconds=0.3)) + await simulate_time(hass, mock_litejet, timedelta(seconds=0.3)) assert len(calls) == 0 - await simulate_release(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER) + await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 -async def test_held_in_range_short(hass, calls, mock_lj): +async def test_held_in_range_short(hass, calls, mock_litejet): """Test an in-range trigger with a too short hold.""" await setup_automation( hass, @@ -222,13 +187,13 @@ async def test_held_in_range_short(hass, calls, mock_lj): }, ) - await simulate_press(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER) - await simulate_time(hass, mock_lj, timedelta(seconds=0.05)) - await simulate_release(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER) + await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) + await simulate_time(hass, mock_litejet, timedelta(seconds=0.05)) + await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 -async def test_held_in_range_just_right(hass, calls, mock_lj): +async def test_held_in_range_just_right(hass, calls, mock_litejet): """Test an in-range trigger with a just right hold.""" await setup_automation( hass, @@ -240,15 +205,15 @@ async def test_held_in_range_just_right(hass, calls, mock_lj): }, ) - await simulate_press(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER) + await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 - await simulate_time(hass, mock_lj, timedelta(seconds=0.2)) + await simulate_time(hass, mock_litejet, timedelta(seconds=0.2)) assert len(calls) == 0 - await simulate_release(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER) + await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 1 -async def test_held_in_range_long(hass, calls, mock_lj): +async def test_held_in_range_long(hass, calls, mock_litejet): """Test an in-range trigger with a too long hold.""" await setup_automation( hass, @@ -260,9 +225,50 @@ async def test_held_in_range_long(hass, calls, mock_lj): }, ) - await simulate_press(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER) + await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 - await simulate_time(hass, mock_lj, timedelta(seconds=0.4)) + await simulate_time(hass, mock_litejet, timedelta(seconds=0.4)) assert len(calls) == 0 - await simulate_release(hass, mock_lj, ENTITY_OTHER_SWITCH_NUMBER) + await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 + + +async def test_reload(hass, calls, mock_litejet): + """Test reloading automation.""" + await setup_automation( + hass, + { + "platform": "litejet", + "number": ENTITY_OTHER_SWITCH_NUMBER, + "held_more_than": {"milliseconds": "100"}, + "held_less_than": {"milliseconds": "300"}, + }, + ) + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={ + "automation": { + "trigger": { + "platform": "litejet", + "number": ENTITY_OTHER_SWITCH_NUMBER, + "held_more_than": {"milliseconds": "1000"}, + }, + "action": {"service": "test.automation"}, + } + }, + ): + await hass.services.async_call( + "automation", + "reload", + blocking=True, + ) + await hass.async_block_till_done() + + await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) + assert len(calls) == 0 + await simulate_time(hass, mock_litejet, timedelta(seconds=0.5)) + assert len(calls) == 0 + await simulate_time(hass, mock_litejet, timedelta(seconds=1.25)) + assert len(calls) == 1 From eb8d723689ec714c9945cf00ae8f19e3d83d9cd7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Feb 2021 14:50:06 -0600 Subject: [PATCH 668/796] Bump pymyq to fix myq in core (#46962) --- 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 9dc8719ed4e..2098480af52 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==3.0.1"], + "requirements": ["pymyq==3.0.4"], "codeowners": ["@bdraco"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 5f971b4ece7..638989b46c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1552,7 +1552,7 @@ pymsteams==0.1.12 pymusiccast==0.1.6 # homeassistant.components.myq -pymyq==3.0.1 +pymyq==3.0.4 # homeassistant.components.mysensors pymysensors==0.20.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c78f2fbb96a..254f8cdf86a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -824,7 +824,7 @@ pymodbus==2.3.0 pymonoprice==0.3 # homeassistant.components.myq -pymyq==3.0.1 +pymyq==3.0.4 # homeassistant.components.mysensors pymysensors==0.20.1 From 272e975a52deee7635beb33a5a728dd3d207ab2f Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 23 Feb 2021 15:38:24 -0600 Subject: [PATCH 669/796] Fix Plex handling of clips (#46667) --- homeassistant/components/plex/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plex/models.py b/homeassistant/components/plex/models.py index 7633c5deaa8..731d5bbc7db 100644 --- a/homeassistant/components/plex/models.py +++ b/homeassistant/components/plex/models.py @@ -70,7 +70,7 @@ class PlexSession: self.media_library_title = "Live TV" else: self.media_library_title = ( - media.section().title if media.section() is not None else "" + media.section().title if media.librarySectionID is not None else "" ) if media.type == "episode": From 089effbe3fae331932d68faf5a138ca4200a61a7 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 23 Feb 2021 22:41:19 +0100 Subject: [PATCH 670/796] Improve zwave_js config flow (#46906) --- .../components/zwave_js/config_flow.py | 205 +++++++++------- .../components/zwave_js/strings.json | 7 +- .../components/zwave_js/translations/en.json | 24 +- tests/components/zwave_js/conftest.py | 25 +- tests/components/zwave_js/test_config_flow.py | 224 +++++++++++++++--- 5 files changed, 329 insertions(+), 156 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index ec74acf9886..fe79796edf1 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -8,8 +8,18 @@ from async_timeout import timeout import voluptuous as vol from zwave_js_server.version import VersionInfo, get_server_version -from homeassistant import config_entries, core, exceptions +from homeassistant import config_entries, exceptions +from homeassistant.components.hassio import ( + async_get_addon_discovery_info, + async_get_addon_info, + async_install_addon, + async_set_addon_options, + async_start_addon, + is_hassio, +) +from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.const import CONF_URL +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -29,13 +39,14 @@ CONF_USB_PATH = "usb_path" DEFAULT_URL = "ws://localhost:3000" TITLE = "Z-Wave JS" -ADDON_SETUP_TIME = 10 +ADDON_SETUP_TIMEOUT = 5 +ADDON_SETUP_TIMEOUT_ROUNDS = 4 ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=True): bool}) STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_URL, default=DEFAULT_URL): str}) -async def validate_input(hass: core.HomeAssistant, user_input: dict) -> VersionInfo: +async def validate_input(hass: HomeAssistant, user_input: dict) -> VersionInfo: """Validate if the user input allows us to connect.""" ws_address = user_input[CONF_URL] @@ -48,9 +59,7 @@ async def validate_input(hass: core.HomeAssistant, user_input: dict) -> VersionI raise InvalidInput("cannot_connect") from err -async def async_get_version_info( - hass: core.HomeAssistant, ws_address: str -) -> VersionInfo: +async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> VersionInfo: """Return Z-Wave JS version info.""" async with timeout(10): try: @@ -58,7 +67,9 @@ async def async_get_version_info( ws_address, async_get_clientsession(hass) ) except (asyncio.TimeoutError, aiohttp.ClientError) as err: - _LOGGER.error("Failed to connect to Z-Wave JS server: %s", err) + # We don't want to spam the log if the add-on isn't started + # or takes a long time to start. + _LOGGER.debug("Failed to connect to Z-Wave JS server: %s", err) raise CannotConnect from err return version_info @@ -72,7 +83,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Set up flow instance.""" - self.addon_config: Optional[dict] = None self.network_key: Optional[str] = None self.usb_path: Optional[str] = None self.use_addon = False @@ -80,12 +90,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # If we install the add-on we should uninstall it on entry remove. self.integration_created_addon = False self.install_task: Optional[asyncio.Task] = None + self.start_task: Optional[asyncio.Task] = None async def async_step_user( self, user_input: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: """Handle the initial step.""" - if self.hass.components.hassio.is_hassio(): + assert self.hass # typing + if is_hassio(self.hass): # type: ignore # no-untyped-call return await self.async_step_on_supervisor() return await self.async_step_manual() @@ -120,7 +132,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="manual", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - async def async_step_hassio( # type: ignore + async def async_step_hassio( # type: ignore # override self, discovery_info: Dict[str, Any] ) -> Dict[str, Any]: """Receive configuration from add-on discovery info. @@ -149,6 +161,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="hassio_confirm") + @callback def _async_create_entry_from_vars(self) -> Dict[str, Any]: """Return a config entry for the flow.""" return self.async_create_entry( @@ -176,28 +189,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.use_addon = True if await self._async_is_addon_running(): - discovery_info = await self._async_get_addon_discovery_info() - self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}" - - if not self.unique_id: - try: - version_info = await async_get_version_info( - self.hass, self.ws_address - ) - except CannotConnect: - return self.async_abort(reason="cannot_connect") - await self.async_set_unique_id( - version_info.home_id, raise_on_progress=False - ) - - 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 self._async_create_entry_from_vars() + return await self.async_step_finish_addon_setup() if await self._async_is_addon_installed(): - return await self.async_step_start_addon() + return await self.async_step_configure_addon() return await self.async_step_install_addon() @@ -213,13 +211,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: await self.install_task - except self.hass.components.hassio.HassioAPIError as err: + except HassioAPIError as err: _LOGGER.error("Failed to install Z-Wave JS add-on: %s", err) return self.async_show_progress_done(next_step_id="install_failed") self.integration_created_addon = True - return self.async_show_progress_done(next_step_id="start_addon") + return self.async_show_progress_done(next_step_id="configure_addon") async def async_step_install_failed( self, user_input: Optional[Dict[str, Any]] = None @@ -227,14 +225,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Add-on installation failed.""" return self.async_abort(reason="addon_install_failed") - async def async_step_start_addon( + async def async_step_configure_addon( self, user_input: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: - """Ask for config and start Z-Wave JS add-on.""" - if self.addon_config is None: - self.addon_config = await self._async_get_addon_config() + """Ask for config for Z-Wave JS add-on.""" + addon_config = await self._async_get_addon_config() - errors = {} + errors: Dict[str, str] = {} if user_input is not None: self.network_key = user_input[CONF_NETWORK_KEY] @@ -245,40 +242,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_ADDON_NETWORK_KEY: self.network_key, } - if new_addon_config != self.addon_config: + if new_addon_config != addon_config: await self._async_set_addon_config(new_addon_config) - try: - await self.hass.components.hassio.async_start_addon(ADDON_SLUG) - except self.hass.components.hassio.HassioAPIError as err: - _LOGGER.error("Failed to start Z-Wave JS add-on: %s", err) - errors["base"] = "addon_start_failed" - else: - # Sleep some seconds to let the add-on start properly before connecting. - await asyncio.sleep(ADDON_SETUP_TIME) - discovery_info = await self._async_get_addon_discovery_info() - self.ws_address = ( - f"ws://{discovery_info['host']}:{discovery_info['port']}" - ) + return await self.async_step_start_addon() - if not self.unique_id: - try: - version_info = await async_get_version_info( - self.hass, self.ws_address - ) - except CannotConnect: - return self.async_abort(reason="cannot_connect") - await self.async_set_unique_id( - version_info.home_id, raise_on_progress=False - ) - - self._abort_if_unique_id_configured() - return self._async_create_entry_from_vars() - - usb_path = self.addon_config.get(CONF_ADDON_DEVICE, self.usb_path or "") - network_key = self.addon_config.get( - CONF_ADDON_NETWORK_KEY, self.network_key or "" - ) + usb_path = addon_config.get(CONF_ADDON_DEVICE, self.usb_path or "") + network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, self.network_key or "") data_schema = vol.Schema( { @@ -288,16 +258,95 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_show_form( - step_id="start_addon", data_schema=data_schema, errors=errors + step_id="configure_addon", data_schema=data_schema, errors=errors ) + async def async_step_start_addon( + self, user_input: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Start Z-Wave JS add-on.""" + assert self.hass + if not self.start_task: + self.start_task = self.hass.async_create_task(self._async_start_addon()) + return self.async_show_progress( + step_id="start_addon", progress_action="start_addon" + ) + + try: + await self.start_task + except (CannotConnect, HassioAPIError) as err: + _LOGGER.error("Failed to start Z-Wave JS add-on: %s", err) + return self.async_show_progress_done(next_step_id="start_failed") + + return self.async_show_progress_done(next_step_id="finish_addon_setup") + + async def async_step_start_failed( + self, user_input: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Add-on start failed.""" + return self.async_abort(reason="addon_start_failed") + + async def _async_start_addon(self) -> None: + """Start the Z-Wave JS add-on.""" + assert self.hass + try: + await async_start_addon(self.hass, ADDON_SLUG) + # Sleep some seconds to let the add-on start properly before connecting. + for _ in range(ADDON_SETUP_TIMEOUT_ROUNDS): + await asyncio.sleep(ADDON_SETUP_TIMEOUT) + try: + if not self.ws_address: + discovery_info = await self._async_get_addon_discovery_info() + self.ws_address = ( + f"ws://{discovery_info['host']}:{discovery_info['port']}" + ) + await async_get_version_info(self.hass, self.ws_address) + except (AbortFlow, CannotConnect) as err: + _LOGGER.debug( + "Add-on not ready yet, waiting %s seconds: %s", + ADDON_SETUP_TIMEOUT, + err, + ) + else: + break + else: + raise CannotConnect("Failed to start add-on: timeout") + finally: + # Continue the flow after show progress when the task is done. + self.hass.async_create_task( + self.hass.config_entries.flow.async_configure(flow_id=self.flow_id) + ) + + async def async_step_finish_addon_setup( + self, user_input: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Prepare info needed to complete the config entry. + + Get add-on discovery info and server version info. + Set unique id and abort if already configured. + """ + assert self.hass + if not self.ws_address: + discovery_info = await self._async_get_addon_discovery_info() + self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}" + + if not self.unique_id: + try: + version_info = await async_get_version_info(self.hass, self.ws_address) + except CannotConnect as err: + raise AbortFlow("cannot_connect") from err + await self.async_set_unique_id( + version_info.home_id, raise_on_progress=False + ) + + self._abort_if_unique_id_configured() + return self._async_create_entry_from_vars() + async def _async_get_addon_info(self) -> dict: """Return and cache Z-Wave JS add-on info.""" try: - addon_info: dict = await self.hass.components.hassio.async_get_addon_info( - ADDON_SLUG - ) - except self.hass.components.hassio.HassioAPIError as err: + addon_info: dict = await async_get_addon_info(self.hass, ADDON_SLUG) + except HassioAPIError as err: _LOGGER.error("Failed to get Z-Wave JS add-on info: %s", err) raise AbortFlow("addon_info_failed") from err @@ -322,17 +371,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Set Z-Wave JS add-on config.""" options = {"options": config} try: - await self.hass.components.hassio.async_set_addon_options( - ADDON_SLUG, options - ) - except self.hass.components.hassio.HassioAPIError as err: + await async_set_addon_options(self.hass, ADDON_SLUG, options) + except HassioAPIError as err: _LOGGER.error("Failed to set Z-Wave JS add-on config: %s", err) raise AbortFlow("addon_set_config_failed") from err async def _async_install_addon(self) -> None: """Install the Z-Wave JS add-on.""" try: - await self.hass.components.hassio.async_install_addon(ADDON_SLUG) + await async_install_addon(self.hass, ADDON_SLUG) finally: # Continue the flow after show progress when the task is done. self.hass.async_create_task( @@ -342,12 +389,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_get_addon_discovery_info(self) -> dict: """Return add-on discovery info.""" try: - discovery_info: dict = ( - await self.hass.components.hassio.async_get_addon_discovery_info( - ADDON_SLUG - ) - ) - except self.hass.components.hassio.HassioAPIError as err: + discovery_info = await async_get_addon_discovery_info(self.hass, ADDON_SLUG) + except HassioAPIError as err: _LOGGER.error("Failed to get Z-Wave JS add-on discovery info: %s", err) raise AbortFlow("addon_get_discovery_info_failed") from err diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 212bef70889..5d3aa730a7c 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -15,13 +15,14 @@ "install_addon": { "title": "The Z-Wave JS add-on installation has started" }, - "start_addon": { + "configure_addon": { "title": "Enter the Z-Wave JS add-on configuration", "data": { "usb_path": "[%key:common::config_flow::data::usb_path%]", "network_key": "Network Key" } }, + "start_addon": { "title": "The Z-Wave JS add-on is starting." }, "hassio_confirm": { "title": "Set up Z-Wave JS integration with the Z-Wave JS add-on" } @@ -38,12 +39,14 @@ "addon_info_failed": "Failed to get Z-Wave JS add-on info.", "addon_install_failed": "Failed to install the Z-Wave JS add-on.", "addon_set_config_failed": "Failed to set Z-Wave JS configuration.", + "addon_start_failed": "Failed to start the Z-Wave JS add-on.", "addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.", "addon_missing_discovery_info": "Missing Z-Wave JS add-on discovery info.", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "progress": { - "install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes." + "install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes.", + "start_addon": "Please wait while the Z-Wave JS add-on start completes. This may take some seconds." } } } diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json index 977651a576b..d8bdafcefee 100644 --- a/homeassistant/components/zwave_js/translations/en.json +++ b/homeassistant/components/zwave_js/translations/en.json @@ -6,6 +6,7 @@ "addon_install_failed": "Failed to install the Z-Wave JS add-on.", "addon_missing_discovery_info": "Missing Z-Wave JS add-on discovery info.", "addon_set_config_failed": "Failed to set Z-Wave JS configuration.", + "addon_start_failed": "Failed to start the Z-Wave JS add-on.", "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", "cannot_connect": "Failed to connect" @@ -17,9 +18,17 @@ "unknown": "Unexpected error" }, "progress": { - "install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes." + "install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes.", + "start_addon": "Please wait while the Z-Wave JS add-on start completes. This may take some seconds." }, "step": { + "configure_addon": { + "data": { + "network_key": "Network Key", + "usb_path": "USB Device Path" + }, + "title": "Enter the Z-Wave JS add-on configuration" + }, "hassio_confirm": { "title": "Set up Z-Wave JS integration with the Z-Wave JS add-on" }, @@ -39,18 +48,9 @@ "title": "Select connection method" }, "start_addon": { - "data": { - "network_key": "Network Key", - "usb_path": "USB Device Path" - }, - "title": "Enter the Z-Wave JS add-on configuration" - }, - "user": { - "data": { - "url": "URL" - } + "title": "The Z-Wave JS add-on is starting." } } }, "title": "Z-Wave JS" -} \ No newline at end of file +} diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 2e856bde362..b4b89fb14a2 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -1,7 +1,7 @@ """Provide common Z-Wave JS fixtures.""" import asyncio import json -from unittest.mock import DEFAULT, AsyncMock, patch +from unittest.mock import AsyncMock, patch import pytest from zwave_js_server.event import Event @@ -22,29 +22,6 @@ async def device_registry_fixture(hass): return await async_get_device_registry(hass) -@pytest.fixture(name="discovery_info") -def discovery_info_fixture(): - """Return the discovery info from the supervisor.""" - return DEFAULT - - -@pytest.fixture(name="discovery_info_side_effect") -def discovery_info_side_effect_fixture(): - """Return the discovery info from the supervisor.""" - return None - - -@pytest.fixture(name="get_addon_discovery_info") -def mock_get_addon_discovery_info(discovery_info, discovery_info_side_effect): - """Mock get add-on discovery info.""" - with patch( - "homeassistant.components.hassio.async_get_addon_discovery_info", - side_effect=discovery_info_side_effect, - return_value=discovery_info, - ) as get_addon_discovery_info: - yield get_addon_discovery_info - - @pytest.fixture(name="controller_state", scope="session") def controller_state_fixture(): """Load the controller state fixture data.""" diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 0270383174e..3c956d42a27 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -1,6 +1,6 @@ """Test the Z-Wave JS config flow.""" import asyncio -from unittest.mock import patch +from unittest.mock import DEFAULT, patch import pytest from zwave_js_server.version import VersionInfo @@ -22,10 +22,35 @@ ADDON_DISCOVERY_INFO = { @pytest.fixture(name="supervisor") def mock_supervisor_fixture(): """Mock Supervisor.""" - with patch("homeassistant.components.hassio.is_hassio", return_value=True): + with patch( + "homeassistant.components.zwave_js.config_flow.is_hassio", return_value=True + ): yield +@pytest.fixture(name="discovery_info") +def discovery_info_fixture(): + """Return the discovery info from the supervisor.""" + return DEFAULT + + +@pytest.fixture(name="discovery_info_side_effect") +def discovery_info_side_effect_fixture(): + """Return the discovery info from the supervisor.""" + return None + + +@pytest.fixture(name="get_addon_discovery_info") +def mock_get_addon_discovery_info(discovery_info, discovery_info_side_effect): + """Mock get add-on discovery info.""" + with patch( + "homeassistant.components.zwave_js.config_flow.async_get_addon_discovery_info", + side_effect=discovery_info_side_effect, + return_value=discovery_info, + ) as get_addon_discovery_info: + yield get_addon_discovery_info + + @pytest.fixture(name="addon_info_side_effect") def addon_info_side_effect_fixture(): """Return the add-on info side effect.""" @@ -36,7 +61,7 @@ def addon_info_side_effect_fixture(): def mock_addon_info(addon_info_side_effect): """Mock Supervisor add-on info.""" with patch( - "homeassistant.components.hassio.async_get_addon_info", + "homeassistant.components.zwave_js.config_flow.async_get_addon_info", side_effect=addon_info_side_effect, ) as addon_info: addon_info.return_value = {} @@ -75,7 +100,7 @@ def set_addon_options_side_effect_fixture(): def mock_set_addon_options(set_addon_options_side_effect): """Mock set add-on options.""" with patch( - "homeassistant.components.hassio.async_set_addon_options", + "homeassistant.components.zwave_js.config_flow.async_set_addon_options", side_effect=set_addon_options_side_effect, ) as set_options: yield set_options @@ -84,7 +109,9 @@ def mock_set_addon_options(set_addon_options_side_effect): @pytest.fixture(name="install_addon") def mock_install_addon(): """Mock install add-on.""" - with patch("homeassistant.components.hassio.async_install_addon") as install_addon: + with patch( + "homeassistant.components.zwave_js.config_flow.async_install_addon" + ) as install_addon: yield install_addon @@ -98,7 +125,7 @@ def start_addon_side_effect_fixture(): def mock_start_addon(start_addon_side_effect): """Mock start add-on.""" with patch( - "homeassistant.components.hassio.async_start_addon", + "homeassistant.components.zwave_js.config_flow.async_start_addon", side_effect=start_addon_side_effect, ) as start_addon: yield start_addon @@ -130,7 +157,7 @@ def mock_get_server_version(server_version_side_effect): def mock_addon_setup_time(): """Mock add-on setup sleep time.""" with patch( - "homeassistant.components.zwave_js.config_flow.ADDON_SETUP_TIME", new=0 + "homeassistant.components.zwave_js.config_flow.ADDON_SETUP_TIMEOUT", new=0 ) as addon_setup_time: yield addon_setup_time @@ -399,12 +426,47 @@ async def test_discovery_addon_not_running( result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["step_id"] == "start_addon" assert result["type"] == "form" + assert result["step_id"] == "configure_addon" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} + ) + + assert result["type"] == "progress" + assert result["step_id"] == "start_addon" + + with patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == TITLE + assert result["data"] == { + "url": "ws://host1:3001", + "usb_path": "/test", + "network_key": "abc123", + "use_addon": True, + "integration_created_addon": False, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 async def test_discovery_addon_not_installed( - hass, supervisor, addon_installed, install_addon, addon_options + hass, + supervisor, + addon_installed, + install_addon, + addon_options, + set_addon_options, + start_addon, ): """Test discovery with add-on not installed.""" addon_installed.return_value["version"] = None @@ -429,8 +491,37 @@ async def test_discovery_addon_not_installed( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "form" + assert result["step_id"] == "configure_addon" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} + ) + + assert result["type"] == "progress" assert result["step_id"] == "start_addon" + with patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == TITLE + assert result["data"] == { + "url": "ws://host1:3001", + "usb_path": "/test", + "network_key": "abc123", + "use_addon": True, + "integration_created_addon": True, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + async def test_not_addon(hass, supervisor): """Test opting out of add-on on Supervisor.""" @@ -559,10 +650,13 @@ async def test_addon_running_failures( hass, supervisor, addon_running, + addon_options, get_addon_discovery_info, abort_reason, ): """Test all failures when add-on is running.""" + 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( @@ -582,9 +676,11 @@ async def test_addon_running_failures( @pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) async def test_addon_running_already_configured( - hass, supervisor, addon_running, get_addon_discovery_info + hass, supervisor, addon_running, addon_options, get_addon_discovery_info ): """Test that only one unique instance is allowed when add-on is running.""" + addon_options["device"] = "/test" + addon_options["network_key"] = "abc123" entry = MockConfigEntry(domain=DOMAIN, data={}, title=TITLE, unique_id=1234) entry.add_to_hass(hass) @@ -629,6 +725,13 @@ async def test_addon_installed( ) assert result["type"] == "form" + assert result["step_id"] == "configure_addon" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} + ) + + assert result["type"] == "progress" assert result["step_id"] == "start_addon" with patch( @@ -637,9 +740,8 @@ async def test_addon_installed( "homeassistant.components.zwave_js.async_setup_entry", return_value=True, ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} - ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) await hass.async_block_till_done() assert result["type"] == "create_entry" @@ -683,40 +785,32 @@ async def test_addon_installed_start_failure( ) assert result["type"] == "form" - assert result["step_id"] == "start_addon" + assert result["step_id"] == "configure_addon" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} ) - assert result["type"] == "form" - assert result["errors"] == {"base": "addon_start_failed"} + assert result["type"] == "progress" + assert result["step_id"] == "start_addon" + + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == "abort" + assert result["reason"] == "addon_start_failed" @pytest.mark.parametrize( - "set_addon_options_side_effect, start_addon_side_effect, discovery_info, " - "server_version_side_effect, abort_reason", + "discovery_info, server_version_side_effect", [ ( - HassioAPIError(), - None, - {"config": ADDON_DISCOVERY_INFO}, - None, - "addon_set_config_failed", - ), - ( - None, - None, {"config": ADDON_DISCOVERY_INFO}, asyncio.TimeoutError, - "cannot_connect", ), ( None, None, - None, - None, - "addon_missing_discovery_info", ), ], ) @@ -728,7 +822,6 @@ async def test_addon_installed_failures( set_addon_options, start_addon, get_addon_discovery_info, - abort_reason, ): """Test all failures when add-on is installed.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -745,14 +838,58 @@ async def test_addon_installed_failures( ) assert result["type"] == "form" + assert result["step_id"] == "configure_addon" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} + ) + + assert result["type"] == "progress" assert result["step_id"] == "start_addon" + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == "abort" + assert result["reason"] == "addon_start_failed" + + +@pytest.mark.parametrize( + "set_addon_options_side_effect, discovery_info", + [(HassioAPIError(), {"config": ADDON_DISCOVERY_INFO})], +) +async def test_addon_installed_set_options_failure( + hass, + supervisor, + addon_installed, + addon_options, + set_addon_options, + start_addon, + get_addon_discovery_info, +): + """Test all failures when add-on is installed.""" + 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["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert result["type"] == "form" + assert result["step_id"] == "configure_addon" + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} ) assert result["type"] == "abort" - assert result["reason"] == abort_reason + assert result["reason"] == "addon_set_config_failed" @pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) @@ -782,12 +919,18 @@ async def test_addon_installed_already_configured( ) assert result["type"] == "form" - assert result["step_id"] == "start_addon" + assert result["step_id"] == "configure_addon" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} ) + assert result["type"] == "progress" + assert result["step_id"] == "start_addon" + + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -819,6 +962,7 @@ async def test_addon_not_installed( ) assert result["type"] == "progress" + assert result["step_id"] == "install_addon" # Make sure the flow continues when the progress task is done. await hass.async_block_till_done() @@ -826,6 +970,13 @@ async def test_addon_not_installed( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "form" + assert result["step_id"] == "configure_addon" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} + ) + + assert result["type"] == "progress" assert result["step_id"] == "start_addon" with patch( @@ -834,9 +985,8 @@ async def test_addon_not_installed( "homeassistant.components.zwave_js.async_setup_entry", return_value=True, ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} - ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) await hass.async_block_till_done() assert result["type"] == "create_entry" From 00dd557cce82014848cb02ce8c74e1b9a0713e40 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 23 Feb 2021 22:42:33 +0100 Subject: [PATCH 671/796] Fix Shelly mireds and color_temp return type (#46112) --- homeassistant/components/shelly/light.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 5422f3fff05..848ef990340 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -118,7 +118,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity): return "white" @property - def brightness(self) -> Optional[int]: + def brightness(self) -> int: """Brightness of light.""" if self.mode == "color": if self.control_result: @@ -133,7 +133,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity): return int(brightness / 100 * 255) @property - def white_value(self) -> Optional[int]: + def white_value(self) -> int: """White value of light.""" if self.control_result: white = self.control_result["white"] @@ -142,7 +142,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity): return int(white) @property - def hs_color(self) -> Optional[Tuple[float, float]]: + def hs_color(self) -> Tuple[float, float]: """Return the hue and saturation color value of light.""" if self.mode == "white": return color_RGB_to_hs(255, 255, 255) @@ -158,7 +158,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity): return color_RGB_to_hs(red, green, blue) @property - def color_temp(self) -> Optional[float]: + def color_temp(self) -> Optional[int]: """Return the CT color value in mireds.""" if self.mode == "color": return None @@ -176,14 +176,14 @@ class ShellyLight(ShellyBlockEntity, LightEntity): return int(color_temperature_kelvin_to_mired(color_temp)) @property - def min_mireds(self) -> Optional[float]: + def min_mireds(self) -> int: """Return the coldest color_temp that this light supports.""" - return color_temperature_kelvin_to_mired(KELVIN_MAX_VALUE) + return int(color_temperature_kelvin_to_mired(KELVIN_MAX_VALUE)) @property - def max_mireds(self) -> Optional[float]: + def max_mireds(self) -> int: """Return the warmest color_temp that this light supports.""" - return color_temperature_kelvin_to_mired(min_kelvin(self.wrapper.model)) + return int(color_temperature_kelvin_to_mired(min_kelvin(self.wrapper.model))) async def async_turn_on(self, **kwargs) -> None: """Turn on light.""" From d68a51ddcec7887474eab3869a968f7ada90ac11 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Feb 2021 15:42:56 -0600 Subject: [PATCH 672/796] Avoid having to ask for the bond token when possible during config (#46845) --- homeassistant/components/bond/config_flow.py | 92 ++++++++++++++----- homeassistant/components/bond/manifest.json | 2 +- homeassistant/components/bond/strings.json | 4 +- .../components/bond/translations/en.json | 4 +- homeassistant/components/bond/utils.py | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bond/common.py | 25 ++++- tests/components/bond/test_config_flow.py | 65 +++++++++++++ 9 files changed, 166 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 0132df486d3..f81e3a0be5c 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -14,16 +14,17 @@ from homeassistant.const import ( HTTP_UNAUTHORIZED, ) -from .const import CONF_BOND_ID from .const import DOMAIN # pylint:disable=unused-import from .utils import BondHub _LOGGER = logging.getLogger(__name__) -DATA_SCHEMA_USER = vol.Schema( + +USER_SCHEMA = vol.Schema( {vol.Required(CONF_HOST): str, vol.Required(CONF_ACCESS_TOKEN): str} ) -DATA_SCHEMA_DISCOVERY = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}) +DISCOVERY_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}) +TOKEN_SCHEMA = vol.Schema({}) async def _validate_input(data: Dict[str, Any]) -> Tuple[str, Optional[str]]: @@ -56,7 +57,30 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH - _discovered: dict = None + def __init__(self): + """Initialize config flow.""" + self._discovered: dict = None + + async def _async_try_automatic_configure(self): + """Try to auto configure the device. + + Failure is acceptable here since the device may have been + online longer then the allowed setup period, and we will + instead ask them to manually enter the token. + """ + bond = Bond(self._discovered[CONF_HOST], "") + try: + response = await bond.token() + except ClientConnectionError: + return + + token = response.get("token") + if token is None: + return + + self._discovered[CONF_ACCESS_TOKEN] = token + _, hub_name = await _validate_input(self._discovered) + self._discovered[CONF_NAME] = hub_name async def async_step_zeroconf( self, discovery_info: Optional[Dict[str, Any]] = None @@ -68,11 +92,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(bond_id) self._abort_if_unique_id_configured({CONF_HOST: host}) - self._discovered = { - CONF_HOST: host, - CONF_BOND_ID: bond_id, - } - self.context.update({"title_placeholders": self._discovered}) + self._discovered = {CONF_HOST: host, CONF_NAME: bond_id} + await self._async_try_automatic_configure() + + self.context.update( + { + "title_placeholders": { + CONF_HOST: self._discovered[CONF_HOST], + CONF_NAME: self._discovered[CONF_NAME], + } + } + ) return await self.async_step_confirm() @@ -82,16 +112,37 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle confirmation flow for discovered bond hub.""" errors = {} if user_input is not None: - data = user_input.copy() - data[CONF_HOST] = self._discovered[CONF_HOST] + if CONF_ACCESS_TOKEN in self._discovered: + return self.async_create_entry( + title=self._discovered[CONF_NAME], + data={ + CONF_ACCESS_TOKEN: self._discovered[CONF_ACCESS_TOKEN], + CONF_HOST: self._discovered[CONF_HOST], + }, + ) + + data = { + CONF_ACCESS_TOKEN: user_input[CONF_ACCESS_TOKEN], + CONF_HOST: self._discovered[CONF_HOST], + } try: - return await self._try_create_entry(data) + _, hub_name = await _validate_input(data) except InputValidationError as error: errors["base"] = error.base + else: + return self.async_create_entry( + title=hub_name, + data=data, + ) + + if CONF_ACCESS_TOKEN in self._discovered: + data_schema = TOKEN_SCHEMA + else: + data_schema = DISCOVERY_SCHEMA return self.async_show_form( step_id="confirm", - data_schema=DATA_SCHEMA_DISCOVERY, + data_schema=data_schema, errors=errors, description_placeholders=self._discovered, ) @@ -103,21 +154,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: try: - return await self._try_create_entry(user_input) + bond_id, hub_name = await _validate_input(user_input) except InputValidationError as error: errors["base"] = error.base + else: + await self.async_set_unique_id(bond_id) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=hub_name, data=user_input) return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors + step_id="user", data_schema=USER_SCHEMA, errors=errors ) - async def _try_create_entry(self, data: Dict[str, Any]) -> Dict[str, Any]: - bond_id, name = await _validate_input(data) - await self.async_set_unique_id(bond_id) - self._abort_if_unique_id_configured() - hub_name = name or bond_id - return self.async_create_entry(title=hub_name, data=data) - class InputValidationError(exceptions.HomeAssistantError): """Error to indicate we cannot proceed due to invalid input.""" diff --git a/homeassistant/components/bond/manifest.json b/homeassistant/components/bond/manifest.json index cf009c11caa..65cb6a83bb2 100644 --- a/homeassistant/components/bond/manifest.json +++ b/homeassistant/components/bond/manifest.json @@ -3,7 +3,7 @@ "name": "Bond", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bond", - "requirements": ["bond-api==0.1.10"], + "requirements": ["bond-api==0.1.11"], "zeroconf": ["_bond._tcp.local."], "codeowners": ["@prystupa"], "quality_scale": "platinum" diff --git a/homeassistant/components/bond/strings.json b/homeassistant/components/bond/strings.json index 5ca2278a3e5..f8eff6ddd9e 100644 --- a/homeassistant/components/bond/strings.json +++ b/homeassistant/components/bond/strings.json @@ -1,9 +1,9 @@ { "config": { - "flow_title": "Bond: {bond_id} ({host})", + "flow_title": "Bond: {name} ({host})", "step": { "confirm": { - "description": "Do you want to set up {bond_id}?", + "description": "Do you want to set up {name}?", "data": { "access_token": "[%key:common::config_flow::data::access_token%]" } diff --git a/homeassistant/components/bond/translations/en.json b/homeassistant/components/bond/translations/en.json index 945b09b8186..d9ce8ab0fe4 100644 --- a/homeassistant/components/bond/translations/en.json +++ b/homeassistant/components/bond/translations/en.json @@ -9,13 +9,13 @@ "old_firmware": "Unsupported old firmware on the Bond device - please upgrade before continuing", "unknown": "Unexpected error" }, - "flow_title": "Bond: {bond_id} ({host})", + "flow_title": "Bond: {name} ({host})", "step": { "confirm": { "data": { "access_token": "Access Token" }, - "description": "Do you want to set up {bond_id}?" + "description": "Do you want to set up {name}?" }, "user": { "data": { diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py index 55ef81778f0..225eec87d98 100644 --- a/homeassistant/components/bond/utils.py +++ b/homeassistant/components/bond/utils.py @@ -150,11 +150,11 @@ class BondHub: return self._version.get("make", BRIDGE_MAKE) @property - def name(self) -> Optional[str]: + def name(self) -> str: """Get the name of this bridge.""" if not self.is_bridge and self._devices: return self._devices[0].name - return self._bridge.get("name") + return self._bridge["name"] @property def location(self) -> Optional[str]: diff --git a/requirements_all.txt b/requirements_all.txt index 638989b46c9..7079c94a00b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -371,7 +371,7 @@ blockchain==1.4.4 # bme680==1.0.5 # homeassistant.components.bond -bond-api==0.1.10 +bond-api==0.1.11 # homeassistant.components.amazon_polly # homeassistant.components.route53 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 254f8cdf86a..a434739be14 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -205,7 +205,7 @@ blebox_uniapi==1.3.2 blinkpy==0.17.0 # homeassistant.components.bond -bond-api==0.1.10 +bond-api==0.1.11 # homeassistant.components.braviatv bravia-tv==1.0.8 diff --git a/tests/components/bond/common.py b/tests/components/bond/common.py index 54d127832b5..061dc23797e 100644 --- a/tests/components/bond/common.py +++ b/tests/components/bond/common.py @@ -30,12 +30,13 @@ async def setup_bond_entity( patch_device_ids=False, patch_platforms=False, patch_bridge=False, + patch_token=False, ): """Set up Bond entity.""" config_entry.add_to_hass(hass) - with patch_start_bpup(), patch_bond_bridge( - enabled=patch_bridge + with patch_start_bpup(), patch_bond_bridge(enabled=patch_bridge), patch_bond_token( + enabled=patch_token ), patch_bond_version(enabled=patch_version), patch_bond_device_ids( enabled=patch_device_ids ), patch_setup_entry( @@ -60,6 +61,7 @@ async def setup_platform( props: Dict[str, Any] = None, state: Dict[str, Any] = None, bridge: Dict[str, Any] = None, + token: Dict[str, Any] = None, ): """Set up the specified Bond platform.""" mock_entry = MockConfigEntry( @@ -71,7 +73,7 @@ async def setup_platform( with patch("homeassistant.components.bond.PLATFORMS", [platform]): with patch_bond_version(return_value=bond_version), patch_bond_bridge( return_value=bridge - ), patch_bond_device_ids( + ), patch_bond_token(return_value=token), patch_bond_device_ids( return_value=[bond_device_id] ), patch_start_bpup(), patch_bond_device( return_value=discovered_device @@ -124,6 +126,23 @@ def patch_bond_bridge( ) +def patch_bond_token( + enabled: bool = True, return_value: Optional[dict] = None, side_effect=None +): + """Patch Bond API token endpoint.""" + if not enabled: + return nullcontext() + + if return_value is None: + return_value = {"locked": 1} + + return patch( + "homeassistant.components.bond.Bond.token", + return_value=return_value, + side_effect=side_effect, + ) + + def patch_bond_device_ids(enabled: bool = True, return_value=None, side_effect=None): """Patch Bond API devices endpoint.""" if not enabled: diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index 2a76e1fa6a0..39fd1a2db5d 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -13,6 +13,7 @@ from .common import ( patch_bond_device, patch_bond_device_ids, patch_bond_device_properties, + patch_bond_token, patch_bond_version, ) @@ -221,6 +222,70 @@ async def test_zeroconf_form(hass: core.HomeAssistant): assert len(mock_setup_entry.mock_calls) == 1 +async def test_zeroconf_form_token_unavailable(hass: core.HomeAssistant): + """Test we get the discovery form and we handle the token being unavailable.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch_bond_version(), patch_bond_token(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={"name": "test-bond-id.some-other-tail-info", "host": "test-host"}, + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["errors"] == {} + + with patch_bond_version(), patch_bond_bridge(), patch_bond_device_ids(), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_ACCESS_TOKEN: "test-token"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "bond-name" + assert result2["data"] == { + CONF_HOST: "test-host", + CONF_ACCESS_TOKEN: "test-token", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_zeroconf_form_with_token_available(hass: core.HomeAssistant): + """Test we get the discovery form when we can get the token.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch_bond_version(return_value={"bondid": "test-bond-id"}), patch_bond_token( + return_value={"token": "discovered-token"} + ), patch_bond_bridge( + return_value={"name": "discovered-name"} + ), patch_bond_device_ids(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={"name": "test-bond-id.some-other-tail-info", "host": "test-host"}, + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["errors"] == {} + + with _patch_async_setup() as mock_setup, _patch_async_setup_entry() 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"] == "discovered-name" + assert result2["data"] == { + CONF_HOST: "test-host", + CONF_ACCESS_TOKEN: "discovered-token", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_zeroconf_already_configured(hass: core.HomeAssistant): """Test starting a flow from discovery when already configured.""" await setup.async_setup_component(hass, "persistent_notification", {}) From b583ded8b5c33aabc596bc5b7bfd8d1e25a512d6 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 23 Feb 2021 22:59:16 +0100 Subject: [PATCH 673/796] Fix KNX services.yaml (#46897) --- homeassistant/components/knx/__init__.py | 43 +++++++++++----------- homeassistant/components/knx/expose.py | 8 ++-- homeassistant/components/knx/schema.py | 3 +- homeassistant/components/knx/services.yaml | 19 +++++++--- 4 files changed, 42 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 8716f03838c..f092bafe404 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -17,6 +17,7 @@ from xknx.telegram import AddressFilter, GroupAddress, Telegram from xknx.telegram.apci import GroupValueRead, GroupValueResponse, GroupValueWrite from homeassistant.const import ( + CONF_ADDRESS, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, @@ -64,7 +65,6 @@ CONF_KNX_RATE_LIMIT = "rate_limit" CONF_KNX_EXPOSE = "expose" SERVICE_KNX_SEND = "send" -SERVICE_KNX_ATTR_ADDRESS = "address" SERVICE_KNX_ATTR_PAYLOAD = "payload" SERVICE_KNX_ATTR_TYPE = "type" SERVICE_KNX_ATTR_REMOVE = "remove" @@ -146,7 +146,7 @@ CONFIG_SCHEMA = vol.Schema( SERVICE_KNX_SEND_SCHEMA = vol.Any( vol.Schema( { - vol.Required(SERVICE_KNX_ATTR_ADDRESS): cv.string, + vol.Required(CONF_ADDRESS): cv.string, vol.Required(SERVICE_KNX_ATTR_PAYLOAD): cv.match_all, vol.Required(SERVICE_KNX_ATTR_TYPE): vol.Any(int, float, str), } @@ -154,7 +154,7 @@ SERVICE_KNX_SEND_SCHEMA = vol.Any( vol.Schema( # without type given payload is treated as raw bytes { - vol.Required(SERVICE_KNX_ATTR_ADDRESS): cv.string, + vol.Required(CONF_ADDRESS): cv.string, vol.Required(SERVICE_KNX_ATTR_PAYLOAD): vol.Any( cv.positive_int, [cv.positive_int] ), @@ -164,7 +164,7 @@ SERVICE_KNX_SEND_SCHEMA = vol.Any( SERVICE_KNX_READ_SCHEMA = vol.Schema( { - vol.Required(SERVICE_KNX_ATTR_ADDRESS): vol.All( + vol.Required(CONF_ADDRESS): vol.All( cv.ensure_list, [cv.string], ) @@ -173,7 +173,7 @@ SERVICE_KNX_READ_SCHEMA = vol.Schema( SERVICE_KNX_EVENT_REGISTER_SCHEMA = vol.Schema( { - vol.Required(SERVICE_KNX_ATTR_ADDRESS): cv.string, + vol.Required(CONF_ADDRESS): cv.string, vol.Optional(SERVICE_KNX_ATTR_REMOVE, default=False): cv.boolean, } ) @@ -187,7 +187,7 @@ SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA = vol.Any( vol.Schema( # for removing only `address` is required { - vol.Required(SERVICE_KNX_ATTR_ADDRESS): cv.string, + vol.Required(CONF_ADDRESS): cv.string, vol.Required(SERVICE_KNX_ATTR_REMOVE): vol.All(cv.boolean, True), }, extra=vol.ALLOW_EXTRA, @@ -198,8 +198,9 @@ SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA = vol.Any( async def async_setup(hass, config): """Set up the KNX component.""" try: - hass.data[DOMAIN] = KNXModule(hass, config) - await hass.data[DOMAIN].start() + knx_module = KNXModule(hass, config) + hass.data[DOMAIN] = knx_module + await knx_module.start() except XKNXException as ex: _LOGGER.warning("Could not connect to KNX interface: %s", ex) hass.components.persistent_notification.async_create( @@ -208,14 +209,14 @@ async def async_setup(hass, config): if CONF_KNX_EXPOSE in config[DOMAIN]: for expose_config in config[DOMAIN][CONF_KNX_EXPOSE]: - hass.data[DOMAIN].exposures.append( - create_knx_exposure(hass, hass.data[DOMAIN].xknx, expose_config) + knx_module.exposures.append( + create_knx_exposure(hass, knx_module.xknx, expose_config) ) for platform in SupportedPlatforms: if platform.value in config[DOMAIN]: for device_config in config[DOMAIN][platform.value]: - create_knx_device(platform, hass.data[DOMAIN].xknx, device_config) + create_knx_device(platform, knx_module.xknx, device_config) # We need to wait until all entities are loaded into the device list since they could also be created from other platforms for platform in SupportedPlatforms: @@ -223,7 +224,7 @@ async def async_setup(hass, config): discovery.async_load_platform(hass, platform.value, DOMAIN, {}, config) ) - if not hass.data[DOMAIN].xknx.devices: + if not knx_module.xknx.devices: _LOGGER.warning( "No KNX devices are configured. Please read " "https://www.home-assistant.io/blog/2020/09/17/release-115/#breaking-changes" @@ -232,14 +233,14 @@ async def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_KNX_SEND, - hass.data[DOMAIN].service_send_to_knx_bus, + knx_module.service_send_to_knx_bus, schema=SERVICE_KNX_SEND_SCHEMA, ) hass.services.async_register( DOMAIN, SERVICE_KNX_READ, - hass.data[DOMAIN].service_read_to_knx_bus, + knx_module.service_read_to_knx_bus, schema=SERVICE_KNX_READ_SCHEMA, ) @@ -247,7 +248,7 @@ async def async_setup(hass, config): hass, DOMAIN, SERVICE_KNX_EVENT_REGISTER, - hass.data[DOMAIN].service_event_register_modify, + knx_module.service_event_register_modify, schema=SERVICE_KNX_EVENT_REGISTER_SCHEMA, ) @@ -255,7 +256,7 @@ async def async_setup(hass, config): hass, DOMAIN, SERVICE_KNX_EXPOSURE_REGISTER, - hass.data[DOMAIN].service_exposure_register_modify, + knx_module.service_exposure_register_modify, schema=SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA, ) @@ -269,7 +270,7 @@ async def async_setup(hass, config): if not config or DOMAIN not in config: return - await hass.data[DOMAIN].xknx.stop() + await knx_module.xknx.stop() await asyncio.gather( *[platform.async_reset() for platform in async_get_platforms(hass, DOMAIN)] @@ -398,7 +399,7 @@ class KNXModule: async def service_event_register_modify(self, call): """Service for adding or removing a GroupAddress to the knx_event filter.""" - group_address = GroupAddress(call.data.get(SERVICE_KNX_ATTR_ADDRESS)) + group_address = GroupAddress(call.data[CONF_ADDRESS]) if call.data.get(SERVICE_KNX_ATTR_REMOVE): try: self._knx_event_callback.group_addresses.remove(group_address) @@ -416,7 +417,7 @@ class KNXModule: async def service_exposure_register_modify(self, call): """Service for adding or removing an exposure to KNX bus.""" - group_address = call.data.get(SERVICE_KNX_ATTR_ADDRESS) + group_address = call.data.get(CONF_ADDRESS) if call.data.get(SERVICE_KNX_ATTR_REMOVE): try: @@ -448,7 +449,7 @@ class KNXModule: async def service_send_to_knx_bus(self, call): """Service for sending an arbitrary KNX message to the KNX bus.""" attr_payload = call.data.get(SERVICE_KNX_ATTR_PAYLOAD) - attr_address = call.data.get(SERVICE_KNX_ATTR_ADDRESS) + attr_address = call.data.get(CONF_ADDRESS) attr_type = call.data.get(SERVICE_KNX_ATTR_TYPE) def calculate_payload(attr_payload): @@ -470,7 +471,7 @@ class KNXModule: async def service_read_to_knx_bus(self, call): """Service for sending a GroupValueRead telegram to the KNX bus.""" - for address in call.data.get(SERVICE_KNX_ATTR_ADDRESS): + for address in call.data.get(CONF_ADDRESS): telegram = Telegram( destination_address=GroupAddress(address), payload=GroupValueRead(), diff --git a/homeassistant/components/knx/expose.py b/homeassistant/components/knx/expose.py index 93abd7d7b43..5abc58f82cc 100644 --- a/homeassistant/components/knx/expose.py +++ b/homeassistant/components/knx/expose.py @@ -5,6 +5,7 @@ from xknx import XKNX from xknx.devices import DateTime, ExposeSensor from homeassistant.const import ( + CONF_ADDRESS, CONF_ENTITY_ID, STATE_OFF, STATE_ON, @@ -23,11 +24,11 @@ def create_knx_exposure( hass: HomeAssistant, xknx: XKNX, config: ConfigType ) -> Union["KNXExposeSensor", "KNXExposeTime"]: """Create exposures from config.""" - expose_type = config.get(ExposeSchema.CONF_KNX_EXPOSE_TYPE) - entity_id = config.get(CONF_ENTITY_ID) + address = config[CONF_ADDRESS] attribute = config.get(ExposeSchema.CONF_KNX_EXPOSE_ATTRIBUTE) + entity_id = config.get(CONF_ENTITY_ID) + expose_type = config.get(ExposeSchema.CONF_KNX_EXPOSE_TYPE) default = config.get(ExposeSchema.CONF_KNX_EXPOSE_DEFAULT) - address = config.get(ExposeSchema.CONF_KNX_EXPOSE_ADDRESS) exposure: Union["KNXExposeSensor", "KNXExposeTime"] if expose_type.lower() in ["time", "date", "datetime"]: @@ -83,6 +84,7 @@ class KNXExposeSensor: """Prepare for deletion.""" if self._remove_listener is not None: self._remove_listener() + self._remove_listener = None if self.device is not None: self.device.shutdown() diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 61909013739..125115f28b2 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -327,7 +327,6 @@ class ExposeSchema: CONF_KNX_EXPOSE_TYPE = CONF_TYPE CONF_KNX_EXPOSE_ATTRIBUTE = "attribute" CONF_KNX_EXPOSE_DEFAULT = "default" - CONF_KNX_EXPOSE_ADDRESS = CONF_ADDRESS SCHEMA = vol.Schema( { @@ -335,7 +334,7 @@ class ExposeSchema: vol.Optional(CONF_ENTITY_ID): cv.entity_id, vol.Optional(CONF_KNX_EXPOSE_ATTRIBUTE): cv.string, vol.Optional(CONF_KNX_EXPOSE_DEFAULT): cv.match_all, - vol.Required(CONF_KNX_EXPOSE_ADDRESS): cv.string, + vol.Required(CONF_ADDRESS): cv.string, } ) diff --git a/homeassistant/components/knx/services.yaml b/homeassistant/components/knx/services.yaml index ef74acc49b1..3fae7dfce0e 100644 --- a/homeassistant/components/knx/services.yaml +++ b/homeassistant/components/knx/services.yaml @@ -1,4 +1,5 @@ send: + name: "Send to KNX bus" description: "Send arbitrary data directly to the KNX bus." fields: address: @@ -13,6 +14,8 @@ send: description: "Payload to send to the bus. Integers are treated as DPT 1/2/3 payloads. For DPTs > 6 bits send a list. Each value represents 1 octet (0-255). Pad with 0 to DPT byte length." required: true example: "[0, 4]" + selector: + object: type: name: "Value type" description: "Optional. If set, the payload will not be sent as raw bytes, but encoded as given DPT. Knx sensor types are valid values (see https://www.home-assistant.io/integrations/sensor.knx)." @@ -21,6 +24,7 @@ send: selector: text: read: + name: "Read from KNX bus" description: "Send GroupValueRead requests to the KNX bus. Response can be used from `knx_event` and will be processed in KNX entities." fields: address: @@ -31,6 +35,7 @@ read: selector: text: event_register: + name: "Register knx_event" description: "Add or remove single group address to knx_event filter for triggering `knx_event`s. Only addresses added with this service can be removed." fields: address: @@ -38,14 +43,16 @@ event_register: description: "Group address that shall be added or removed." required: true example: "1/1/0" + selector: + text: remove: name: "Remove event registration" description: "Optional. If `True` the group address will be removed." - required: false default: false selector: boolean: exposure_register: + name: "Expose to KNX bus" description: "Add or remove exposures to KNX bus. Only exposures added with this service can be removed." fields: address: @@ -72,19 +79,21 @@ exposure_register: attribute: name: "Entity attribute" description: "Optional. Attribute of the entity that shall be sent to the KNX bus. If not set the state will be sent. Eg. for a light the state is eigther “on” or “off” - with attribute you can expose its “brightness”." - required: false example: "brightness" + selector: + text: default: name: "Default value" description: "Optional. Default value to send to the bus if the state or attribute value is None. Eg. a light with state “off” has no brightness attribute so a default value of 0 could be used. If not set (or None) no value would be sent to the bus and a GroupReadRequest to the address would return the last known value." - required: false example: "0" + selector: + object: remove: name: "Remove exposure" description: "Optional. If `True` the exposure will be removed. Only `address` is required for removal." - required: false default: false selector: boolean: reload: - description: "Reload KNX configuration." + name: "Reload KNX configuration" + description: "Reload the KNX configuration from YAML." From d96249e39c8dbfa6568bb05688bd49110abfcb9b Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 23 Feb 2021 23:23:50 +0100 Subject: [PATCH 674/796] Implement additional DataUpdateCoordinator to harmonize the data update handling of Synology DSM (#46113) --- .../components/synology_dsm/__init__.py | 237 +++++++++--------- .../components/synology_dsm/binary_sensor.py | 21 +- .../components/synology_dsm/camera.py | 19 +- .../components/synology_dsm/config_flow.py | 3 +- .../components/synology_dsm/const.py | 6 +- .../components/synology_dsm/sensor.py | 42 +++- .../components/synology_dsm/switch.py | 58 ++--- 7 files changed, 210 insertions(+), 176 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index b0d78ca6716..6f0476b403c 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -39,12 +39,6 @@ from homeassistant.core import ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -53,9 +47,12 @@ from homeassistant.helpers.update_coordinator import ( ) from .const import ( + CONF_DEVICE_TOKEN, CONF_SERIAL, CONF_VOLUMES, - COORDINATOR_SURVEILLANCE, + COORDINATOR_CAMERAS, + COORDINATOR_CENTRAL, + COORDINATOR_SWITCHES, DEFAULT_SCAN_INTERVAL, DEFAULT_USE_SSL, DEFAULT_VERIFY_SSL, @@ -73,6 +70,7 @@ from .const import ( STORAGE_DISK_SENSORS, STORAGE_VOL_SENSORS, SYNO_API, + SYSTEM_LOADED, TEMP_SENSORS_KEYS, UNDO_UPDATE_LISTENER, UTILISATION_SENSORS, @@ -196,12 +194,11 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): _LOGGER.debug("async_setup_entry() - Unable to connect to DSM: %s", err) raise ConfigEntryNotReady from err - undo_listener = entry.add_update_listener(_async_update_listener) - hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.unique_id] = { + UNDO_UPDATE_LISTENER: entry.add_update_listener(_async_update_listener), SYNO_API: api, - UNDO_UPDATE_LISTENER: undo_listener, + SYSTEM_LOADED: True, } # Services @@ -214,32 +211,82 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): entry, data={**entry.data, CONF_MAC: network.macs} ) - # setup DataUpdateCoordinator - async def async_coordinator_update_data_surveillance_station(): - """Fetch all surveillance station data from api.""" + async def async_coordinator_update_data_cameras(): + """Fetch all camera data from api.""" + if not hass.data[DOMAIN][entry.unique_id][SYSTEM_LOADED]: + raise UpdateFailed("System not fully loaded") + + if SynoSurveillanceStation.CAMERA_API_KEY not in api.dsm.apis: + return None + surveillance_station = api.surveillance_station + try: async with async_timeout.timeout(10): await hass.async_add_executor_job(surveillance_station.update) except SynologyDSMAPIErrorException as err: + _LOGGER.debug( + "async_coordinator_update_data_cameras() - exception: %s", err + ) raise UpdateFailed(f"Error communicating with API: {err}") from err - if SynoSurveillanceStation.CAMERA_API_KEY not in api.dsm.apis: - return - return { "cameras": { camera.id: camera for camera in surveillance_station.get_all_cameras() } } - hass.data[DOMAIN][entry.unique_id][ - COORDINATOR_SURVEILLANCE - ] = DataUpdateCoordinator( + async def async_coordinator_update_data_central(): + """Fetch all device and sensor data from api.""" + try: + await api.async_update() + except Exception as err: + _LOGGER.debug( + "async_coordinator_update_data_central() - exception: %s", err + ) + raise UpdateFailed(f"Error communicating with API: {err}") from err + return None + + async def async_coordinator_update_data_switches(): + """Fetch all switch data from api.""" + if not hass.data[DOMAIN][entry.unique_id][SYSTEM_LOADED]: + raise UpdateFailed("System not fully loaded") + if SynoSurveillanceStation.HOME_MODE_API_KEY not in api.dsm.apis: + return None + + surveillance_station = api.surveillance_station + + return { + "switches": { + "home_mode": await hass.async_add_executor_job( + surveillance_station.get_home_mode_status + ) + } + } + + hass.data[DOMAIN][entry.unique_id][COORDINATOR_CAMERAS] = DataUpdateCoordinator( hass, _LOGGER, - name=f"{entry.unique_id}_surveillance_station", - update_method=async_coordinator_update_data_surveillance_station, + name=f"{entry.unique_id}_cameras", + update_method=async_coordinator_update_data_cameras, + update_interval=timedelta(seconds=30), + ) + + hass.data[DOMAIN][entry.unique_id][COORDINATOR_CENTRAL] = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"{entry.unique_id}_central", + update_method=async_coordinator_update_data_central, + update_interval=timedelta( + minutes=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + ), + ) + + hass.data[DOMAIN][entry.unique_id][COORDINATOR_SWITCHES] = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"{entry.unique_id}_switches", + update_method=async_coordinator_update_data_switches, update_interval=timedelta(seconds=30), ) @@ -304,10 +351,11 @@ async def _async_setup_services(hass: HomeAssistantType): _LOGGER.debug("%s DSM with serial %s", call.service, serial) dsm_api = dsm_device[SYNO_API] + dsm_device[SYSTEM_LOADED] = False if call.service == SERVICE_REBOOT: await dsm_api.async_reboot() elif call.service == SERVICE_SHUTDOWN: - await dsm_api.system.shutdown() + await dsm_api.async_shutdown() for service in SERVICES: hass.services.async_register(DOMAIN, service, service_handler) @@ -342,16 +390,8 @@ class SynoApi: self._with_upgrade = True self._with_utilisation = True - self._unsub_dispatcher = None - - @property - def signal_sensor_update(self) -> str: - """Event specific per Synology DSM entry to signal updates in sensors.""" - return f"{DOMAIN}-{self.information.serial}-sensor-update" - async def async_setup(self): """Start interacting with the NAS.""" - # init SynologyDSM object and login self.dsm = SynologyDSM( self._entry.data[CONF_HOST], self._entry.data[CONF_PORT], @@ -360,7 +400,7 @@ class SynoApi: self._entry.data[CONF_SSL], self._entry.data[CONF_VERIFY_SSL], timeout=self._entry.options.get(CONF_TIMEOUT), - device_token=self._entry.data.get("device_token"), + device_token=self._entry.data.get(CONF_DEVICE_TOKEN), ) await self._hass.async_add_executor_job(self.dsm.login) @@ -373,24 +413,14 @@ class SynoApi: self._with_surveillance_station, ) - self._async_setup_api_requests() + self._setup_api_requests() await self._hass.async_add_executor_job(self._fetch_device_configuration) await self.async_update() - self._unsub_dispatcher = async_track_time_interval( - self._hass, - self.async_update, - timedelta( - minutes=self._entry.options.get( - CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL - ) - ), - ) - @callback def subscribe(self, api_key, unique_id): - """Subscribe an entity from API fetches.""" + """Subscribe an entity to API fetches.""" _LOGGER.debug( "SynoAPI.subscribe() - api_key:%s, unique_id:%s", api_key, unique_id ) @@ -401,31 +431,35 @@ class SynoApi: @callback def unsubscribe() -> None: """Unsubscribe an entity from API fetches (when disable).""" + _LOGGER.debug( + "SynoAPI.unsubscribe() - api_key:%s, unique_id:%s", api_key, unique_id + ) self._fetching_entities[api_key].remove(unique_id) + if len(self._fetching_entities[api_key]) == 0: + self._fetching_entities.pop(api_key) return unsubscribe @callback - def _async_setup_api_requests(self): + def _setup_api_requests(self): """Determine if we should fetch each API, if one entity needs it.""" - _LOGGER.debug( - "SynoAPI._async_setup_api_requests() - self._fetching_entities:%s", - self._fetching_entities, - ) - # Entities not added yet, fetch all if not self._fetching_entities: _LOGGER.debug( - "SynoAPI._async_setup_api_requests() - Entities not added yet, fetch all" + "SynoAPI._setup_api_requests() - Entities not added yet, fetch all" ) return # Determine if we should fetch an API + self._with_system = bool(self.dsm.apis.get(SynoCoreSystem.API_KEY)) + self._with_surveillance_station = bool( + self.dsm.apis.get(SynoSurveillanceStation.CAMERA_API_KEY) + ) or bool(self.dsm.apis.get(SynoSurveillanceStation.HOME_MODE_API_KEY)) + self._with_security = bool( self._fetching_entities.get(SynoCoreSecurity.API_KEY) ) self._with_storage = bool(self._fetching_entities.get(SynoStorage.API_KEY)) - self._with_system = bool(self._fetching_entities.get(SynoCoreSystem.API_KEY)) self._with_upgrade = bool(self._fetching_entities.get(SynoCoreUpgrade.API_KEY)) self._with_utilisation = bool( self._fetching_entities.get(SynoCoreUtilization.API_KEY) @@ -433,39 +467,36 @@ class SynoApi: self._with_information = bool( self._fetching_entities.get(SynoDSMInformation.API_KEY) ) - self._with_surveillance_station = bool( - self.dsm.apis.get(SynoSurveillanceStation.CAMERA_API_KEY) - ) # Reset not used API, information is not reset since it's used in device_info if not self._with_security: - _LOGGER.debug("SynoAPI._async_setup_api_requests() - disable security") + _LOGGER.debug("SynoAPI._setup_api_requests() - disable security") self.dsm.reset(self.security) self.security = None if not self._with_storage: - _LOGGER.debug("SynoAPI._async_setup_api_requests() - disable storage") + _LOGGER.debug("SynoAPI._setup_api_requests() - disable storage") self.dsm.reset(self.storage) self.storage = None if not self._with_system: - _LOGGER.debug("SynoAPI._async_setup_api_requests() - disable system") + _LOGGER.debug("SynoAPI._setup_api_requests() - disable system") self.dsm.reset(self.system) self.system = None if not self._with_upgrade: - _LOGGER.debug("SynoAPI._async_setup_api_requests() - disable upgrade") + _LOGGER.debug("SynoAPI._setup_api_requests() - disable upgrade") self.dsm.reset(self.upgrade) self.upgrade = None if not self._with_utilisation: - _LOGGER.debug("SynoAPI._async_setup_api_requests() - disable utilisation") + _LOGGER.debug("SynoAPI._setup_api_requests() - disable utilisation") self.dsm.reset(self.utilisation) self.utilisation = None if not self._with_surveillance_station: _LOGGER.debug( - "SynoAPI._async_setup_api_requests() - disable surveillance_station" + "SynoAPI._setup_api_requests() - disable surveillance_station" ) self.dsm.reset(self.surveillance_station) self.surveillance_station = None @@ -504,26 +535,31 @@ class SynoApi: async def async_reboot(self): """Reboot NAS.""" - if not self.system: - _LOGGER.debug("SynoAPI.async_reboot() - System API not ready: %s", self) - return - await self._hass.async_add_executor_job(self.system.reboot) + try: + await self._hass.async_add_executor_job(self.system.reboot) + except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: + _LOGGER.error("Reboot not possible, please try again later") + _LOGGER.debug("Exception:%s", err) async def async_shutdown(self): """Shutdown NAS.""" - if not self.system: - _LOGGER.debug("SynoAPI.async_shutdown() - System API not ready: %s", self) - return - await self._hass.async_add_executor_job(self.system.shutdown) + try: + await self._hass.async_add_executor_job(self.system.shutdown) + except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: + _LOGGER.error("Shutdown not possible, please try again later") + _LOGGER.debug("Exception:%s", err) async def async_unload(self): """Stop interacting with the NAS and prepare for removal from hass.""" - self._unsub_dispatcher() + try: + await self._hass.async_add_executor_job(self.dsm.logout) + except (SynologyDSMAPIErrorException, SynologyDSMRequestException) as err: + _LOGGER.debug("Logout not possible:%s", err) async def async_update(self, now=None): """Update function for updating API information.""" _LOGGER.debug("SynoAPI.async_update()") - self._async_setup_api_requests() + self._setup_api_requests() try: await self._hass.async_add_executor_job( self.dsm.update, self._with_information @@ -535,10 +571,9 @@ class SynoApi: _LOGGER.debug("SynoAPI.async_update() - exception: %s", err) await self._hass.config_entries.async_reload(self._entry.entry_id) return - async_dispatcher_send(self._hass, self.signal_sensor_update) -class SynologyDSMBaseEntity(Entity): +class SynologyDSMBaseEntity(CoordinatorEntity): """Representation of a Synology NAS entry.""" def __init__( @@ -546,8 +581,11 @@ class SynologyDSMBaseEntity(Entity): api: SynoApi, entity_type: str, entity_info: Dict[str, str], + coordinator: DataUpdateCoordinator, ): """Initialize the Synology DSM entity.""" + super().__init__(coordinator) + self._api = api self._api_key = entity_type.split(":")[0] self.entity_type = entity_type.split(":")[-1] @@ -606,59 +644,13 @@ class SynologyDSMBaseEntity(Entity): """Return if the entity should be enabled when first added to the entity registry.""" return self._enable_default - -class SynologyDSMDispatcherEntity(SynologyDSMBaseEntity, Entity): - """Representation of a Synology NAS entry.""" - - def __init__( - self, - api: SynoApi, - entity_type: str, - entity_info: Dict[str, str], - ): - """Initialize the Synology DSM entity.""" - super().__init__(api, entity_type, entity_info) - Entity.__init__(self) - - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False - - async def async_update(self): - """Only used by the generic entity update service.""" - if not self.enabled: - return - - await self._api.async_update() - async def async_added_to_hass(self): - """Register state update callback.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, self._api.signal_sensor_update, self.async_write_ha_state - ) - ) - + """Register entity for updates from API.""" self.async_on_remove(self._api.subscribe(self._api_key, self.unique_id)) + await super().async_added_to_hass() -class SynologyDSMCoordinatorEntity(SynologyDSMBaseEntity, CoordinatorEntity): - """Representation of a Synology NAS entry.""" - - def __init__( - self, - api: SynoApi, - entity_type: str, - entity_info: Dict[str, str], - coordinator: DataUpdateCoordinator, - ): - """Initialize the Synology DSM entity.""" - super().__init__(api, entity_type, entity_info) - CoordinatorEntity.__init__(self, coordinator) - - -class SynologyDSMDeviceEntity(SynologyDSMDispatcherEntity): +class SynologyDSMDeviceEntity(SynologyDSMBaseEntity): """Representation of a Synology NAS disk or volume entry.""" def __init__( @@ -666,10 +658,11 @@ class SynologyDSMDeviceEntity(SynologyDSMDispatcherEntity): api: SynoApi, entity_type: str, entity_info: Dict[str, str], + coordinator: DataUpdateCoordinator, device_id: str = None, ): """Initialize the Synology DSM disk or volume entity.""" - super().__init__(api, entity_type, entity_info) + super().__init__(api, entity_type, entity_info, coordinator) self._device_id = device_id self._device_name = None self._device_manufacturer = None diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index 2bbfb8f4641..6e89f3d7a84 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -6,8 +6,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DISKS from homeassistant.helpers.typing import HomeAssistantType -from . import SynologyDSMDeviceEntity, SynologyDSMDispatcherEntity +from . import SynologyDSMBaseEntity, SynologyDSMDeviceEntity from .const import ( + COORDINATOR_CENTRAL, DOMAIN, SECURITY_BINARY_SENSORS, STORAGE_DISK_BINARY_SENSORS, @@ -21,18 +22,20 @@ async def async_setup_entry( ) -> None: """Set up the Synology NAS binary sensor.""" - api = hass.data[DOMAIN][entry.unique_id][SYNO_API] + data = hass.data[DOMAIN][entry.unique_id] + api = data[SYNO_API] + coordinator = data[COORDINATOR_CENTRAL] entities = [ SynoDSMSecurityBinarySensor( - api, sensor_type, SECURITY_BINARY_SENSORS[sensor_type] + api, sensor_type, SECURITY_BINARY_SENSORS[sensor_type], coordinator ) for sensor_type in SECURITY_BINARY_SENSORS ] entities += [ SynoDSMUpgradeBinarySensor( - api, sensor_type, UPGRADE_BINARY_SENSORS[sensor_type] + api, sensor_type, UPGRADE_BINARY_SENSORS[sensor_type], coordinator ) for sensor_type in UPGRADE_BINARY_SENSORS ] @@ -42,7 +45,11 @@ async def async_setup_entry( for disk in entry.data.get(CONF_DISKS, api.storage.disks_ids): entities += [ SynoDSMStorageBinarySensor( - api, sensor_type, STORAGE_DISK_BINARY_SENSORS[sensor_type], disk + api, + sensor_type, + STORAGE_DISK_BINARY_SENSORS[sensor_type], + coordinator, + disk, ) for sensor_type in STORAGE_DISK_BINARY_SENSORS ] @@ -50,7 +57,7 @@ async def async_setup_entry( async_add_entities(entities) -class SynoDSMSecurityBinarySensor(SynologyDSMDispatcherEntity, BinarySensorEntity): +class SynoDSMSecurityBinarySensor(SynologyDSMBaseEntity, BinarySensorEntity): """Representation a Synology Security binary sensor.""" @property @@ -78,7 +85,7 @@ class SynoDSMStorageBinarySensor(SynologyDSMDeviceEntity, BinarySensorEntity): return getattr(self._api.storage, self.entity_type)(self._device_id) -class SynoDSMUpgradeBinarySensor(SynologyDSMDispatcherEntity, BinarySensorEntity): +class SynoDSMUpgradeBinarySensor(SynologyDSMBaseEntity, BinarySensorEntity): """Representation a Synology Upgrade binary sensor.""" @property diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index f24615bd28e..c0e0ded72ed 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -3,16 +3,19 @@ import logging from typing import Dict from synology_dsm.api.surveillance_station import SynoSurveillanceStation -from synology_dsm.exceptions import SynologyDSMAPIErrorException +from synology_dsm.exceptions import ( + SynologyDSMAPIErrorException, + SynologyDSMRequestException, +) from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import SynoApi, SynologyDSMCoordinatorEntity +from . import SynoApi, SynologyDSMBaseEntity from .const import ( - COORDINATOR_SURVEILLANCE, + COORDINATOR_CAMERAS, DOMAIN, ENTITY_CLASS, ENTITY_ENABLE, @@ -37,7 +40,7 @@ async def async_setup_entry( return # initial data fetch - coordinator = data[COORDINATOR_SURVEILLANCE] + coordinator = data[COORDINATOR_CAMERAS] await coordinator.async_refresh() async_add_entities( @@ -46,7 +49,7 @@ async def async_setup_entry( ) -class SynoDSMCamera(SynologyDSMCoordinatorEntity, Camera): +class SynoDSMCamera(SynologyDSMBaseEntity, Camera): """Representation a Synology camera.""" def __init__( @@ -125,7 +128,11 @@ class SynoDSMCamera(SynologyDSMCoordinatorEntity, Camera): return None try: return self._api.surveillance_station.get_camera_image(self._camera_id) - except (SynologyDSMAPIErrorException) as err: + except ( + SynologyDSMAPIErrorException, + SynologyDSMRequestException, + ConnectionRefusedError, + ) as err: _LOGGER.debug( "SynoDSMCamera.camera_image(%s) - Exception:%s", self.camera_data.name, diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index f4638a5ec73..e7b510bb399 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -31,6 +31,7 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from .const import ( + CONF_DEVICE_TOKEN, CONF_VOLUMES, DEFAULT_PORT, DEFAULT_PORT_SSL, @@ -180,7 +181,7 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_MAC: api.network.macs, } if otp_code: - config_data["device_token"] = api.device_token + config_data[CONF_DEVICE_TOKEN] = api.device_token if user_input.get(CONF_DISKS): config_data[CONF_DISKS] = user_input[CONF_DISKS] if user_input.get(CONF_VOLUMES): diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 1c4e004f749..97f378c8e76 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -19,7 +19,10 @@ from homeassistant.const import ( DOMAIN = "synology_dsm" PLATFORMS = ["binary_sensor", "camera", "sensor", "switch"] -COORDINATOR_SURVEILLANCE = "coordinator_surveillance_station" +COORDINATOR_CAMERAS = "coordinator_cameras" +COORDINATOR_CENTRAL = "coordinator_central" +COORDINATOR_SWITCHES = "coordinator_switches" +SYSTEM_LOADED = "system_loaded" # Entry keys SYNO_API = "syno_api" @@ -28,6 +31,7 @@ UNDO_UPDATE_LISTENER = "undo_update_listener" # Configuration CONF_SERIAL = "serial" CONF_VOLUMES = "volumes" +CONF_DEVICE_TOKEN = "device_token" DEFAULT_USE_SSL = True DEFAULT_VERIFY_SSL = False diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 7dd4e5e9870..79350ce89d3 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -13,11 +13,13 @@ from homeassistant.const import ( ) from homeassistant.helpers.temperature import display_temp from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.dt import utcnow -from . import SynoApi, SynologyDSMDeviceEntity, SynologyDSMDispatcherEntity +from . import SynoApi, SynologyDSMBaseEntity, SynologyDSMDeviceEntity from .const import ( CONF_VOLUMES, + COORDINATOR_CENTRAL, DOMAIN, ENTITY_UNIT_LOAD, INFORMATION_SENSORS, @@ -34,10 +36,14 @@ async def async_setup_entry( ) -> None: """Set up the Synology NAS Sensor.""" - api = hass.data[DOMAIN][entry.unique_id][SYNO_API] + data = hass.data[DOMAIN][entry.unique_id] + api = data[SYNO_API] + coordinator = data[COORDINATOR_CENTRAL] entities = [ - SynoDSMUtilSensor(api, sensor_type, UTILISATION_SENSORS[sensor_type]) + SynoDSMUtilSensor( + api, sensor_type, UTILISATION_SENSORS[sensor_type], coordinator + ) for sensor_type in UTILISATION_SENSORS ] @@ -46,7 +52,11 @@ async def async_setup_entry( for volume in entry.data.get(CONF_VOLUMES, api.storage.volumes_ids): entities += [ SynoDSMStorageSensor( - api, sensor_type, STORAGE_VOL_SENSORS[sensor_type], volume + api, + sensor_type, + STORAGE_VOL_SENSORS[sensor_type], + coordinator, + volume, ) for sensor_type in STORAGE_VOL_SENSORS ] @@ -56,20 +66,26 @@ async def async_setup_entry( for disk in entry.data.get(CONF_DISKS, api.storage.disks_ids): entities += [ SynoDSMStorageSensor( - api, sensor_type, STORAGE_DISK_SENSORS[sensor_type], disk + api, + sensor_type, + STORAGE_DISK_SENSORS[sensor_type], + coordinator, + disk, ) for sensor_type in STORAGE_DISK_SENSORS ] entities += [ - SynoDSMInfoSensor(api, sensor_type, INFORMATION_SENSORS[sensor_type]) + SynoDSMInfoSensor( + api, sensor_type, INFORMATION_SENSORS[sensor_type], coordinator + ) for sensor_type in INFORMATION_SENSORS ] async_add_entities(entities) -class SynoDSMUtilSensor(SynologyDSMDispatcherEntity): +class SynoDSMUtilSensor(SynologyDSMBaseEntity): """Representation a Synology Utilisation sensor.""" @property @@ -122,12 +138,18 @@ class SynoDSMStorageSensor(SynologyDSMDeviceEntity): return attr -class SynoDSMInfoSensor(SynologyDSMDispatcherEntity): +class SynoDSMInfoSensor(SynologyDSMBaseEntity): """Representation a Synology information sensor.""" - def __init__(self, api: SynoApi, entity_type: str, entity_info: Dict[str, str]): + def __init__( + self, + api: SynoApi, + entity_type: str, + entity_info: Dict[str, str], + coordinator: DataUpdateCoordinator, + ): """Initialize the Synology SynoDSMInfoSensor entity.""" - super().__init__(api, entity_type, entity_info) + super().__init__(api, entity_type, entity_info, coordinator) self._previous_uptime = None self._last_boot = None diff --git a/homeassistant/components/synology_dsm/switch.py b/homeassistant/components/synology_dsm/switch.py index 21511757cf3..998f74adf2a 100644 --- a/homeassistant/components/synology_dsm/switch.py +++ b/homeassistant/components/synology_dsm/switch.py @@ -7,9 +7,10 @@ from synology_dsm.api.surveillance_station import SynoSurveillanceStation from homeassistant.components.switch import ToggleEntity from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import SynoApi, SynologyDSMDispatcherEntity -from .const import DOMAIN, SURVEILLANCE_SWITCH, SYNO_API +from . import SynoApi, SynologyDSMBaseEntity +from .const import COORDINATOR_SWITCHES, DOMAIN, SURVEILLANCE_SWITCH, SYNO_API _LOGGER = logging.getLogger(__name__) @@ -19,16 +20,21 @@ async def async_setup_entry( ) -> None: """Set up the Synology NAS switch.""" - api = hass.data[DOMAIN][entry.unique_id][SYNO_API] + data = hass.data[DOMAIN][entry.unique_id] + api = data[SYNO_API] entities = [] if SynoSurveillanceStation.INFO_API_KEY in api.dsm.apis: info = await hass.async_add_executor_job(api.dsm.surveillance_station.get_info) version = info["data"]["CMSMinVersion"] + + # initial data fetch + coordinator = data[COORDINATOR_SWITCHES] + await coordinator.async_refresh() entities += [ SynoDSMSurveillanceHomeModeToggle( - api, sensor_type, SURVEILLANCE_SWITCH[sensor_type], version + api, sensor_type, SURVEILLANCE_SWITCH[sensor_type], version, coordinator ) for sensor_type in SURVEILLANCE_SWITCH ] @@ -36,58 +42,52 @@ async def async_setup_entry( async_add_entities(entities, True) -class SynoDSMSurveillanceHomeModeToggle(SynologyDSMDispatcherEntity, ToggleEntity): +class SynoDSMSurveillanceHomeModeToggle(SynologyDSMBaseEntity, ToggleEntity): """Representation a Synology Surveillance Station Home Mode toggle.""" def __init__( - self, api: SynoApi, entity_type: str, entity_info: Dict[str, str], version: str + self, + api: SynoApi, + entity_type: str, + entity_info: Dict[str, str], + version: str, + coordinator: DataUpdateCoordinator, ): """Initialize a Synology Surveillance Station Home Mode.""" super().__init__( api, entity_type, entity_info, + coordinator, ) self._version = version - self._state = None @property def is_on(self) -> bool: """Return the state.""" - if self.entity_type == "home_mode": - return self._state - return None + return self.coordinator.data["switches"][self.entity_type] - @property - def should_poll(self) -> bool: - """No polling needed.""" - return True - - async def async_update(self): - """Update the toggle state.""" - _LOGGER.debug( - "SynoDSMSurveillanceHomeModeToggle.async_update(%s)", - self._api.information.serial, - ) - self._state = await self.hass.async_add_executor_job( - self._api.surveillance_station.get_home_mode_status - ) - - def turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs) -> None: """Turn on Home mode.""" _LOGGER.debug( "SynoDSMSurveillanceHomeModeToggle.turn_on(%s)", self._api.information.serial, ) - self._api.surveillance_station.set_home_mode(True) + await self.hass.async_add_executor_job( + self._api.dsm.surveillance_station.set_home_mode, True + ) + await self.coordinator.async_request_refresh() - def turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: """Turn off Home mode.""" _LOGGER.debug( "SynoDSMSurveillanceHomeModeToggle.turn_off(%s)", self._api.information.serial, ) - self._api.surveillance_station.set_home_mode(False) + await self.hass.async_add_executor_job( + self._api.dsm.surveillance_station.set_home_mode, False + ) + await self.coordinator.async_request_refresh() @property def available(self) -> bool: From 23b2953773152e6563a2ff1e1c8f817a746fed65 Mon Sep 17 00:00:00 2001 From: MHV33 <41445347+MHV33@users.noreply.github.com> Date: Tue, 23 Feb 2021 16:25:50 -0600 Subject: [PATCH 675/796] Add more shopping list services (#45591) Co-authored-by: Franck Nijhof --- .../components/shopping_list/__init__.py | 54 ++++++++++++++++++- .../components/shopping_list/services.yaml | 13 +++++ tests/components/shopping_list/test_init.py | 34 ++++++++++++ 3 files changed, 99 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index 1831f894cec..e438bf3b8f4 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -15,17 +15,21 @@ from homeassistant.util.json import load_json, save_json from .const import DOMAIN ATTR_NAME = "name" +ATTR_COMPLETE = "complete" _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({DOMAIN: {}}, extra=vol.ALLOW_EXTRA) EVENT = "shopping_list_updated" -ITEM_UPDATE_SCHEMA = vol.Schema({"complete": bool, ATTR_NAME: str}) +ITEM_UPDATE_SCHEMA = vol.Schema({ATTR_COMPLETE: bool, ATTR_NAME: str}) PERSISTENCE = ".shopping_list.json" SERVICE_ADD_ITEM = "add_item" SERVICE_COMPLETE_ITEM = "complete_item" - +SERVICE_INCOMPLETE_ITEM = "incomplete_item" +SERVICE_COMPLETE_ALL = "complete_all" +SERVICE_INCOMPLETE_ALL = "incomplete_all" SERVICE_ITEM_SCHEMA = vol.Schema({vol.Required(ATTR_NAME): vol.Any(None, cv.string)}) +SERVICE_LIST_SCHEMA = vol.Schema({}) WS_TYPE_SHOPPING_LIST_ITEMS = "shopping_list/items" WS_TYPE_SHOPPING_LIST_ADD_ITEM = "shopping_list/items/add" @@ -92,6 +96,27 @@ async def async_setup_entry(hass, config_entry): else: await data.async_update(item["id"], {"name": name, "complete": True}) + async def incomplete_item_service(call): + """Mark the item provided via `name` as incomplete.""" + data = hass.data[DOMAIN] + name = call.data.get(ATTR_NAME) + if name is None: + return + try: + item = [item for item in data.items if item["name"] == name][0] + except IndexError: + _LOGGER.error("Restoring of item failed: %s cannot be found", name) + else: + await data.async_update(item["id"], {"name": name, "complete": False}) + + async def complete_all_service(call): + """Mark all items in the list as complete.""" + await data.async_update_list({"complete": True}) + + async def incomplete_all_service(call): + """Mark all items in the list as incomplete.""" + await data.async_update_list({"complete": False}) + data = hass.data[DOMAIN] = ShoppingData(hass) await data.async_load() @@ -101,6 +126,24 @@ async def async_setup_entry(hass, config_entry): hass.services.async_register( DOMAIN, SERVICE_COMPLETE_ITEM, complete_item_service, schema=SERVICE_ITEM_SCHEMA ) + hass.services.async_register( + DOMAIN, + SERVICE_INCOMPLETE_ITEM, + incomplete_item_service, + schema=SERVICE_ITEM_SCHEMA, + ) + hass.services.async_register( + DOMAIN, + SERVICE_COMPLETE_ALL, + complete_all_service, + schema=SERVICE_LIST_SCHEMA, + ) + hass.services.async_register( + DOMAIN, + SERVICE_INCOMPLETE_ALL, + incomplete_all_service, + schema=SERVICE_LIST_SCHEMA, + ) hass.http.register_view(ShoppingListView) hass.http.register_view(CreateShoppingListItemView) @@ -165,6 +208,13 @@ class ShoppingData: self.items = [itm for itm in self.items if not itm["complete"]] await self.hass.async_add_executor_job(self.save) + async def async_update_list(self, info): + """Update all items in the list.""" + for item in self.items: + item.update(info) + await self.hass.async_add_executor_job(self.save) + return self.items + @callback def async_reorder(self, item_ids): """Reorder items.""" diff --git a/homeassistant/components/shopping_list/services.yaml b/homeassistant/components/shopping_list/services.yaml index 2a1e89b9786..73540210232 100644 --- a/homeassistant/components/shopping_list/services.yaml +++ b/homeassistant/components/shopping_list/services.yaml @@ -21,3 +21,16 @@ complete_item: example: Beer selector: text: + +incomplete_item: + description: Marks an item as incomplete in the shopping list. + fields: + name: + description: The name of the item to mark as incomplete. + example: Beer + +complete_all: + description: Marks all items as completed in the shopping list. It does not remove the items. + +incomplete_all: + description: Marks all items as incomplete in the shopping list. diff --git a/tests/components/shopping_list/test_init.py b/tests/components/shopping_list/test_init.py index 0be4c70ef18..48482787f4d 100644 --- a/tests/components/shopping_list/test_init.py +++ b/tests/components/shopping_list/test_init.py @@ -1,5 +1,6 @@ """Test shopping list component.""" +from homeassistant.components.shopping_list.const import DOMAIN from homeassistant.components.websocket_api.const import ( ERR_INVALID_FORMAT, ERR_NOT_FOUND, @@ -19,6 +20,39 @@ async def test_add_item(hass, sl_setup): assert response.speech["plain"]["speech"] == "I've added beer to your shopping list" +async def test_update_list(hass, sl_setup): + """Test updating all list items.""" + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} + ) + + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "cheese"}} + ) + + # Update a single attribute, other attributes shouldn't change + await hass.data[DOMAIN].async_update_list({"complete": True}) + + beer = hass.data[DOMAIN].items[0] + assert beer["name"] == "beer" + assert beer["complete"] is True + + cheese = hass.data[DOMAIN].items[1] + assert cheese["name"] == "cheese" + assert cheese["complete"] is True + + # Update multiple attributes + await hass.data[DOMAIN].async_update_list({"name": "dupe", "complete": False}) + + beer = hass.data[DOMAIN].items[0] + assert beer["name"] == "dupe" + assert beer["complete"] is False + + cheese = hass.data[DOMAIN].items[1] + assert cheese["name"] == "dupe" + assert cheese["complete"] is False + + async def test_recent_items_intent(hass, sl_setup): """Test recent items.""" From c9847920affcdbfd4ad4b0e377691b73a16ded58 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Tue, 23 Feb 2021 17:32:39 -0500 Subject: [PATCH 676/796] Use core constants for dht (#46029) --- homeassistant/components/dht/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/dht/sensor.py b/homeassistant/components/dht/sensor.py index 57e12d03ffe..0bddd5a187e 100644 --- a/homeassistant/components/dht/sensor.py +++ b/homeassistant/components/dht/sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, + CONF_PIN, PERCENTAGE, TEMP_FAHRENHEIT, ) @@ -19,7 +20,6 @@ from homeassistant.util.temperature import celsius_to_fahrenheit _LOGGER = logging.getLogger(__name__) -CONF_PIN = "pin" CONF_SENSOR = "sensor" CONF_HUMIDITY_OFFSET = "humidity_offset" CONF_TEMPERATURE_OFFSET = "temperature_offset" @@ -56,7 +56,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the DHT sensor.""" - SENSOR_TYPES[SENSOR_TEMPERATURE][1] = hass.config.units.temperature_unit available_sensors = { "AM2302": Adafruit_DHT.AM2302, From 425d56d024d82dc593703803520184307558f224 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 23 Feb 2021 16:36:46 -0600 Subject: [PATCH 677/796] Fix Plex showing removed shared users (#46971) --- homeassistant/components/plex/server.py | 20 +++++++++++++------- tests/components/plex/conftest.py | 10 ++++++++++ tests/components/plex/test_init.py | 4 ++++ tests/fixtures/plex/plextv_shared_users.xml | 9 +++++++++ 4 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 tests/fixtures/plex/plextv_shared_users.xml diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 1baceb78ff1..8f9d4d1cc51 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -213,21 +213,27 @@ class PlexServer: try: system_accounts = self._plex_server.systemAccounts() + shared_users = self.account.users() if self.account else [] except Unauthorized: _LOGGER.warning( "Plex account has limited permissions, shared account filtering will not be available" ) else: - self._accounts = [ - account.name for account in system_accounts if account.name - ] + self._accounts = [] + for user in shared_users: + for shared_server in user.servers: + if shared_server.machineIdentifier == self.machine_identifier: + self._accounts.append(user.title) + _LOGGER.debug("Linked accounts: %s", self.accounts) - owner_account = [ - account.name for account in system_accounts if account.accountID == 1 - ] + owner_account = next( + (account.name for account in system_accounts if account.accountID == 1), + None, + ) if owner_account: - self._owner_username = owner_account[0] + self._owner_username = owner_account + self._accounts.append(owner_account) _LOGGER.debug("Server owner found: '%s'", self._owner_username) self._version = self._plex_server.version diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py index d3e66cc4989..372a06f15b6 100644 --- a/tests/components/plex/conftest.py +++ b/tests/components/plex/conftest.py @@ -218,6 +218,12 @@ def plextv_resources_fixture(plextv_resources_base): return plextv_resources_base.format(second_server_enabled=0) +@pytest.fixture(name="plextv_shared_users", scope="session") +def plextv_shared_users_fixture(plextv_resources_base): + """Load payload for plex.tv shared users and return it.""" + return load_fixture("plex/plextv_shared_users.xml") + + @pytest.fixture(name="session_base", scope="session") def session_base_fixture(): """Load the base session payload and return it.""" @@ -293,6 +299,7 @@ def mock_plex_calls( children_200, children_300, empty_library, + empty_payload, grandchildren_300, library, library_sections, @@ -310,12 +317,15 @@ def mock_plex_calls( playlist_500, plextv_account, plextv_resources, + plextv_shared_users, plex_server_accounts, plex_server_clients, plex_server_default, security_token, ): """Mock Plex API calls.""" + requests_mock.get("https://plex.tv/api/users/", text=plextv_shared_users) + requests_mock.get("https://plex.tv/api/invites/requested", text=empty_payload) requests_mock.get("https://plex.tv/users/account", text=plextv_account) requests_mock.get("https://plex.tv/api/resources", text=plextv_resources) diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index 95d2ef9bddb..2e5a30ce11a 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -116,6 +116,7 @@ async def test_setup_when_certificate_changed( plex_server_default, plextv_account, plextv_resources, + plextv_shared_users, ): """Test setup component when the Plex certificate has changed.""" await async_setup_component(hass, "persistent_notification", {}) @@ -141,6 +142,9 @@ async def test_setup_when_certificate_changed( unique_id=DEFAULT_DATA["server_id"], ) + requests_mock.get("https://plex.tv/api/users/", text=plextv_shared_users) + requests_mock.get("https://plex.tv/api/invites/requested", text=empty_payload) + requests_mock.get("https://plex.tv/users/account", text=plextv_account) requests_mock.get("https://plex.tv/api/resources", text=plextv_resources) requests_mock.get(old_url, exc=WrongCertHostnameException) diff --git a/tests/fixtures/plex/plextv_shared_users.xml b/tests/fixtures/plex/plextv_shared_users.xml new file mode 100644 index 00000000000..9421bdfa17a --- /dev/null +++ b/tests/fixtures/plex/plextv_shared_users.xml @@ -0,0 +1,9 @@ + + + + + + + + + From 228096847bddc56d095459597e0681daf49fdeb5 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Tue, 23 Feb 2021 23:42:24 +0100 Subject: [PATCH 678/796] Fix typo in fireservicerota strings (#46973) --- homeassistant/components/fireservicerota/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fireservicerota/strings.json b/homeassistant/components/fireservicerota/strings.json index c44673d6c2c..aef6f1b6849 100644 --- a/homeassistant/components/fireservicerota/strings.json +++ b/homeassistant/components/fireservicerota/strings.json @@ -9,7 +9,7 @@ } }, "reauth": { - "description": "Authentication tokens baceame invalid, login to recreate them.", + "description": "Authentication tokens became invalid, login to recreate them.", "data": { "password": "[%key:common::config_flow::data::password%]" } From 1a99562e91a06a4523d9a4071db4d4d8d8aa7fdd Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 23 Feb 2021 18:58:04 -0500 Subject: [PATCH 679/796] Add zwave_js.refresh_value service (#46944) * add poll_value service * switch vol.All to vol.Schema * more relevant log message * switch service name to refresh_value, add parameter to refresh all watched values, fix tests * rename parameter and create task for polling command so we don't wait for a response * raise ValueError for unknown entity * better error message * fix test --- homeassistant/components/zwave_js/__init__.py | 4 +- homeassistant/components/zwave_js/const.py | 4 ++ homeassistant/components/zwave_js/entity.py | 38 ++++++++++ homeassistant/components/zwave_js/services.py | 31 +++++++- .../components/zwave_js/services.yaml | 16 +++++ tests/components/zwave_js/common.py | 3 + tests/components/zwave_js/test_climate.py | 8 ++- tests/components/zwave_js/test_services.py | 71 ++++++++++++++++++- 8 files changed, 168 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index a70716ad421..062b28cf6a9 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -15,7 +15,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_DOMAIN, CONF_URL, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import device_registry +from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -193,7 +193,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_UNSUBSCRIBE: unsubscribe_callbacks, } - services = ZWaveServices(hass) + services = ZWaveServices(hass, entity_registry.async_get(hass)) services.async_register() # Set up websocket API diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 1031a51719a..dba4e6d33a3 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -40,4 +40,8 @@ ATTR_CONFIG_PARAMETER = "parameter" ATTR_CONFIG_PARAMETER_BITMASK = "bitmask" ATTR_CONFIG_VALUE = "value" +SERVICE_REFRESH_VALUE = "refresh_value" + +ATTR_REFRESH_ALL_VALUES = "refresh_all_values" + ADDON_SLUG = "core_zwave_js" diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 3141dd0caea..cb898e861e9 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -8,8 +8,10 @@ from zwave_js_server.model.value import Value as ZwaveValue, get_value_id from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity +from .const import DOMAIN from .discovery import ZwaveDiscoveryInfo from .helpers import get_device_id @@ -39,6 +41,35 @@ class ZWaveBaseEntity(Entity): To be overridden by platforms needing this event. """ + async def async_poll_value(self, refresh_all_values: bool) -> None: + """Poll a value.""" + assert self.hass + if not refresh_all_values: + self.hass.async_create_task( + self.info.node.async_poll_value(self.info.primary_value) + ) + LOGGER.info( + ( + "Refreshing primary value %s for %s, " + "state update may be delayed for devices on battery" + ), + self.info.primary_value, + self.entity_id, + ) + return + + for value_id in self.watched_value_ids: + self.hass.async_create_task(self.info.node.async_poll_value(value_id)) + + LOGGER.info( + ( + "Refreshing values %s for %s, state update may be delayed for " + "devices on battery" + ), + ", ".join(self.watched_value_ids), + self.entity_id, + ) + async def async_added_to_hass(self) -> None: """Call when entity is added.""" assert self.hass # typing @@ -46,6 +77,13 @@ class ZWaveBaseEntity(Entity): self.async_on_remove( self.info.node.on(EVENT_VALUE_UPDATED, self._value_changed) ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{DOMAIN}_{self.unique_id}_poll_value", + self.async_poll_value, + ) + ) @property def device_info(self) -> dict: diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index da60ddab666..c971891b35b 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -10,6 +10,8 @@ from zwave_js_server.util.node import async_set_config_parameter from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID from homeassistant.core import HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity_registry import EntityRegistry from . import const from .helpers import async_get_node_from_device_id, async_get_node_from_entity_id @@ -41,9 +43,10 @@ BITMASK_SCHEMA = vol.All( class ZWaveServices: """Class that holds our services (Zwave Commands) that should be published to hass.""" - def __init__(self, hass: HomeAssistant): + def __init__(self, hass: HomeAssistant, ent_reg: EntityRegistry): """Initialize with hass object.""" self._hass = hass + self._ent_reg = ent_reg @callback def async_register(self) -> None: @@ -71,6 +74,18 @@ class ZWaveServices: ), ) + self._hass.services.async_register( + const.DOMAIN, + const.SERVICE_REFRESH_VALUE, + self.async_poll_value, + schema=vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(const.ATTR_REFRESH_ALL_VALUES, default=False): bool, + } + ), + ) + async def async_set_config_parameter(self, service: ServiceCall) -> None: """Set a config value on a node.""" nodes: Set[ZwaveNode] = set() @@ -108,3 +123,17 @@ class ZWaveServices: f"Unable to set configuration parameter on Node {node} with " f"value {new_value}" ) + + async def async_poll_value(self, service: ServiceCall) -> None: + """Poll value on a node.""" + for entity_id in service.data[ATTR_ENTITY_ID]: + entry = self._ent_reg.async_get(entity_id) + if entry is None or entry.platform != const.DOMAIN: + raise ValueError( + f"Entity {entity_id} is not a valid {const.DOMAIN} entity." + ) + async_dispatcher_send( + self._hass, + f"{const.DOMAIN}_{entry.unique_id}_poll_value", + service.data[const.ATTR_REFRESH_ALL_VALUES], + ) diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml index a5e9efd7216..8e6d907fc96 100644 --- a/homeassistant/components/zwave_js/services.yaml +++ b/homeassistant/components/zwave_js/services.yaml @@ -66,3 +66,19 @@ set_config_parameter: advanced: true selector: object: + +refresh_value: + name: Refresh value(s) of a Z-Wave entity + description: Force update value(s) for a Z-Wave entity + target: + entity: + integration: zwave_js + fields: + refresh_all_values: + name: Refresh all values? + description: Whether to refresh all values (true) or just the primary value (false) + required: false + example: true + default: false + selector: + boolean: diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index 9c6adb100fa..ebba16136a0 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -13,3 +13,6 @@ NOTIFICATION_MOTION_SENSOR = "sensor.multisensor_6_home_security_motion_sensor_s PROPERTY_DOOR_STATUS_BINARY_SENSOR = ( "binary_sensor.august_smart_lock_pro_3rd_gen_the_current_status_of_the_door" ) +CLIMATE_RADIO_THERMOSTAT_ENTITY = "climate.z_wave_thermostat" +CLIMATE_DANFOSS_LC13_ENTITY = "climate.living_connect_z_thermostat" +CLIMATE_FLOOR_THERMOSTAT_ENTITY = "climate.floor_thermostat" diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index 17f4dd38144..1ccf6f82017 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -28,9 +28,11 @@ from homeassistant.components.climate.const import ( from homeassistant.components.zwave_js.climate import ATTR_FAN_STATE from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE -CLIMATE_RADIO_THERMOSTAT_ENTITY = "climate.z_wave_thermostat" -CLIMATE_DANFOSS_LC13_ENTITY = "climate.living_connect_z_thermostat" -CLIMATE_FLOOR_THERMOSTAT_ENTITY = "climate.floor_thermostat" +from .common import ( + CLIMATE_DANFOSS_LC13_ENTITY, + CLIMATE_FLOOR_THERMOSTAT_ENTITY, + CLIMATE_RADIO_THERMOSTAT_ENTITY, +) async def test_thermostat_v2( diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index b085d9e32fb..8e882b9547c 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -6,14 +6,16 @@ from homeassistant.components.zwave_js.const import ( ATTR_CONFIG_PARAMETER, ATTR_CONFIG_PARAMETER_BITMASK, ATTR_CONFIG_VALUE, + ATTR_REFRESH_ALL_VALUES, DOMAIN, + SERVICE_REFRESH_VALUE, SERVICE_SET_CONFIG_PARAMETER, ) from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID from homeassistant.helpers.device_registry import async_get as async_get_dev_reg from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg -from .common import AIR_TEMPERATURE_SENSOR +from .common import AIR_TEMPERATURE_SENSOR, CLIMATE_RADIO_THERMOSTAT_ENTITY from tests.common import MockConfigEntry @@ -293,3 +295,70 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): }, blocking=True, ) + + +async def test_poll_value( + hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, integration +): + """Test the poll_value service.""" + # Test polling the primary value + client.async_send_command.return_value = {"result": 2} + await hass.services.async_call( + DOMAIN, + SERVICE_REFRESH_VALUE, + {ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY}, + blocking=True, + ) + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.poll_value" + assert args["nodeId"] == 26 + assert args["valueId"] == { + "commandClassName": "Thermostat Mode", + "commandClass": 64, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "min": 0, + "max": 31, + "label": "Thermostat mode", + "states": { + "0": "Off", + "1": "Heat", + "2": "Cool", + "3": "Auto", + "11": "Energy heat", + "12": "Energy cool", + }, + }, + "value": 1, + "ccVersion": 2, + } + + client.async_send_command.reset_mock() + + # Test polling all watched values + client.async_send_command.return_value = {"result": 2} + await hass.services.async_call( + DOMAIN, + SERVICE_REFRESH_VALUE, + { + ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY, + ATTR_REFRESH_ALL_VALUES: True, + }, + blocking=True, + ) + assert len(client.async_send_command.call_args_list) == 8 + + # Test polling against an invalid entity raises ValueError + with pytest.raises(ValueError): + await hass.services.async_call( + DOMAIN, + SERVICE_REFRESH_VALUE, + {ATTR_ENTITY_ID: "sensor.fake_entity_id"}, + blocking=True, + ) From b657fd02cdb3a839adee909323ce9b69504b83b4 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 23 Feb 2021 18:58:25 -0500 Subject: [PATCH 680/796] deep copy zwave_js state in test fixtures so tests are more isolated (#46976) --- tests/components/zwave_js/conftest.py | 42 +++++++++++++++------------ 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index b4b89fb14a2..2f082621c45 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -1,5 +1,6 @@ """Provide common Z-Wave JS fixtures.""" import asyncio +import copy import json from unittest.mock import AsyncMock, patch @@ -188,7 +189,7 @@ def mock_client_fixture(controller_state, version_state): @pytest.fixture(name="multisensor_6") def multisensor_6_fixture(client, multisensor_6_state): """Mock a multisensor 6 node.""" - node = Node(client, multisensor_6_state) + node = Node(client, copy.deepcopy(multisensor_6_state)) client.driver.controller.nodes[node.node_id] = node return node @@ -196,7 +197,7 @@ def multisensor_6_fixture(client, multisensor_6_state): @pytest.fixture(name="ecolink_door_sensor") def legacy_binary_sensor_fixture(client, ecolink_door_sensor_state): """Mock a legacy_binary_sensor node.""" - node = Node(client, ecolink_door_sensor_state) + node = Node(client, copy.deepcopy(ecolink_door_sensor_state)) client.driver.controller.nodes[node.node_id] = node return node @@ -204,7 +205,7 @@ def legacy_binary_sensor_fixture(client, ecolink_door_sensor_state): @pytest.fixture(name="hank_binary_switch") def hank_binary_switch_fixture(client, hank_binary_switch_state): """Mock a binary switch node.""" - node = Node(client, hank_binary_switch_state) + node = Node(client, copy.deepcopy(hank_binary_switch_state)) client.driver.controller.nodes[node.node_id] = node return node @@ -212,7 +213,7 @@ def hank_binary_switch_fixture(client, hank_binary_switch_state): @pytest.fixture(name="bulb_6_multi_color") def bulb_6_multi_color_fixture(client, bulb_6_multi_color_state): """Mock a bulb 6 multi-color node.""" - node = Node(client, bulb_6_multi_color_state) + node = Node(client, copy.deepcopy(bulb_6_multi_color_state)) client.driver.controller.nodes[node.node_id] = node return node @@ -220,7 +221,7 @@ def bulb_6_multi_color_fixture(client, bulb_6_multi_color_state): @pytest.fixture(name="eaton_rf9640_dimmer") def eaton_rf9640_dimmer_fixture(client, eaton_rf9640_dimmer_state): """Mock a Eaton RF9640 (V4 compatible) dimmer node.""" - node = Node(client, eaton_rf9640_dimmer_state) + node = Node(client, copy.deepcopy(eaton_rf9640_dimmer_state)) client.driver.controller.nodes[node.node_id] = node return node @@ -228,7 +229,7 @@ def eaton_rf9640_dimmer_fixture(client, eaton_rf9640_dimmer_state): @pytest.fixture(name="lock_schlage_be469") def lock_schlage_be469_fixture(client, lock_schlage_be469_state): """Mock a schlage lock node.""" - node = Node(client, lock_schlage_be469_state) + node = Node(client, copy.deepcopy(lock_schlage_be469_state)) client.driver.controller.nodes[node.node_id] = node return node @@ -236,7 +237,7 @@ def lock_schlage_be469_fixture(client, lock_schlage_be469_state): @pytest.fixture(name="lock_august_pro") def lock_august_asl03_fixture(client, lock_august_asl03_state): """Mock a August Pro lock node.""" - node = Node(client, lock_august_asl03_state) + node = Node(client, copy.deepcopy(lock_august_asl03_state)) client.driver.controller.nodes[node.node_id] = node return node @@ -246,7 +247,7 @@ def climate_radio_thermostat_ct100_plus_fixture( client, climate_radio_thermostat_ct100_plus_state ): """Mock a climate radio thermostat ct100 plus node.""" - node = Node(client, climate_radio_thermostat_ct100_plus_state) + node = Node(client, copy.deepcopy(climate_radio_thermostat_ct100_plus_state)) client.driver.controller.nodes[node.node_id] = node return node @@ -256,7 +257,10 @@ def climate_radio_thermostat_ct100_plus_different_endpoints_fixture( client, climate_radio_thermostat_ct100_plus_different_endpoints_state ): """Mock a climate radio thermostat ct100 plus node with values on different endpoints.""" - node = Node(client, climate_radio_thermostat_ct100_plus_different_endpoints_state) + node = Node( + client, + copy.deepcopy(climate_radio_thermostat_ct100_plus_different_endpoints_state), + ) client.driver.controller.nodes[node.node_id] = node return node @@ -264,7 +268,7 @@ def climate_radio_thermostat_ct100_plus_different_endpoints_fixture( @pytest.fixture(name="climate_danfoss_lc_13") def climate_danfoss_lc_13_fixture(client, climate_danfoss_lc_13_state): """Mock a climate radio danfoss LC-13 node.""" - node = Node(client, climate_danfoss_lc_13_state) + node = Node(client, copy.deepcopy(climate_danfoss_lc_13_state)) client.driver.controller.nodes[node.node_id] = node return node @@ -272,7 +276,7 @@ def climate_danfoss_lc_13_fixture(client, climate_danfoss_lc_13_state): @pytest.fixture(name="climate_heatit_z_trm3") def climate_heatit_z_trm3_fixture(client, climate_heatit_z_trm3_state): """Mock a climate radio HEATIT Z-TRM3 node.""" - node = Node(client, climate_heatit_z_trm3_state) + node = Node(client, copy.deepcopy(climate_heatit_z_trm3_state)) client.driver.controller.nodes[node.node_id] = node return node @@ -280,7 +284,7 @@ def climate_heatit_z_trm3_fixture(client, climate_heatit_z_trm3_state): @pytest.fixture(name="nortek_thermostat") def nortek_thermostat_fixture(client, nortek_thermostat_state): """Mock a nortek thermostat node.""" - node = Node(client, nortek_thermostat_state) + node = Node(client, copy.deepcopy(nortek_thermostat_state)) client.driver.controller.nodes[node.node_id] = node return node @@ -317,7 +321,7 @@ async def integration_fixture(hass, client): @pytest.fixture(name="chain_actuator_zws12") def window_cover_fixture(client, chain_actuator_zws12_state): """Mock a window cover node.""" - node = Node(client, chain_actuator_zws12_state) + node = Node(client, copy.deepcopy(chain_actuator_zws12_state)) client.driver.controller.nodes[node.node_id] = node return node @@ -325,7 +329,7 @@ def window_cover_fixture(client, chain_actuator_zws12_state): @pytest.fixture(name="in_wall_smart_fan_control") def in_wall_smart_fan_control_fixture(client, in_wall_smart_fan_control_state): """Mock a fan node.""" - node = Node(client, in_wall_smart_fan_control_state) + node = Node(client, copy.deepcopy(in_wall_smart_fan_control_state)) client.driver.controller.nodes[node.node_id] = node return node @@ -335,9 +339,9 @@ def multiple_devices_fixture( client, climate_radio_thermostat_ct100_plus_state, lock_schlage_be469_state ): """Mock a client with multiple devices.""" - node = Node(client, climate_radio_thermostat_ct100_plus_state) + node = Node(client, copy.deepcopy(climate_radio_thermostat_ct100_plus_state)) client.driver.controller.nodes[node.node_id] = node - node = Node(client, lock_schlage_be469_state) + node = Node(client, copy.deepcopy(lock_schlage_be469_state)) client.driver.controller.nodes[node.node_id] = node return client.driver.controller.nodes @@ -345,7 +349,7 @@ def multiple_devices_fixture( @pytest.fixture(name="gdc_zw062") def motorized_barrier_cover_fixture(client, gdc_zw062_state): """Mock a motorized barrier node.""" - node = Node(client, gdc_zw062_state) + node = Node(client, copy.deepcopy(gdc_zw062_state)) client.driver.controller.nodes[node.node_id] = node return node @@ -353,7 +357,7 @@ def motorized_barrier_cover_fixture(client, gdc_zw062_state): @pytest.fixture(name="iblinds_v2") def iblinds_cover_fixture(client, iblinds_v2_state): """Mock an iBlinds v2.0 window cover node.""" - node = Node(client, iblinds_v2_state) + node = Node(client, copy.deepcopy(iblinds_v2_state)) client.driver.controller.nodes[node.node_id] = node return node @@ -361,6 +365,6 @@ def iblinds_cover_fixture(client, iblinds_v2_state): @pytest.fixture(name="ge_12730") def ge_12730_fixture(client, ge_12730_state): """Mock a GE 12730 fan controller node.""" - node = Node(client, ge_12730_state) + node = Node(client, copy.deepcopy(ge_12730_state)) client.driver.controller.nodes[node.node_id] = node return node From 9159f5490084b7f19bfd713a5ff6476095d7002a Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 24 Feb 2021 00:04:14 +0000 Subject: [PATCH 681/796] [ci skip] Translation update --- .../components/abode/translations/nl.json | 8 +++- .../components/acmeda/translations/nl.json | 3 ++ .../advantage_air/translations/nl.json | 4 ++ .../components/aemet/translations/nl.json | 21 +++++++++ .../components/agent_dvr/translations/nl.json | 1 + .../components/airnow/translations/nl.json | 24 ++++++++++ .../components/airvisual/translations/nl.json | 21 ++++++++- .../ambiclimate/translations/nl.json | 3 +- .../components/apple_tv/translations/nl.json | 36 ++++++++++++++- .../components/asuswrt/translations/nl.json | 35 +++++++++++++++ .../components/aurora/translations/nl.json | 26 +++++++++++ .../components/awair/translations/nl.json | 14 +++++- .../components/axis/translations/nl.json | 3 +- .../azure_devops/translations/nl.json | 1 + .../components/blink/translations/nl.json | 8 ++++ .../bmw_connected_drive/translations/nl.json | 20 +++++++++ .../components/broadlink/translations/nl.json | 2 + .../components/bsblan/translations/nl.json | 4 +- .../components/cloud/translations/nl.json | 15 +++++++ .../cloudflare/translations/nl.json | 28 +++++++++++- .../components/daikin/translations/nl.json | 3 ++ .../components/denonavr/translations/nl.json | 3 +- .../devolo_home_control/translations/nl.json | 5 +++ .../dialogflow/translations/nl.json | 3 +- .../components/ecobee/translations/nl.json | 3 ++ .../components/econet/translations/nl.json | 22 ++++++++++ .../components/epson/translations/nl.json | 16 +++++++ .../fireservicerota/translations/en.json | 2 +- .../fireservicerota/translations/nl.json | 28 ++++++++++++ .../components/fritzbox/translations/nl.json | 12 ++++- .../fritzbox_callmonitor/translations/nl.json | 21 +++++++++ .../components/geofency/translations/nl.json | 3 +- .../components/gpslogger/translations/nl.json | 3 +- .../components/gree/translations/nl.json | 13 ++++++ .../components/habitica/translations/nl.json | 17 +++++++ .../homeassistant/translations/nl.json | 3 ++ .../components/homekit/translations/nl.json | 11 ++++- .../components/hue/translations/nl.json | 2 +- .../components/hyperion/translations/nl.json | 22 ++++++++++ .../components/icloud/translations/nl.json | 2 + .../keenetic_ndms2/translations/nl.json | 29 ++++++++++++ .../components/kmtronic/translations/nl.json | 21 +++++++++ .../components/kmtronic/translations/ru.json | 21 +++++++++ .../kmtronic/translations/zh-Hant.json | 21 +++++++++ .../components/kulersky/translations/nl.json | 13 ++++++ .../components/life360/translations/nl.json | 6 ++- .../components/litejet/translations/et.json | 19 ++++++++ .../components/litejet/translations/nl.json | 18 ++++++++ .../litterrobot/translations/nl.json | 20 +++++++++ .../litterrobot/translations/ru.json | 20 +++++++++ .../litterrobot/translations/zh-Hant.json | 20 +++++++++ .../components/local_ip/translations/nl.json | 1 + .../logi_circle/translations/nl.json | 4 +- .../components/luftdaten/translations/nl.json | 1 + .../lutron_caseta/translations/nl.json | 39 ++++++++++++++++ .../components/lyric/translations/nl.json | 16 +++++++ .../components/mazda/translations/nl.json | 15 +++++++ .../meteo_france/translations/nl.json | 3 ++ .../components/mill/translations/nl.json | 5 ++- .../motion_blinds/translations/nl.json | 32 ++++++++++++++ .../components/mysensors/translations/nl.json | 10 +++++ .../components/neato/translations/nl.json | 15 ++++++- .../components/nest/translations/nl.json | 18 +++++++- .../components/nuki/translations/nl.json | 18 ++++++++ .../ondilo_ico/translations/nl.json | 17 +++++++ .../components/onewire/translations/nl.json | 5 +++ .../opentherm_gw/translations/nl.json | 1 + .../ovo_energy/translations/nl.json | 6 +++ .../components/ozw/translations/nl.json | 15 +++++++ .../philips_js/translations/nl.json | 19 ++++++++ .../components/plaato/translations/nl.json | 28 +++++++++++- .../components/plex/translations/nl.json | 1 + .../components/point/translations/nl.json | 3 +- .../components/poolsense/translations/nl.json | 1 + .../components/powerwall/translations/nl.json | 7 ++- .../components/profiler/translations/nl.json | 5 +++ .../progettihwsw/translations/nl.json | 18 ++++++++ .../components/ps4/translations/nl.json | 1 + .../rainmachine/translations/nl.json | 10 +++++ .../recollect_waste/translations/nl.json | 25 +++++++++++ .../components/rfxtrx/translations/nl.json | 25 +++++++++++ .../translations/zh-Hant.json | 21 +++++++++ .../components/roku/translations/nl.json | 1 + .../components/roomba/translations/nl.json | 6 +++ .../ruckus_unleashed/translations/nl.json | 7 ++- .../components/shelly/translations/nl.json | 13 ++++++ .../components/smappee/translations/nl.json | 8 +++- .../components/smarthab/translations/nl.json | 2 + .../components/smarttub/translations/nl.json | 21 +++++++++ .../components/solaredge/translations/nl.json | 7 ++- .../somfy_mylink/translations/nl.json | 39 +++++++++++++++- .../components/sonarr/translations/nl.json | 5 +++ .../srp_energy/translations/nl.json | 22 ++++++++++ .../components/subaru/translations/nl.json | 38 ++++++++++++++++ .../subaru/translations/zh-Hant.json | 44 +++++++++++++++++++ .../tellduslive/translations/nl.json | 6 ++- .../components/tesla/translations/nl.json | 7 ++- .../components/tibber/translations/nl.json | 1 + .../components/tile/translations/nl.json | 3 ++ .../components/toon/translations/nl.json | 3 +- .../totalconnect/translations/nl.json | 6 ++- .../totalconnect/translations/ru.json | 17 ++++++- .../totalconnect/translations/zh-Hant.json | 17 ++++++- .../components/traccar/translations/nl.json | 3 +- .../transmission/translations/nl.json | 1 + .../components/tuya/translations/nl.json | 13 ++++++ .../twentemilieu/translations/nl.json | 1 + .../components/twilio/translations/nl.json | 3 +- .../components/twinkly/translations/nl.json | 6 +++ .../components/unifi/translations/nl.json | 1 + .../components/upcloud/translations/nl.json | 4 +- .../components/vesync/translations/nl.json | 3 ++ .../components/vizio/translations/nl.json | 1 + .../components/weather/translations/et.json | 2 +- .../components/xbox/translations/nl.json | 17 +++++++ .../xiaomi_aqara/translations/nl.json | 10 ++++- .../xiaomi_miio/translations/nl.json | 9 ++++ .../xiaomi_miio/translations/ru.json | 1 + .../xiaomi_miio/translations/zh-Hant.json | 1 + .../components/zerproc/translations/nl.json | 13 ++++++ .../zoneminder/translations/nl.json | 3 +- .../components/zwave_js/translations/ca.json | 14 +++--- .../components/zwave_js/translations/cs.json | 10 ++--- .../components/zwave_js/translations/en.json | 7 ++- .../components/zwave_js/translations/es.json | 14 +++--- .../components/zwave_js/translations/et.json | 14 +++--- .../components/zwave_js/translations/fr.json | 14 +++--- .../components/zwave_js/translations/it.json | 14 +++--- .../components/zwave_js/translations/ko.json | 10 ++--- .../components/zwave_js/translations/nl.json | 14 +++--- .../components/zwave_js/translations/no.json | 14 +++--- .../components/zwave_js/translations/pl.json | 14 +++--- .../components/zwave_js/translations/ru.json | 14 +++--- .../components/zwave_js/translations/tr.json | 14 +++--- .../zwave_js/translations/zh-Hant.json | 14 +++--- 135 files changed, 1472 insertions(+), 138 deletions(-) create mode 100644 homeassistant/components/aemet/translations/nl.json create mode 100644 homeassistant/components/airnow/translations/nl.json create mode 100644 homeassistant/components/asuswrt/translations/nl.json create mode 100644 homeassistant/components/aurora/translations/nl.json create mode 100644 homeassistant/components/bmw_connected_drive/translations/nl.json create mode 100644 homeassistant/components/cloud/translations/nl.json create mode 100644 homeassistant/components/econet/translations/nl.json create mode 100644 homeassistant/components/epson/translations/nl.json create mode 100644 homeassistant/components/fireservicerota/translations/nl.json create mode 100644 homeassistant/components/fritzbox_callmonitor/translations/nl.json create mode 100644 homeassistant/components/gree/translations/nl.json create mode 100644 homeassistant/components/habitica/translations/nl.json create mode 100644 homeassistant/components/keenetic_ndms2/translations/nl.json create mode 100644 homeassistant/components/kmtronic/translations/nl.json create mode 100644 homeassistant/components/kmtronic/translations/ru.json create mode 100644 homeassistant/components/kmtronic/translations/zh-Hant.json create mode 100644 homeassistant/components/kulersky/translations/nl.json create mode 100644 homeassistant/components/litejet/translations/et.json create mode 100644 homeassistant/components/litejet/translations/nl.json create mode 100644 homeassistant/components/litterrobot/translations/nl.json create mode 100644 homeassistant/components/litterrobot/translations/ru.json create mode 100644 homeassistant/components/litterrobot/translations/zh-Hant.json create mode 100644 homeassistant/components/lyric/translations/nl.json create mode 100644 homeassistant/components/motion_blinds/translations/nl.json create mode 100644 homeassistant/components/nuki/translations/nl.json create mode 100644 homeassistant/components/ondilo_ico/translations/nl.json create mode 100644 homeassistant/components/philips_js/translations/nl.json create mode 100644 homeassistant/components/recollect_waste/translations/nl.json create mode 100644 homeassistant/components/rituals_perfume_genie/translations/zh-Hant.json create mode 100644 homeassistant/components/smarttub/translations/nl.json create mode 100644 homeassistant/components/srp_energy/translations/nl.json create mode 100644 homeassistant/components/subaru/translations/nl.json create mode 100644 homeassistant/components/subaru/translations/zh-Hant.json create mode 100644 homeassistant/components/xbox/translations/nl.json create mode 100644 homeassistant/components/zerproc/translations/nl.json diff --git a/homeassistant/components/abode/translations/nl.json b/homeassistant/components/abode/translations/nl.json index 9177b1deb7c..7b6a8b5aace 100644 --- a/homeassistant/components/abode/translations/nl.json +++ b/homeassistant/components/abode/translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "Herauthenticatie was succesvol", "single_instance_allowed": "Slechts een enkele configuratie van Abode is toegestaan." }, "error": { @@ -12,9 +13,14 @@ "mfa": { "data": { "mfa_code": "MFA-code (6-cijfers)" - } + }, + "title": "Voer uw MFA-code voor Abode in" }, "reauth_confirm": { + "data": { + "password": "Wachtwoord", + "username": "E-mail" + }, "title": "Vul uw Abode-inloggegevens in" }, "user": { diff --git a/homeassistant/components/acmeda/translations/nl.json b/homeassistant/components/acmeda/translations/nl.json index 470e0f8f698..aac926ec048 100644 --- a/homeassistant/components/acmeda/translations/nl.json +++ b/homeassistant/components/acmeda/translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_devices_found": "Geen apparaten gevonden op het netwerk" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/advantage_air/translations/nl.json b/homeassistant/components/advantage_air/translations/nl.json index 95395d24bca..3206c7a3165 100644 --- a/homeassistant/components/advantage_air/translations/nl.json +++ b/homeassistant/components/advantage_air/translations/nl.json @@ -3,9 +3,13 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd" }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, "step": { "user": { "data": { + "ip_address": "IP-adres", "port": "Poort" }, "description": "Maak verbinding met de API van uw Advantage Air-tablet voor wandmontage.", diff --git a/homeassistant/components/aemet/translations/nl.json b/homeassistant/components/aemet/translations/nl.json new file mode 100644 index 00000000000..02415dde1e6 --- /dev/null +++ b/homeassistant/components/aemet/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Locatie is al geconfigureerd." + }, + "error": { + "invalid_api_key": "Ongeldige API-sleutel" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "name": "Naam van de integratie" + }, + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/nl.json b/homeassistant/components/agent_dvr/translations/nl.json index ad625c169c8..7c679f66c11 100644 --- a/homeassistant/components/agent_dvr/translations/nl.json +++ b/homeassistant/components/agent_dvr/translations/nl.json @@ -4,6 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { + "already_in_progress": "De configuratiestroom is al aan de gang", "cannot_connect": "Kan geen verbinding maken" }, "step": { diff --git a/homeassistant/components/airnow/translations/nl.json b/homeassistant/components/airnow/translations/nl.json new file mode 100644 index 00000000000..011498269f8 --- /dev/null +++ b/homeassistant/components/airnow/translations/nl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "invalid_location": "Geen resultaten gevonden voor die locatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad" + }, + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/nl.json b/homeassistant/components/airvisual/translations/nl.json index 85f8be5f8e0..ecf2322c801 100644 --- a/homeassistant/components/airvisual/translations/nl.json +++ b/homeassistant/components/airvisual/translations/nl.json @@ -1,12 +1,14 @@ { "config": { "abort": { - "already_configured": "Deze co\u00f6rdinaten of Node / Pro ID zijn al geregistreerd." + "already_configured": "Deze co\u00f6rdinaten of Node / Pro ID zijn al geregistreerd.", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "cannot_connect": "Kan geen verbinding maken", "general_error": "Er is een onbekende fout opgetreden.", - "invalid_api_key": "Ongeldige API-sleutel" + "invalid_api_key": "Ongeldige API-sleutel", + "location_not_found": "Locatie niet gevonden" }, "step": { "geography": { @@ -18,6 +20,21 @@ "description": "Gebruik de AirVisual cloud API om een geografische locatie te bewaken.", "title": "Configureer een geografie" }, + "geography_by_coords": { + "data": { + "api_key": "API-sleutel", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad" + } + }, + "geography_by_name": { + "data": { + "api_key": "API-sleutel", + "city": "Stad", + "country": "Land" + }, + "description": "Gebruik de AirVisual-cloud-API om een stad/staat/land te bewaken." + }, "node_pro": { "data": { "ip_address": "IP adres/hostname van component", diff --git a/homeassistant/components/ambiclimate/translations/nl.json b/homeassistant/components/ambiclimate/translations/nl.json index 52f8cfc40d3..1d7652a370e 100644 --- a/homeassistant/components/ambiclimate/translations/nl.json +++ b/homeassistant/components/ambiclimate/translations/nl.json @@ -2,7 +2,8 @@ "config": { "abort": { "access_token": "Onbekende fout bij het genereren van een toegangstoken.", - "already_configured": "Account is al geconfigureerd" + "already_configured": "Account is al geconfigureerd", + "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen." }, "create_entry": { "default": "Succesvol geverifieerd met Ambiclimate" diff --git a/homeassistant/components/apple_tv/translations/nl.json b/homeassistant/components/apple_tv/translations/nl.json index a11488ebca9..d809ac749b7 100644 --- a/homeassistant/components/apple_tv/translations/nl.json +++ b/homeassistant/components/apple_tv/translations/nl.json @@ -1,9 +1,41 @@ { "config": { "abort": { + "already_configured_device": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", "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." + "invalid_config": "De configuratie voor dit apparaat is onvolledig. Probeer het opnieuw toe te voegen.", + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "unknown": "Onverwachte fout" + }, + "error": { + "already_configured": "Apparaat is al geconfigureerd", + "invalid_auth": "Ongeldige authenticatie", + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "unknown": "Onverwachte fout" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "title": "Bevestig het toevoegen van Apple TV" + }, + "pair_no_pin": { + "title": "Koppelen" + }, + "pair_with_pin": { + "data": { + "pin": "PIN-code" + }, + "title": "Koppelen" + }, + "user": { + "data": { + "device_input": "Apparaat" + }, + "title": "Stel een nieuwe Apple TV in" + } } - } + }, + "title": "Apple TV" } \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/nl.json b/homeassistant/components/asuswrt/translations/nl.json new file mode 100644 index 00000000000..1128a820cd5 --- /dev/null +++ b/homeassistant/components/asuswrt/translations/nl.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_host": "Ongeldige hostnaam of IP-adres", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host", + "mode": "Mode", + "name": "Naam", + "password": "Wachtwoord", + "port": "Poort", + "username": "Gebruikersnaam" + }, + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "track_unknown": "Volg onbekende / naamloze apparaten" + }, + "title": "AsusWRT-opties" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/nl.json b/homeassistant/components/aurora/translations/nl.json new file mode 100644 index 00000000000..fe7b4809f13 --- /dev/null +++ b/homeassistant/components/aurora/translations/nl.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "name": "Naam" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Drempel (%)" + } + } + } + }, + "title": "NOAA Aurora Sensor" +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/nl.json b/homeassistant/components/awair/translations/nl.json index 08a30a52250..5d20aed2fdb 100644 --- a/homeassistant/components/awair/translations/nl.json +++ b/homeassistant/components/awair/translations/nl.json @@ -2,14 +2,26 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "no_devices_found": "Geen apparaten op het netwerk gevonden" + "no_devices_found": "Geen apparaten op het netwerk gevonden", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { + "invalid_access_token": "Ongeldig toegangstoken", "unknown": "Onverwachte fout" }, "step": { "reauth": { + "data": { + "access_token": "Toegangstoken", + "email": "E-mail" + }, "description": "Voer uw Awair-ontwikkelaarstoegangstoken opnieuw in." + }, + "user": { + "data": { + "access_token": "Toegangstoken", + "email": "E-mail" + } } } } diff --git a/homeassistant/components/axis/translations/nl.json b/homeassistant/components/axis/translations/nl.json index 483acefec15..345e6622e93 100644 --- a/homeassistant/components/axis/translations/nl.json +++ b/homeassistant/components/axis/translations/nl.json @@ -8,7 +8,8 @@ "error": { "already_configured": "Apparaat is al geconfigureerd", "already_in_progress": "De configuratiestroom voor het apparaat is al in volle gang.", - "cannot_connect": "Kan geen verbinding maken" + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie" }, "flow_title": "Axis apparaat: {name} ({host})", "step": { diff --git a/homeassistant/components/azure_devops/translations/nl.json b/homeassistant/components/azure_devops/translations/nl.json index 9abecd187fe..aef6a717895 100644 --- a/homeassistant/components/azure_devops/translations/nl.json +++ b/homeassistant/components/azure_devops/translations/nl.json @@ -4,6 +4,7 @@ "already_configured": "Account is al geconfigureerd" }, "error": { + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie" } } diff --git a/homeassistant/components/blink/translations/nl.json b/homeassistant/components/blink/translations/nl.json index c1ab971dbf0..4067bf75f83 100644 --- a/homeassistant/components/blink/translations/nl.json +++ b/homeassistant/components/blink/translations/nl.json @@ -4,6 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { + "invalid_access_token": "Ongeldig toegangstoken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, @@ -23,5 +24,12 @@ "title": "Aanmelden met Blink account" } } + }, + "options": { + "step": { + "simple_options": { + "title": "Blink opties" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/nl.json b/homeassistant/components/bmw_connected_drive/translations/nl.json new file mode 100644 index 00000000000..83ae0b9ff7d --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "region": "ConnectedDrive-regio", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/nl.json b/homeassistant/components/broadlink/translations/nl.json index 7205512d368..2f3a7313f75 100644 --- a/homeassistant/components/broadlink/translations/nl.json +++ b/homeassistant/components/broadlink/translations/nl.json @@ -3,11 +3,13 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "cannot_connect": "Kon niet verbinden", + "invalid_host": "Ongeldige hostnaam of IP-adres", "not_supported": "Apparaat wordt niet ondersteund", "unknown": "Onverwachte fout" }, "error": { "cannot_connect": "Kon niet verbinden", + "invalid_host": "Ongeldige hostnaam of IP-adres", "unknown": "Onverwachte fout" }, "flow_title": "{name} ({model} bij {host})", diff --git a/homeassistant/components/bsblan/translations/nl.json b/homeassistant/components/bsblan/translations/nl.json index 850f942df2e..415cd759a8a 100644 --- a/homeassistant/components/bsblan/translations/nl.json +++ b/homeassistant/components/bsblan/translations/nl.json @@ -12,7 +12,9 @@ "data": { "host": "Host", "passkey": "Passkey-tekenreeks", - "port": "Poort" + "password": "Wachtwoord", + "port": "Poort", + "username": "Gebruikersnaam" }, "description": "Stel uw BSB-Lan-apparaat in om te integreren met Home Assistant.", "title": "Maak verbinding met het BSB-Lan-apparaat" diff --git a/homeassistant/components/cloud/translations/nl.json b/homeassistant/components/cloud/translations/nl.json new file mode 100644 index 00000000000..d9aa78afecb --- /dev/null +++ b/homeassistant/components/cloud/translations/nl.json @@ -0,0 +1,15 @@ +{ + "system_health": { + "info": { + "alexa_enabled": "Alexa ingeschakeld", + "can_reach_cloud": "Bereik Home Assistant Cloud", + "can_reach_cloud_auth": "Bereik authenticatieserver", + "google_enabled": "Google ingeschakeld", + "logged_in": "Ingelogd", + "relayer_connected": "Relayer verbonden", + "remote_connected": "Op afstand verbonden", + "remote_enabled": "Op afstand ingeschakeld", + "subscription_expiration": "Afloop abonnement" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/nl.json b/homeassistant/components/cloudflare/translations/nl.json index 37162761d86..35c765d5da7 100644 --- a/homeassistant/components/cloudflare/translations/nl.json +++ b/homeassistant/components/cloudflare/translations/nl.json @@ -1,7 +1,33 @@ { "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", + "unknown": "Onverwachte fout" + }, "error": { - "invalid_auth": "Ongeldige authenticatie" + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "invalid_zone": "Ongeldige zone" + }, + "flow_title": "Cloudflare: {name}", + "step": { + "records": { + "data": { + "records": "Records" + }, + "title": "Kies de records die u wilt bijwerken" + }, + "user": { + "data": { + "api_token": "API-token" + }, + "title": "Verbinden met Cloudflare" + }, + "zone": { + "data": { + "zone": "Zone" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/nl.json b/homeassistant/components/daikin/translations/nl.json index 2d1e1edbdbb..69d52436beb 100644 --- a/homeassistant/components/daikin/translations/nl.json +++ b/homeassistant/components/daikin/translations/nl.json @@ -4,6 +4,9 @@ "already_configured": "Apparaat is al geconfigureerd", "cannot_connect": "Kon niet verbinden" }, + "error": { + "invalid_auth": "Ongeldige authenticatie" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/denonavr/translations/nl.json b/homeassistant/components/denonavr/translations/nl.json index 9f79aebeb60..6a00e03765f 100644 --- a/homeassistant/components/denonavr/translations/nl.json +++ b/homeassistant/components/denonavr/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang" }, "flow_title": "Denon AVR Network Receiver: {name}", "step": { diff --git a/homeassistant/components/devolo_home_control/translations/nl.json b/homeassistant/components/devolo_home_control/translations/nl.json index d61f9183cc5..5d79d2ec9e9 100644 --- a/homeassistant/components/devolo_home_control/translations/nl.json +++ b/homeassistant/components/devolo_home_control/translations/nl.json @@ -3,9 +3,14 @@ "abort": { "already_configured": "Account is al geconfigureerd" }, + "error": { + "invalid_auth": "Ongeldige authenticatie" + }, "step": { "user": { "data": { + "home_control_url": "Home Control URL", + "mydevolo_url": "mydevolo URL", "password": "Wachtwoord", "username": "E-mail adres / devolo ID" } diff --git a/homeassistant/components/dialogflow/translations/nl.json b/homeassistant/components/dialogflow/translations/nl.json index 7cccf8ecb9b..82fe7daea00 100644 --- a/homeassistant/components/dialogflow/translations/nl.json +++ b/homeassistant/components/dialogflow/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.", + "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." }, "create_entry": { "default": "Om evenementen naar de Home Assistant te verzenden, moet u [webhookintegratie van Dialogflow]({dialogflow_url}) instellen. \n\n Vul de volgende info in: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\nZie [de documentatie]({docs_url}) voor verdere informatie." diff --git a/homeassistant/components/ecobee/translations/nl.json b/homeassistant/components/ecobee/translations/nl.json index 62405b05ff1..957d2f8244d 100644 --- a/homeassistant/components/ecobee/translations/nl.json +++ b/homeassistant/components/ecobee/translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, "error": { "pin_request_failed": "Fout bij het aanvragen van pincode bij ecobee; Controleer of de API-sleutel correct is.", "token_request_failed": "Fout bij het aanvragen van tokens bij ecobee; probeer het opnieuw." diff --git a/homeassistant/components/econet/translations/nl.json b/homeassistant/components/econet/translations/nl.json new file mode 100644 index 00000000000..226c1611e2b --- /dev/null +++ b/homeassistant/components/econet/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Wachtwoord" + }, + "title": "Stel Rheem EcoNet-account in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/epson/translations/nl.json b/homeassistant/components/epson/translations/nl.json new file mode 100644 index 00000000000..d5ae90c0e38 --- /dev/null +++ b/homeassistant/components/epson/translations/nl.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Naam", + "port": "Poort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/en.json b/homeassistant/components/fireservicerota/translations/en.json index 288b89c31b8..a059081760d 100644 --- a/homeassistant/components/fireservicerota/translations/en.json +++ b/homeassistant/components/fireservicerota/translations/en.json @@ -15,7 +15,7 @@ "data": { "password": "Password" }, - "description": "Authentication tokens baceame invalid, login to recreate them." + "description": "Authentication tokens became invalid, login to recreate them." }, "user": { "data": { diff --git a/homeassistant/components/fireservicerota/translations/nl.json b/homeassistant/components/fireservicerota/translations/nl.json new file mode 100644 index 00000000000..7289d53e71f --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/nl.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "create_entry": { + "default": "Succesvol geauthenticeerd" + }, + "error": { + "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "reauth": { + "data": { + "password": "Wachtwoord" + } + }, + "user": { + "data": { + "password": "Wachtwoord", + "url": "Website", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/nl.json b/homeassistant/components/fritzbox/translations/nl.json index b72374547bc..71a80dbd577 100644 --- a/homeassistant/components/fritzbox/translations/nl.json +++ b/homeassistant/components/fritzbox/translations/nl.json @@ -3,7 +3,11 @@ "abort": { "already_configured": "Deze AVM FRITZ!Box is al geconfigureerd.", "already_in_progress": "AVM FRITZ!Box configuratie is al bezig.", - "not_supported": "Verbonden met AVM FRITZ! Box, maar het kan geen Smart Home-apparaten bedienen." + "not_supported": "Verbonden met AVM FRITZ! Box, maar het kan geen Smart Home-apparaten bedienen.", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "invalid_auth": "Ongeldige authenticatie" }, "flow_title": "AVM FRITZ!Box: {name}", "step": { @@ -14,6 +18,12 @@ }, "description": "Wilt u {name} instellen?" }, + "reauth_confirm": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + }, "user": { "data": { "host": "Host of IP-adres", diff --git a/homeassistant/components/fritzbox_callmonitor/translations/nl.json b/homeassistant/components/fritzbox_callmonitor/translations/nl.json new file mode 100644 index 00000000000..3381ed0d9b2 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "no_devices_found": "Geen apparaten gevonden op het netwerk" + }, + "error": { + "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord", + "port": "Poort", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/translations/nl.json b/homeassistant/components/geofency/translations/nl.json index 763d903a8ba..59ed1cf6b5b 100644 --- a/homeassistant/components/geofency/translations/nl.json +++ b/homeassistant/components/geofency/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.", + "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." }, "create_entry": { "default": "Om locaties naar Home Assistant te sturen, moet u de Webhook-functie instellen in Geofency.\n\n Vul de volgende info in: \n\n - URL: `{webhook_url}` \n - Methode: POST \n\n Zie [de documentatie]({docs_url}) voor meer informatie." diff --git a/homeassistant/components/gpslogger/translations/nl.json b/homeassistant/components/gpslogger/translations/nl.json index dbf7f47a2e9..d90b648760d 100644 --- a/homeassistant/components/gpslogger/translations/nl.json +++ b/homeassistant/components/gpslogger/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.", + "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." }, "create_entry": { "default": "Om evenementen naar Home Assistant te verzenden, moet u de webhook-functie instellen in GPSLogger. \n\n Vul de volgende info in: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n\n Zie [de documentatie] ( {docs_url} ) voor meer informatie." diff --git a/homeassistant/components/gree/translations/nl.json b/homeassistant/components/gree/translations/nl.json new file mode 100644 index 00000000000..d11896014fd --- /dev/null +++ b/homeassistant/components/gree/translations/nl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "step": { + "confirm": { + "description": "Wil je beginnen met instellen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/nl.json b/homeassistant/components/habitica/translations/nl.json new file mode 100644 index 00000000000..13a4fd6c729 --- /dev/null +++ b/homeassistant/components/habitica/translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_credentials": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel", + "url": "URL" + } + } + } + }, + "title": "Habitica" +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/nl.json b/homeassistant/components/homeassistant/translations/nl.json index 338a019019f..47b69068ea3 100644 --- a/homeassistant/components/homeassistant/translations/nl.json +++ b/homeassistant/components/homeassistant/translations/nl.json @@ -1,12 +1,15 @@ { "system_health": { "info": { + "arch": "CPU-architectuur", + "chassis": "Chassis", "dev": "Ontwikkeling", "docker": "Docker", "docker_version": "Docker", "hassio": "Supervisor", "host_os": "Home Assistant OS", "installation_type": "Type installatie", + "os_name": "Besturingssysteemfamilie", "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 2733d6bd12d..bcf61fe9868 100644 --- a/homeassistant/components/homekit/translations/nl.json +++ b/homeassistant/components/homekit/translations/nl.json @@ -4,6 +4,11 @@ "port_name_in_use": "Er is al een bridge of apparaat met dezelfde naam of poort geconfigureerd." }, "step": { + "accessory_mode": { + "data": { + "entity_id": "Entiteit" + } + }, "pairing": { "description": "Zodra de {name} klaar is, is het koppelen beschikbaar in \"Meldingen\" als \"HomeKit Bridge Setup\".", "title": "Koppel HomeKit Bridge" @@ -11,7 +16,8 @@ "user": { "data": { "auto_start": "Automatisch starten (uitschakelen als u Z-Wave of een ander vertraagd startsysteem gebruikt)", - "include_domains": "Domeinen om op te nemen" + "include_domains": "Domeinen om op te nemen", + "mode": "Mode" }, "description": "De HomeKit-integratie geeft u toegang tot uw Home Assistant-entiteiten in HomeKit. In bridge-modus zijn HomeKit-bruggen beperkt tot 150 accessoires per exemplaar, inclusief de brug zelf. Als u meer dan het maximale aantal accessoires wilt overbruggen, is het aan te raden om meerdere HomeKit-bridges voor verschillende domeinen te gebruiken. Gedetailleerde entiteitsconfiguratie is alleen beschikbaar via YAML voor de primaire bridge.", "title": "Activeer HomeKit Bridge" @@ -37,7 +43,8 @@ }, "include_exclude": { "data": { - "entities": "Entiteiten" + "entities": "Entiteiten", + "mode": "Mode" } }, "init": { diff --git a/homeassistant/components/hue/translations/nl.json b/homeassistant/components/hue/translations/nl.json index f04d372bf6a..cead9dd21c6 100644 --- a/homeassistant/components/hue/translations/nl.json +++ b/homeassistant/components/hue/translations/nl.json @@ -8,7 +8,7 @@ "discover_timeout": "Hue bridges kunnen niet worden gevonden", "no_bridges": "Geen Philips Hue bridges ontdekt", "not_hue_bridge": "Dit is geen Hue bridge", - "unknown": "Onbekende fout opgetreden" + "unknown": "Onverwachte fout" }, "error": { "linking": "Er is een onbekende verbindingsfout opgetreden.", diff --git a/homeassistant/components/hyperion/translations/nl.json b/homeassistant/components/hyperion/translations/nl.json index d93018f8a3c..0898272e4a2 100644 --- a/homeassistant/components/hyperion/translations/nl.json +++ b/homeassistant/components/hyperion/translations/nl.json @@ -1,10 +1,32 @@ { "config": { + "abort": { + "already_configured": "Service is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", + "auth_new_token_not_granted_error": "Nieuw aangemaakte token is niet goedgekeurd in Hyperion UI", + "auth_new_token_not_work_error": "Verificatie met nieuw aangemaakt token mislukt", + "auth_required_error": "Kan niet bepalen of autorisatie vereist is", + "cannot_connect": "Kan geen verbinding maken", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_access_token": "Ongeldig toegangstoken" + }, "step": { "auth": { "data": { "create_token": "Maak automatisch een nieuw token aan" } + }, + "create_token_external": { + "title": "Accepteer nieuwe token in Hyperion UI" + }, + "user": { + "data": { + "host": "Host", + "port": "Poort" + } } } } diff --git a/homeassistant/components/icloud/translations/nl.json b/homeassistant/components/icloud/translations/nl.json index 537d310b0a7..97673069054 100644 --- a/homeassistant/components/icloud/translations/nl.json +++ b/homeassistant/components/icloud/translations/nl.json @@ -5,6 +5,7 @@ "no_device": "Op geen van uw apparaten is \"Find my iPhone\" geactiveerd" }, "error": { + "invalid_auth": "Ongeldige authenticatie", "send_verification_code": "Kan verificatiecode niet verzenden", "validate_verification_code": "Kan uw verificatiecode niet verifi\u00ebren, kies een vertrouwensapparaat en start de verificatie opnieuw" }, @@ -25,6 +26,7 @@ "user": { "data": { "password": "Wachtwoord", + "username": "E-mail", "with_family": "Met gezin" }, "description": "Voer uw gegevens in", diff --git a/homeassistant/components/keenetic_ndms2/translations/nl.json b/homeassistant/components/keenetic_ndms2/translations/nl.json new file mode 100644 index 00000000000..f422e2641f6 --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/nl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord", + "port": "Poort", + "username": "Gebruikersnaam" + } + } + } + }, + "options": { + "step": { + "user": { + "data": { + "interfaces": "Kies interfaces om te scannen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/nl.json b/homeassistant/components/kmtronic/translations/nl.json new file mode 100644 index 00000000000..8ad15260b0d --- /dev/null +++ b/homeassistant/components/kmtronic/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/ru.json b/homeassistant/components/kmtronic/translations/ru.json new file mode 100644 index 00000000000..9e0db9fcf94 --- /dev/null +++ b/homeassistant/components/kmtronic/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/zh-Hant.json b/homeassistant/components/kmtronic/translations/zh-Hant.json new file mode 100644 index 00000000000..cad7d736a9d --- /dev/null +++ b/homeassistant/components/kmtronic/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/nl.json b/homeassistant/components/kulersky/translations/nl.json new file mode 100644 index 00000000000..d11896014fd --- /dev/null +++ b/homeassistant/components/kulersky/translations/nl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "step": { + "confirm": { + "description": "Wil je beginnen met instellen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/nl.json b/homeassistant/components/life360/translations/nl.json index c3b667722d0..612b0d5c4f7 100644 --- a/homeassistant/components/life360/translations/nl.json +++ b/homeassistant/components/life360/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "invalid_auth": "Ongeldige authenticatie" + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" }, "create_entry": { "default": "Om geavanceerde opties in te stellen, zie [Life360 documentatie]({docs_url})." @@ -9,7 +10,8 @@ "error": { "already_configured": "Account is al geconfigureerd", "invalid_auth": "Ongeldige authenticatie", - "invalid_username": "Ongeldige gebruikersnaam" + "invalid_username": "Ongeldige gebruikersnaam", + "unknown": "Onverwachte fout" }, "step": { "user": { diff --git a/homeassistant/components/litejet/translations/et.json b/homeassistant/components/litejet/translations/et.json new file mode 100644 index 00000000000..6e50b5dcdf3 --- /dev/null +++ b/homeassistant/components/litejet/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." + }, + "error": { + "open_failed": "valitud jadaporti ei saa avada." + }, + "step": { + "user": { + "data": { + "port": "Port" + }, + "description": "\u00dchenda LiteJeti RS232-2 port arvutiga ja sisesta jadapordi seadme tee.\n\nLiteJet MCP peab olema konfigureeritud: 19200 boodi, 8 andmebitti, 1 stopp bitt, paarsus puudub ja edastada \"CR\" p\u00e4rast iga vastust.", + "title": "Loo \u00fchendus LiteJetiga" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/nl.json b/homeassistant/components/litejet/translations/nl.json new file mode 100644 index 00000000000..f16f25a3987 --- /dev/null +++ b/homeassistant/components/litejet/translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "error": { + "open_failed": "Kan de opgegeven seri\u00eble poort niet openen." + }, + "step": { + "user": { + "data": { + "port": "Poort" + }, + "title": "Maak verbinding met LiteJet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/nl.json b/homeassistant/components/litterrobot/translations/nl.json new file mode 100644 index 00000000000..50b4c3f2fe6 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/ru.json b/homeassistant/components/litterrobot/translations/ru.json new file mode 100644 index 00000000000..3f4677a050e --- /dev/null +++ b/homeassistant/components/litterrobot/translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/zh-Hant.json b/homeassistant/components/litterrobot/translations/zh-Hant.json new file mode 100644 index 00000000000..d232b491b68 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/translations/nl.json b/homeassistant/components/local_ip/translations/nl.json index ba75a9b2a4d..57547adedd8 100644 --- a/homeassistant/components/local_ip/translations/nl.json +++ b/homeassistant/components/local_ip/translations/nl.json @@ -8,6 +8,7 @@ "data": { "name": "Sensor Naam" }, + "description": "Wil je beginnen met instellen?", "title": "Lokaal IP-adres" } } diff --git a/homeassistant/components/logi_circle/translations/nl.json b/homeassistant/components/logi_circle/translations/nl.json index b521af1f969..36970feb48b 100644 --- a/homeassistant/components/logi_circle/translations/nl.json +++ b/homeassistant/components/logi_circle/translations/nl.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "Account is al geconfigureerd", "external_error": "Uitzondering opgetreden uit een andere stroom.", - "external_setup": "Logi Circle is met succes geconfigureerd vanuit een andere stroom." + "external_setup": "Logi Circle is met succes geconfigureerd vanuit een andere stroom.", + "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen." }, "error": { + "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", "follow_link": "Volg de link en authenticeer voordat u op Verzenden drukt.", "invalid_auth": "Ongeldige authenticatie" }, diff --git a/homeassistant/components/luftdaten/translations/nl.json b/homeassistant/components/luftdaten/translations/nl.json index b3bdf2442b8..dc913232e8c 100644 --- a/homeassistant/components/luftdaten/translations/nl.json +++ b/homeassistant/components/luftdaten/translations/nl.json @@ -2,6 +2,7 @@ "config": { "error": { "already_configured": "Service is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken", "invalid_sensor": "Sensor niet beschikbaar of ongeldig" }, "step": { diff --git a/homeassistant/components/lutron_caseta/translations/nl.json b/homeassistant/components/lutron_caseta/translations/nl.json index 8e48dea075d..17e6fc47fd8 100644 --- a/homeassistant/components/lutron_caseta/translations/nl.json +++ b/homeassistant/components/lutron_caseta/translations/nl.json @@ -7,10 +7,49 @@ "error": { "cannot_connect": "Kon niet verbinden" }, + "flow_title": "Lutron Cas\u00e9ta {name} ({host})", "step": { "import_failed": { "description": "Kan bridge (host: {host} ) niet instellen, ge\u00efmporteerd uit configuration.yaml." + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Voer het IP-adres van het apparaat in." } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Eerste knop", + "button_2": "Tweede knop", + "button_3": "Derde knop", + "button_4": "Vierde knop", + "close_1": "Sluit 1", + "close_2": "Sluit 2", + "close_3": "Sluit 3", + "close_4": "Sluit 4", + "close_all": "Sluit alles", + "group_1_button_1": "Eerste Groep eerste knop", + "group_1_button_2": "Eerste Groep tweede knop", + "group_2_button_1": "Tweede Groep eerste knop", + "group_2_button_2": "Tweede Groep tweede knop", + "off": "Uit", + "on": "Aan", + "open_1": "Open 1", + "open_2": "Open 2", + "open_3": "Open 3", + "open_4": "Open 4", + "stop": "Stop (favoriet)", + "stop_1": "Stop 1", + "stop_2": "Stop 2", + "stop_3": "Stop 3", + "stop_4": "Stop 4" + }, + "trigger_type": { + "press": "\" {subtype} \" ingedrukt", + "release": "\"{subtype}\" losgelaten" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/nl.json b/homeassistant/components/lyric/translations/nl.json new file mode 100644 index 00000000000..d490acb1b59 --- /dev/null +++ b/homeassistant/components/lyric/translations/nl.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen." + }, + "create_entry": { + "default": "Succesvol geauthenticeerd" + }, + "step": { + "pick_implementation": { + "title": "Kies een authenticatie methode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/nl.json b/homeassistant/components/mazda/translations/nl.json index c820f481b9d..86f1e656e51 100644 --- a/homeassistant/components/mazda/translations/nl.json +++ b/homeassistant/components/mazda/translations/nl.json @@ -9,6 +9,21 @@ "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" + }, + "step": { + "reauth": { + "data": { + "email": "E-mail", + "password": "Wachtwoord" + } + }, + "user": { + "data": { + "email": "E-mail", + "password": "Wachtwoord", + "region": "Regio" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/meteo_france/translations/nl.json b/homeassistant/components/meteo_france/translations/nl.json index 27dfb56f8d7..61925da4cd3 100644 --- a/homeassistant/components/meteo_france/translations/nl.json +++ b/homeassistant/components/meteo_france/translations/nl.json @@ -5,6 +5,9 @@ "unknown": "Onbekende fout: probeer het later nog eens" }, "step": { + "cities": { + "title": "M\u00e9t\u00e9o-France" + }, "user": { "data": { "city": "Stad" diff --git a/homeassistant/components/mill/translations/nl.json b/homeassistant/components/mill/translations/nl.json index 4699b6fb733..fff0a8232e4 100644 --- a/homeassistant/components/mill/translations/nl.json +++ b/homeassistant/components/mill/translations/nl.json @@ -3,10 +3,13 @@ "abort": { "already_configured": "Account is al geconfigureerd" }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, "step": { "user": { "data": { - "password": "Password", + "password": "Wachtwoord", "username": "Gebruikersnaam" } } diff --git a/homeassistant/components/motion_blinds/translations/nl.json b/homeassistant/components/motion_blinds/translations/nl.json new file mode 100644 index 00000000000..01cb117bb5b --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/nl.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", + "connection_error": "Kan geen verbinding maken" + }, + "error": { + "discovery_error": "Kan geen Motion Gateway vinden" + }, + "step": { + "connect": { + "data": { + "api_key": "API-sleutel" + }, + "title": "Motion Blinds" + }, + "select": { + "data": { + "select_ip": "IP-adres" + }, + "title": "Selecteer de Motion Gateway waarmee u verbinding wilt maken" + }, + "user": { + "data": { + "api_key": "API-sleutel", + "host": "IP-adres" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/nl.json b/homeassistant/components/mysensors/translations/nl.json index ebbcbf9a36e..e41f67c7730 100644 --- a/homeassistant/components/mysensors/translations/nl.json +++ b/homeassistant/components/mysensors/translations/nl.json @@ -1,6 +1,16 @@ { "config": { "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken", + "duplicate_topic": "Topic is al in gebruik", + "invalid_auth": "Ongeldige authenticatie", + "invalid_device": "Ongeldig apparaat", + "invalid_ip": "Ongeldig IP-adres", + "invalid_port": "Ongeldig poortnummer", + "invalid_publish_topic": "Ongeldig publiceer topic", + "invalid_serial": "Ongeldige seri\u00eble poort", + "invalid_version": "Ongeldige MySensors-versie", "not_a_number": "Voer een nummer in", "port_out_of_range": "Poortnummer moet minimaal 1 en maximaal 65535 zijn", "same_topic": "De topics abonneren en publiceren zijn hetzelfde", diff --git a/homeassistant/components/neato/translations/nl.json b/homeassistant/components/neato/translations/nl.json index 26e5a647b1d..563e6500c16 100644 --- a/homeassistant/components/neato/translations/nl.json +++ b/homeassistant/components/neato/translations/nl.json @@ -2,7 +2,11 @@ "config": { "abort": { "already_configured": "Al geconfigureerd", - "invalid_auth": "Ongeldige authenticatie" + "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "invalid_auth": "Ongeldige authenticatie", + "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", + "reauth_successful": "Herauthenticatie was succesvol" }, "create_entry": { "default": "Zie [Neato-documentatie] ({docs_url})." @@ -12,6 +16,12 @@ "unknown": "Onverwachte fout" }, "step": { + "pick_implementation": { + "title": "Kies een authenticatie methode" + }, + "reauth_confirm": { + "title": "Wil je beginnen met instellen?" + }, "user": { "data": { "password": "Wachtwoord", @@ -22,5 +32,6 @@ "title": "Neato-account info" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/nl.json b/homeassistant/components/nest/translations/nl.json index 931b8aa770e..387b1effcb0 100644 --- a/homeassistant/components/nest/translations/nl.json +++ b/homeassistant/components/nest/translations/nl.json @@ -2,10 +2,19 @@ "config": { "abort": { "authorize_url_fail": "Onbekende fout bij het genereren van een autoriseer-URL.", - "authorize_url_timeout": "Toestemming voor het genereren van autoriseer-url." + "authorize_url_timeout": "Toestemming voor het genereren van autoriseer-url.", + "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", + "reauth_successful": "Herauthenticatie was succesvol", + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", + "unknown_authorize_url_generation": "Onbekende fout bij het genereren van een autorisatie-URL." + }, + "create_entry": { + "default": "Succesvol geauthenticeerd" }, "error": { "internal_error": "Interne foutvalidatiecode", + "invalid_pin": "Ongeldige PIN-code", "timeout": "Time-out validatie van code", "unknown": "Onbekende foutvalidatiecode" }, @@ -23,6 +32,13 @@ }, "description": "Als je je Nest-account wilt koppelen, [autoriseer je account] ( {url} ). \n\nNa autorisatie, kopieer en plak de voorziene pincode hieronder.", "title": "Koppel Nest-account" + }, + "pick_implementation": { + "title": "Kies een authenticatie methode" + }, + "reauth_confirm": { + "description": "De Nest-integratie moet je account opnieuw verifi\u00ebren", + "title": "Verifieer de integratie opnieuw" } } }, diff --git a/homeassistant/components/nuki/translations/nl.json b/homeassistant/components/nuki/translations/nl.json new file mode 100644 index 00000000000..4e220dbe78d --- /dev/null +++ b/homeassistant/components/nuki/translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Poort", + "token": "Toegangstoken" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/nl.json b/homeassistant/components/ondilo_ico/translations/nl.json new file mode 100644 index 00000000000..8a91dff086f --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen." + }, + "create_entry": { + "default": "Succesvol geauthenticeerd" + }, + "step": { + "pick_implementation": { + "title": "Kies een authenticatie methode" + } + } + }, + "title": "Ondilo ICO" +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/nl.json b/homeassistant/components/onewire/translations/nl.json index ae155ccf2c2..77ac79c1597 100644 --- a/homeassistant/components/onewire/translations/nl.json +++ b/homeassistant/components/onewire/translations/nl.json @@ -4,10 +4,15 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { + "cannot_connect": "Kan geen verbinding maken", "invalid_path": "Directory niet gevonden." }, "step": { "owserver": { + "data": { + "host": "Host", + "port": "Poort" + }, "title": "Owserver-details instellen" }, "user": { diff --git a/homeassistant/components/opentherm_gw/translations/nl.json b/homeassistant/components/opentherm_gw/translations/nl.json index 7c9c89381e8..e832e790c1e 100644 --- a/homeassistant/components/opentherm_gw/translations/nl.json +++ b/homeassistant/components/opentherm_gw/translations/nl.json @@ -2,6 +2,7 @@ "config": { "error": { "already_configured": "Gateway al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken", "id_exists": "Gateway id bestaat al" }, "step": { diff --git a/homeassistant/components/ovo_energy/translations/nl.json b/homeassistant/components/ovo_energy/translations/nl.json index daa12f9e569..7a2b5b757bb 100644 --- a/homeassistant/components/ovo_energy/translations/nl.json +++ b/homeassistant/components/ovo_energy/translations/nl.json @@ -2,10 +2,16 @@ "config": { "error": { "already_configured": "Account is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie" }, + "flow_title": "OVO Energy: {username}", "step": { "reauth": { + "data": { + "password": "Wachtwoord" + }, + "description": "Authenticatie mislukt voor OVO Energy. Voer uw huidige inloggegevens in.", "title": "Opnieuw verifi\u00ebren" }, "user": { diff --git a/homeassistant/components/ozw/translations/nl.json b/homeassistant/components/ozw/translations/nl.json index 4497654e7f3..80ef72a061e 100644 --- a/homeassistant/components/ozw/translations/nl.json +++ b/homeassistant/components/ozw/translations/nl.json @@ -1,8 +1,23 @@ { "config": { "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", "mqtt_required": "De [%%] integratie is niet ingesteld", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." + }, + "step": { + "hassio_confirm": { + "title": "OpenZWave integratie instellen met de OpenZWave add-on" + }, + "install_addon": { + "title": "De OpenZWave add-on installatie is gestart" + }, + "start_addon": { + "data": { + "usb_path": "USB-apparaatpad" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/nl.json b/homeassistant/components/philips_js/translations/nl.json new file mode 100644 index 00000000000..23cd7e47043 --- /dev/null +++ b/homeassistant/components/philips_js/translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "api_version": "API Versie", + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/nl.json b/homeassistant/components/plaato/translations/nl.json index 6545a659427..c0a9d1e04fb 100644 --- a/homeassistant/components/plaato/translations/nl.json +++ b/homeassistant/components/plaato/translations/nl.json @@ -1,16 +1,42 @@ { "config": { "abort": { - "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." + "already_configured": "Account is al geconfigureerd", + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", + "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." }, "create_entry": { "default": "Om evenementen naar de Home Assistant te sturen, moet u de webhook-functie instellen in Plaato Airlock. \n\n Vul de volgende info in: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n\n Zie [de documentatie] ( {docs_url} ) voor meer informatie." }, + "error": { + "no_auth_token": "U moet een verificatie token toevoegen" + }, "step": { + "api_method": { + "data": { + "token": "Plak hier de verificatie-token", + "use_webhook": "Webhook gebruiken" + }, + "title": "Selecteer API-methode" + }, "user": { + "data": { + "device_name": "Geef uw apparaat een naam", + "device_type": "Type Plaato-apparaat" + }, "description": "Weet u zeker dat u de Plaato-airlock wilt instellen?", "title": "Stel de Plaato Webhook in" } } + }, + "options": { + "step": { + "user": { + "title": "Opties voor Plaato" + }, + "webhook": { + "title": "Opties voor Plaato Airlock" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/translations/nl.json b/homeassistant/components/plex/translations/nl.json index 00c2b30c490..6c89b0b8d5f 100644 --- a/homeassistant/components/plex/translations/nl.json +++ b/homeassistant/components/plex/translations/nl.json @@ -4,6 +4,7 @@ "all_configured": "Alle gekoppelde servers zijn al geconfigureerd", "already_configured": "Deze Plex-server is al geconfigureerd", "already_in_progress": "Plex wordt geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol", "token_request_timeout": "Time-out verkrijgen van token", "unknown": "Mislukt om onbekende reden" }, diff --git a/homeassistant/components/point/translations/nl.json b/homeassistant/components/point/translations/nl.json index a257ba3e111..94763a412a0 100644 --- a/homeassistant/components/point/translations/nl.json +++ b/homeassistant/components/point/translations/nl.json @@ -5,7 +5,8 @@ "authorize_url_fail": "Onbekende fout bij het genereren van een autoriseer-URL.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", "external_setup": "Punt succesvol geconfigureerd vanuit een andere stroom.", - "no_flows": "U moet Point configureren voordat u zich ermee kunt verifi\u00ebren. [Gelieve de instructies te lezen](https://www.home-assistant.io/components/nest/)." + "no_flows": "U moet Point configureren voordat u zich ermee kunt verifi\u00ebren. [Gelieve de instructies te lezen](https://www.home-assistant.io/components/nest/).", + "unknown_authorize_url_generation": "Onbekende fout bij het genereren van een autorisatie-URL." }, "create_entry": { "default": "Succesvol geverifieerd met Minut voor uw Point appara(a)t(en)" diff --git a/homeassistant/components/poolsense/translations/nl.json b/homeassistant/components/poolsense/translations/nl.json index 7482a0bbe7c..46fc915d7bd 100644 --- a/homeassistant/components/poolsense/translations/nl.json +++ b/homeassistant/components/poolsense/translations/nl.json @@ -9,6 +9,7 @@ "step": { "user": { "data": { + "email": "E-mail", "password": "Wachtwoord" } } diff --git a/homeassistant/components/powerwall/translations/nl.json b/homeassistant/components/powerwall/translations/nl.json index 779da2086eb..c4ae2616f46 100644 --- a/homeassistant/components/powerwall/translations/nl.json +++ b/homeassistant/components/powerwall/translations/nl.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "De powerwall is al geconfigureerd" + "already_configured": "De powerwall is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout", "wrong_version": "Uw powerwall gebruikt een softwareversie die niet wordt ondersteund. Overweeg om dit probleem te upgraden of te melden, zodat het kan worden opgelost." }, @@ -12,7 +14,8 @@ "step": { "user": { "data": { - "ip_address": "IP-adres" + "ip_address": "IP-adres", + "password": "Wachtwoord" }, "title": "Maak verbinding met de powerwall" } diff --git a/homeassistant/components/profiler/translations/nl.json b/homeassistant/components/profiler/translations/nl.json index 703ac8614c4..8690611b1c9 100644 --- a/homeassistant/components/profiler/translations/nl.json +++ b/homeassistant/components/profiler/translations/nl.json @@ -2,6 +2,11 @@ "config": { "abort": { "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." + }, + "step": { + "user": { + "description": "Wil je beginnen met instellen?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/nl.json b/homeassistant/components/progettihwsw/translations/nl.json index ba10aee5ea2..7810a8018a4 100644 --- a/homeassistant/components/progettihwsw/translations/nl.json +++ b/homeassistant/components/progettihwsw/translations/nl.json @@ -9,6 +9,24 @@ }, "step": { "relay_modes": { + "data": { + "relay_1": "Relais 1", + "relay_10": "Relais 10", + "relay_11": "Relais 11", + "relay_12": "Relais 12", + "relay_13": "Relais 13", + "relay_14": "Relais 14", + "relay_15": "Relais 15", + "relay_16": "Relais 16", + "relay_2": "Relais 2", + "relay_3": "Relais 3", + "relay_4": "Relais 4", + "relay_5": "Relais 5", + "relay_6": "Relais 6", + "relay_7": "Relais 7", + "relay_8": "Relais 8", + "relay_9": "Relais 9" + }, "title": "Stel relais in" }, "user": { diff --git a/homeassistant/components/ps4/translations/nl.json b/homeassistant/components/ps4/translations/nl.json index d86240b2c0a..326917e4960 100644 --- a/homeassistant/components/ps4/translations/nl.json +++ b/homeassistant/components/ps4/translations/nl.json @@ -8,6 +8,7 @@ "port_997_bind_error": "Kon niet binden aan poort 997. Raadpleeg de [documentatie] (https://www.home-assistant.io/components/ps4/) voor aanvullende informatie." }, "error": { + "cannot_connect": "Kan geen verbinding maken", "credential_timeout": "Time-out van inlog service. Druk op Submit om opnieuw te starten.", "login_failed": "Kan niet koppelen met PlayStation 4. Controleer of de pincode juist is.", "no_ipaddress": "Voer het IP-adres in van de PlayStation 4 die je wilt configureren." diff --git a/homeassistant/components/rainmachine/translations/nl.json b/homeassistant/components/rainmachine/translations/nl.json index adaa8cb5f30..02411ea999f 100644 --- a/homeassistant/components/rainmachine/translations/nl.json +++ b/homeassistant/components/rainmachine/translations/nl.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Deze RainMachine controller is al geconfigureerd." }, + "error": { + "invalid_auth": "Ongeldige authenticatie" + }, "step": { "user": { "data": { @@ -13,5 +16,12 @@ "title": "Vul uw gegevens in" } } + }, + "options": { + "step": { + "init": { + "title": "Configureer RainMachine" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/nl.json b/homeassistant/components/recollect_waste/translations/nl.json new file mode 100644 index 00000000000..6ce4a8f8a9f --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/nl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "invalid_place_or_service_id": "Ongeldige plaats of service-ID" + }, + "step": { + "user": { + "data": { + "place_id": "Plaats-ID", + "service_id": "Service-ID" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "Configureer Recollect Waste" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/nl.json b/homeassistant/components/rfxtrx/translations/nl.json index 0dc56206f66..0b6e8997b18 100644 --- a/homeassistant/components/rfxtrx/translations/nl.json +++ b/homeassistant/components/rfxtrx/translations/nl.json @@ -10,10 +10,23 @@ "step": { "setup_network": { "data": { + "host": "Host", "port": "Poort" }, "title": "Selecteer verbindingsadres" }, + "setup_serial": { + "data": { + "device": "Selecteer apparaat" + }, + "title": "Apparaat" + }, + "setup_serial_manual_path": { + "data": { + "device": "USB-apparaatpad" + }, + "title": "Pad" + }, "user": { "data": { "type": "Verbindingstype" @@ -25,7 +38,19 @@ "options": { "error": { "already_configured_device": "Apparaat is al geconfigureerd", + "invalid_event_code": "Ongeldige gebeurteniscode", "unknown": "Onverwachte fout" + }, + "step": { + "prompt_options": { + "data": { + "automatic_add": "Schakel automatisch toevoegen in", + "debug": "Foutopsporing inschakelen" + } + }, + "set_device_options": { + "title": "Configureer apparaatopties" + } } } } \ No newline at end of file diff --git a/homeassistant/components/rituals_perfume_genie/translations/zh-Hant.json b/homeassistant/components/rituals_perfume_genie/translations/zh-Hant.json new file mode 100644 index 00000000000..c91a500edd8 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "email": "\u96fb\u5b50\u90f5\u4ef6", + "password": "\u5bc6\u78bc" + }, + "title": "\u9023\u7dda\u81f3 Rituals \u5e33\u865f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/nl.json b/homeassistant/components/roku/translations/nl.json index 529b01b64c2..d892d2c78d2 100644 --- a/homeassistant/components/roku/translations/nl.json +++ b/homeassistant/components/roku/translations/nl.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Roku-apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", "unknown": "Onverwachte fout" }, "error": { diff --git a/homeassistant/components/roomba/translations/nl.json b/homeassistant/components/roomba/translations/nl.json index 754ff2e51a8..0177adaea1f 100644 --- a/homeassistant/components/roomba/translations/nl.json +++ b/homeassistant/components/roomba/translations/nl.json @@ -28,6 +28,12 @@ "description": "Het wachtwoord kon niet automatisch van het apparaat worden opgehaald. Volg de stappen zoals beschreven in de documentatie op: {auth_help_url}", "title": "Voer wachtwoord in" }, + "manual": { + "data": { + "blid": "BLID", + "host": "Host" + } + }, "user": { "data": { "blid": "BLID", diff --git a/homeassistant/components/ruckus_unleashed/translations/nl.json b/homeassistant/components/ruckus_unleashed/translations/nl.json index 7482a0bbe7c..0569c39321a 100644 --- a/homeassistant/components/ruckus_unleashed/translations/nl.json +++ b/homeassistant/components/ruckus_unleashed/translations/nl.json @@ -4,12 +4,15 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "invalid_auth": "Ongeldige authenticatie" + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" }, "step": { "user": { "data": { - "password": "Wachtwoord" + "host": "Host", + "password": "Wachtwoord", + "username": "Gebruikersnaam" } } } diff --git a/homeassistant/components/shelly/translations/nl.json b/homeassistant/components/shelly/translations/nl.json index 75a2d2771d6..7084a972e29 100644 --- a/homeassistant/components/shelly/translations/nl.json +++ b/homeassistant/components/shelly/translations/nl.json @@ -25,5 +25,18 @@ } } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Knop", + "button1": "Eerste knop", + "button2": "Tweede knop", + "button3": "Derde knop" + }, + "trigger_type": { + "double": "{subtype} dubbel geklikt", + "single_long": "{subtype} een keer geklikt en daarna lang geklikt", + "triple": "{subtype} driemaal geklikt" + } } } \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/nl.json b/homeassistant/components/smappee/translations/nl.json index 86f4a40c6f9..ebcc16dafac 100644 --- a/homeassistant/components/smappee/translations/nl.json +++ b/homeassistant/components/smappee/translations/nl.json @@ -3,7 +3,10 @@ "abort": { "already_configured_device": "Apparaat is al geconfigureerd", "already_configured_local_device": "Lokale apparaten zijn al geconfigureerd. Verwijder deze eerst voordat u een cloudapparaat configureert.", - "cannot_connect": "Kan geen verbinding maken" + "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "cannot_connect": "Kan geen verbinding maken", + "invalid_mdns": "Niet-ondersteund apparaat voor de Smappee-integratie.", + "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen." }, "step": { "local": { @@ -11,6 +14,9 @@ "host": "Host" }, "description": "Voer de host in om de lokale Smappee-integratie te starten" + }, + "pick_implementation": { + "title": "Kies een authenticatie methode" } } } diff --git a/homeassistant/components/smarthab/translations/nl.json b/homeassistant/components/smarthab/translations/nl.json index 9dabac8aa55..7f5fc7fe27c 100644 --- a/homeassistant/components/smarthab/translations/nl.json +++ b/homeassistant/components/smarthab/translations/nl.json @@ -1,12 +1,14 @@ { "config": { "error": { + "invalid_auth": "Ongeldige authenticatie", "service": "Fout bij het bereiken van SmartHab. De service is mogelijk uitgevallen. Controleer uw verbinding.", "unknown": "Onverwachte fout" }, "step": { "user": { "data": { + "email": "E-mail", "password": "Wachtwoord" } } diff --git a/homeassistant/components/smarttub/translations/nl.json b/homeassistant/components/smarttub/translations/nl.json new file mode 100644 index 00000000000..a5f20db8a32 --- /dev/null +++ b/homeassistant/components/smarttub/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Wachtwoord" + }, + "title": "Inloggen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/translations/nl.json b/homeassistant/components/solaredge/translations/nl.json index 4b468218410..3fe28971f29 100644 --- a/homeassistant/components/solaredge/translations/nl.json +++ b/homeassistant/components/solaredge/translations/nl.json @@ -1,10 +1,15 @@ { "config": { "abort": { + "already_configured": "Apparaat is al geconfigureerd", "site_exists": "Deze site_id is al geconfigureerd" }, "error": { - "site_exists": "Deze site_id is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "could_not_connect": "Kon geen verbinding maken met de solaredge API", + "invalid_api_key": "Ongeldige API-sleutel", + "site_exists": "Deze site_id is al geconfigureerd", + "site_not_active": "De site is niet actief" }, "step": { "user": { diff --git a/homeassistant/components/somfy_mylink/translations/nl.json b/homeassistant/components/somfy_mylink/translations/nl.json index 208c032227a..a63320919c6 100644 --- a/homeassistant/components/somfy_mylink/translations/nl.json +++ b/homeassistant/components/somfy_mylink/translations/nl.json @@ -1,5 +1,40 @@ { "config": { - "flow_title": "Somfy MyLink {mac} ( {ip} )" - } + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "flow_title": "Somfy MyLink {mac} ( {ip} )", + "step": { + "user": { + "data": { + "host": "Host", + "port": "Poort", + "system_id": "Systeem-ID" + } + } + } + }, + "options": { + "abort": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "entity_config": { + "description": "Configureer opties voor `{entity_id}`", + "title": "Entiteit configureren" + }, + "init": { + "data": { + "entity_id": "Configureer een specifieke entiteit." + }, + "title": "Configureer MyLink-opties" + } + } + }, + "title": "Somfy MyLink" } \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/nl.json b/homeassistant/components/sonarr/translations/nl.json index 58db7f57dd4..08ef9bb2ece 100644 --- a/homeassistant/components/sonarr/translations/nl.json +++ b/homeassistant/components/sonarr/translations/nl.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Service is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol", "unknown": "Onverwachte fout" }, "error": { @@ -9,6 +10,10 @@ "invalid_auth": "Ongeldige authenticatie" }, "step": { + "reauth_confirm": { + "description": "De Sonarr-integratie moet handmatig opnieuw worden geverifieerd met de Sonarr-API die wordt gehost op: {host}", + "title": "Verifieer de integratie opnieuw" + }, "user": { "data": { "api_key": "API-sleutel", diff --git a/homeassistant/components/srp_energy/translations/nl.json b/homeassistant/components/srp_energy/translations/nl.json new file mode 100644 index 00000000000..cd06c36b661 --- /dev/null +++ b/homeassistant/components/srp_energy/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_account": "Account-ID moet een 9-cijferig nummer zijn", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + } + } + }, + "title": "SRP Energy" +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/nl.json b/homeassistant/components/subaru/translations/nl.json new file mode 100644 index 00000000000..5a9bd4119ff --- /dev/null +++ b/homeassistant/components/subaru/translations/nl.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken" + }, + "error": { + "bad_pin_format": "De pincode moet uit 4 cijfers bestaan", + "cannot_connect": "Kan geen verbinding maken", + "incorrect_pin": "Onjuiste PIN", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "pin": { + "data": { + "pin": "PIN" + }, + "title": "Subaru Starlink Configuratie" + }, + "user": { + "data": { + "country": "Selecteer land", + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "title": "Subaru Starlink-configuratie" + } + } + }, + "options": { + "step": { + "init": { + "title": "Subaru Starlink-opties" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/zh-Hant.json b/homeassistant/components/subaru/translations/zh-Hant.json new file mode 100644 index 00000000000..22eaa589fe2 --- /dev/null +++ b/homeassistant/components/subaru/translations/zh-Hant.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "error": { + "bad_pin_format": "PIN \u78bc\u61c9\u8a72\u70ba 4 \u4f4d\u6578\u5b57", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "incorrect_pin": "PIN \u78bc\u932f\u8aa4", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "pin": { + "data": { + "pin": "PIN" + }, + "description": "\u8acb\u8f38\u5165 MySubaru PIN \u78bc\n\u6ce8\u610f\uff1a\u6240\u4ee5\u5e33\u865f\u5167\u8eca\u8f1b\u90fd\u5fc5\u9808\u4f7f\u7528\u76f8\u540c PIN \u78bc", + "title": "Subaru Starlink \u8a2d\u5b9a" + }, + "user": { + "data": { + "country": "\u9078\u64c7\u570b\u5bb6", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8acb\u8f38\u5165 MySubaru \u8a8d\u8b49\n\u6ce8\u610f\uff1a\u555f\u59cb\u8a2d\u5b9a\u5927\u7d04\u9700\u8981 30 \u79d2", + "title": "Subaru Starlink \u8a2d\u5b9a" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "update_enabled": "\u958b\u555f\u8eca\u8f1b\u8cc7\u6599\u4e0b\u8f09" + }, + "description": "\u958b\u555f\u5f8c\uff0c\u5c07\u6703\u6bcf 2 \u5c0f\u6642\u50b3\u9001\u9060\u7aef\u547d\u4ee4\u81f3\u8eca\u8f1b\u4ee5\u7372\u5f97\u6700\u65b0\u50b3\u611f\u5668\u8cc7\u6599\u3002\u5982\u679c\u6c92\u6709\u958b\u555f\uff0c\u50b3\u611f\u5668\u65b0\u8cc7\u6599\u50c5\u6703\u65bc\u8eca\u8f1b\u81ea\u52d5\u63a8\u9001\u8cc7\u6599\u6642\u63a5\u6536\uff08\u901a\u5e38\u70ba\u5f15\u64ce\u7184\u706b\u4e4b\u5f8c\uff09\u3002", + "title": "Subaru Starlink \u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/nl.json b/homeassistant/components/tellduslive/translations/nl.json index b3874dac77e..4eb6d40a142 100644 --- a/homeassistant/components/tellduslive/translations/nl.json +++ b/homeassistant/components/tellduslive/translations/nl.json @@ -4,7 +4,11 @@ "already_configured": "Service is al geconfigureerd", "authorize_url_fail": "Onbekende fout bij het genereren van een autorisatie url.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "unknown": "Onbekende fout opgetreden" + "unknown": "Onbekende fout opgetreden", + "unknown_authorize_url_generation": "Onbekende fout bij het genereren van een autorisatie-URL." + }, + "error": { + "invalid_auth": "Ongeldige authenticatie" }, "step": { "auth": { diff --git a/homeassistant/components/tesla/translations/nl.json b/homeassistant/components/tesla/translations/nl.json index 9e79b35165d..f6289de6d9d 100644 --- a/homeassistant/components/tesla/translations/nl.json +++ b/homeassistant/components/tesla/translations/nl.json @@ -1,8 +1,13 @@ { "config": { + "abort": { + "already_configured": "Account is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" + }, "error": { "already_configured": "Account is al geconfigureerd", - "cannot_connect": "Kan geen verbinding maken" + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie" }, "step": { "user": { diff --git a/homeassistant/components/tibber/translations/nl.json b/homeassistant/components/tibber/translations/nl.json index 4a89639cf50..4a5e518f306 100644 --- a/homeassistant/components/tibber/translations/nl.json +++ b/homeassistant/components/tibber/translations/nl.json @@ -4,6 +4,7 @@ "already_configured": "Service is al geconfigureerd" }, "error": { + "cannot_connect": "Kan geen verbinding maken", "invalid_access_token": "Ongeldig toegangstoken", "timeout": "Time-out om verbinding te maken met Tibber" }, diff --git a/homeassistant/components/tile/translations/nl.json b/homeassistant/components/tile/translations/nl.json index 26c57268689..c160ac631ee 100644 --- a/homeassistant/components/tile/translations/nl.json +++ b/homeassistant/components/tile/translations/nl.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd" }, + "error": { + "invalid_auth": "Ongeldige authenticatie" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/toon/translations/nl.json b/homeassistant/components/toon/translations/nl.json index 4f63d7d09da..cf77b94d025 100644 --- a/homeassistant/components/toon/translations/nl.json +++ b/homeassistant/components/toon/translations/nl.json @@ -4,7 +4,8 @@ "already_configured": "De geselecteerde overeenkomst is al geconfigureerd.", "authorize_url_fail": "Onbekende fout bij het genereren van een autorisatie-URL.", "no_agreements": "Dit account heeft geen Toon schermen.", - "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout [check the help section] ( {docs_url} )" + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout [check the help section] ( {docs_url} )", + "unknown_authorize_url_generation": "Onbekende fout bij het genereren van een autorisatie-URL." } } } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/nl.json b/homeassistant/components/totalconnect/translations/nl.json index c72b7e368ac..1f4fb5490d1 100644 --- a/homeassistant/components/totalconnect/translations/nl.json +++ b/homeassistant/components/totalconnect/translations/nl.json @@ -1,12 +1,16 @@ { "config": { "abort": { - "already_configured": "Account al geconfigureerd" + "already_configured": "Account al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "invalid_auth": "Ongeldige authenticatie" }, "step": { + "reauth_confirm": { + "title": "Verifieer de integratie opnieuw" + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/totalconnect/translations/ru.json b/homeassistant/components/totalconnect/translations/ru.json index 054f207b2b0..0f067541dec 100644 --- a/homeassistant/components/totalconnect/translations/ru.json +++ b/homeassistant/components/totalconnect/translations/ru.json @@ -1,12 +1,25 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { - "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "usercode": "\u041a\u043e\u0434 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0432 \u044d\u0442\u043e\u043c \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0438." }, "step": { + "locations": { + "data": { + "location": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0432 \u044d\u0442\u043e\u043c \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0438.", + "title": "\u041a\u043e\u0434\u044b \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0434\u043b\u044f \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f" + }, + "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 Total Connect", + "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" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/totalconnect/translations/zh-Hant.json b/homeassistant/components/totalconnect/translations/zh-Hant.json index c20dd4065b6..96921baf007 100644 --- a/homeassistant/components/totalconnect/translations/zh-Hant.json +++ b/homeassistant/components/totalconnect/translations/zh-Hant.json @@ -1,12 +1,25 @@ { "config": { "abort": { - "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "usercode": "\u4f7f\u7528\u8005\u4ee3\u78bc\u4e0d\u652f\u63f4\u6b64\u5ea7\u6a19" }, "step": { + "locations": { + "data": { + "location": "\u5ea7\u6a19" + }, + "description": "\u8f38\u5165\u4f7f\u7528\u8005\u65bc\u6b64\u5ea7\u6a19\u4e4b\u4f7f\u7528\u8005\u4ee3\u78bc", + "title": "\u5ea7\u6a19\u4f7f\u7528\u8005\u4ee3\u78bc" + }, + "reauth_confirm": { + "description": "Total Connect \u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u5e33\u865f", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "password": "\u5bc6\u78bc", diff --git a/homeassistant/components/traccar/translations/nl.json b/homeassistant/components/traccar/translations/nl.json index 251e16d0763..0b4563d69fc 100644 --- a/homeassistant/components/traccar/translations/nl.json +++ b/homeassistant/components/traccar/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.", + "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." }, "create_entry": { "default": "Voor het verzenden van gebeurtenissen naar Home Assistant, moet u de webhook-functie in Traccar instellen.\n\nGebruik de volgende URL: ' {webhook_url} '\n\nZie [de documentatie] ({docs_url}) voor meer informatie." diff --git a/homeassistant/components/transmission/translations/nl.json b/homeassistant/components/transmission/translations/nl.json index 8cfa9333ba4..df9a4590e66 100644 --- a/homeassistant/components/transmission/translations/nl.json +++ b/homeassistant/components/transmission/translations/nl.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "Kan geen verbinding maken met host", + "invalid_auth": "Ongeldige authenticatie", "name_exists": "Naam bestaat al" }, "step": { diff --git a/homeassistant/components/tuya/translations/nl.json b/homeassistant/components/tuya/translations/nl.json index 5a0e3691d5b..46a0228843b 100644 --- a/homeassistant/components/tuya/translations/nl.json +++ b/homeassistant/components/tuya/translations/nl.json @@ -2,8 +2,12 @@ "config": { "abort": { "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", "single_instance_allowed": "Al geconfigureerd. Er is maar een configuratie mogelijk." }, + "error": { + "invalid_auth": "Ongeldige authenticatie" + }, "flow_title": "Tuya-configuratie", "step": { "user": { @@ -19,7 +23,16 @@ } }, "options": { + "abort": { + "cannot_connect": "Kan geen verbinding maken" + }, + "error": { + "dev_not_found": "Apparaat niet gevonden" + }, "step": { + "device": { + "title": "Configureer Tuya Apparaat" + }, "init": { "title": "Configureer Tuya opties" } diff --git a/homeassistant/components/twentemilieu/translations/nl.json b/homeassistant/components/twentemilieu/translations/nl.json index ca5abd7e37c..54611aa9ab8 100644 --- a/homeassistant/components/twentemilieu/translations/nl.json +++ b/homeassistant/components/twentemilieu/translations/nl.json @@ -4,6 +4,7 @@ "already_configured": "Locatie is al geconfigureerd" }, "error": { + "cannot_connect": "Kan geen verbinding maken", "invalid_address": "Adres niet gevonden in servicegebied Twente Milieu." }, "step": { diff --git a/homeassistant/components/twilio/translations/nl.json b/homeassistant/components/twilio/translations/nl.json index ee97ef4f6cd..55db4ef5e48 100644 --- a/homeassistant/components/twilio/translations/nl.json +++ b/homeassistant/components/twilio/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.", + "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." }, "create_entry": { "default": "Om evenementen naar de Home Assistant te verzenden, moet u [Webhooks with Twilio] ( {twilio_url} ) instellen. \n\n Vul de volgende info in: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhoudstype: application / x-www-form-urlencoded \n\n Zie [de documentatie] ( {docs_url} ) voor informatie over het configureren van automatiseringen om binnenkomende gegevens te verwerken." diff --git a/homeassistant/components/twinkly/translations/nl.json b/homeassistant/components/twinkly/translations/nl.json index 861ee57283c..97a55150447 100644 --- a/homeassistant/components/twinkly/translations/nl.json +++ b/homeassistant/components/twinkly/translations/nl.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "device_exists": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/nl.json b/homeassistant/components/unifi/translations/nl.json index 96ffc0c0ace..7f0baf4a3be 100644 --- a/homeassistant/components/unifi/translations/nl.json +++ b/homeassistant/components/unifi/translations/nl.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Controller site is al geconfigureerd", + "configuration_updated": "Configuratie bijgewerkt.", "reauth_successful": "Herauthenticatie was succesvol" }, "error": { diff --git a/homeassistant/components/upcloud/translations/nl.json b/homeassistant/components/upcloud/translations/nl.json index 783032a1da0..312b117208c 100644 --- a/homeassistant/components/upcloud/translations/nl.json +++ b/homeassistant/components/upcloud/translations/nl.json @@ -1,12 +1,14 @@ { "config": { "error": { + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie" }, "step": { "user": { "data": { - "password": "Wachtwoord" + "password": "Wachtwoord", + "username": "Gebruikersnaam" } } } diff --git a/homeassistant/components/vesync/translations/nl.json b/homeassistant/components/vesync/translations/nl.json index 0dc21373c14..36c7f315bcc 100644 --- a/homeassistant/components/vesync/translations/nl.json +++ b/homeassistant/components/vesync/translations/nl.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "Al geconfigureerd. Slecht \u00e9\u00e9n configuratie mogelijk." }, + "error": { + "invalid_auth": "Ongeldige authenticatie" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/vizio/translations/nl.json b/homeassistant/components/vizio/translations/nl.json index 9841eaa7f50..48fd831d61c 100644 --- a/homeassistant/components/vizio/translations/nl.json +++ b/homeassistant/components/vizio/translations/nl.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured_device": "Dit apparaat is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken", "updated_entry": "Dit item is al ingesteld, maar de naam en/of opties die zijn gedefinieerd in de configuratie komen niet overeen met de eerder ge\u00efmporteerde configuratie, dus het configuratie-item is dienovereenkomstig bijgewerkt." }, "error": { diff --git a/homeassistant/components/weather/translations/et.json b/homeassistant/components/weather/translations/et.json index f035d37d62e..2de9158c085 100644 --- a/homeassistant/components/weather/translations/et.json +++ b/homeassistant/components/weather/translations/et.json @@ -3,7 +3,7 @@ "_": { "clear-night": "Selge \u00f6\u00f6", "cloudy": "Pilves", - "exceptional": "Erakordne", + "exceptional": "Ohtlikud olud", "fog": "Udu", "hail": "Rahe", "lightning": "\u00c4ikeseline", diff --git a/homeassistant/components/xbox/translations/nl.json b/homeassistant/components/xbox/translations/nl.json new file mode 100644 index 00000000000..858fd264eaf --- /dev/null +++ b/homeassistant/components/xbox/translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "create_entry": { + "default": "Succesvol geauthenticeerd" + }, + "step": { + "pick_implementation": { + "title": "Kies een authenticatie methode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/nl.json b/homeassistant/components/xiaomi_aqara/translations/nl.json index e17b3b572d1..81f984a5a05 100644 --- a/homeassistant/components/xiaomi_aqara/translations/nl.json +++ b/homeassistant/components/xiaomi_aqara/translations/nl.json @@ -1,11 +1,19 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang" + }, + "error": { + "invalid_host": "Ongeldige hostnaam of IP-adres, zie https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", + "invalid_mac": "Ongeldig MAC-adres" }, "flow_title": "Xiaomi Aqara Gateway: {name}", "step": { "select": { + "data": { + "select_ip": "IP-adres" + }, "description": "Voer de installatie opnieuw uit als u extra gateways wilt aansluiten", "title": "Selecteer de Xiaomi Aqara Gateway waarmee u verbinding wilt maken" }, diff --git a/homeassistant/components/xiaomi_miio/translations/nl.json b/homeassistant/components/xiaomi_miio/translations/nl.json index eea72c1c4c0..3ea12e3a465 100644 --- a/homeassistant/components/xiaomi_miio/translations/nl.json +++ b/homeassistant/components/xiaomi_miio/translations/nl.json @@ -5,9 +5,18 @@ "already_in_progress": "De configuratiestroom voor dit Xiaomi Miio-apparaat is al bezig." }, "error": { + "cannot_connect": "Kan geen verbinding maken", "no_device_selected": "Geen apparaat geselecteerd, selecteer 1 apparaat alstublieft" }, "step": { + "device": { + "data": { + "host": "IP-adres", + "model": "Apparaatmodel (Optioneel)", + "name": "Naam van het apparaat", + "token": "API-token" + } + }, "gateway": { "data": { "host": "IP-adres", diff --git a/homeassistant/components/xiaomi_miio/translations/ru.json b/homeassistant/components/xiaomi_miio/translations/ru.json index 5113128cac5..5c5064ac347 100644 --- a/homeassistant/components/xiaomi_miio/translations/ru.json +++ b/homeassistant/components/xiaomi_miio/translations/ru.json @@ -14,6 +14,7 @@ "device": { "data": { "host": "IP-\u0430\u0434\u0440\u0435\u0441", + "model": "\u041c\u043e\u0434\u0435\u043b\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", "token": "\u0422\u043e\u043a\u0435\u043d API" }, diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json index 43e3e10df20..dce2002faa9 100644 --- a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json @@ -14,6 +14,7 @@ "device": { "data": { "host": "IP \u4f4d\u5740", + "model": "\u88dd\u7f6e\u578b\u865f\uff08\u9078\u9805\uff09", "name": "\u88dd\u7f6e\u540d\u7a31", "token": "API \u5bc6\u9470" }, diff --git a/homeassistant/components/zerproc/translations/nl.json b/homeassistant/components/zerproc/translations/nl.json new file mode 100644 index 00000000000..d11896014fd --- /dev/null +++ b/homeassistant/components/zerproc/translations/nl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "step": { + "confirm": { + "description": "Wil je beginnen met instellen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/nl.json b/homeassistant/components/zoneminder/translations/nl.json index ebfd26329dc..f4f071d9097 100644 --- a/homeassistant/components/zoneminder/translations/nl.json +++ b/homeassistant/components/zoneminder/translations/nl.json @@ -12,7 +12,8 @@ "error": { "auth_fail": "Gebruikersnaam of wachtwoord is onjuist.", "cannot_connect": "Kon niet verbinden", - "connection_error": "Kan geen verbinding maken met een ZoneMinder-server." + "connection_error": "Kan geen verbinding maken met een ZoneMinder-server.", + "invalid_auth": "Ongeldige authenticatie" }, "flow_title": "ZoneMinder", "step": { diff --git a/homeassistant/components/zwave_js/translations/ca.json b/homeassistant/components/zwave_js/translations/ca.json index 93ec53a644e..6806b5072c1 100644 --- a/homeassistant/components/zwave_js/translations/ca.json +++ b/homeassistant/components/zwave_js/translations/ca.json @@ -20,6 +20,13 @@ "install_addon": "Espera mentre finalitza la instal\u00b7laci\u00f3 del complement Z-Wave JS. Pot tardar uns quants minuts." }, "step": { + "configure_addon": { + "data": { + "network_key": "Clau de xarxa", + "usb_path": "Ruta del port USB del dispositiu" + }, + "title": "Introdueix la configuraci\u00f3 del complement Z-Wave JS" + }, "hassio_confirm": { "title": "Configura la integraci\u00f3 Z-Wave JS mitjan\u00e7ant el complement Z-Wave JS" }, @@ -38,13 +45,6 @@ "description": "Vols utilitzar el complement Supervisor de Z-Wave JS?", "title": "Selecciona el m\u00e8tode de connexi\u00f3" }, - "start_addon": { - "data": { - "network_key": "Clau de xarxa", - "usb_path": "Ruta del port USB del dispositiu" - }, - "title": "Introdueix la configuraci\u00f3 del complement Z-Wave JS" - }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/zwave_js/translations/cs.json b/homeassistant/components/zwave_js/translations/cs.json index 96073b579ed..57e7a6b74db 100644 --- a/homeassistant/components/zwave_js/translations/cs.json +++ b/homeassistant/components/zwave_js/translations/cs.json @@ -10,16 +10,16 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "configure_addon": { + "data": { + "usb_path": "Cesta k USB za\u0159\u00edzen\u00ed" + } + }, "manual": { "data": { "url": "URL" } }, - "start_addon": { - "data": { - "usb_path": "Cesta k USB za\u0159\u00edzen\u00ed" - } - }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json index d8bdafcefee..5be980d52cb 100644 --- a/homeassistant/components/zwave_js/translations/en.json +++ b/homeassistant/components/zwave_js/translations/en.json @@ -49,8 +49,13 @@ }, "start_addon": { "title": "The Z-Wave JS add-on is starting." + }, + "user": { + "data": { + "url": "URL" + } } } }, "title": "Z-Wave JS" -} +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/es.json b/homeassistant/components/zwave_js/translations/es.json index 69a638e1f40..32d7a6d2e6d 100644 --- a/homeassistant/components/zwave_js/translations/es.json +++ b/homeassistant/components/zwave_js/translations/es.json @@ -20,6 +20,13 @@ "install_addon": "Espera mientras termina la instalaci\u00f3n del complemento Z-Wave JS. Puede tardar varios minutos." }, "step": { + "configure_addon": { + "data": { + "network_key": "Clave de red", + "usb_path": "Ruta del dispositivo USB" + }, + "title": "Introduzca la configuraci\u00f3n del complemento Z-Wave JS" + }, "hassio_confirm": { "title": "Configurar la integraci\u00f3n de Z-Wave JS con el complemento Z-Wave JS" }, @@ -38,13 +45,6 @@ "description": "\u00bfQuieres utilizar el complemento Z-Wave JS Supervisor?", "title": "Selecciona el m\u00e9todo de conexi\u00f3n" }, - "start_addon": { - "data": { - "network_key": "Clave de red", - "usb_path": "Ruta del dispositivo USB" - }, - "title": "Introduzca la configuraci\u00f3n del complemento Z-Wave JS" - }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/zwave_js/translations/et.json b/homeassistant/components/zwave_js/translations/et.json index 7a7aadfb841..d51507b616f 100644 --- a/homeassistant/components/zwave_js/translations/et.json +++ b/homeassistant/components/zwave_js/translations/et.json @@ -20,6 +20,13 @@ "install_addon": "Palun oota kuni Z-Wave JS lisandmoodul on paigaldatud. See v\u00f5ib v\u00f5tta mitu minutit." }, "step": { + "configure_addon": { + "data": { + "network_key": "V\u00f5rgu v\u00f5ti", + "usb_path": "USB-seadme asukoha rada" + }, + "title": "Sisesta Z-Wave JS lisandmooduli seaded" + }, "hassio_confirm": { "title": "Seadista Z-Wave JS-i sidumine Z-Wave JS-i lisandmooduliga" }, @@ -38,13 +45,6 @@ "description": "Kas soovid kasutada Z-Wave JSi halduri lisandmoodulit?", "title": "Vali \u00fchendusviis" }, - "start_addon": { - "data": { - "network_key": "V\u00f5rgu v\u00f5ti", - "usb_path": "USB-seadme asukoha rada" - }, - "title": "Sisesta Z-Wave JS lisandmooduli seaded" - }, "user": { "data": { "url": "" diff --git a/homeassistant/components/zwave_js/translations/fr.json b/homeassistant/components/zwave_js/translations/fr.json index 2196ed0259e..040e3997ac1 100644 --- a/homeassistant/components/zwave_js/translations/fr.json +++ b/homeassistant/components/zwave_js/translations/fr.json @@ -20,6 +20,13 @@ "install_addon": "Veuillez patienter pendant l'installation du module compl\u00e9mentaire Z-Wave JS. Cela peut prendre plusieurs minutes." }, "step": { + "configure_addon": { + "data": { + "network_key": "Cl\u00e9 r\u00e9seau", + "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" + }, + "title": "Entrez la configuration du module compl\u00e9mentaire Z-Wave JS" + }, "hassio_confirm": { "title": "Configurer l'int\u00e9gration Z-Wave JS avec le module compl\u00e9mentaire Z-Wave JS" }, @@ -38,13 +45,6 @@ "description": "Voulez-vous utiliser le module compl\u00e9mentaire Z-Wave JS Supervisor?", "title": "S\u00e9lectionner la m\u00e9thode de connexion" }, - "start_addon": { - "data": { - "network_key": "Cl\u00e9 r\u00e9seau", - "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" - }, - "title": "Entrez la configuration du module compl\u00e9mentaire Z-Wave JS" - }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/zwave_js/translations/it.json b/homeassistant/components/zwave_js/translations/it.json index fc76b309a34..5f0868a3f74 100644 --- a/homeassistant/components/zwave_js/translations/it.json +++ b/homeassistant/components/zwave_js/translations/it.json @@ -20,6 +20,13 @@ "install_addon": "Attendi il termine dell'installazione del componente aggiuntivo Z-Wave JS. Questa operazione pu\u00f2 richiedere diversi minuti." }, "step": { + "configure_addon": { + "data": { + "network_key": "Chiave di rete", + "usb_path": "Percorso del dispositivo USB" + }, + "title": "Accedi alla configurazione del componente aggiuntivo Z-Wave JS" + }, "hassio_confirm": { "title": "Configura l'integrazione di Z-Wave JS con il componente aggiuntivo Z-Wave JS" }, @@ -38,13 +45,6 @@ "description": "Desideri utilizzare il componente aggiuntivo Z-Wave JS Supervisor?", "title": "Seleziona il metodo di connessione" }, - "start_addon": { - "data": { - "network_key": "Chiave di rete", - "usb_path": "Percorso del dispositivo USB" - }, - "title": "Accedi alla configurazione del componente aggiuntivo Z-Wave JS" - }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/zwave_js/translations/ko.json b/homeassistant/components/zwave_js/translations/ko.json index 283b0aa17b6..9c86a064151 100644 --- a/homeassistant/components/zwave_js/translations/ko.json +++ b/homeassistant/components/zwave_js/translations/ko.json @@ -10,16 +10,16 @@ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { + "configure_addon": { + "data": { + "usb_path": "USB \uc7a5\uce58 \uacbd\ub85c" + } + }, "manual": { "data": { "url": "URL \uc8fc\uc18c" } }, - "start_addon": { - "data": { - "usb_path": "USB \uc7a5\uce58 \uacbd\ub85c" - } - }, "user": { "data": { "url": "URL \uc8fc\uc18c" diff --git a/homeassistant/components/zwave_js/translations/nl.json b/homeassistant/components/zwave_js/translations/nl.json index 74b4db46de1..7f46c02ece5 100644 --- a/homeassistant/components/zwave_js/translations/nl.json +++ b/homeassistant/components/zwave_js/translations/nl.json @@ -20,6 +20,13 @@ "install_addon": "Een ogenblik geduld terwijl de installatie van de Z-Wave JS add-on is voltooid. Dit kan enkele minuten duren." }, "step": { + "configure_addon": { + "data": { + "network_key": "Netwerksleutel", + "usb_path": "USB-apparaatpad" + }, + "title": "Voer de Z-Wave JS add-on configuratie in" + }, "hassio_confirm": { "title": "Z-Wave JS integratie instellen met de Z-Wave JS add-on" }, @@ -38,13 +45,6 @@ "description": "Wilt u de Z-Wave JS Supervisor add-on gebruiken?", "title": "Selecteer verbindingsmethode" }, - "start_addon": { - "data": { - "network_key": "Netwerksleutel", - "usb_path": "USB-apparaatpad" - }, - "title": "Voer de Z-Wave JS add-on configuratie in" - }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/zwave_js/translations/no.json b/homeassistant/components/zwave_js/translations/no.json index e16425b59ec..b724fb34e48 100644 --- a/homeassistant/components/zwave_js/translations/no.json +++ b/homeassistant/components/zwave_js/translations/no.json @@ -20,6 +20,13 @@ "install_addon": "Vent mens installasjonen av Z-Wave JS-tillegg er ferdig. Dette kan ta flere minutter." }, "step": { + "configure_addon": { + "data": { + "network_key": "Nettverksn\u00f8kkel", + "usb_path": "USB enhetsbane" + }, + "title": "Angi konfigurasjon for Z-Wave JS-tillegg" + }, "hassio_confirm": { "title": "Sett opp Z-Wave JS-integrasjon med Z-Wave JS-tillegg" }, @@ -38,13 +45,6 @@ "description": "Vil du bruke Z-Wave JS Supervisor-tillegg?", "title": "Velg tilkoblingsmetode" }, - "start_addon": { - "data": { - "network_key": "Nettverksn\u00f8kkel", - "usb_path": "USB enhetsbane" - }, - "title": "Angi konfigurasjon for Z-Wave JS-tillegg" - }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/zwave_js/translations/pl.json b/homeassistant/components/zwave_js/translations/pl.json index 47e263c6101..b139b0dacc6 100644 --- a/homeassistant/components/zwave_js/translations/pl.json +++ b/homeassistant/components/zwave_js/translations/pl.json @@ -20,6 +20,13 @@ "install_addon": "Poczekaj, a\u017c zako\u0144czy si\u0119 instalacja dodatku Z-Wave JS. Mo\u017ce to zaj\u0105\u0107 kilka minut." }, "step": { + "configure_addon": { + "data": { + "network_key": "Klucz sieci", + "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" + }, + "title": "Wprowad\u017a konfiguracj\u0119 dodatku Z-Wave JS" + }, "hassio_confirm": { "title": "Skonfiguruj integracj\u0119 Z-Wave JS z dodatkiem Z-Wave JS" }, @@ -38,13 +45,6 @@ "description": "Czy chcesz skorzysta\u0107 z dodatku Z-Wave JS Supervisor?", "title": "Wybierz metod\u0119 po\u0142\u0105czenia" }, - "start_addon": { - "data": { - "network_key": "Klucz sieci", - "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" - }, - "title": "Wprowad\u017a konfiguracj\u0119 dodatku Z-Wave JS" - }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/zwave_js/translations/ru.json b/homeassistant/components/zwave_js/translations/ru.json index 2d9609e9d00..5b7e5f47017 100644 --- a/homeassistant/components/zwave_js/translations/ru.json +++ b/homeassistant/components/zwave_js/translations/ru.json @@ -20,6 +20,13 @@ "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 Z-Wave JS. \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": { + "configure_addon": { + "data": { + "network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438", + "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, + "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS" + }, "hassio_confirm": { "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Z-Wave JS (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant Z-Wave JS)" }, @@ -38,13 +45,6 @@ "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Supervisor Z-Wave JS?", "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" }, - "start_addon": { - "data": { - "network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438", - "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" - }, - "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS" - }, "user": { "data": { "url": "URL-\u0430\u0434\u0440\u0435\u0441" diff --git a/homeassistant/components/zwave_js/translations/tr.json b/homeassistant/components/zwave_js/translations/tr.json index 2faa8ba4307..04ddcc5252c 100644 --- a/homeassistant/components/zwave_js/translations/tr.json +++ b/homeassistant/components/zwave_js/translations/tr.json @@ -20,6 +20,13 @@ "install_addon": "L\u00fctfen Z-Wave JS eklenti kurulumu bitene kadar bekleyin. Bu birka\u00e7 dakika s\u00fcrebilir." }, "step": { + "configure_addon": { + "data": { + "network_key": "A\u011f Anahtar\u0131", + "usb_path": "USB Ayg\u0131t Yolu" + }, + "title": "Z-Wave JS eklenti yap\u0131land\u0131rmas\u0131na girin" + }, "hassio_confirm": { "title": "Z-Wave JS eklentisiyle Z-Wave JS entegrasyonunu ayarlay\u0131n" }, @@ -38,13 +45,6 @@ "description": "Z-Wave JS Supervisor eklentisini kullanmak istiyor musunuz?", "title": "Ba\u011flant\u0131 y\u00f6ntemini se\u00e7in" }, - "start_addon": { - "data": { - "network_key": "A\u011f Anahtar\u0131", - "usb_path": "USB Ayg\u0131t Yolu" - }, - "title": "Z-Wave JS eklenti yap\u0131land\u0131rmas\u0131na girin" - }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/zwave_js/translations/zh-Hant.json b/homeassistant/components/zwave_js/translations/zh-Hant.json index 1cbde8f886b..b9ff1b41920 100644 --- a/homeassistant/components/zwave_js/translations/zh-Hant.json +++ b/homeassistant/components/zwave_js/translations/zh-Hant.json @@ -20,6 +20,13 @@ "install_addon": "\u8acb\u7a0d\u7b49 Z-Wave JS add-on \u5b89\u88dd\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002" }, "step": { + "configure_addon": { + "data": { + "network_key": "\u7db2\u8def\u5bc6\u9470", + "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" + }, + "title": "\u8f38\u5165 Z-Wave JS \u9644\u52a0\u8a2d\u5b9a" + }, "hassio_confirm": { "title": "\u4ee5 Z-Wave JS add-on \u8a2d\u5b9a Z-Wave JS \u6574\u5408" }, @@ -38,13 +45,6 @@ "description": "\u662f\u5426\u8981\u4f7f\u7528 Z-Wave JS Supervisor add-on\uff1f", "title": "\u9078\u64c7\u9023\u7dda\u985e\u578b" }, - "start_addon": { - "data": { - "network_key": "\u7db2\u8def\u5bc6\u9470", - "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" - }, - "title": "\u8f38\u5165 Z-Wave JS \u9644\u52a0\u8a2d\u5b9a" - }, "user": { "data": { "url": "\u7db2\u5740" From 87cbbcb014f1ef5c5f83f7ab4331df9895588daa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Feb 2021 18:22:23 -0600 Subject: [PATCH 682/796] Automatically create HomeKit accessory mode entries (#46473) When we set up HomeKit, we asked users if they wanted to create an entry in bridge or accessory mode. This approach required the user to understand how HomeKit works and choose which type to create. When the user includes the media player or camera domains, we exclude them from the bridge and create the additional entries for each entity in accessory mode. --- homeassistant/components/homekit/__init__.py | 57 ++-- .../components/homekit/accessories.py | 10 +- .../components/homekit/config_flow.py | 203 +++++++----- homeassistant/components/homekit/const.py | 2 + homeassistant/components/homekit/strings.json | 22 +- .../components/homekit/translations/en.json | 29 +- homeassistant/components/homekit/util.py | 37 ++- tests/components/homekit/test_accessories.py | 14 +- tests/components/homekit/test_config_flow.py | 201 +++++++---- tests/components/homekit/test_homekit.py | 313 ++++++------------ tests/components/homekit/test_util.py | 12 + 11 files changed, 472 insertions(+), 428 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 396f36f7c03..534ea3c6f95 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -5,7 +5,7 @@ import logging import os from aiohttp import web -from pyhap.const import CATEGORY_CAMERA, CATEGORY_TELEVISION, STANDALONE_AID +from pyhap.const import STANDALONE_AID import voluptuous as vol from homeassistant.components import zeroconf @@ -70,6 +70,7 @@ from .const import ( CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_ENTRY_INDEX, + CONF_EXCLUDE_ACCESSORY_MODE, CONF_FILTER, CONF_HOMEKIT_MODE, CONF_LINKED_BATTERY_CHARGING_SENSOR, @@ -81,6 +82,7 @@ from .const import ( CONF_ZEROCONF_DEFAULT_INTERFACE, CONFIG_OPTIONS, DEFAULT_AUTO_START, + DEFAULT_EXCLUDE_ACCESSORY_MODE, DEFAULT_HOMEKIT_MODE, DEFAULT_PORT, DEFAULT_SAFE_MODE, @@ -97,11 +99,13 @@ from .const import ( UNDO_UPDATE_LISTENER, ) from .util import ( + accessory_friendly_name, dismiss_setup_message, get_persist_fullpath_for_entry_id, port_is_available, remove_state_files_for_entry_id, show_setup_message, + state_needs_accessory_mode, validate_entity_config, ) @@ -243,6 +247,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # ip_address and advertise_ip are yaml only ip_address = conf.get(CONF_IP_ADDRESS) advertise_ip = conf.get(CONF_ADVERTISE_IP) + # exclude_accessory_mode is only used for config flow + # to indicate that the config entry was setup after + # we started creating config entries for entities that + # to run in accessory mode and that we should never include + # these entities on the bridge. For backwards compatibility + # with users who have not migrated yet we do not do exclude + # these entities by default as we cannot migrate automatically + # since it requires a re-pairing. + exclude_accessory_mode = conf.get( + CONF_EXCLUDE_ACCESSORY_MODE, DEFAULT_EXCLUDE_ACCESSORY_MODE + ) homekit_mode = options.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE) entity_config = options.get(CONF_ENTITY_CONFIG, {}).copy() auto_start = options.get(CONF_AUTO_START, DEFAULT_AUTO_START) @@ -254,10 +269,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): port, ip_address, entity_filter, + exclude_accessory_mode, entity_config, homekit_mode, advertise_ip, entry.entry_id, + entry.title, ) zeroconf_instance = await zeroconf.async_get_instance(hass) @@ -427,10 +444,12 @@ class HomeKit: port, ip_address, entity_filter, + exclude_accessory_mode, entity_config, homekit_mode, advertise_ip=None, entry_id=None, + entry_title=None, ): """Initialize a HomeKit object.""" self.hass = hass @@ -439,8 +458,10 @@ class HomeKit: self._ip_address = ip_address self._filter = entity_filter self._config = entity_config + self._exclude_accessory_mode = exclude_accessory_mode self._advertise_ip = advertise_ip self._entry_id = entry_id + self._entry_title = entry_title self._homekit_mode = homekit_mode self.status = STATUS_READY @@ -457,6 +478,7 @@ class HomeKit: self.hass, self._entry_id, self._name, + self._entry_title, loop=self.hass.loop, address=ip_addr, port=self._port, @@ -518,6 +540,18 @@ class HomeKit: ) return + if state_needs_accessory_mode(state): + if self._exclude_accessory_mode: + return + _LOGGER.warning( + "The bridge %s has entity %s. For best performance, " + "and to prevent unexpected unavailability, create and " + "pair a separate HomeKit instance in accessory mode for " + "this entity.", + self._name, + state.entity_id, + ) + aid = self.hass.data[DOMAIN][self._entry_id][ AID_STORAGE ].get_or_allocate_aid_for_entity_id(state.entity_id) @@ -528,24 +562,6 @@ class HomeKit: try: acc = get_accessory(self.hass, self.driver, state, aid, conf) if acc is not None: - if acc.category == CATEGORY_CAMERA: - _LOGGER.warning( - "The bridge %s has camera %s. For best performance, " - "and to prevent unexpected unavailability, create and " - "pair a separate HomeKit instance in accessory mode for " - "each camera.", - self._name, - acc.entity_id, - ) - elif acc.category == CATEGORY_TELEVISION: - _LOGGER.warning( - "The bridge %s has tv %s. For best performance, " - "and to prevent unexpected unavailability, create and " - "pair a separate HomeKit instance in accessory mode for " - "each tv media player.", - self._name, - acc.entity_id, - ) self.bridge.add_accessory(acc) except Exception: # pylint: disable=broad-except _LOGGER.exception( @@ -650,6 +666,7 @@ class HomeKit: state = entity_states[0] conf = self._config.pop(state.entity_id, {}) acc = get_accessory(self.hass, self.driver, state, STANDALONE_AID, conf) + self.driver.add_accessory(acc) else: self.bridge = HomeBridge(self.hass, self.driver, self._name) @@ -663,7 +680,7 @@ class HomeKit: show_setup_message( self.hass, self._entry_id, - self._name, + accessory_friendly_name(self._entry_title, self.driver.accessory), self.driver.state.pincode, self.driver.accessory.xhm_uri(), ) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index e31b9ec842e..7e68daf4b62 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -71,6 +71,7 @@ from .const import ( TYPE_VALVE, ) from .util import ( + accessory_friendly_name, convert_to_float, dismiss_setup_message, format_sw_version, @@ -489,12 +490,13 @@ class HomeBridge(Bridge): class HomeDriver(AccessoryDriver): """Adapter class for AccessoryDriver.""" - def __init__(self, hass, entry_id, bridge_name, **kwargs): + def __init__(self, hass, entry_id, bridge_name, entry_title, **kwargs): """Initialize a AccessoryDriver object.""" super().__init__(**kwargs) self.hass = hass self._entry_id = entry_id self._bridge_name = bridge_name + self._entry_title = entry_title def pair(self, client_uuid, client_public): """Override super function to dismiss setup message if paired.""" @@ -506,10 +508,14 @@ class HomeDriver(AccessoryDriver): def unpair(self, client_uuid): """Override super function to show setup message if unpaired.""" super().unpair(client_uuid) + + if self.state.paired: + return + show_setup_message( self.hass, self._entry_id, - self._bridge_name, + accessory_friendly_name(self._entry_title, self.accessory), self.state.pincode, self.accessory.xhm_uri(), ) diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index d6278c0ca94..c21c27fba83 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -1,10 +1,13 @@ """Config flow for HomeKit integration.""" import random +import re import string import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_FRIENDLY_NAME, @@ -26,6 +29,7 @@ from homeassistant.helpers.entityfilter import ( from .const import ( CONF_AUTO_START, CONF_ENTITY_CONFIG, + CONF_EXCLUDE_ACCESSORY_MODE, CONF_FILTER, CONF_HOMEKIT_MODE, CONF_VIDEO_CODEC, @@ -33,13 +37,13 @@ from .const import ( DEFAULT_CONFIG_FLOW_PORT, DEFAULT_HOMEKIT_MODE, HOMEKIT_MODE_ACCESSORY, + HOMEKIT_MODE_BRIDGE, HOMEKIT_MODES, - SHORT_ACCESSORY_NAME, SHORT_BRIDGE_NAME, VIDEO_CODEC_COPY, ) from .const import DOMAIN # pylint:disable=unused-import -from .util import async_find_next_available_port +from .util import async_find_next_available_port, state_needs_accessory_mode CONF_CAMERA_COPY = "camera_copy" CONF_INCLUDE_EXCLUDE_MODE = "include_exclude_mode" @@ -49,11 +53,16 @@ MODE_EXCLUDE = "exclude" INCLUDE_EXCLUDE_MODES = [MODE_EXCLUDE, MODE_INCLUDE] +DOMAINS_NEED_ACCESSORY_MODE = [CAMERA_DOMAIN, MEDIA_PLAYER_DOMAIN] +NEVER_BRIDGED_DOMAINS = [CAMERA_DOMAIN] + +CAMERA_ENTITY_PREFIX = f"{CAMERA_DOMAIN}." + SUPPORTED_DOMAINS = [ "alarm_control_panel", "automation", "binary_sensor", - "camera", + CAMERA_DOMAIN, "climate", "cover", "demo", @@ -63,7 +72,7 @@ SUPPORTED_DOMAINS = [ "input_boolean", "light", "lock", - "media_player", + MEDIA_PLAYER_DOMAIN, "person", "remote", "scene", @@ -77,22 +86,18 @@ SUPPORTED_DOMAINS = [ DEFAULT_DOMAINS = [ "alarm_control_panel", "climate", + CAMERA_DOMAIN, "cover", "humidifier", "fan", "light", "lock", - "media_player", + MEDIA_PLAYER_DOMAIN, "switch", "vacuum", "water_heater", ] -DOMAINS_PREFER_ACCESSORY_MODE = ["camera", "media_player"] - -CAMERA_DOMAIN = "camera" -CAMERA_ENTITY_PREFIX = f"{CAMERA_DOMAIN}." - _EMPTY_ENTITY_FILTER = { CONF_INCLUDE_DOMAINS: [], CONF_EXCLUDE_DOMAINS: [], @@ -110,32 +115,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize config flow.""" self.hk_data = {} - self.entry_title = None - async def async_step_accessory_mode(self, user_input=None): - """Choose specific entity in accessory mode.""" - if user_input is not None: - entity_id = user_input[CONF_ENTITY_ID] - entity_filter = _EMPTY_ENTITY_FILTER.copy() - entity_filter[CONF_INCLUDE_ENTITIES] = [entity_id] - self.hk_data[CONF_FILTER] = entity_filter - if entity_id.startswith(CAMERA_ENTITY_PREFIX): - self.hk_data[CONF_ENTITY_CONFIG] = { - entity_id: {CONF_VIDEO_CODEC: VIDEO_CODEC_COPY} - } - return await self.async_step_pairing() - - all_supported_entities = _async_get_matching_entities( - self.hass, domains=DOMAINS_PREFER_ACCESSORY_MODE - ) - return self.async_show_form( - step_id="accessory_mode", - data_schema=vol.Schema( - {vol.Required(CONF_ENTITY_ID): vol.In(all_supported_entities)} - ), - ) - - async def async_step_bridge_mode(self, user_input=None): + async def async_step_user(self, user_input=None): """Choose specific domains in bridge mode.""" if user_input is not None: entity_filter = _EMPTY_ENTITY_FILTER.copy() @@ -143,9 +124,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.hk_data[CONF_FILTER] = entity_filter return await self.async_step_pairing() + self.hk_data[CONF_HOMEKIT_MODE] = HOMEKIT_MODE_BRIDGE default_domains = [] if self._async_current_names() else DEFAULT_DOMAINS return self.async_show_form( - step_id="bridge_mode", + step_id="user", data_schema=vol.Schema( { vol.Required( @@ -158,43 +140,72 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pairing(self, user_input=None): """Pairing instructions.""" if user_input is not None: - return self.async_create_entry(title=self.entry_title, data=self.hk_data) + port = await async_find_next_available_port( + self.hass, DEFAULT_CONFIG_FLOW_PORT + ) + await self._async_add_entries_for_accessory_mode_entities(port) + self.hk_data[CONF_PORT] = port + include_domains_filter = self.hk_data[CONF_FILTER][CONF_INCLUDE_DOMAINS] + for domain in NEVER_BRIDGED_DOMAINS: + if domain in include_domains_filter: + include_domains_filter.remove(domain) + return self.async_create_entry( + title=f"{self.hk_data[CONF_NAME]}:{self.hk_data[CONF_PORT]}", + data=self.hk_data, + ) - self.hk_data[CONF_PORT] = await async_find_next_available_port( - self.hass, DEFAULT_CONFIG_FLOW_PORT - ) - self.hk_data[CONF_NAME] = self._async_available_name( - self.hk_data[CONF_HOMEKIT_MODE] - ) - self.entry_title = f"{self.hk_data[CONF_NAME]}:{self.hk_data[CONF_PORT]}" + self.hk_data[CONF_NAME] = self._async_available_name(SHORT_BRIDGE_NAME) + self.hk_data[CONF_EXCLUDE_ACCESSORY_MODE] = True return self.async_show_form( step_id="pairing", description_placeholders={CONF_NAME: self.hk_data[CONF_NAME]}, ) - async def async_step_user(self, user_input=None): - """Handle the initial step.""" - errors = {} + async def _async_add_entries_for_accessory_mode_entities(self, last_assigned_port): + """Generate new flows for entities that need their own instances.""" + accessory_mode_entity_ids = _async_get_entity_ids_for_accessory_mode( + self.hass, self.hk_data[CONF_FILTER][CONF_INCLUDE_DOMAINS] + ) + exiting_entity_ids_accessory_mode = _async_entity_ids_with_accessory_mode( + self.hass + ) + next_port_to_check = last_assigned_port + 1 + for entity_id in accessory_mode_entity_ids: + if entity_id in exiting_entity_ids_accessory_mode: + continue + port = await async_find_next_available_port(self.hass, next_port_to_check) + next_port_to_check = port + 1 + self.hass.async_create_task( + self.hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "accessory"}, + data={CONF_ENTITY_ID: entity_id, CONF_PORT: port}, + ) + ) - if user_input is not None: - self.hk_data = { - CONF_HOMEKIT_MODE: user_input[CONF_HOMEKIT_MODE], + async def async_step_accessory(self, accessory_input): + """Handle creation a single accessory in accessory mode.""" + entity_id = accessory_input[CONF_ENTITY_ID] + port = accessory_input[CONF_PORT] + + state = self.hass.states.get(entity_id) + name = state.attributes.get(ATTR_FRIENDLY_NAME) or state.entity_id + entity_filter = _EMPTY_ENTITY_FILTER.copy() + entity_filter[CONF_INCLUDE_ENTITIES] = [entity_id] + + entry_data = { + CONF_PORT: port, + CONF_NAME: self._async_available_name(name), + CONF_HOMEKIT_MODE: HOMEKIT_MODE_ACCESSORY, + CONF_FILTER: entity_filter, + } + if entity_id.startswith(CAMERA_ENTITY_PREFIX): + entry_data[CONF_ENTITY_CONFIG] = { + entity_id: {CONF_VIDEO_CODEC: VIDEO_CODEC_COPY} } - if user_input[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_ACCESSORY: - return await self.async_step_accessory_mode() - return await self.async_step_bridge_mode() - homekit_mode = self.hk_data.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE) - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required(CONF_HOMEKIT_MODE, default=homekit_mode): vol.In( - HOMEKIT_MODES - ) - } - ), - errors=errors, + return self.async_create_entry( + title=f"{name}:{entry_data[CONF_PORT]}", data=entry_data ) async def async_step_import(self, user_input=None): @@ -215,21 +226,19 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } @callback - def _async_available_name(self, homekit_mode): + def _async_available_name(self, requested_name): """Return an available for the bridge.""" + current_names = self._async_current_names() + valid_mdns_name = re.sub("[^A-Za-z0-9 ]+", " ", requested_name) - base_name = SHORT_BRIDGE_NAME - if homekit_mode == HOMEKIT_MODE_ACCESSORY: - base_name = SHORT_ACCESSORY_NAME + if valid_mdns_name not in current_names: + return valid_mdns_name - # We always pick a RANDOM name to avoid Zeroconf - # name collisions. If the name has been seen before - # pairing will probably fail. - acceptable_chars = string.ascii_uppercase + string.digits + acceptable_mdns_chars = string.ascii_uppercase + string.digits suggested_name = None - while not suggested_name or suggested_name in self._async_current_names(): - trailer = "".join(random.choices(acceptable_chars, k=4)) - suggested_name = f"{base_name} {trailer}" + while not suggested_name or suggested_name in current_names: + trailer = "".join(random.choices(acceptable_mdns_chars, k=2)) + suggested_name = f"{valid_mdns_name} {trailer}" return suggested_name @@ -447,7 +456,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): def _async_get_matching_entities(hass, domains=None): """Fetch all entities or entities in the given domains.""" return { - state.entity_id: f"{state.entity_id} ({state.attributes.get(ATTR_FRIENDLY_NAME, state.entity_id)})" + state.entity_id: f"{state.attributes.get(ATTR_FRIENDLY_NAME, state.entity_id)} ({state.entity_id})" for state in sorted( hass.states.async_all(domains and set(domains)), key=lambda item: item.entity_id, @@ -457,7 +466,41 @@ def _async_get_matching_entities(hass, domains=None): def _domains_set_from_entities(entity_ids): """Build a set of domains for the given entity ids.""" - domains = set() - for entity_id in entity_ids: - domains.add(split_entity_id(entity_id)[0]) - return domains + return {split_entity_id(entity_id)[0] for entity_id in entity_ids} + + +@callback +def _async_get_entity_ids_for_accessory_mode(hass, include_domains): + """Build a list of entities that should be paired in accessory mode.""" + accessory_mode_domains = { + domain for domain in include_domains if domain in DOMAINS_NEED_ACCESSORY_MODE + } + + if not accessory_mode_domains: + return [] + + return [ + state.entity_id + for state in hass.states.async_all(accessory_mode_domains) + if state_needs_accessory_mode(state) + ] + + +@callback +def _async_entity_ids_with_accessory_mode(hass): + """Return a set of entity ids that have config entries in accessory mode.""" + + entity_ids = set() + + current_entries = hass.config_entries.async_entries(DOMAIN) + for entry in current_entries: + # We have to handle the case where the data has not yet + # been migrated to options because the data was just + # imported and the entry was never started + target = entry.options if CONF_HOMEKIT_MODE in entry.options else entry.data + if target.get(CONF_HOMEKIT_MODE) != HOMEKIT_MODE_ACCESSORY: + continue + + entity_ids.add(target[CONF_FILTER][CONF_INCLUDE_ENTITIES][0]) + + return entity_ids diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index fac4168a79b..67312903b50 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -42,6 +42,7 @@ CONF_ENTITY_CONFIG = "entity_config" CONF_FEATURE = "feature" CONF_FEATURE_LIST = "feature_list" CONF_FILTER = "filter" +CONF_EXCLUDE_ACCESSORY_MODE = "exclude_accessory_mode" CONF_LINKED_BATTERY_SENSOR = "linked_battery_sensor" CONF_LINKED_BATTERY_CHARGING_SENSOR = "linked_battery_charging_sensor" CONF_LINKED_DOORBELL_SENSOR = "linked_doorbell_sensor" @@ -68,6 +69,7 @@ DEFAULT_AUDIO_CODEC = AUDIO_CODEC_OPUS DEFAULT_AUDIO_MAP = "0:a:0" DEFAULT_AUDIO_PACKET_SIZE = 188 DEFAULT_AUTO_START = True +DEFAULT_EXCLUDE_ACCESSORY_MODE = False DEFAULT_LOW_BATTERY_THRESHOLD = 20 DEFAULT_MAX_FPS = 30 DEFAULT_MAX_HEIGHT = 1080 diff --git a/homeassistant/components/homekit/strings.json b/homeassistant/components/homekit/strings.json index ed825ada23c..a9b7c1c6cc1 100644 --- a/homeassistant/components/homekit/strings.json +++ b/homeassistant/components/homekit/strings.json @@ -8,7 +8,7 @@ "init": { "data": { "mode": "[%key:common::config_flow::data::mode%]", - "include_domains": "[%key:component::homekit::config::step::bridge_mode::data::include_domains%]" + "include_domains": "[%key:component::homekit::config::step::user::data::include_domains%]" }, "description": "HomeKit can be configured expose a bridge or a single accessory. In accessory mode, only a single entity can be used. Accessory mode is required for media players with the TV device class to function properly. Entities in the \u201cDomains to include\u201d will be included to HomeKit. You will be able to select which entities to include or exclude from this list on the next screen.", "title": "Select domains to be included." @@ -18,7 +18,7 @@ "mode": "[%key:common::config_flow::data::mode%]", "entities": "Entities" }, - "description": "Choose the entities to be included. In accessory mode, only a single entity is included. In bridge include mode, all entities in the domain will be included unless specific entities are selected. In bridge exclude mode, all entities in the domain will be included except for the excluded entities. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.", + "description": "Choose the entities to be included. In accessory mode, only a single entity is included. In bridge include mode, all entities in the domain will be included unless specific entities are selected. In bridge exclude mode, all entities in the domain will be included except for the excluded entities. For best performance, a seperate HomeKit accessory will beeach tv media player and camera.", "title": "Select entities to be included" }, "cameras": { @@ -40,29 +40,15 @@ "config": { "step": { "user": { - "data": { - "mode": "[%key:common::config_flow::data::mode%]" - }, - "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 best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.", - "title": "Activate HomeKit" - }, - "accessory_mode": { - "data": { - "entity_id": "Entity" - }, - "description": "Choose the entity to be included. In accessory mode, only a single entity is included.", - "title": "Select entity to be included" - }, - "bridge_mode": { "data": { "include_domains": "Domains to include" }, - "description": "Choose the domains to be included. All supported entities in the domain will be included.", + "description": "Choose the domains to be included. All supported entities in the domain will be included. A separate HomeKit instance in accessory mode will be created for each tv media player and camera.", "title": "Select domains to be included" }, "pairing": { "title": "Pair HomeKit", - "description": "As soon as the {name} is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d." + "description": "To complete pairing following the instructions in \u201cNotifications\u201d under \u201cHomeKit Pairing\u201d." } }, "abort": { diff --git a/homeassistant/components/homekit/translations/en.json b/homeassistant/components/homekit/translations/en.json index db0656c0450..e9aaeb60df8 100644 --- a/homeassistant/components/homekit/translations/en.json +++ b/homeassistant/components/homekit/translations/en.json @@ -4,32 +4,16 @@ "port_name_in_use": "An accessory or bridge with the same name or port is already configured." }, "step": { - "accessory_mode": { - "data": { - "entity_id": "Entity" - }, - "description": "Choose the entity to be included. In accessory mode, only a single entity is included.", - "title": "Select entity to be included" - }, - "bridge_mode": { - "data": { - "include_domains": "Domains to include" - }, - "description": "Choose the domains to be included. All supported entities in the domain will be included.", - "title": "Select domains to be included" - }, "pairing": { - "description": "As soon as the {name} is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d.", + "description": "To complete pairing following the instructions in \u201cNotifications\u201d under \u201cHomeKit Pairing\u201d.", "title": "Pair HomeKit" }, "user": { "data": { - "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", - "include_domains": "Domains to include", - "mode": "Mode" + "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 best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.", - "title": "Activate HomeKit" + "description": "Choose the domains to be included. All supported entities in the domain will be included. A separate HomeKit instance in accessory mode will be created for each tv media player and camera.", + "title": "Select domains to be included" } } }, @@ -37,8 +21,7 @@ "step": { "advanced": { "data": { - "auto_start": "Autostart (disable if you are calling the homekit.start service manually)", - "safe_mode": "Safe Mode (enable only if pairing fails)" + "auto_start": "Autostart (disable if you are calling the homekit.start service manually)" }, "description": "These settings only need to be adjusted if HomeKit is not functional.", "title": "Advanced Configuration" @@ -55,7 +38,7 @@ "entities": "Entities", "mode": "Mode" }, - "description": "Choose the entities to be included. In accessory mode, only a single entity is included. In bridge include mode, all entities in the domain will be included unless specific entities are selected. In bridge exclude mode, all entities in the domain will be included except for the excluded entities. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.", + "description": "Choose the entities to be included. In accessory mode, only a single entity is included. In bridge include mode, all entities in the domain will be included unless specific entities are selected. In bridge exclude mode, all entities in the domain will be included except for the excluded entities. For best performance, a seperate HomeKit accessory will beeach tv media player and camera.", "title": "Select entities to be included" }, "init": { diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index c23b8c1baaf..46b893bb96d 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -11,8 +11,14 @@ import pyqrcode import voluptuous as vol from homeassistant.components import binary_sensor, media_player, sensor +from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.media_player import ( + DEVICE_CLASS_TV, + DOMAIN as MEDIA_PLAYER_DOMAIN, +) from homeassistant.const import ( ATTR_CODE, + ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_PORT, @@ -328,9 +334,7 @@ def show_setup_message(hass, entry_id, bridge_name, pincode, uri): f"### {pin}\n" f"![image](/api/homekit/pairingqr?{entry_id}-{pairing_secret})" ) - hass.components.persistent_notification.create( - message, "HomeKit Bridge Setup", entry_id - ) + hass.components.persistent_notification.create(message, "HomeKit Pairing", entry_id) def dismiss_setup_message(hass, entry_id): @@ -473,3 +477,30 @@ def pid_is_alive(pid) -> bool: except OSError: pass return False + + +def accessory_friendly_name(hass_name, accessory): + """Return the combined name for the accessory. + + The mDNS name and the Home Assistant config entry + name are usually different which means they need to + see both to identify the accessory. + """ + accessory_mdns_name = accessory.display_name + if hass_name.startswith(accessory_mdns_name): + return hass_name + return f"{hass_name} ({accessory_mdns_name})" + + +def state_needs_accessory_mode(state): + """Return if the entity represented by the state must be paired in accessory mode.""" + if state.domain == CAMERA_DOMAIN: + return True + + if ( + state.domain == MEDIA_PLAYER_DOMAIN + and state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TV + ): + return True + + return False diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index e308ed21537..afaa9ea0892 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -603,13 +603,19 @@ def test_home_driver(): with patch("pyhap.accessory_driver.AccessoryDriver.__init__") as mock_driver: driver = HomeDriver( - "hass", "entry_id", "name", address=ip_address, port=port, persist_file=path + "hass", + "entry_id", + "name", + "title", + address=ip_address, + port=port, + persist_file=path, ) mock_driver.assert_called_with(address=ip_address, port=port, persist_file=path) - driver.state = Mock(pincode=pin) + driver.state = Mock(pincode=pin, paired=False) xhm_uri_mock = Mock(return_value="X-HM://0") - driver.accessory = Mock(xhm_uri=xhm_uri_mock) + driver.accessory = Mock(display_name="any", xhm_uri=xhm_uri_mock) # pair with patch("pyhap.accessory_driver.AccessoryDriver.pair") as mock_pair, patch( @@ -627,4 +633,4 @@ def test_home_driver(): driver.unpair("client_uuid") mock_unpair.assert_called_with("client_uuid") - mock_show_msg.assert_called_with("hass", "entry_id", "name", pin, "X-HM://0") + mock_show_msg.assert_called_with("hass", "entry_id", "title (any)", pin, "X-HM://0") diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index 34c7ab2ecc0..3d94672cd8a 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.homekit.const import DOMAIN +from homeassistant.components.homekit.const import DOMAIN, SHORT_BRIDGE_NAME from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_NAME, CONF_PORT @@ -39,48 +39,41 @@ async def test_setup_in_bridge_mode(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" - assert result["errors"] == {} + assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"mode": "bridge"}, + {"include_domains": ["light"]}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["step_id"] == "bridge_mode" + assert result2["step_id"] == "pairing" with patch( "homeassistant.components.homekit.config_flow.async_find_next_available_port", return_value=12345, - ): - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - {"include_domains": ["light"]}, - ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result3["step_id"] == "pairing" - - with patch( + ), patch( "homeassistant.components.homekit.async_setup", return_value=True ) as mock_setup, patch( "homeassistant.components.homekit.async_setup_entry", return_value=True, ) as mock_setup_entry: - result4 = await hass.config_entries.flow.async_configure( - result3["flow_id"], + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], {}, ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result4["title"][:11] == "HASS Bridge" - bridge_name = (result4["title"].split(":"))[0] - assert result4["data"] == { + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + bridge_name = (result3["title"].split(":"))[0] + assert bridge_name == SHORT_BRIDGE_NAME + assert result3["data"] == { "filter": { "exclude_domains": [], "exclude_entities": [], "include_domains": ["light"], "include_entities": [], }, + "exclude_accessory_mode": True, "mode": "bridge", "name": bridge_name, "port": 12345, @@ -89,64 +82,147 @@ async def test_setup_in_bridge_mode(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_setup_in_accessory_mode(hass): - """Test we can setup a new instance in accessory.""" +async def test_setup_in_bridge_mode_name_taken(hass): + """Test we can setup a new instance in bridge mode when the name is taken.""" await setup.async_setup_component(hass, "persistent_notification", {}) - hass.states.async_set("camera.mine", "off") + + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: SHORT_BRIDGE_NAME, CONF_PORT: 8000}, + ) + entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" - assert result["errors"] == {} + assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"mode": "accessory"}, + {"include_domains": ["light"]}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["step_id"] == "accessory_mode" + assert result2["step_id"] == "pairing" with patch( "homeassistant.components.homekit.config_flow.async_find_next_available_port", return_value=12345, - ): - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - {"entity_id": "camera.mine"}, - ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result3["step_id"] == "pairing" - - with patch( + ), patch( "homeassistant.components.homekit.async_setup", return_value=True ) as mock_setup, patch( "homeassistant.components.homekit.async_setup_entry", return_value=True, ) as mock_setup_entry: - result4 = await hass.config_entries.flow.async_configure( - result3["flow_id"], + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], {}, ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result4["title"][:14] == "HASS Accessory" - bridge_name = (result4["title"].split(":"))[0] - assert result4["data"] == { + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["title"] != SHORT_BRIDGE_NAME + assert result3["title"].startswith(SHORT_BRIDGE_NAME) + bridge_name = (result3["title"].split(":"))[0] + assert result3["data"] == { "filter": { "exclude_domains": [], "exclude_entities": [], - "include_domains": [], - "include_entities": ["camera.mine"], + "include_domains": ["light"], + "include_entities": [], }, - "mode": "accessory", + "exclude_accessory_mode": True, + "mode": "bridge", "name": bridge_name, - "entity_config": {"camera.mine": {"video_codec": "copy"}}, "port": 12345, } assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 2 + + +async def test_setup_creates_entries_for_accessory_mode_devices(hass): + """Test we can setup a new instance and we create entries for accessory mode devices.""" + hass.states.async_set("camera.one", "on") + hass.states.async_set("camera.existing", "on") + hass.states.async_set("media_player.two", "on", {"device_class": "tv"}) + + bridge_mode_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "bridge", CONF_PORT: 8001}, + options={ + "mode": "bridge", + "filter": { + "include_entities": ["camera.existing"], + }, + }, + ) + bridge_mode_entry.add_to_hass(hass) + accessory_mode_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "accessory", CONF_PORT: 8000}, + options={ + "mode": "accessory", + "filter": { + "include_entities": ["camera.existing"], + }, + }, + ) + accessory_mode_entry.add_to_hass(hass) + + 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 + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"include_domains": ["camera", "media_player", "light"]}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "pairing" + + with patch( + "homeassistant.components.homekit.config_flow.async_find_next_available_port", + return_value=12345, + ), patch( + "homeassistant.components.homekit.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.homekit.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["title"][:11] == "HASS Bridge" + bridge_name = (result3["title"].split(":"))[0] + assert result3["data"] == { + "filter": { + "exclude_domains": [], + "exclude_entities": [], + "include_domains": ["media_player", "light"], + "include_entities": [], + }, + "exclude_accessory_mode": True, + "mode": "bridge", + "name": bridge_name, + "port": 12345, + } + assert len(mock_setup.mock_calls) == 1 + # + # Existing accessory mode entries should get setup but not duplicated + # + # 1 - existing accessory for camera.existing + # 2 - existing bridge for camera.one + # 3 - new bridge + # 4 - camera.one in accessory mode + # 5 - media_player.two in accessory mode + assert len(mock_setup_entry.mock_calls) == 5 async def test_import(hass): @@ -656,55 +732,48 @@ async def test_converting_bridge_to_accessory_mode(hass, hk_driver): DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" - assert result["errors"] == {} + assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"mode": "bridge"}, + {"include_domains": ["light"]}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["step_id"] == "bridge_mode" - - with patch( - "homeassistant.components.homekit.config_flow.async_find_next_available_port", - return_value=12345, - ): - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - {"include_domains": ["light"]}, - ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result3["step_id"] == "pairing" + assert result2["step_id"] == "pairing" # We need to actually setup the config entry or the data # will not get migrated to options with patch( + "homeassistant.components.homekit.config_flow.async_find_next_available_port", + return_value=12345, + ), patch( "homeassistant.components.homekit.HomeKit.async_start", return_value=True, ) as mock_async_start: - result4 = await hass.config_entries.flow.async_configure( - result3["flow_id"], + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], {}, ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result4["title"][:11] == "HASS Bridge" - bridge_name = (result4["title"].split(":"))[0] - assert result4["data"] == { + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["title"][:11] == "HASS Bridge" + bridge_name = (result3["title"].split(":"))[0] + assert result3["data"] == { "filter": { "exclude_domains": [], "exclude_entities": [], "include_domains": ["light"], "include_entities": [], }, + "exclude_accessory_mode": True, "mode": "bridge", "name": bridge_name, "port": 12345, } assert len(mock_async_start.mock_calls) == 1 - config_entry = result4["result"] + config_entry = result3["result"] hass.states.async_set("camera.tv", "off") hass.states.async_set("camera.sonos", "off") diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index b0213ee7e8b..ec324602684 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -82,6 +82,22 @@ def entity_reg_fixture(hass): return mock_registry(hass) +def _mock_homekit(hass, entry, homekit_mode, entity_filter=None): + return HomeKit( + hass=hass, + name=BRIDGE_NAME, + port=DEFAULT_PORT, + ip_address=None, + entity_filter=entity_filter or generate_filter([], [], [], []), + exclude_accessory_mode=False, + entity_config={}, + homekit_mode=homekit_mode, + advertise_ip=None, + entry_id=entry.entry_id, + entry_title=entry.title, + ) + + async def test_setup_min(hass, mock_zeroconf): """Test async_setup with min config options.""" entry = MockConfigEntry( @@ -103,10 +119,12 @@ async def test_setup_min(hass, mock_zeroconf): DEFAULT_PORT, None, ANY, + ANY, {}, HOMEKIT_MODE_BRIDGE, None, entry.entry_id, + entry.title, ) assert mock_homekit().setup.called is True @@ -139,10 +157,12 @@ async def test_setup_auto_start_disabled(hass, mock_zeroconf): 11111, "172.0.0.0", ANY, + ANY, {}, HOMEKIT_MODE_BRIDGE, None, entry.entry_id, + entry.title, ) assert mock_homekit().setup.called is True @@ -184,11 +204,13 @@ async def test_homekit_setup(hass, hk_driver, mock_zeroconf): BRIDGE_NAME, DEFAULT_PORT, None, + True, {}, {}, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, + entry_title=entry.title, ) hass.states.async_set("light.demo", "on") @@ -205,6 +227,7 @@ async def test_homekit_setup(hass, hk_driver, mock_zeroconf): hass, entry.entry_id, BRIDGE_NAME, + entry.title, loop=hass.loop, address=IP_ADDRESS, port=DEFAULT_PORT, @@ -230,11 +253,13 @@ async def test_homekit_setup_ip_address(hass, hk_driver, mock_zeroconf): BRIDGE_NAME, DEFAULT_PORT, "172.0.0.0", + True, {}, {}, HOMEKIT_MODE_BRIDGE, None, entry_id=entry.entry_id, + entry_title=entry.title, ) mock_zeroconf = MagicMock() @@ -245,6 +270,7 @@ async def test_homekit_setup_ip_address(hass, hk_driver, mock_zeroconf): hass, entry.entry_id, BRIDGE_NAME, + entry.title, loop=hass.loop, address="172.0.0.0", port=DEFAULT_PORT, @@ -266,11 +292,13 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_zeroconf): BRIDGE_NAME, DEFAULT_PORT, "0.0.0.0", + True, {}, {}, HOMEKIT_MODE_BRIDGE, "192.168.1.100", entry_id=entry.entry_id, + entry_title=entry.title, ) zeroconf_instance = MagicMock() @@ -281,6 +309,7 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_zeroconf): hass, entry.entry_id, BRIDGE_NAME, + entry.title, loop=hass.loop, address="0.0.0.0", port=DEFAULT_PORT, @@ -292,40 +321,40 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_zeroconf): async def test_homekit_add_accessory(hass, mock_zeroconf): """Add accessory if config exists and get_acc returns an accessory.""" - entry = await async_init_integration(hass) - homekit = HomeKit( - hass, - None, - None, - None, - lambda entity_id: True, - {}, - HOMEKIT_MODE_BRIDGE, - advertise_ip=None, - entry_id=entry.entry_id, + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) + entry.add_to_hass(hass) + + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) homekit.driver = "driver" homekit.bridge = mock_bridge = Mock() homekit.bridge.accessories = range(10) + homekit.async_start = AsyncMock() + + with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() mock_acc = Mock(category="any") - await async_init_integration(hass) - with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc: mock_get_acc.side_effect = [None, mock_acc, None] - homekit.add_bridge_accessory(State("light.demo", "on")) - mock_get_acc.assert_called_with(hass, "driver", ANY, 1403373688, {}) + state = State("light.demo", "on") + homekit.add_bridge_accessory(state) + mock_get_acc.assert_called_with(hass, ANY, ANY, 1403373688, {}) assert not mock_bridge.add_accessory.called - homekit.add_bridge_accessory(State("demo.test", "on")) - mock_get_acc.assert_called_with(hass, "driver", ANY, 600325356, {}) + state = State("demo.test", "on") + homekit.add_bridge_accessory(state) + mock_get_acc.assert_called_with(hass, ANY, ANY, 600325356, {}) assert mock_bridge.add_accessory.called - homekit.add_bridge_accessory(State("demo.test_2", "on")) - mock_get_acc.assert_called_with(hass, "driver", ANY, 1467253281, {}) - mock_bridge.add_accessory.assert_called_with(mock_acc) + state = State("demo.test_2", "on") + homekit.add_bridge_accessory(state) + mock_get_acc.assert_called_with(hass, ANY, ANY, 1467253281, {}) + assert mock_bridge.add_accessory.called @pytest.mark.parametrize("acc_category", [CATEGORY_TELEVISION, CATEGORY_CAMERA]) @@ -333,37 +362,30 @@ async def test_homekit_warn_add_accessory_bridge( hass, acc_category, mock_zeroconf, caplog ): """Test we warn when adding cameras or tvs to a bridge.""" - entry = await async_init_integration(hass) - - homekit = HomeKit( - hass, - None, - None, - None, - lambda entity_id: True, - {}, - HOMEKIT_MODE_BRIDGE, - advertise_ip=None, - entry_id=entry.entry_id, + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) + entry.add_to_hass(hass) + + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) homekit.driver = "driver" homekit.bridge = mock_bridge = Mock() homekit.bridge.accessories = range(10) + homekit.async_start = AsyncMock() + + with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() mock_camera_acc = Mock(category=acc_category) - await async_init_integration(hass) - with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc: mock_get_acc.side_effect = [None, mock_camera_acc, None] - homekit.add_bridge_accessory(State("light.demo", "on")) - mock_get_acc.assert_called_with(hass, "driver", ANY, 1403373688, {}) + state = State("camera.test", "on") + homekit.add_bridge_accessory(state) + mock_get_acc.assert_called_with(hass, ANY, ANY, 1508819236, {}) assert not mock_bridge.add_accessory.called - homekit.add_bridge_accessory(State("camera.test", "on")) - mock_get_acc.assert_called_with(hass, "driver", ANY, 1508819236, {}) - assert mock_bridge.add_accessory.called - assert "accessory mode" in caplog.text @@ -371,17 +393,8 @@ async def test_homekit_remove_accessory(hass, mock_zeroconf): """Remove accessory from bridge.""" entry = await async_init_integration(hass) - homekit = HomeKit( - hass, - None, - None, - None, - lambda entity_id: True, - {}, - HOMEKIT_MODE_BRIDGE, - advertise_ip=None, - entry_id=entry.entry_id, - ) + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) + homekit.driver = "driver" homekit.bridge = mock_bridge = Mock() mock_bridge.accessories = {"light.demo": "acc"} @@ -396,17 +409,8 @@ async def test_homekit_entity_filter(hass, mock_zeroconf): entry = await async_init_integration(hass) entity_filter = generate_filter(["cover"], ["demo.test"], [], []) - homekit = HomeKit( - hass, - None, - None, - None, - entity_filter, - {}, - HOMEKIT_MODE_BRIDGE, - advertise_ip=None, - entry_id=entry.entry_id, - ) + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE, entity_filter) + homekit.bridge = Mock() homekit.bridge.accessories = {} @@ -432,17 +436,8 @@ async def test_homekit_entity_glob_filter(hass, mock_zeroconf): entity_filter = generate_filter( ["cover"], ["demo.test"], [], [], ["*.included_*"], ["*.excluded_*"] ) - homekit = HomeKit( - hass, - None, - None, - None, - entity_filter, - {}, - HOMEKIT_MODE_BRIDGE, - advertise_ip=None, - entry_id=entry.entry_id, - ) + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE, entity_filter) + homekit.bridge = Mock() homekit.bridge.accessories = {} @@ -471,17 +466,8 @@ async def test_homekit_start(hass, hk_driver, device_reg): entry = await async_init_integration(hass) pin = b"123-45-678" - homekit = HomeKit( - hass, - None, - None, - None, - {}, - {}, - HOMEKIT_MODE_BRIDGE, - advertise_ip=None, - entry_id=entry.entry_id, - ) + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) + homekit.bridge = Mock() homekit.bridge.accessories = [] homekit.driver = hk_driver @@ -513,7 +499,9 @@ async def test_homekit_start(hass, hk_driver, device_reg): await hass.async_block_till_done() mock_add_acc.assert_any_call(state) - mock_setup_msg.assert_called_with(hass, entry.entry_id, None, pin, ANY) + mock_setup_msg.assert_called_with( + hass, entry.entry_id, "Mock Title (any)", pin, ANY + ) hk_driver_add_acc.assert_called_with(homekit.bridge) assert hk_driver_start.called assert homekit.status == STATUS_RUNNING @@ -563,17 +551,7 @@ async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, mock_zeroc entity_filter = generate_filter(["cover", "light"], ["demo.test"], [], []) await async_init_entry(hass, entry) - homekit = HomeKit( - hass, - None, - None, - None, - entity_filter, - {}, - HOMEKIT_MODE_BRIDGE, - advertise_ip=None, - entry_id=entry.entry_id, - ) + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE, entity_filter) homekit.bridge = Mock() homekit.bridge.accessories = [] @@ -593,7 +571,9 @@ async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, mock_zeroc await homekit.async_start() await hass.async_block_till_done() - mock_setup_msg.assert_called_with(hass, entry.entry_id, None, pin, ANY) + mock_setup_msg.assert_called_with( + hass, entry.entry_id, "Mock Title (any)", pin, ANY + ) hk_driver_add_acc.assert_called_with(homekit.bridge) assert hk_driver_start.called assert homekit.status == STATUS_RUNNING @@ -608,18 +588,8 @@ async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, mock_zeroc async def test_homekit_stop(hass): """Test HomeKit stop method.""" entry = await async_init_integration(hass) + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) - homekit = HomeKit( - hass, - None, - None, - None, - {}, - {}, - HOMEKIT_MODE_BRIDGE, - advertise_ip=None, - entry_id=entry.entry_id, - ) homekit.driver = Mock() homekit.driver.async_stop = AsyncMock() homekit.bridge = Mock() @@ -649,17 +619,8 @@ async def test_homekit_reset_accessories(hass, mock_zeroconf): domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) entity_id = "light.demo" - homekit = HomeKit( - hass, - None, - None, - None, - {}, - {entity_id: {}}, - HOMEKIT_MODE_BRIDGE, - advertise_ip=None, - entry_id=entry.entry_id, - ) + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) + homekit.bridge = Mock() homekit.bridge.accessories = {} @@ -697,17 +658,7 @@ async def test_homekit_too_many_accessories(hass, hk_driver, caplog, mock_zeroco entity_filter = generate_filter(["cover", "light"], ["demo.test"], [], []) - homekit = HomeKit( - hass, - None, - None, - None, - entity_filter, - {}, - HOMEKIT_MODE_BRIDGE, - advertise_ip=None, - entry_id=entry.entry_id, - ) + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE, entity_filter) def _mock_bridge(*_): mock_bridge = HomeBridge(hass, hk_driver, "mock_bridge") @@ -738,17 +689,8 @@ async def test_homekit_finds_linked_batteries( """Test HomeKit start method.""" entry = await async_init_integration(hass) - homekit = HomeKit( - hass, - None, - None, - None, - {}, - {"light.demo": {}}, - HOMEKIT_MODE_BRIDGE, - advertise_ip=None, - entry_id=entry.entry_id, - ) + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) + homekit.driver = hk_driver # pylint: disable=protected-access homekit._filter = Mock(return_value=True) @@ -792,9 +734,6 @@ async def test_homekit_finds_linked_batteries( ) hass.states.async_set(light.entity_id, STATE_ON) - def _mock_get_accessory(*args, **kwargs): - return [None, "acc", None] - with patch.object(homekit.bridge, "add_accessory"), patch( f"{PATH_HOMEKIT}.show_setup_message" ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( @@ -823,18 +762,8 @@ async def test_homekit_async_get_integration_fails( ): """Test that we continue if async_get_integration fails.""" entry = await async_init_integration(hass) + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) - homekit = HomeKit( - hass, - None, - None, - None, - {}, - {"light.demo": {}}, - HOMEKIT_MODE_BRIDGE, - advertise_ip=None, - entry_id=entry.entry_id, - ) homekit.driver = hk_driver # pylint: disable=protected-access homekit._filter = Mock(return_value=True) @@ -877,9 +806,6 @@ async def test_homekit_async_get_integration_fails( ) hass.states.async_set(light.entity_id, STATE_ON) - def _mock_get_accessory(*args, **kwargs): - return [None, "acc", None] - with patch.object(homekit.bridge, "add_accessory"), patch( f"{PATH_HOMEKIT}.show_setup_message" ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( @@ -927,10 +853,12 @@ async def test_yaml_updates_update_config_entry_for_name(hass, mock_zeroconf): 12345, None, ANY, + ANY, {}, HOMEKIT_MODE_BRIDGE, None, entry.entry_id, + entry.title, ) assert mock_homekit().setup.called is True @@ -989,18 +917,8 @@ async def test_homekit_ignored_missing_devices( ): """Test HomeKit handles a device in the entity registry but missing from the device registry.""" entry = await async_init_integration(hass) + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) - homekit = HomeKit( - hass, - None, - None, - None, - {}, - {"light.demo": {}}, - HOMEKIT_MODE_BRIDGE, - advertise_ip=None, - entry_id=entry.entry_id, - ) homekit.driver = hk_driver # pylint: disable=protected-access homekit._filter = Mock(return_value=True) @@ -1041,9 +959,6 @@ async def test_homekit_ignored_missing_devices( hass.states.async_set(light.entity_id, STATE_ON) hass.states.async_set("light.two", STATE_ON) - def _mock_get_accessory(*args, **kwargs): - return [None, "acc", None] - with patch.object(homekit.bridge, "add_accessory"), patch( f"{PATH_HOMEKIT}.show_setup_message" ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( @@ -1071,17 +986,8 @@ async def test_homekit_finds_linked_motion_sensors( """Test HomeKit start method.""" entry = await async_init_integration(hass) - homekit = HomeKit( - hass, - None, - None, - None, - {}, - {"camera.camera_demo": {}}, - HOMEKIT_MODE_BRIDGE, - advertise_ip=None, - entry_id=entry.entry_id, - ) + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) + homekit.driver = hk_driver # pylint: disable=protected-access homekit._filter = Mock(return_value=True) @@ -1115,9 +1021,6 @@ async def test_homekit_finds_linked_motion_sensors( ) hass.states.async_set(camera.entity_id, STATE_ON) - def _mock_get_accessory(*args, **kwargs): - return [None, "acc", None] - with patch.object(homekit.bridge, "add_accessory"), patch( f"{PATH_HOMEKIT}.show_setup_message" ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( @@ -1146,17 +1049,8 @@ async def test_homekit_finds_linked_humidity_sensors( """Test HomeKit start method.""" entry = await async_init_integration(hass) - homekit = HomeKit( - hass, - None, - None, - None, - {}, - {"humidifier.humidifier": {}}, - HOMEKIT_MODE_BRIDGE, - advertise_ip=None, - entry_id=entry.entry_id, - ) + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) + homekit.driver = hk_driver homekit._filter = Mock(return_value=True) homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge") @@ -1192,9 +1086,6 @@ async def test_homekit_finds_linked_humidity_sensors( ) hass.states.async_set(humidifier.entity_id, STATE_ON) - def _mock_get_accessory(*args, **kwargs): - return [None, "acc", None] - with patch.object(homekit.bridge, "add_accessory"), patch( f"{PATH_HOMEKIT}.show_setup_message" ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( @@ -1241,10 +1132,12 @@ async def test_reload(hass, mock_zeroconf): 12345, None, ANY, + False, {}, HOMEKIT_MODE_BRIDGE, None, entry.entry_id, + entry.title, ) assert mock_homekit().setup.called is True yaml_path = os.path.join( @@ -1277,10 +1170,12 @@ async def test_reload(hass, mock_zeroconf): 45678, None, ANY, + False, {}, HOMEKIT_MODE_BRIDGE, None, entry.entry_id, + entry.title, ) assert mock_homekit2().setup.called is True @@ -1294,17 +1189,9 @@ async def test_homekit_start_in_accessory_mode(hass, hk_driver, device_reg): entry = await async_init_integration(hass) pin = b"123-45-678" - homekit = HomeKit( - hass, - None, - None, - None, - {}, - {}, - HOMEKIT_MODE_ACCESSORY, - advertise_ip=None, - entry_id=entry.entry_id, - ) + + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_ACCESSORY) + homekit.bridge = Mock() homekit.bridge.accessories = [] homekit.driver = hk_driver @@ -1323,6 +1210,8 @@ async def test_homekit_start_in_accessory_mode(hass, hk_driver, device_reg): await hass.async_block_till_done() mock_add_acc.assert_not_called() - mock_setup_msg.assert_called_with(hass, entry.entry_id, None, pin, ANY) + mock_setup_msg.assert_called_with( + hass, entry.entry_id, "Mock Title (any)", pin, ANY + ) assert hk_driver_start.called assert homekit.status == STATUS_RUNNING diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index afa1408a06b..9b03d616002 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -1,4 +1,6 @@ """Test HomeKit util module.""" +from unittest.mock import Mock + import pytest import voluptuous as vol @@ -22,6 +24,7 @@ from homeassistant.components.homekit.const import ( TYPE_VALVE, ) from homeassistant.components.homekit.util import ( + accessory_friendly_name, async_find_next_available_port, cleanup_name_for_homekit, convert_to_float, @@ -284,3 +287,12 @@ async def test_format_sw_version(): assert format_sw_version("56.0-76060") == "56.0.76060" assert format_sw_version(3.6) == "3.6" assert format_sw_version("unknown") is None + + +async def test_accessory_friendly_name(): + """Test we provide a helpful friendly name.""" + + accessory = Mock() + accessory.display_name = "same" + assert accessory_friendly_name("same", accessory) == "same" + assert accessory_friendly_name("hass title", accessory) == "hass title (same)" From d02b27a5d03370ccb2a06308969bee71bd2e69fc Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 24 Feb 2021 01:26:17 +0100 Subject: [PATCH 683/796] Update xknx to 0.17.1 (#46974) --- homeassistant/components/knx/__init__.py | 20 +++++++++--------- homeassistant/components/knx/const.py | 24 +++++++++++----------- homeassistant/components/knx/factory.py | 24 +++++++++++----------- homeassistant/components/knx/fan.py | 6 +++--- homeassistant/components/knx/manifest.json | 2 +- homeassistant/components/knx/schema.py | 4 ++-- requirements_all.txt | 2 +- 7 files changed, 41 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index f092bafe404..8265ba42b72 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -106,34 +106,34 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_KNX_EXPOSE): vol.All( cv.ensure_list, [ExposeSchema.SCHEMA] ), - vol.Optional(SupportedPlatforms.cover.value): vol.All( + vol.Optional(SupportedPlatforms.COVER.value): vol.All( cv.ensure_list, [CoverSchema.SCHEMA] ), - vol.Optional(SupportedPlatforms.binary_sensor.value): vol.All( + vol.Optional(SupportedPlatforms.BINARY_SENSOR.value): vol.All( cv.ensure_list, [BinarySensorSchema.SCHEMA] ), - vol.Optional(SupportedPlatforms.light.value): vol.All( + vol.Optional(SupportedPlatforms.LIGHT.value): vol.All( cv.ensure_list, [LightSchema.SCHEMA] ), - vol.Optional(SupportedPlatforms.climate.value): vol.All( + vol.Optional(SupportedPlatforms.CLIMATE.value): vol.All( cv.ensure_list, [ClimateSchema.SCHEMA] ), - vol.Optional(SupportedPlatforms.notify.value): vol.All( + vol.Optional(SupportedPlatforms.NOTIFY.value): vol.All( cv.ensure_list, [NotifySchema.SCHEMA] ), - vol.Optional(SupportedPlatforms.switch.value): vol.All( + vol.Optional(SupportedPlatforms.SWITCH.value): vol.All( cv.ensure_list, [SwitchSchema.SCHEMA] ), - vol.Optional(SupportedPlatforms.sensor.value): vol.All( + vol.Optional(SupportedPlatforms.SENSOR.value): vol.All( cv.ensure_list, [SensorSchema.SCHEMA] ), - vol.Optional(SupportedPlatforms.scene.value): vol.All( + vol.Optional(SupportedPlatforms.SCENE.value): vol.All( cv.ensure_list, [SceneSchema.SCHEMA] ), - vol.Optional(SupportedPlatforms.weather.value): vol.All( + vol.Optional(SupportedPlatforms.WEATHER.value): vol.All( cv.ensure_list, [WeatherSchema.SCHEMA] ), - vol.Optional(SupportedPlatforms.fan.value): vol.All( + vol.Optional(SupportedPlatforms.FAN.value): vol.All( cv.ensure_list, [FanSchema.SCHEMA] ), } diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 6a76de6a97f..83ffc2557c2 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -26,23 +26,23 @@ CONF_RESET_AFTER = "reset_after" class ColorTempModes(Enum): """Color temperature modes for config validation.""" - absolute = "DPT-7.600" - relative = "DPT-5.001" + ABSOLUTE = "DPT-7.600" + RELATIVE = "DPT-5.001" class SupportedPlatforms(Enum): """Supported platforms.""" - binary_sensor = "binary_sensor" - climate = "climate" - cover = "cover" - fan = "fan" - light = "light" - notify = "notify" - scene = "scene" - sensor = "sensor" - switch = "switch" - weather = "weather" + BINARY_SENSOR = "binary_sensor" + CLIMATE = "climate" + COVER = "cover" + FAN = "fan" + LIGHT = "light" + NOTIFY = "notify" + SCENE = "scene" + SENSOR = "sensor" + SWITCH = "switch" + WEATHER = "weather" # Map KNX controller modes to HA modes. This list might not be complete. diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index 8d3464b25ad..51a94bc06e3 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -40,34 +40,34 @@ def create_knx_device( config: ConfigType, ) -> XknxDevice: """Return the requested XKNX device.""" - if platform is SupportedPlatforms.light: + if platform is SupportedPlatforms.LIGHT: return _create_light(knx_module, config) - if platform is SupportedPlatforms.cover: + if platform is SupportedPlatforms.COVER: return _create_cover(knx_module, config) - if platform is SupportedPlatforms.climate: + if platform is SupportedPlatforms.CLIMATE: return _create_climate(knx_module, config) - if platform is SupportedPlatforms.switch: + if platform is SupportedPlatforms.SWITCH: return _create_switch(knx_module, config) - if platform is SupportedPlatforms.sensor: + if platform is SupportedPlatforms.SENSOR: return _create_sensor(knx_module, config) - if platform is SupportedPlatforms.notify: + if platform is SupportedPlatforms.NOTIFY: return _create_notify(knx_module, config) - if platform is SupportedPlatforms.scene: + if platform is SupportedPlatforms.SCENE: return _create_scene(knx_module, config) - if platform is SupportedPlatforms.binary_sensor: + if platform is SupportedPlatforms.BINARY_SENSOR: return _create_binary_sensor(knx_module, config) - if platform is SupportedPlatforms.weather: + if platform is SupportedPlatforms.WEATHER: return _create_weather(knx_module, config) - if platform is SupportedPlatforms.fan: + if platform is SupportedPlatforms.FAN: return _create_fan(knx_module, config) @@ -121,12 +121,12 @@ def _create_light(knx_module: XKNX, config: ConfigType) -> XknxLight: group_address_tunable_white_state = None group_address_color_temp = None group_address_color_temp_state = None - if config[LightSchema.CONF_COLOR_TEMP_MODE] == ColorTempModes.absolute: + if config[LightSchema.CONF_COLOR_TEMP_MODE] == ColorTempModes.ABSOLUTE: group_address_color_temp = config.get(LightSchema.CONF_COLOR_TEMP_ADDRESS) group_address_color_temp_state = config.get( LightSchema.CONF_COLOR_TEMP_STATE_ADDRESS ) - elif config[LightSchema.CONF_COLOR_TEMP_MODE] == ColorTempModes.relative: + elif config[LightSchema.CONF_COLOR_TEMP_MODE] == ColorTempModes.RELATIVE: group_address_tunable_white = config.get(LightSchema.CONF_COLOR_TEMP_ADDRESS) group_address_tunable_white_state = config.get( LightSchema.CONF_COLOR_TEMP_STATE_ADDRESS diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index 6aa4f722892..43d1cd7d6f2 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -34,14 +34,14 @@ class KNXFan(KnxEntity, FanEntity): """Initialize of KNX fan.""" super().__init__(device) - if self._device.mode == FanSpeedMode.Step: + if self._device.mode == FanSpeedMode.STEP: self._step_range = (1, device.max_step) else: self._step_range = None async def async_set_percentage(self, percentage: int) -> None: """Set the speed of the fan, as a percentage.""" - if self._device.mode == FanSpeedMode.Step: + if self._device.mode == FanSpeedMode.STEP: step = math.ceil(percentage_to_ranged_value(self._step_range, percentage)) await self._device.set_speed(step) else: @@ -63,7 +63,7 @@ class KNXFan(KnxEntity, FanEntity): if self._device.current_speed is None: return None - if self._device.mode == FanSpeedMode.Step: + if self._device.mode == FanSpeedMode.STEP: return ranged_value_to_percentage( self._step_range, self._device.current_speed ) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 834e1604e9e..7ca7657d0ff 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.17.0"], + "requirements": ["xknx==0.17.1"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver" } diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 125115f28b2..1a48d7945bd 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -174,7 +174,7 @@ class LightSchema: vol.Optional(CONF_COLOR_TEMP_STATE_ADDRESS): cv.string, vol.Optional( CONF_COLOR_TEMP_MODE, default=DEFAULT_COLOR_TEMP_MODE - ): cv.enum(ColorTempModes), + ): vol.All(vol.Upper, cv.enum(ColorTempModes)), vol.Exclusive(CONF_RGBW_ADDRESS, "color"): cv.string, vol.Optional(CONF_RGBW_STATE_ADDRESS): cv.string, vol.Optional(CONF_MIN_KELVIN, default=DEFAULT_MIN_KELVIN): vol.All( @@ -256,7 +256,7 @@ class ClimateSchema: vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional( CONF_SETPOINT_SHIFT_MODE, default=DEFAULT_SETPOINT_SHIFT_MODE - ): cv.enum(SetpointShiftMode), + ): vol.All(vol.Upper, cv.enum(SetpointShiftMode)), vol.Optional( CONF_SETPOINT_SHIFT_MAX, default=DEFAULT_SETPOINT_SHIFT_MAX ): vol.All(int, vol.Range(min=0, max=32)), diff --git a/requirements_all.txt b/requirements_all.txt index 7079c94a00b..3ac487c8fb9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2333,7 +2333,7 @@ xbox-webapi==2.0.8 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.17.0 +xknx==0.17.1 # homeassistant.components.bluesound # homeassistant.components.rest From d4d68ebc64b63936030003aadd2747afeaeed9bc Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 24 Feb 2021 01:31:24 +0100 Subject: [PATCH 684/796] Extend zwave_js discovery scheme for lights (#46907) --- .../components/zwave_js/discovery.py | 25 +- tests/components/zwave_js/conftest.py | 14 + tests/components/zwave_js/test_light.py | 7 + .../zwave_js/aeon_smart_switch_6_state.json | 1247 +++++++++++++++++ 4 files changed, 1277 insertions(+), 16 deletions(-) create mode 100644 tests/fixtures/zwave_js/aeon_smart_switch_6_state.json diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 9379ab69b34..f4f5c359e22 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -221,22 +221,6 @@ DISCOVERY_SCHEMAS = [ type={"number"}, ), ), - # lights - # primary value is the currentValue (brightness) - ZWaveDiscoverySchema( - platform="light", - device_class_generic={"Multilevel Switch", "Remote Switch"}, - device_class_specific={ - "Tunable Color Light", - "Binary Tunable Color Light", - "Tunable Color Switch", - "Multilevel Remote Switch", - "Multilevel Power Switch", - "Multilevel Scene Switch", - "Unused", - }, - primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, - ), # binary sensors ZWaveDiscoverySchema( platform="binary_sensor", @@ -381,6 +365,15 @@ DISCOVERY_SCHEMAS = [ device_class_specific={"Fan Switch"}, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, ), + # lights + # primary value is the currentValue (brightness) + # catch any device with multilevel CC as light + # NOTE: keep this at the bottom of the discovery scheme, + # to handle all others that need the multilevel CC first + ZWaveDiscoverySchema( + platform="light", + primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, + ), ] diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 2f082621c45..a9618acc64d 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -148,6 +148,12 @@ def iblinds_v2_state_fixture(): return json.loads(load_fixture("zwave_js/cover_iblinds_v2_state.json")) +@pytest.fixture(name="aeon_smart_switch_6_state", scope="session") +def aeon_smart_switch_6_state_fixture(): + """Load the AEON Labs (ZW096) Smart Switch 6 node state fixture data.""" + return json.loads(load_fixture("zwave_js/aeon_smart_switch_6_state.json")) + + @pytest.fixture(name="ge_12730_state", scope="session") def ge_12730_state_fixture(): """Load the GE 12730 node state fixture data.""" @@ -362,6 +368,14 @@ def iblinds_cover_fixture(client, iblinds_v2_state): return node +@pytest.fixture(name="aeon_smart_switch_6") +def aeon_smart_switch_6_fixture(client, aeon_smart_switch_6_state): + """Mock an AEON Labs (ZW096) Smart Switch 6 node.""" + node = Node(client, aeon_smart_switch_6_state) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="ge_12730") def ge_12730_fixture(client, ge_12730_state): """Mock a GE 12730 fan controller node.""" diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py index 40950362ad7..f48b02223d0 100644 --- a/tests/components/zwave_js/test_light.py +++ b/tests/components/zwave_js/test_light.py @@ -14,6 +14,7 @@ from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON BULB_6_MULTI_COLOR_LIGHT_ENTITY = "light.bulb_6_multi_color" EATON_RF9640_ENTITY = "light.allloaddimmer" +AEON_SMART_SWITCH_LIGHT_ENTITY = "light.smart_switch_6" async def test_light(hass, client, bulb_6_multi_color, integration): @@ -403,3 +404,9 @@ async def test_v4_dimmer_light(hass, client, eaton_rf9640_dimmer, integration): assert state.state == STATE_ON # the light should pick currentvalue which has zwave value 22 assert state.attributes[ATTR_BRIGHTNESS] == 57 + + +async def test_optional_light(hass, client, aeon_smart_switch_6, integration): + """Test a device that has an additional light endpoint being identified as light.""" + state = hass.states.get(AEON_SMART_SWITCH_LIGHT_ENTITY) + assert state.state == STATE_ON diff --git a/tests/fixtures/zwave_js/aeon_smart_switch_6_state.json b/tests/fixtures/zwave_js/aeon_smart_switch_6_state.json new file mode 100644 index 00000000000..2da1c203561 --- /dev/null +++ b/tests/fixtures/zwave_js/aeon_smart_switch_6_state.json @@ -0,0 +1,1247 @@ +{ + "nodeId": 102, + "index": 0, + "installerIcon": 1792, + "userIcon": 1792, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Routing Slave", + "generic": "Binary Switch", + "specific": "Binary Power Switch", + "mandatorySupportedCCs": ["Basic", "Binary Switch", "All Switch"], + "mandatoryControlCCs": [] + }, + "isListening": true, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": true, + "version": 4, + "isBeaming": true, + "manufacturerId": 134, + "productId": 96, + "productType": 3, + "firmwareVersion": "1.1", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 5, + "deviceConfig": { + "manufacturerId": 134, + "manufacturer": "AEON Labs", + "label": "ZW096", + "description": "Smart Switch 6", + "devices": [ + { "productType": "0x0003", "productId": "0x0060" }, + { "productType": "0x0103", "productId": "0x0060" }, + { "productType": "0x0203", "productId": "0x0060" }, + { "productType": "0x1d03", "productId": "0x0060" } + ], + "firmwareVersion": { "min": "0.0", "max": "255.255" }, + "associations": {}, + "paramInformation": { "_map": {} } + }, + "label": "ZW096", + "neighbors": [1, 63, 90, 117], + "interviewAttempts": 1, + "interviewStage": 7, + "endpoints": [ + { + "nodeId": 102, + "index": 0, + "installerIcon": 1792, + "userIcon": 1792 + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value" + }, + "value": true + }, + { + "endpoint": 0, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 99, + "label": "Target value" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Transition duration" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 99, + "label": "Current value" + }, + "value": 99 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { "switchType": 2 } + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { "switchType": 2 } + } + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 65537, + "propertyName": "value", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [kWh]", + "unit": "kWh", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 0 } + }, + "value": 659.813 + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "previousValue", + "propertyKey": 65537, + "propertyName": "previousValue", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [kWh] (prev. value)", + "unit": "kWh", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 0 } + }, + "value": 659.813 + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "deltaTime", + "propertyKey": 65537, + "propertyName": "deltaTime", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [kWh] (prev. time delta)", + "unit": "s", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 0 } + }, + "value": 1200 + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66049, + "propertyName": "value", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [W]", + "unit": "W", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 2 } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "deltaTime", + "propertyKey": 66049, + "propertyName": "deltaTime", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [W] (prev. time delta)", + "unit": "s", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 2 } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66561, + "propertyName": "value", + "propertyKeyName": "Electric_V_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [V]", + "unit": "V", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 4 } + }, + "value": 229.935 + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "deltaTime", + "propertyKey": 66561, + "propertyName": "deltaTime", + "propertyKeyName": "Electric_V_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [V] (prev. time delta)", + "unit": "s", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 4 } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66817, + "propertyName": "value", + "propertyKeyName": "Electric_A_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [A]", + "unit": "A", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 5 } + }, + "value": 9.699 + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "deltaTime", + "propertyKey": 66817, + "propertyName": "deltaTime", + "propertyKeyName": "Electric_A_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [A] (prev. time delta)", + "unit": "s", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 5 } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "reset", + "propertyName": "reset", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Reset accumulated values" + } + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "previousValue", + "propertyKey": 66049, + "propertyName": "previousValue", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [W] (prev. value)", + "unit": "W", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 2 } + } + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "previousValue", + "propertyKey": 66561, + "propertyName": "previousValue", + "propertyKeyName": "Electric_V_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [V] (prev. value)", + "unit": "V", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 4 } + } + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "previousValue", + "propertyKey": 66817, + "propertyName": "previousValue", + "propertyKeyName": "Electric_A_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [A] (prev. value)", + "unit": "A", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 5 } + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Remaining duration" + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "currentColor", + "propertyKey": 2, + "propertyName": "currentColor", + "propertyKeyName": "Red", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Current value (Red)", + "description": "The current value of the Red color." + }, + "value": 27 + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "hexColor", + "propertyName": "hexColor", + "ccVersion": 1, + "metadata": { + "type": "color", + "readable": true, + "writeable": true, + "minLength": 6, + "maxLength": 7, + "label": "RGB Color" + }, + "value": "1b141b" + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "currentColor", + "propertyKey": 3, + "propertyName": "currentColor", + "propertyKeyName": "Green", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Current value (Green)", + "description": "The current value of the Green color." + }, + "value": 20 + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "currentColor", + "propertyKey": 4, + "propertyName": "currentColor", + "propertyKeyName": "Blue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Current value (Blue)", + "description": "The current value of the Blue color." + }, + "value": 27 + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "targetColor", + "propertyKey": 2, + "propertyName": "targetColor", + "propertyKeyName": "Red", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Target value (Red)", + "description": "The target value of the Red color." + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "targetColor", + "propertyKey": 3, + "propertyName": "targetColor", + "propertyKeyName": "Green", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Target value (Green)", + "description": "The target value of the Green color." + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "targetColor", + "propertyKey": 4, + "propertyName": "targetColor", + "propertyKeyName": "Blue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Target value (Blue)", + "description": "The target value of the Blue color." + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Current overload protection enable", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { "0": "disabled", "1": "enabled" }, + "label": "Current overload protection enable", + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 20, + "propertyName": "Output load after re-power", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 2, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "last status", + "1": "always on", + "2": "always off" + }, + "label": "Output load after re-power", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 80, + "propertyName": "Enable send to associated devices", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 2, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "nothing", + "1": "hail CC", + "2": "basic CC report" + }, + "label": "Enable send to associated devices", + "description": "Enable to send notifications to Group 1", + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 81, + "propertyName": "Configure LED state", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 2, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "LED follows load", + "1": "LED follows load for 5 seconds", + "2": "Night light mode" + }, + "label": "Configure LED state", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 90, + "propertyName": "Enable items 91 and 92", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { "0": "disabled", "1": "enabled" }, + "label": "Enable items 91 and 92", + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 91, + "propertyName": "Wattage Threshold", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 60000, + "default": 25, + "format": 1, + "allowManualEntry": true, + "label": "Wattage Threshold", + "description": "minimum change in wattage to trigger", + "isFromConfig": true + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 92, + "propertyName": "Wattage Percent Change", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 5, + "format": 0, + "allowManualEntry": true, + "label": "Wattage Percent Change", + "description": "minimum change in wattage percent", + "isFromConfig": true + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 101, + "propertyName": "Values to send to group 1", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 15, + "default": 4, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Nothing", + "1": "Voltage", + "2": "Current", + "4": "Wattage", + "8": "kWh", + "15": "All Values" + }, + "label": "Values to send to group 1", + "description": "Which reports need to send in Report group 1", + "isFromConfig": true + }, + "value": 8 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 102, + "propertyName": "Values to send to group 2", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 15, + "default": 8, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Nothing", + "1": "Voltage", + "2": "Current", + "4": "Wattage", + "8": "kWh", + "15": "All Values" + }, + "label": "Values to send to group 2", + "description": "Which reports need to send in Report group 2", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 103, + "propertyName": "Values to send to group 3", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 15, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Nothing", + "1": "Voltage", + "2": "Current", + "4": "Wattage", + "8": "kWh", + "15": "All Values" + }, + "label": "Values to send to group 3", + "description": "Which reports need to send in Report group 3", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 111, + "propertyName": "Time interval for sending to group 1", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 1, + "max": 2147483647, + "default": 3, + "format": 0, + "allowManualEntry": true, + "label": "Time interval for sending to group 1", + "description": "Group 1 automatic update interval", + "isFromConfig": true + }, + "value": 1200 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 112, + "propertyName": "Time interval for sending to group 2", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 1, + "max": 2147483647, + "default": 600, + "format": 0, + "allowManualEntry": true, + "label": "Time interval for sending to group 2", + "description": "Group 2 automatic update interval", + "isFromConfig": true + }, + "value": 120 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 113, + "propertyName": "Time interval for sending to group 3", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 1, + "max": 2147483647, + "default": 600, + "format": 0, + "allowManualEntry": true, + "label": "Time interval for sending to group 3", + "description": "Group 3 automatic update interval", + "isFromConfig": true + }, + "value": 65460 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 252, + "propertyName": "Configuration Locked", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { "0": "disabled", "1": "enabled" }, + "label": "Configuration Locked", + "description": "Enable/disable Configuration Locked (0 =disable, 1 = enable).", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 83, + "propertyKey": 255, + "propertyName": "Blue night light color value", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 3, + "min": 0, + "max": 255, + "default": 221, + "format": 0, + "allowManualEntry": true, + "label": "Blue night light color value", + "isFromConfig": true + }, + "value": 27 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 83, + "propertyKey": 65280, + "propertyName": "Green night light color value", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 3, + "min": 0, + "max": 255, + "default": 160, + "format": 0, + "allowManualEntry": true, + "label": "Green night light color value", + "isFromConfig": true + }, + "value": 20 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 83, + "propertyKey": 16711680, + "propertyName": "Red night light color value", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 3, + "min": 0, + "max": 255, + "default": 221, + "format": 0, + "allowManualEntry": true, + "label": "Red night light color value", + "description": "Configure the RGB value when it is in Night light mode", + "isFromConfig": true + }, + "value": 27 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 84, + "propertyKey": 255, + "propertyName": "Green brightness in energy mode (%)", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 3, + "min": 0, + "max": 100, + "default": 50, + "format": 0, + "allowManualEntry": true, + "label": "Green brightness in energy mode (%)", + "isFromConfig": true + }, + "value": 50 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 84, + "propertyKey": 65280, + "propertyName": "Yellow brightness in energy mode (%)", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 3, + "min": 0, + "max": 100, + "default": 50, + "format": 0, + "allowManualEntry": true, + "label": "Yellow brightness in energy mode (%)", + "isFromConfig": true + }, + "value": 50 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 84, + "propertyKey": 16711680, + "propertyName": "Red brightness in energy mode (%)", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 3, + "min": 0, + "max": 100, + "default": 50, + "format": 0, + "allowManualEntry": true, + "label": "Red brightness in energy mode (%)", + "isFromConfig": true + }, + "value": 50 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 33, + "propertyName": "RGB LED color testing", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 0, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "RGB LED color testing", + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 100, + "propertyName": "Set 101\u2010103 to default.", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { "0": "False", "1": "True" }, + "label": "Set 101\u2010103 to default.", + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 110, + "propertyName": "Set 111\u2010113 to default.", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { "0": "False", "1": "True" }, + "label": "Set 111\u2010113 to default.", + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 255, + "propertyName": "RESET", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "RESET", + "description": "Reset the device to defaults", + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 134 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 96 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.54" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["1.1"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + } + ] + } \ No newline at end of file From b8f7bc12ee9dad5b65a4b201b035aa25948058ca Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Tue, 23 Feb 2021 19:34:25 -0700 Subject: [PATCH 685/796] Add switches and sensors to Litter-Robot (#46942) --- .../components/litterrobot/__init__.py | 2 +- .../components/litterrobot/sensor.py | 54 +++++++++++++++ .../components/litterrobot/switch.py | 68 +++++++++++++++++++ .../components/litterrobot/vacuum.py | 13 ++++ tests/components/litterrobot/conftest.py | 24 ++++++- tests/components/litterrobot/test_sensor.py | 20 ++++++ tests/components/litterrobot/test_switch.py | 59 ++++++++++++++++ tests/components/litterrobot/test_vacuum.py | 25 ++----- 8 files changed, 242 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/litterrobot/sensor.py create mode 100644 homeassistant/components/litterrobot/switch.py create mode 100644 tests/components/litterrobot/test_sensor.py create mode 100644 tests/components/litterrobot/test_switch.py diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index bf43d5c465e..19e76b9bb19 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -10,7 +10,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from .const import DOMAIN from .hub import LitterRobotHub -PLATFORMS = ["vacuum"] +PLATFORMS = ["sensor", "switch", "vacuum"] async def async_setup(hass: HomeAssistant, config: dict): diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py new file mode 100644 index 00000000000..2843660bcee --- /dev/null +++ b/homeassistant/components/litterrobot/sensor.py @@ -0,0 +1,54 @@ +"""Support for Litter-Robot sensors.""" +from homeassistant.const import PERCENTAGE +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN +from .hub import LitterRobotEntity + +WASTE_DRAWER = "Waste Drawer" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Litter-Robot sensors using config entry.""" + hub = hass.data[DOMAIN][config_entry.entry_id] + + entities = [] + for robot in hub.account.robots: + entities.append(LitterRobotSensor(robot, WASTE_DRAWER, hub)) + + if entities: + async_add_entities(entities, True) + + +class LitterRobotSensor(LitterRobotEntity, Entity): + """Litter-Robot sensors.""" + + @property + def state(self): + """Return the state.""" + return self.robot.waste_drawer_gauge + + @property + def unit_of_measurement(self): + """Return unit of measurement.""" + return PERCENTAGE + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + if self.robot.waste_drawer_gauge <= 10: + return "mdi:gauge-empty" + if self.robot.waste_drawer_gauge < 50: + return "mdi:gauge-low" + if self.robot.waste_drawer_gauge <= 90: + return "mdi:gauge" + return "mdi:gauge-full" + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + return { + "cycle_count": self.robot.cycle_count, + "cycle_capacity": self.robot.cycle_capacity, + "cycles_after_drawer_full": self.robot.cycles_after_drawer_full, + } diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py new file mode 100644 index 00000000000..b94b29a35e1 --- /dev/null +++ b/homeassistant/components/litterrobot/switch.py @@ -0,0 +1,68 @@ +"""Support for Litter-Robot switches.""" +from homeassistant.helpers.entity import ToggleEntity + +from .const import DOMAIN +from .hub import LitterRobotEntity + + +class LitterRobotNightLightModeSwitch(LitterRobotEntity, ToggleEntity): + """Litter-Robot Night Light Mode Switch.""" + + @property + def is_on(self): + """Return true if switch is on.""" + return self.robot.night_light_active + + @property + def icon(self): + """Return the icon.""" + return "mdi:lightbulb-on" if self.is_on else "mdi:lightbulb-off" + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + await self.perform_action_and_refresh(self.robot.set_night_light, True) + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + await self.perform_action_and_refresh(self.robot.set_night_light, False) + + +class LitterRobotPanelLockoutSwitch(LitterRobotEntity, ToggleEntity): + """Litter-Robot Panel Lockout Switch.""" + + @property + def is_on(self): + """Return true if switch is on.""" + return self.robot.panel_lock_active + + @property + def icon(self): + """Return the icon.""" + return "mdi:lock" if self.is_on else "mdi:lock-open" + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + await self.perform_action_and_refresh(self.robot.set_panel_lockout, True) + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + await self.perform_action_and_refresh(self.robot.set_panel_lockout, False) + + +ROBOT_SWITCHES = { + "Night Light Mode": LitterRobotNightLightModeSwitch, + "Panel Lockout": LitterRobotPanelLockoutSwitch, +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Litter-Robot switches using config entry.""" + hub = hass.data[DOMAIN][config_entry.entry_id] + + entities = [] + for robot in hub.account.robots: + for switch_type, switch_class in ROBOT_SWITCHES.items(): + entities.append(switch_class(robot, switch_type, hub)) + + if entities: + async_add_entities(entities, True) diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index a57c1ffead5..6ee92993869 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -14,6 +14,7 @@ from homeassistant.components.vacuum import ( VacuumEntity, ) from homeassistant.const import STATE_OFF +import homeassistant.util.dt as dt_util from .const import DOMAIN from .hub import LitterRobotEntity @@ -118,9 +119,21 @@ class LitterRobotCleaner(LitterRobotEntity, VacuumEntity): @property def device_state_attributes(self): """Return device specific state attributes.""" + [sleep_mode_start_time, sleep_mode_end_time] = [None, None] + + if self.robot.sleep_mode_active: + sleep_mode_start_time = dt_util.as_local( + self.robot.sleep_mode_start_time + ).strftime("%H:%M:00") + sleep_mode_end_time = dt_util.as_local( + self.robot.sleep_mode_end_time + ).strftime("%H:%M:00") + return { "clean_cycle_wait_time_minutes": self.robot.clean_cycle_wait_time_minutes, "is_sleeping": self.robot.is_sleeping, + "sleep_mode_start_time": sleep_mode_start_time, + "sleep_mode_end_time": sleep_mode_end_time, "power_status": self.robot.power_status, "unit_status_code": self.robot.unit_status.name, "last_seen": self.robot.last_seen, diff --git a/tests/components/litterrobot/conftest.py b/tests/components/litterrobot/conftest.py index 2f967d266bc..dae183b4cf6 100644 --- a/tests/components/litterrobot/conftest.py +++ b/tests/components/litterrobot/conftest.py @@ -1,5 +1,5 @@ """Configure pytest for Litter-Robot tests.""" -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock, MagicMock, patch from pylitterbot import Robot import pytest @@ -7,7 +7,9 @@ import pytest from homeassistant.components import litterrobot from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .common import ROBOT_DATA +from .common import CONFIG, ROBOT_DATA + +from tests.common import MockConfigEntry def create_mock_robot(hass): @@ -17,6 +19,8 @@ def create_mock_robot(hass): robot.set_power_status = AsyncMock() robot.reset_waste_drawer = AsyncMock() robot.set_sleep_mode = AsyncMock() + robot.set_night_light = AsyncMock() + robot.set_panel_lockout = AsyncMock() return robot @@ -33,3 +37,19 @@ def mock_hub(hass): hub.coordinator.last_update_success = True hub.account.robots = [create_mock_robot(hass)] return hub + + +async def setup_hub(hass, mock_hub, platform_domain): + """Load a Litter-Robot platform with the provided hub.""" + entry = MockConfigEntry( + domain=litterrobot.DOMAIN, + data=CONFIG[litterrobot.DOMAIN], + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.litterrobot.LitterRobotHub", + return_value=mock_hub, + ): + await hass.config_entries.async_forward_entry_setup(entry, platform_domain) + await hass.async_block_till_done() diff --git a/tests/components/litterrobot/test_sensor.py b/tests/components/litterrobot/test_sensor.py new file mode 100644 index 00000000000..2421489e237 --- /dev/null +++ b/tests/components/litterrobot/test_sensor.py @@ -0,0 +1,20 @@ +"""Test the Litter-Robot sensor entity.""" +from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN +from homeassistant.const import PERCENTAGE + +from .conftest import setup_hub + +ENTITY_ID = "sensor.test_waste_drawer" + + +async def test_sensor(hass, mock_hub): + """Tests the sensor entity was set up.""" + await setup_hub(hass, mock_hub, PLATFORM_DOMAIN) + + sensor = hass.states.get(ENTITY_ID) + assert sensor + assert sensor.state == "50" + assert sensor.attributes["cycle_count"] == 15 + assert sensor.attributes["cycle_capacity"] == 30 + assert sensor.attributes["cycles_after_drawer_full"] == 0 + assert sensor.attributes["unit_of_measurement"] == PERCENTAGE diff --git a/tests/components/litterrobot/test_switch.py b/tests/components/litterrobot/test_switch.py new file mode 100644 index 00000000000..c7f85db7412 --- /dev/null +++ b/tests/components/litterrobot/test_switch.py @@ -0,0 +1,59 @@ +"""Test the Litter-Robot switch entity.""" +from datetime import timedelta + +import pytest + +from homeassistant.components.litterrobot.hub import REFRESH_WAIT_TIME +from homeassistant.components.switch import ( + DOMAIN as PLATFORM_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_ON +from homeassistant.util.dt import utcnow + +from .conftest import setup_hub + +from tests.common import async_fire_time_changed + +NIGHT_LIGHT_MODE_ENTITY_ID = "switch.test_night_light_mode" +PANEL_LOCKOUT_ENTITY_ID = "switch.test_panel_lockout" + + +async def test_switch(hass, mock_hub): + """Tests the switch entity was set up.""" + await setup_hub(hass, mock_hub, PLATFORM_DOMAIN) + + switch = hass.states.get(NIGHT_LIGHT_MODE_ENTITY_ID) + assert switch + assert switch.state == STATE_ON + + +@pytest.mark.parametrize( + "entity_id,robot_command", + [ + (NIGHT_LIGHT_MODE_ENTITY_ID, "set_night_light"), + (PANEL_LOCKOUT_ENTITY_ID, "set_panel_lockout"), + ], +) +async def test_on_off_commands(hass, mock_hub, entity_id, robot_command): + """Test sending commands to the switch.""" + await setup_hub(hass, mock_hub, PLATFORM_DOMAIN) + + switch = hass.states.get(entity_id) + assert switch + + data = {ATTR_ENTITY_ID: entity_id} + + count = 0 + for service in [SERVICE_TURN_ON, SERVICE_TURN_OFF]: + count += 1 + await hass.services.async_call( + PLATFORM_DOMAIN, + service, + data, + blocking=True, + ) + future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME) + async_fire_time_changed(hass, future) + assert getattr(mock_hub.account.robots[0], robot_command).call_count == count diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index b47eff64e13..03e63b472b6 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -1,10 +1,8 @@ """Test the Litter-Robot vacuum entity.""" from datetime import timedelta -from unittest.mock import patch import pytest -from homeassistant.components import litterrobot from homeassistant.components.litterrobot.hub import REFRESH_WAIT_TIME from homeassistant.components.vacuum import ( ATTR_PARAMS, @@ -18,32 +16,19 @@ from homeassistant.components.vacuum import ( from homeassistant.const import ATTR_COMMAND, ATTR_ENTITY_ID from homeassistant.util.dt import utcnow -from .common import CONFIG +from .conftest import setup_hub -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import async_fire_time_changed ENTITY_ID = "vacuum.test_litter_box" -async def setup_hub(hass, mock_hub): - """Load the Litter-Robot vacuum platform with the provided hub.""" - hass.config.components.add(litterrobot.DOMAIN) - entry = MockConfigEntry( - domain=litterrobot.DOMAIN, - data=CONFIG[litterrobot.DOMAIN], - ) - - with patch.dict(hass.data, {litterrobot.DOMAIN: {entry.entry_id: mock_hub}}): - await hass.config_entries.async_forward_entry_setup(entry, PLATFORM_DOMAIN) - await hass.async_block_till_done() - - async def test_vacuum(hass, mock_hub): """Tests the vacuum entity was set up.""" - await setup_hub(hass, mock_hub) + await setup_hub(hass, mock_hub, PLATFORM_DOMAIN) vacuum = hass.states.get(ENTITY_ID) - assert vacuum is not None + assert vacuum assert vacuum.state == STATE_DOCKED assert vacuum.attributes["is_sleeping"] is False @@ -71,7 +56,7 @@ async def test_vacuum(hass, mock_hub): ) async def test_commands(hass, mock_hub, service, command, extra): """Test sending commands to the vacuum.""" - await setup_hub(hass, mock_hub) + await setup_hub(hass, mock_hub, PLATFORM_DOMAIN) vacuum = hass.states.get(ENTITY_ID) assert vacuum is not None From 19f5b467b72413dc63fb307659a8a7534afc1bff Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 23 Feb 2021 18:38:52 -0800 Subject: [PATCH 686/796] Make FAN_ON use the max duration rather than 15 min default (#46489) --- homeassistant/components/nest/climate_sdm.py | 7 ++++++- tests/components/nest/climate_sdm_test.py | 16 +++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index 6413b2e0dfe..b0c64329ffd 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -75,6 +75,8 @@ FAN_MODE_MAP = { } FAN_INV_MODE_MAP = {v: k for k, v in FAN_MODE_MAP.items()} +MAX_FAN_DURATION = 43200 # 15 hours is the max in the SDM API + async def async_setup_sdm_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities @@ -322,4 +324,7 @@ class ThermostatEntity(ClimateEntity): if fan_mode not in self.fan_modes: raise ValueError(f"Unsupported fan_mode '{fan_mode}'") trait = self._device.traits[FanTrait.NAME] - await trait.set_timer(FAN_INV_MODE_MAP[fan_mode]) + duration = None + if fan_mode != FAN_OFF: + duration = MAX_FAN_DURATION + await trait.set_timer(FAN_INV_MODE_MAP[fan_mode], duration=duration) diff --git a/tests/components/nest/climate_sdm_test.py b/tests/components/nest/climate_sdm_test.py index ef332d0e848..888227b9cde 100644 --- a/tests/components/nest/climate_sdm_test.py +++ b/tests/components/nest/climate_sdm_test.py @@ -819,6 +819,20 @@ async def test_thermostat_set_fan(hass, auth): "params": {"timerMode": "OFF"}, } + # Turn on fan mode + await common.async_set_fan_mode(hass, FAN_ON) + await hass.async_block_till_done() + + assert auth.method == "post" + assert auth.url == "some-device-id:executeCommand" + assert auth.json == { + "command": "sdm.devices.commands.Fan.SetTimer", + "params": { + "duration": "43200s", + "timerMode": "ON", + }, + } + async def test_thermostat_fan_empty(hass): """Test a fan trait with an empty response.""" @@ -938,7 +952,7 @@ async def test_thermostat_set_hvac_fan_only(hass, auth): assert url == "some-device-id:executeCommand" assert json == { "command": "sdm.devices.commands.Fan.SetTimer", - "params": {"timerMode": "ON"}, + "params": {"duration": "43200s", "timerMode": "ON"}, } (method, url, json, headers) = auth.captured_requests.pop(0) assert method == "post" From 3e26e2adad9fc077ddf63d7b254c324cff3c5cb2 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Wed, 24 Feb 2021 04:00:47 +0100 Subject: [PATCH 687/796] Handle device IP change in upnp (#46859) --- homeassistant/components/upnp/__init__.py | 5 ++++- homeassistant/components/upnp/config_flow.py | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 5d251ce7dd8..d5be0757cf3 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -123,7 +123,10 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) ) # Ensure entry has a hostname, for older entries. - if CONFIG_ENTRY_HOSTNAME not in config_entry.data: + if ( + CONFIG_ENTRY_HOSTNAME not in config_entry.data + or config_entry.data[CONFIG_ENTRY_HOSTNAME] != device.hostname + ): hass.config_entries.async_update_entry( entry=config_entry, data={CONFIG_ENTRY_HOSTNAME: device.hostname, **config_entry.data}, diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 1d212441bfa..e1101c3713c 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -179,7 +179,9 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): discovery = await Device.async_supplement_discovery(self.hass, discovery) unique_id = discovery[DISCOVERY_UNIQUE_ID] await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured( + updates={CONFIG_ENTRY_HOSTNAME: discovery[DISCOVERY_HOSTNAME]} + ) # Handle devices changing their UDN, only allow a single existing_entries = self.hass.config_entries.async_entries(DOMAIN) From 42fd3be0e8ad51735d93aa67ecfd24d487cbd06e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 24 Feb 2021 07:46:00 +0100 Subject: [PATCH 688/796] Add template support to service targets (#46977) Co-authored-by: Paulus Schoutsen --- homeassistant/helpers/config_validation.py | 15 +++++++++++---- homeassistant/helpers/service.py | 18 +++++++++++++++++- tests/helpers/test_service.py | 18 ++++++++++++++++++ 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 6b0737ae346..422f940e98e 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -847,11 +847,18 @@ PLATFORM_SCHEMA = vol.Schema( PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) ENTITY_SERVICE_FIELDS = { - vol.Optional(ATTR_ENTITY_ID): comp_entity_ids, - vol.Optional(ATTR_DEVICE_ID): vol.Any( - ENTITY_MATCH_NONE, vol.All(ensure_list, [str]) + # Either accept static entity IDs, a single dynamic template or a mixed list + # of static and dynamic templates. While this could be solved with a single + # complex template, handling it like this, keeps config validation useful. + vol.Optional(ATTR_ENTITY_ID): vol.Any( + comp_entity_ids, dynamic_template, vol.All(list, template_complex) + ), + vol.Optional(ATTR_DEVICE_ID): vol.Any( + ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)]) + ), + vol.Optional(ATTR_AREA_ID): vol.Any( + ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)]) ), - vol.Optional(ATTR_AREA_ID): vol.Any(ENTITY_MATCH_NONE, vol.All(ensure_list, [str])), } diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 7983190dbe8..932384493f3 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -28,6 +28,7 @@ from homeassistant.const import ( ATTR_AREA_ID, ATTR_DEVICE_ID, ATTR_ENTITY_ID, + CONF_ENTITY_ID, CONF_SERVICE, CONF_SERVICE_DATA, CONF_SERVICE_TEMPLATE, @@ -189,7 +190,22 @@ def async_prepare_call_from_config( domain, service = domain_service.split(".", 1) - target = config.get(CONF_TARGET) + target = {} + if CONF_TARGET in config: + conf = config.get(CONF_TARGET) + try: + template.attach(hass, conf) + target.update(template.render_complex(conf, variables)) + if CONF_ENTITY_ID in target: + target[CONF_ENTITY_ID] = cv.comp_entity_ids(target[CONF_ENTITY_ID]) + except TemplateError as ex: + raise HomeAssistantError( + f"Error rendering service target template: {ex}" + ) from ex + except vol.Invalid as ex: + raise HomeAssistantError( + f"Template rendered invalid entity IDs: {target[CONF_ENTITY_ID]}" + ) from ex service_data = {} diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 95ccdc84395..92cbd5514e6 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -195,6 +195,24 @@ class TestServiceHelpers(unittest.TestCase): "area_id": ["test-area-id"], } + config = { + "service": "{{ 'test_domain.test_service' }}", + "target": { + "area_id": ["area-42", "{{ 'area-51' }}"], + "device_id": ["abcdef", "{{ 'fedcba' }}"], + "entity_id": ["light.static", "{{ 'light.dynamic' }}"], + }, + } + + service.call_from_config(self.hass, config) + self.hass.block_till_done() + + assert dict(self.calls[1].data) == { + "area_id": ["area-42", "area-51"], + "device_id": ["abcdef", "fedcba"], + "entity_id": ["light.static", "light.dynamic"], + } + def test_service_template_service_call(self): """Test legacy service_template call with templating.""" config = { From a6322155411575d5e1d00af59a793773ccc2f189 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 24 Feb 2021 08:37:41 +0100 Subject: [PATCH 689/796] Validate KNX addresses (#46933) --- homeassistant/components/knx/__init__.py | 14 +- homeassistant/components/knx/schema.py | 453 ++++++++++++----------- 2 files changed, 246 insertions(+), 221 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 8265ba42b72..b8feb010e29 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -47,6 +47,8 @@ from .schema import ( SensorSchema, SwitchSchema, WeatherSchema, + ga_validator, + ia_validator, ) _LOGGER = logging.getLogger(__name__) @@ -92,7 +94,7 @@ CONFIG_SCHEMA = vol.Schema( ), vol.Optional( CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS - ): cv.string, + ): ia_validator, vol.Optional( CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP ): cv.string, @@ -146,7 +148,7 @@ CONFIG_SCHEMA = vol.Schema( SERVICE_KNX_SEND_SCHEMA = vol.Any( vol.Schema( { - vol.Required(CONF_ADDRESS): cv.string, + vol.Required(CONF_ADDRESS): ga_validator, vol.Required(SERVICE_KNX_ATTR_PAYLOAD): cv.match_all, vol.Required(SERVICE_KNX_ATTR_TYPE): vol.Any(int, float, str), } @@ -154,7 +156,7 @@ SERVICE_KNX_SEND_SCHEMA = vol.Any( vol.Schema( # without type given payload is treated as raw bytes { - vol.Required(CONF_ADDRESS): cv.string, + vol.Required(CONF_ADDRESS): ga_validator, vol.Required(SERVICE_KNX_ATTR_PAYLOAD): vol.Any( cv.positive_int, [cv.positive_int] ), @@ -166,14 +168,14 @@ SERVICE_KNX_READ_SCHEMA = vol.Schema( { vol.Required(CONF_ADDRESS): vol.All( cv.ensure_list, - [cv.string], + [ga_validator], ) } ) SERVICE_KNX_EVENT_REGISTER_SCHEMA = vol.Schema( { - vol.Required(CONF_ADDRESS): cv.string, + vol.Required(CONF_ADDRESS): ga_validator, vol.Optional(SERVICE_KNX_ATTR_REMOVE, default=False): cv.boolean, } ) @@ -187,7 +189,7 @@ SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA = vol.Any( vol.Schema( # for removing only `address` is required { - vol.Required(CONF_ADDRESS): cv.string, + vol.Required(CONF_ADDRESS): ga_validator, vol.Required(SERVICE_KNX_ATTR_REMOVE): vol.All(cv.boolean, True), }, extra=vol.ALLOW_EXTRA, diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 1a48d7945bd..08a8c62adc4 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -2,6 +2,7 @@ import voluptuous as vol from xknx.devices.climate import SetpointShiftMode from xknx.io import DEFAULT_MCAST_PORT +from xknx.telegram.address import GroupAddress, IndividualAddress from homeassistant.const import ( CONF_ADDRESS, @@ -24,6 +25,33 @@ from .const import ( ColorTempModes, ) +################## +# KNX VALIDATORS +################## + +ga_validator = vol.Any( + cv.matches_regex(GroupAddress.ADDRESS_RE), + vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), + msg="value does not match pattern for KNX group address '
//', '
/' or '' (eg.'1/2/3', '9/234', '123')", +) + +ia_validator = vol.Any( + cv.matches_regex(IndividualAddress.ADDRESS_RE), + vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), + msg="value does not match pattern for KNX individual address '..' (eg.'1.1.100')", +) + +sync_state_validator = vol.Any( + vol.All(vol.Coerce(int), vol.Range(min=2, max=1440)), + cv.boolean, + cv.matches_regex(r"^(init|expire|every)( \d*)?$"), +) + + +############## +# CONNECTION +############## + class ConnectionSchema: """Voluptuous schema for KNX connection.""" @@ -41,45 +69,9 @@ class ConnectionSchema: ROUTING_SCHEMA = vol.Maybe(vol.Schema({vol.Optional(CONF_KNX_LOCAL_IP): cv.string})) -class CoverSchema: - """Voluptuous schema for KNX covers.""" - - CONF_MOVE_LONG_ADDRESS = "move_long_address" - CONF_MOVE_SHORT_ADDRESS = "move_short_address" - CONF_STOP_ADDRESS = "stop_address" - CONF_POSITION_ADDRESS = "position_address" - CONF_POSITION_STATE_ADDRESS = "position_state_address" - CONF_ANGLE_ADDRESS = "angle_address" - CONF_ANGLE_STATE_ADDRESS = "angle_state_address" - CONF_TRAVELLING_TIME_DOWN = "travelling_time_down" - CONF_TRAVELLING_TIME_UP = "travelling_time_up" - CONF_INVERT_POSITION = "invert_position" - CONF_INVERT_ANGLE = "invert_angle" - - DEFAULT_TRAVEL_TIME = 25 - DEFAULT_NAME = "KNX Cover" - - SCHEMA = vol.Schema( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MOVE_LONG_ADDRESS): cv.string, - vol.Optional(CONF_MOVE_SHORT_ADDRESS): cv.string, - vol.Optional(CONF_STOP_ADDRESS): cv.string, - vol.Optional(CONF_POSITION_ADDRESS): cv.string, - vol.Optional(CONF_POSITION_STATE_ADDRESS): cv.string, - vol.Optional(CONF_ANGLE_ADDRESS): cv.string, - vol.Optional(CONF_ANGLE_STATE_ADDRESS): cv.string, - vol.Optional( - CONF_TRAVELLING_TIME_DOWN, default=DEFAULT_TRAVEL_TIME - ): cv.positive_int, - vol.Optional( - CONF_TRAVELLING_TIME_UP, default=DEFAULT_TRAVEL_TIME - ): cv.positive_int, - vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, - vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean, - vol.Optional(CONF_DEVICE_CLASS): cv.string, - } - ) +############# +# PLATFORMS +############# class BinarySensorSchema: @@ -100,13 +92,9 @@ class BinarySensorSchema: vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_SYNC_STATE, default=True): vol.Any( - vol.All(vol.Coerce(int), vol.Range(min=2, max=1440)), - cv.boolean, - cv.string, - ), + vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, vol.Optional(CONF_IGNORE_INTERNAL_STATE, default=False): cv.boolean, - vol.Required(CONF_STATE_ADDRESS): cv.string, + vol.Required(CONF_STATE_ADDRESS): ga_validator, vol.Optional(CONF_CONTEXT_TIMEOUT): vol.All( vol.Coerce(float), vol.Range(min=0, max=10) ), @@ -118,95 +106,6 @@ class BinarySensorSchema: ) -class LightSchema: - """Voluptuous schema for KNX lights.""" - - CONF_STATE_ADDRESS = CONF_STATE_ADDRESS - CONF_BRIGHTNESS_ADDRESS = "brightness_address" - CONF_BRIGHTNESS_STATE_ADDRESS = "brightness_state_address" - CONF_COLOR_ADDRESS = "color_address" - CONF_COLOR_STATE_ADDRESS = "color_state_address" - CONF_COLOR_TEMP_ADDRESS = "color_temperature_address" - CONF_COLOR_TEMP_STATE_ADDRESS = "color_temperature_state_address" - CONF_COLOR_TEMP_MODE = "color_temperature_mode" - CONF_RGBW_ADDRESS = "rgbw_address" - CONF_RGBW_STATE_ADDRESS = "rgbw_state_address" - CONF_MIN_KELVIN = "min_kelvin" - CONF_MAX_KELVIN = "max_kelvin" - - DEFAULT_NAME = "KNX Light" - DEFAULT_COLOR_TEMP_MODE = "absolute" - DEFAULT_MIN_KELVIN = 2700 # 370 mireds - DEFAULT_MAX_KELVIN = 6000 # 166 mireds - - CONF_INDIVIDUAL_COLORS = "individual_colors" - CONF_RED = "red" - CONF_GREEN = "green" - CONF_BLUE = "blue" - CONF_WHITE = "white" - - COLOR_SCHEMA = vol.Schema( - { - vol.Optional(CONF_ADDRESS): cv.string, - vol.Optional(CONF_STATE_ADDRESS): cv.string, - vol.Required(CONF_BRIGHTNESS_ADDRESS): cv.string, - vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): cv.string, - } - ) - - SCHEMA = vol.All( - vol.Schema( - { - vol.Optional(CONF_ADDRESS): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_STATE_ADDRESS): cv.string, - vol.Optional(CONF_BRIGHTNESS_ADDRESS): cv.string, - vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): cv.string, - vol.Exclusive(CONF_INDIVIDUAL_COLORS, "color"): { - vol.Inclusive(CONF_RED, "colors"): COLOR_SCHEMA, - vol.Inclusive(CONF_GREEN, "colors"): COLOR_SCHEMA, - vol.Inclusive(CONF_BLUE, "colors"): COLOR_SCHEMA, - vol.Optional(CONF_WHITE): COLOR_SCHEMA, - }, - vol.Exclusive(CONF_COLOR_ADDRESS, "color"): cv.string, - vol.Optional(CONF_COLOR_STATE_ADDRESS): cv.string, - vol.Optional(CONF_COLOR_TEMP_ADDRESS): cv.string, - vol.Optional(CONF_COLOR_TEMP_STATE_ADDRESS): cv.string, - vol.Optional( - CONF_COLOR_TEMP_MODE, default=DEFAULT_COLOR_TEMP_MODE - ): vol.All(vol.Upper, cv.enum(ColorTempModes)), - vol.Exclusive(CONF_RGBW_ADDRESS, "color"): cv.string, - vol.Optional(CONF_RGBW_STATE_ADDRESS): cv.string, - vol.Optional(CONF_MIN_KELVIN, default=DEFAULT_MIN_KELVIN): vol.All( - vol.Coerce(int), vol.Range(min=1) - ), - vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All( - vol.Coerce(int), vol.Range(min=1) - ), - } - ), - vol.Any( - # either global "address" or all addresses for individual colors are required - vol.Schema( - { - vol.Required(CONF_INDIVIDUAL_COLORS): { - vol.Required(CONF_RED): {vol.Required(CONF_ADDRESS): object}, - vol.Required(CONF_GREEN): {vol.Required(CONF_ADDRESS): object}, - vol.Required(CONF_BLUE): {vol.Required(CONF_ADDRESS): object}, - }, - }, - extra=vol.ALLOW_EXTRA, - ), - vol.Schema( - { - vol.Required(CONF_ADDRESS): object, - }, - extra=vol.ALLOW_EXTRA, - ), - ), - ) - - class ClimateSchema: """Voluptuous schema for KNX climate devices.""" @@ -266,25 +165,27 @@ class ClimateSchema: vol.Optional( CONF_TEMPERATURE_STEP, default=DEFAULT_TEMPERATURE_STEP ): vol.All(float, vol.Range(min=0, max=2)), - vol.Required(CONF_TEMPERATURE_ADDRESS): cv.string, - vol.Required(CONF_TARGET_TEMPERATURE_STATE_ADDRESS): cv.string, - vol.Optional(CONF_TARGET_TEMPERATURE_ADDRESS): cv.string, - vol.Optional(CONF_SETPOINT_SHIFT_ADDRESS): cv.string, - vol.Optional(CONF_SETPOINT_SHIFT_STATE_ADDRESS): cv.string, - vol.Optional(CONF_OPERATION_MODE_ADDRESS): cv.string, - vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): cv.string, - vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): cv.string, - vol.Optional(CONF_CONTROLLER_STATUS_STATE_ADDRESS): cv.string, - vol.Optional(CONF_CONTROLLER_MODE_ADDRESS): cv.string, - vol.Optional(CONF_CONTROLLER_MODE_STATE_ADDRESS): cv.string, - vol.Optional(CONF_HEAT_COOL_ADDRESS): cv.string, - vol.Optional(CONF_HEAT_COOL_STATE_ADDRESS): cv.string, - vol.Optional(CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS): cv.string, - vol.Optional(CONF_OPERATION_MODE_NIGHT_ADDRESS): cv.string, - vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): cv.string, - vol.Optional(CONF_OPERATION_MODE_STANDBY_ADDRESS): cv.string, - vol.Optional(CONF_ON_OFF_ADDRESS): cv.string, - vol.Optional(CONF_ON_OFF_STATE_ADDRESS): cv.string, + vol.Required(CONF_TEMPERATURE_ADDRESS): ga_validator, + vol.Required(CONF_TARGET_TEMPERATURE_STATE_ADDRESS): ga_validator, + vol.Optional(CONF_TARGET_TEMPERATURE_ADDRESS): ga_validator, + vol.Optional(CONF_SETPOINT_SHIFT_ADDRESS): ga_validator, + vol.Optional(CONF_SETPOINT_SHIFT_STATE_ADDRESS): ga_validator, + vol.Optional(CONF_OPERATION_MODE_ADDRESS): ga_validator, + vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): ga_validator, + vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): ga_validator, + vol.Optional(CONF_CONTROLLER_STATUS_STATE_ADDRESS): ga_validator, + vol.Optional(CONF_CONTROLLER_MODE_ADDRESS): ga_validator, + vol.Optional(CONF_CONTROLLER_MODE_STATE_ADDRESS): ga_validator, + vol.Optional(CONF_HEAT_COOL_ADDRESS): ga_validator, + vol.Optional(CONF_HEAT_COOL_STATE_ADDRESS): ga_validator, + vol.Optional( + CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS + ): ga_validator, + vol.Optional(CONF_OPERATION_MODE_NIGHT_ADDRESS): ga_validator, + vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): ga_validator, + vol.Optional(CONF_OPERATION_MODE_STANDBY_ADDRESS): ga_validator, + vol.Optional(CONF_ON_OFF_ADDRESS): ga_validator, + vol.Optional(CONF_ON_OFF_STATE_ADDRESS): ga_validator, vol.Optional( CONF_ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT ): cv.boolean, @@ -304,19 +205,43 @@ class ClimateSchema: ) -class SwitchSchema: - """Voluptuous schema for KNX switches.""" +class CoverSchema: + """Voluptuous schema for KNX covers.""" - CONF_INVERT = CONF_INVERT - CONF_STATE_ADDRESS = CONF_STATE_ADDRESS + CONF_MOVE_LONG_ADDRESS = "move_long_address" + CONF_MOVE_SHORT_ADDRESS = "move_short_address" + CONF_STOP_ADDRESS = "stop_address" + CONF_POSITION_ADDRESS = "position_address" + CONF_POSITION_STATE_ADDRESS = "position_state_address" + CONF_ANGLE_ADDRESS = "angle_address" + CONF_ANGLE_STATE_ADDRESS = "angle_state_address" + CONF_TRAVELLING_TIME_DOWN = "travelling_time_down" + CONF_TRAVELLING_TIME_UP = "travelling_time_up" + CONF_INVERT_POSITION = "invert_position" + CONF_INVERT_ANGLE = "invert_angle" + + DEFAULT_TRAVEL_TIME = 25 + DEFAULT_NAME = "KNX Cover" - DEFAULT_NAME = "KNX Switch" SCHEMA = vol.Schema( { - vol.Required(CONF_ADDRESS): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_STATE_ADDRESS): cv.string, - vol.Optional(CONF_INVERT): cv.boolean, + vol.Optional(CONF_MOVE_LONG_ADDRESS): ga_validator, + vol.Optional(CONF_MOVE_SHORT_ADDRESS): ga_validator, + vol.Optional(CONF_STOP_ADDRESS): ga_validator, + vol.Optional(CONF_POSITION_ADDRESS): ga_validator, + vol.Optional(CONF_POSITION_STATE_ADDRESS): ga_validator, + vol.Optional(CONF_ANGLE_ADDRESS): ga_validator, + vol.Optional(CONF_ANGLE_STATE_ADDRESS): ga_validator, + vol.Optional( + CONF_TRAVELLING_TIME_DOWN, default=DEFAULT_TRAVEL_TIME + ): cv.positive_float, + vol.Optional( + CONF_TRAVELLING_TIME_UP, default=DEFAULT_TRAVEL_TIME + ): cv.positive_float, + vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, + vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean, + vol.Optional(CONF_DEVICE_CLASS): cv.string, } ) @@ -331,14 +256,125 @@ class ExposeSchema: SCHEMA = vol.Schema( { vol.Required(CONF_KNX_EXPOSE_TYPE): vol.Any(int, float, str), + vol.Required(CONF_ADDRESS): ga_validator, vol.Optional(CONF_ENTITY_ID): cv.entity_id, vol.Optional(CONF_KNX_EXPOSE_ATTRIBUTE): cv.string, vol.Optional(CONF_KNX_EXPOSE_DEFAULT): cv.match_all, - vol.Required(CONF_ADDRESS): cv.string, } ) +class FanSchema: + """Voluptuous schema for KNX fans.""" + + CONF_STATE_ADDRESS = CONF_STATE_ADDRESS + CONF_OSCILLATION_ADDRESS = "oscillation_address" + CONF_OSCILLATION_STATE_ADDRESS = "oscillation_state_address" + CONF_MAX_STEP = "max_step" + + DEFAULT_NAME = "KNX Fan" + + SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_ADDRESS): ga_validator, + vol.Optional(CONF_STATE_ADDRESS): ga_validator, + vol.Optional(CONF_OSCILLATION_ADDRESS): ga_validator, + vol.Optional(CONF_OSCILLATION_STATE_ADDRESS): ga_validator, + vol.Optional(CONF_MAX_STEP): cv.byte, + } + ) + + +class LightSchema: + """Voluptuous schema for KNX lights.""" + + CONF_STATE_ADDRESS = CONF_STATE_ADDRESS + CONF_BRIGHTNESS_ADDRESS = "brightness_address" + CONF_BRIGHTNESS_STATE_ADDRESS = "brightness_state_address" + CONF_COLOR_ADDRESS = "color_address" + CONF_COLOR_STATE_ADDRESS = "color_state_address" + CONF_COLOR_TEMP_ADDRESS = "color_temperature_address" + CONF_COLOR_TEMP_STATE_ADDRESS = "color_temperature_state_address" + CONF_COLOR_TEMP_MODE = "color_temperature_mode" + CONF_RGBW_ADDRESS = "rgbw_address" + CONF_RGBW_STATE_ADDRESS = "rgbw_state_address" + CONF_MIN_KELVIN = "min_kelvin" + CONF_MAX_KELVIN = "max_kelvin" + + DEFAULT_NAME = "KNX Light" + DEFAULT_COLOR_TEMP_MODE = "absolute" + DEFAULT_MIN_KELVIN = 2700 # 370 mireds + DEFAULT_MAX_KELVIN = 6000 # 166 mireds + + CONF_INDIVIDUAL_COLORS = "individual_colors" + CONF_RED = "red" + CONF_GREEN = "green" + CONF_BLUE = "blue" + CONF_WHITE = "white" + + COLOR_SCHEMA = vol.Schema( + { + vol.Optional(CONF_ADDRESS): ga_validator, + vol.Optional(CONF_STATE_ADDRESS): ga_validator, + vol.Required(CONF_BRIGHTNESS_ADDRESS): ga_validator, + vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): ga_validator, + } + ) + + SCHEMA = vol.All( + vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ADDRESS): ga_validator, + vol.Optional(CONF_STATE_ADDRESS): ga_validator, + vol.Optional(CONF_BRIGHTNESS_ADDRESS): ga_validator, + vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): ga_validator, + vol.Exclusive(CONF_INDIVIDUAL_COLORS, "color"): { + vol.Inclusive(CONF_RED, "colors"): COLOR_SCHEMA, + vol.Inclusive(CONF_GREEN, "colors"): COLOR_SCHEMA, + vol.Inclusive(CONF_BLUE, "colors"): COLOR_SCHEMA, + vol.Optional(CONF_WHITE): COLOR_SCHEMA, + }, + vol.Exclusive(CONF_COLOR_ADDRESS, "color"): ga_validator, + vol.Optional(CONF_COLOR_STATE_ADDRESS): ga_validator, + vol.Optional(CONF_COLOR_TEMP_ADDRESS): ga_validator, + vol.Optional(CONF_COLOR_TEMP_STATE_ADDRESS): ga_validator, + vol.Optional( + CONF_COLOR_TEMP_MODE, default=DEFAULT_COLOR_TEMP_MODE + ): vol.All(vol.Upper, cv.enum(ColorTempModes)), + vol.Exclusive(CONF_RGBW_ADDRESS, "color"): ga_validator, + vol.Optional(CONF_RGBW_STATE_ADDRESS): ga_validator, + vol.Optional(CONF_MIN_KELVIN, default=DEFAULT_MIN_KELVIN): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + } + ), + vol.Any( + # either global "address" or all addresses for individual colors are required + vol.Schema( + { + vol.Required(CONF_INDIVIDUAL_COLORS): { + vol.Required(CONF_RED): {vol.Required(CONF_ADDRESS): object}, + vol.Required(CONF_GREEN): {vol.Required(CONF_ADDRESS): object}, + vol.Required(CONF_BLUE): {vol.Required(CONF_ADDRESS): object}, + }, + }, + extra=vol.ALLOW_EXTRA, + ), + vol.Schema( + { + vol.Required(CONF_ADDRESS): object, + }, + extra=vol.ALLOW_EXTRA, + ), + ), + ) + + class NotifySchema: """Voluptuous schema for KNX notifications.""" @@ -346,8 +382,23 @@ class NotifySchema: SCHEMA = vol.Schema( { - vol.Required(CONF_ADDRESS): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_ADDRESS): ga_validator, + } + ) + + +class SceneSchema: + """Voluptuous schema for KNX scenes.""" + + CONF_SCENE_NUMBER = "scene_number" + + DEFAULT_NAME = "KNX SCENE" + SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_ADDRESS): ga_validator, + vol.Required(CONF_SCENE_NUMBER): cv.positive_int, } ) @@ -363,29 +414,27 @@ class SensorSchema: SCHEMA = vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_SYNC_STATE, default=True): vol.Any( - vol.All(vol.Coerce(int), vol.Range(min=2, max=1440)), - cv.boolean, - cv.string, - ), + vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, vol.Optional(CONF_ALWAYS_CALLBACK, default=False): cv.boolean, - vol.Required(CONF_STATE_ADDRESS): cv.string, + vol.Required(CONF_STATE_ADDRESS): ga_validator, vol.Required(CONF_TYPE): vol.Any(int, float, str), } ) -class SceneSchema: - """Voluptuous schema for KNX scenes.""" +class SwitchSchema: + """Voluptuous schema for KNX switches.""" - CONF_SCENE_NUMBER = "scene_number" + CONF_INVERT = CONF_INVERT + CONF_STATE_ADDRESS = CONF_STATE_ADDRESS - DEFAULT_NAME = "KNX SCENE" + DEFAULT_NAME = "KNX Switch" SCHEMA = vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_ADDRESS): cv.string, - vol.Required(CONF_SCENE_NUMBER): cv.positive_int, + vol.Required(CONF_ADDRESS): ga_validator, + vol.Optional(CONF_STATE_ADDRESS): ga_validator, + vol.Optional(CONF_INVERT): cv.boolean, } ) @@ -414,46 +463,20 @@ class WeatherSchema: SCHEMA = vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_SYNC_STATE, default=True): vol.Any( - vol.All(vol.Coerce(int), vol.Range(min=2, max=1440)), - cv.boolean, - cv.string, - ), + vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, vol.Optional(CONF_KNX_CREATE_SENSORS, default=False): cv.boolean, - vol.Required(CONF_KNX_TEMPERATURE_ADDRESS): cv.string, - vol.Optional(CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS): cv.string, - vol.Optional(CONF_KNX_BRIGHTNESS_EAST_ADDRESS): cv.string, - vol.Optional(CONF_KNX_BRIGHTNESS_WEST_ADDRESS): cv.string, - vol.Optional(CONF_KNX_BRIGHTNESS_NORTH_ADDRESS): cv.string, - vol.Optional(CONF_KNX_WIND_SPEED_ADDRESS): cv.string, - vol.Optional(CONF_KNX_WIND_BEARING_ADDRESS): cv.string, - vol.Optional(CONF_KNX_RAIN_ALARM_ADDRESS): cv.string, - vol.Optional(CONF_KNX_FROST_ALARM_ADDRESS): cv.string, - vol.Optional(CONF_KNX_WIND_ALARM_ADDRESS): cv.string, - vol.Optional(CONF_KNX_DAY_NIGHT_ADDRESS): cv.string, - vol.Optional(CONF_KNX_AIR_PRESSURE_ADDRESS): cv.string, - vol.Optional(CONF_KNX_HUMIDITY_ADDRESS): cv.string, - } - ) - - -class FanSchema: - """Voluptuous schema for KNX fans.""" - - CONF_STATE_ADDRESS = CONF_STATE_ADDRESS - CONF_OSCILLATION_ADDRESS = "oscillation_address" - CONF_OSCILLATION_STATE_ADDRESS = "oscillation_state_address" - CONF_MAX_STEP = "max_step" - - DEFAULT_NAME = "KNX Fan" - - SCHEMA = vol.Schema( - { - vol.Required(CONF_ADDRESS): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_STATE_ADDRESS): cv.string, - vol.Optional(CONF_OSCILLATION_ADDRESS): cv.string, - vol.Optional(CONF_OSCILLATION_STATE_ADDRESS): cv.string, - vol.Optional(CONF_MAX_STEP): cv.byte, + vol.Required(CONF_KNX_TEMPERATURE_ADDRESS): ga_validator, + vol.Optional(CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS): ga_validator, + vol.Optional(CONF_KNX_BRIGHTNESS_EAST_ADDRESS): ga_validator, + vol.Optional(CONF_KNX_BRIGHTNESS_WEST_ADDRESS): ga_validator, + vol.Optional(CONF_KNX_BRIGHTNESS_NORTH_ADDRESS): ga_validator, + vol.Optional(CONF_KNX_WIND_SPEED_ADDRESS): ga_validator, + vol.Optional(CONF_KNX_WIND_BEARING_ADDRESS): ga_validator, + vol.Optional(CONF_KNX_RAIN_ALARM_ADDRESS): ga_validator, + vol.Optional(CONF_KNX_FROST_ALARM_ADDRESS): ga_validator, + vol.Optional(CONF_KNX_WIND_ALARM_ADDRESS): ga_validator, + vol.Optional(CONF_KNX_DAY_NIGHT_ADDRESS): ga_validator, + vol.Optional(CONF_KNX_AIR_PRESSURE_ADDRESS): ga_validator, + vol.Optional(CONF_KNX_HUMIDITY_ADDRESS): ga_validator, } ) From eccdae60bfb72d3af1c137e4629c8476caffb34c Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 24 Feb 2021 03:34:27 -0500 Subject: [PATCH 690/796] Add ClimaCell weather integration (#36547) Co-authored-by: Martin Hjelmare --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/climacell/__init__.py | 262 +++++++++++++++ .../components/climacell/config_flow.py | 146 +++++++++ homeassistant/components/climacell/const.py | 79 +++++ .../components/climacell/manifest.json | 8 + .../components/climacell/strings.json | 34 ++ homeassistant/components/climacell/weather.py | 305 ++++++++++++++++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/climacell/__init__.py | 1 + tests/components/climacell/conftest.py | 42 +++ tests/components/climacell/const.py | 9 + .../components/climacell/test_config_flow.py | 167 ++++++++++ tests/components/climacell/test_init.py | 82 +++++ 16 files changed, 1144 insertions(+) create mode 100644 homeassistant/components/climacell/__init__.py create mode 100644 homeassistant/components/climacell/config_flow.py create mode 100644 homeassistant/components/climacell/const.py create mode 100644 homeassistant/components/climacell/manifest.json create mode 100644 homeassistant/components/climacell/strings.json create mode 100644 homeassistant/components/climacell/weather.py create mode 100644 tests/components/climacell/__init__.py create mode 100644 tests/components/climacell/conftest.py create mode 100644 tests/components/climacell/const.py create mode 100644 tests/components/climacell/test_config_flow.py create mode 100644 tests/components/climacell/test_init.py diff --git a/.coveragerc b/.coveragerc index 899577f2acf..c0a28f70a4f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -145,6 +145,7 @@ omit = homeassistant/components/clickatell/notify.py homeassistant/components/clicksend/notify.py homeassistant/components/clicksend_tts/notify.py + homeassistant/components/climacell/weather.py homeassistant/components/cmus/media_player.py homeassistant/components/co2signal/* homeassistant/components/coinbase/* diff --git a/CODEOWNERS b/CODEOWNERS index e4e2ab59615..2db89f09948 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -83,6 +83,7 @@ homeassistant/components/circuit/* @braam homeassistant/components/cisco_ios/* @fbradyirl homeassistant/components/cisco_mobility_express/* @fbradyirl homeassistant/components/cisco_webex_teams/* @fbradyirl +homeassistant/components/climacell/* @raman325 homeassistant/components/cloud/* @home-assistant/cloud homeassistant/components/cloudflare/* @ludeeus @ctalkington homeassistant/components/color_extractor/* @GenericStudent diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py new file mode 100644 index 00000000000..d6bf0ec4e12 --- /dev/null +++ b/homeassistant/components/climacell/__init__.py @@ -0,0 +1,262 @@ +"""The ClimaCell integration.""" +import asyncio +from datetime import timedelta +import logging +from math import ceil +from typing import Any, Dict, Optional, Union + +from pyclimacell import ClimaCell +from pyclimacell.const import ( + FORECAST_DAILY, + FORECAST_HOURLY, + FORECAST_NOWCAST, + REALTIME, +) +from pyclimacell.pyclimacell import ( + CantConnectException, + InvalidAPIKeyException, + RateLimitedException, + UnknownException, +) + +from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import ( + ATTRIBUTION, + CONF_TIMESTEP, + CURRENT, + DAILY, + DEFAULT_TIMESTEP, + DOMAIN, + FORECASTS, + HOURLY, + MAX_REQUESTS_PER_DAY, + NOWCAST, +) + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [WEATHER_DOMAIN] + + +def _set_update_interval( + hass: HomeAssistantType, current_entry: ConfigEntry +) -> timedelta: + """Recalculate update_interval based on existing ClimaCell instances and update them.""" + # We check how many ClimaCell configured instances are using the same API key and + # calculate interval to not exceed allowed numbers of requests. Divide 90% of + # MAX_REQUESTS_PER_DAY by 4 because every update requires four API calls and we want + # a buffer in the number of API calls left at the end of the day. + other_instance_entry_ids = [ + entry.entry_id + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.entry_id != current_entry.entry_id + and entry.data[CONF_API_KEY] == current_entry.data[CONF_API_KEY] + ] + + interval = timedelta( + minutes=( + ceil( + (24 * 60 * (len(other_instance_entry_ids) + 1) * 4) + / (MAX_REQUESTS_PER_DAY * 0.9) + ) + ) + ) + + for entry_id in other_instance_entry_ids: + if entry_id in hass.data[DOMAIN]: + hass.data[DOMAIN][entry_id].update_interval = interval + + return interval + + +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: + """Set up the ClimaCell API component.""" + return True + + +async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: + """Set up ClimaCell API from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + + # If config entry options not set up, set them up + if not config_entry.options: + hass.config_entries.async_update_entry( + config_entry, + options={ + CONF_TIMESTEP: DEFAULT_TIMESTEP, + }, + ) + + coordinator = ClimaCellDataUpdateCoordinator( + hass, + config_entry, + ClimaCell( + config_entry.data[CONF_API_KEY], + config_entry.data.get(CONF_LATITUDE, hass.config.latitude), + config_entry.data.get(CONF_LONGITUDE, hass.config.longitude), + session=async_get_clientsession(hass), + ), + _set_update_interval(hass, config_entry), + ) + + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + hass.data[DOMAIN][config_entry.entry_id] = coordinator + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, component) + ) + + return True + + +async def async_unload_entry( + hass: HomeAssistantType, config_entry: ConfigEntry +) -> bool: + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in PLATFORMS + ] + ) + ) + + hass.data[DOMAIN].pop(config_entry.entry_id) + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) + + return unload_ok + + +class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator): + """Define an object to hold ClimaCell data.""" + + def __init__( + self, + hass: HomeAssistantType, + config_entry: ConfigEntry, + api: ClimaCell, + update_interval: timedelta, + ) -> None: + """Initialize.""" + + self._config_entry = config_entry + self._api = api + self.name = config_entry.data[CONF_NAME] + self.data = {CURRENT: {}, FORECASTS: {}} + + super().__init__( + hass, + _LOGGER, + name=config_entry.data[CONF_NAME], + update_interval=update_interval, + ) + + async def _async_update_data(self) -> Dict[str, Any]: + """Update data via library.""" + data = {FORECASTS: {}} + try: + data[CURRENT] = await self._api.realtime( + self._api.available_fields(REALTIME) + ) + data[FORECASTS][HOURLY] = await self._api.forecast_hourly( + self._api.available_fields(FORECAST_HOURLY), + None, + timedelta(hours=24), + ) + + data[FORECASTS][DAILY] = await self._api.forecast_daily( + self._api.available_fields(FORECAST_DAILY), None, timedelta(days=14) + ) + + data[FORECASTS][NOWCAST] = await self._api.forecast_nowcast( + self._api.available_fields(FORECAST_NOWCAST), + None, + timedelta( + minutes=min(300, self._config_entry.options[CONF_TIMESTEP] * 30) + ), + self._config_entry.options[CONF_TIMESTEP], + ) + except ( + CantConnectException, + InvalidAPIKeyException, + RateLimitedException, + UnknownException, + ) as error: + raise UpdateFailed from error + + return data + + +class ClimaCellEntity(CoordinatorEntity): + """Base ClimaCell Entity.""" + + def __init__( + self, config_entry: ConfigEntry, coordinator: ClimaCellDataUpdateCoordinator + ) -> None: + """Initialize ClimaCell Entity.""" + super().__init__(coordinator) + self._config_entry = config_entry + + @staticmethod + def _get_cc_value( + weather_dict: Dict[str, Any], key: str + ) -> Optional[Union[int, float, str]]: + """Return property from weather_dict.""" + items = weather_dict.get(key, {}) + # Handle cases where value returned is a list. + # Optimistically find the best value to return. + if isinstance(items, list): + if len(items) == 1: + return items[0].get("value") + return next( + (item.get("value") for item in items if "max" in item), + next( + (item.get("value") for item in items if "min" in item), + items[0].get("value", None), + ), + ) + + return items.get("value") + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._config_entry.data[CONF_NAME] + + @property + def unique_id(self) -> str: + """Return the unique id of the entity.""" + return self._config_entry.unique_id + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def device_info(self) -> Dict[str, Any]: + """Return device registry information.""" + return { + "identifiers": {(DOMAIN, self._config_entry.data[CONF_API_KEY])}, + "name": self.name, + "manufacturer": "ClimaCell", + "entry_type": "service", + } diff --git a/homeassistant/components/climacell/config_flow.py b/homeassistant/components/climacell/config_flow.py new file mode 100644 index 00000000000..09e02f3f559 --- /dev/null +++ b/homeassistant/components/climacell/config_flow.py @@ -0,0 +1,146 @@ +"""Config flow for ClimaCell integration.""" +import logging +from typing import Any, Dict + +from pyclimacell import ClimaCell +from pyclimacell.const import REALTIME +from pyclimacell.exceptions import ( + CantConnectException, + InvalidAPIKeyException, + RateLimitedException, +) +import voluptuous as vol + +from homeassistant import config_entries, core +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType + +from .const import CONF_TIMESTEP, DEFAULT_NAME, DEFAULT_TIMESTEP +from .const import DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +def _get_config_schema( + hass: core.HomeAssistant, input_dict: Dict[str, Any] = None +) -> vol.Schema: + """ + Return schema defaults for init step based on user input/config dict. + + Retain info already provided for future form views by setting them as + defaults in schema. + """ + if input_dict is None: + input_dict = {} + + return vol.Schema( + { + vol.Required( + CONF_NAME, default=input_dict.get(CONF_NAME, DEFAULT_NAME) + ): str, + vol.Required(CONF_API_KEY, default=input_dict.get(CONF_API_KEY)): str, + vol.Inclusive( + CONF_LATITUDE, + "location", + default=input_dict.get(CONF_LATITUDE, hass.config.latitude), + ): cv.latitude, + vol.Inclusive( + CONF_LONGITUDE, + "location", + default=input_dict.get(CONF_LONGITUDE, hass.config.longitude), + ): cv.longitude, + }, + extra=vol.REMOVE_EXTRA, + ) + + +def _get_unique_id(hass: HomeAssistantType, input_dict: Dict[str, Any]): + """Return unique ID from config data.""" + return ( + f"{input_dict[CONF_API_KEY]}" + f"_{input_dict.get(CONF_LATITUDE, hass.config.latitude)}" + f"_{input_dict.get(CONF_LONGITUDE, hass.config.longitude)}" + ) + + +class ClimaCellOptionsConfigFlow(config_entries.OptionsFlow): + """Handle ClimaCell options.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize ClimaCell options flow.""" + self._config_entry = config_entry + + async def async_step_init( + self, user_input: Dict[str, Any] = None + ) -> Dict[str, Any]: + """Manage the ClimaCell options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options_schema = { + vol.Required( + CONF_TIMESTEP, + default=self._config_entry.options.get(CONF_TIMESTEP, DEFAULT_TIMESTEP), + ): vol.All(vol.Coerce(int), vol.Range(min=1, max=60)), + } + + return self.async_show_form( + step_id="init", data_schema=vol.Schema(options_schema) + ) + + +class ClimaCellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for ClimaCell Weather API.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> ClimaCellOptionsConfigFlow: + """Get the options flow for this handler.""" + return ClimaCellOptionsConfigFlow(config_entry) + + async def async_step_user( + self, user_input: Dict[str, Any] = None + ) -> Dict[str, Any]: + """Handle the initial step.""" + assert self.hass + errors = {} + if user_input is not None: + await self.async_set_unique_id( + unique_id=_get_unique_id(self.hass, user_input) + ) + self._abort_if_unique_id_configured() + + try: + await ClimaCell( + user_input[CONF_API_KEY], + str(user_input.get(CONF_LATITUDE, self.hass.config.latitude)), + str(user_input.get(CONF_LONGITUDE, self.hass.config.longitude)), + session=async_get_clientsession(self.hass), + ).realtime(ClimaCell.first_field(REALTIME)) + + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) + except CantConnectException: + errors["base"] = "cannot_connect" + except InvalidAPIKeyException: + errors[CONF_API_KEY] = "invalid_api_key" + except RateLimitedException: + errors[CONF_API_KEY] = "rate_limited" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", + data_schema=_get_config_schema(self.hass, user_input), + errors=errors, + ) diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py new file mode 100644 index 00000000000..28117a164f8 --- /dev/null +++ b/homeassistant/components/climacell/const.py @@ -0,0 +1,79 @@ +"""Constants for the ClimaCell integration.""" + +from homeassistant.components.weather import ( + ATTR_CONDITION_CLEAR_NIGHT, + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_FOG, + ATTR_CONDITION_HAIL, + ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_SUNNY, +) + +CONF_TIMESTEP = "timestep" + +DAILY = "daily" +HOURLY = "hourly" +NOWCAST = "nowcast" +FORECAST_TYPES = [DAILY, HOURLY, NOWCAST] + +CURRENT = "current" +FORECASTS = "forecasts" + +DEFAULT_NAME = "ClimaCell" +DEFAULT_TIMESTEP = 15 +DEFAULT_FORECAST_TYPE = DAILY +DOMAIN = "climacell" +ATTRIBUTION = "Powered by ClimaCell" + +MAX_REQUESTS_PER_DAY = 1000 + +CONDITIONS = { + "freezing_rain_heavy": ATTR_CONDITION_SNOWY_RAINY, + "freezing_rain": ATTR_CONDITION_SNOWY_RAINY, + "freezing_rain_light": ATTR_CONDITION_SNOWY_RAINY, + "freezing_drizzle": ATTR_CONDITION_SNOWY_RAINY, + "ice_pellets_heavy": ATTR_CONDITION_HAIL, + "ice_pellets": ATTR_CONDITION_HAIL, + "ice_pellets_light": ATTR_CONDITION_HAIL, + "snow_heavy": ATTR_CONDITION_SNOWY, + "snow": ATTR_CONDITION_SNOWY, + "snow_light": ATTR_CONDITION_SNOWY, + "flurries": ATTR_CONDITION_SNOWY, + "tstorm": ATTR_CONDITION_LIGHTNING, + "rain_heavy": ATTR_CONDITION_POURING, + "rain": ATTR_CONDITION_RAINY, + "rain_light": ATTR_CONDITION_RAINY, + "drizzle": ATTR_CONDITION_RAINY, + "fog_light": ATTR_CONDITION_FOG, + "fog": ATTR_CONDITION_FOG, + "cloudy": ATTR_CONDITION_CLOUDY, + "mostly_cloudy": ATTR_CONDITION_CLOUDY, + "partly_cloudy": ATTR_CONDITION_PARTLYCLOUDY, +} + +CLEAR_CONDITIONS = {"night": ATTR_CONDITION_CLEAR_NIGHT, "day": ATTR_CONDITION_SUNNY} + +CC_ATTR_TIMESTAMP = "observation_time" +CC_ATTR_TEMPERATURE = "temp" +CC_ATTR_TEMPERATURE_HIGH = "max" +CC_ATTR_TEMPERATURE_LOW = "min" +CC_ATTR_PRESSURE = "baro_pressure" +CC_ATTR_HUMIDITY = "humidity" +CC_ATTR_WIND_SPEED = "wind_speed" +CC_ATTR_WIND_DIRECTION = "wind_direction" +CC_ATTR_OZONE = "o3" +CC_ATTR_CONDITION = "weather_code" +CC_ATTR_VISIBILITY = "visibility" +CC_ATTR_PRECIPITATION = "precipitation" +CC_ATTR_PRECIPITATION_DAILY = "precipitation_accumulation" +CC_ATTR_PRECIPITATION_PROBABILITY = "precipitation_probability" +CC_ATTR_PM_2_5 = "pm25" +CC_ATTR_PM_10 = "pm10" +CC_ATTR_CARBON_MONOXIDE = "co" +CC_ATTR_SULPHUR_DIOXIDE = "so2" +CC_ATTR_NITROGEN_DIOXIDE = "no2" diff --git a/homeassistant/components/climacell/manifest.json b/homeassistant/components/climacell/manifest.json new file mode 100644 index 00000000000..f410c2275a9 --- /dev/null +++ b/homeassistant/components/climacell/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "climacell", + "name": "ClimaCell", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/climacell", + "requirements": ["pyclimacell==0.14.0"], + "codeowners": ["@raman325"] +} diff --git a/homeassistant/components/climacell/strings.json b/homeassistant/components/climacell/strings.json new file mode 100644 index 00000000000..45a1d5b7404 --- /dev/null +++ b/homeassistant/components/climacell/strings.json @@ -0,0 +1,34 @@ +{ + "title": "ClimaCell", + "config": { + "step": { + "user": { + "description": "If [%key:component::climacell::config::step::user::data::latitude%] and [%key:component::climacell::config::step::user::data::longitude%] are not provided, the default values in the Home Assistant configuration will be used. An entity will be created for each forecast type but only the ones you select will be enabled by default.", + "data": { + "name": "Name", + "api_key": "[%key:common::config_flow::data::api_key%]", + "latitude": "Latitude", + "longitude": "Longitude" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "rate_limited": "Currently rate limited, please try again later." + } + }, + "options": { + "step": { + "init": { + "title": "Update [%key:component::climacell::title%] Options", + "description": "If you choose to enable the `nowcast` forecast entity, you can configure the number of minutes between each forecast. The number of forecasts provided depends on the number of minutes chosen between forecasts.", + "data": { + "timestep": "Min. Between NowCast Forecasts", + "forecast_types": "Forecast Type(s)" + } + } + } + } +} diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py new file mode 100644 index 00000000000..a4802586bf1 --- /dev/null +++ b/homeassistant/components/climacell/weather.py @@ -0,0 +1,305 @@ +"""Weather component that handles meteorological data for your location.""" +import logging +from typing import Any, Callable, Dict, List, Optional + +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, + WeatherEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + LENGTH_FEET, + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_MILES, + PRESSURE_HPA, + PRESSURE_INHG, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.sun import is_up +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import dt as dt_util +from homeassistant.util.distance import convert as distance_convert +from homeassistant.util.pressure import convert as pressure_convert +from homeassistant.util.temperature import convert as temp_convert + +from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity +from .const import ( + CC_ATTR_CONDITION, + CC_ATTR_HUMIDITY, + CC_ATTR_OZONE, + CC_ATTR_PRECIPITATION, + CC_ATTR_PRECIPITATION_DAILY, + CC_ATTR_PRECIPITATION_PROBABILITY, + CC_ATTR_PRESSURE, + CC_ATTR_TEMPERATURE, + CC_ATTR_TEMPERATURE_HIGH, + CC_ATTR_TEMPERATURE_LOW, + CC_ATTR_TIMESTAMP, + CC_ATTR_VISIBILITY, + CC_ATTR_WIND_DIRECTION, + CC_ATTR_WIND_SPEED, + CLEAR_CONDITIONS, + CONDITIONS, + CONF_TIMESTEP, + CURRENT, + DAILY, + DEFAULT_FORECAST_TYPE, + DOMAIN, + FORECASTS, + HOURLY, + NOWCAST, +) + +# mypy: allow-untyped-defs, no-check-untyped-defs + +_LOGGER = logging.getLogger(__name__) + + +def _translate_condition( + condition: Optional[str], sun_is_up: bool = True +) -> Optional[str]: + """Translate ClimaCell condition into an HA condition.""" + if not condition: + return None + if "clear" in condition.lower(): + if sun_is_up: + return CLEAR_CONDITIONS["day"] + return CLEAR_CONDITIONS["night"] + return CONDITIONS[condition] + + +def _forecast_dict( + hass: HomeAssistantType, + time: str, + use_datetime: bool, + condition: str, + precipitation: Optional[float], + precipitation_probability: Optional[float], + temp: Optional[float], + temp_low: Optional[float], + wind_direction: Optional[float], + wind_speed: Optional[float], +) -> Dict[str, Any]: + """Return formatted Forecast dict from ClimaCell forecast data.""" + if use_datetime: + translated_condition = _translate_condition( + condition, + is_up(hass, dt_util.as_utc(dt_util.parse_datetime(time))), + ) + else: + translated_condition = _translate_condition(condition, True) + + if hass.config.units.is_metric: + if precipitation: + precipitation = ( + distance_convert(precipitation / 12, LENGTH_FEET, LENGTH_METERS) * 1000 + ) + if temp: + temp = temp_convert(temp, TEMP_FAHRENHEIT, TEMP_CELSIUS) + if temp_low: + temp_low = temp_convert(temp_low, TEMP_FAHRENHEIT, TEMP_CELSIUS) + if wind_speed: + wind_speed = distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS) + + data = { + ATTR_FORECAST_TIME: time, + ATTR_FORECAST_CONDITION: translated_condition, + ATTR_FORECAST_PRECIPITATION: precipitation, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability, + ATTR_FORECAST_TEMP: temp, + ATTR_FORECAST_TEMP_LOW: temp_low, + ATTR_FORECAST_WIND_BEARING: wind_direction, + ATTR_FORECAST_WIND_SPEED: wind_speed, + } + + return {k: v for k, v in data.items() if v is not None} + + +async def async_setup_entry( + hass: HomeAssistantType, + config_entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up a config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + + entities = [ + ClimaCellWeatherEntity(config_entry, coordinator, forecast_type) + for forecast_type in [DAILY, HOURLY, NOWCAST] + ] + async_add_entities(entities) + + +class ClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): + """Entity that talks to ClimaCell API to retrieve weather data.""" + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: ClimaCellDataUpdateCoordinator, + forecast_type: str, + ) -> None: + """Initialize ClimaCell weather entity.""" + super().__init__(config_entry, coordinator) + self.forecast_type = forecast_type + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + if self.forecast_type == DEFAULT_FORECAST_TYPE: + return True + + return False + + @property + def name(self) -> str: + """Return the name of the entity.""" + return f"{super().name} - {self.forecast_type.title()}" + + @property + def unique_id(self) -> str: + """Return the unique id of the entity.""" + return f"{super().unique_id}_{self.forecast_type}" + + @property + def temperature(self): + """Return the platform temperature.""" + return self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_TEMPERATURE) + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_FAHRENHEIT + + @property + def pressure(self): + """Return the pressure.""" + pressure = self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_PRESSURE) + if self.hass.config.units.is_metric and pressure: + return pressure_convert(pressure, PRESSURE_INHG, PRESSURE_HPA) + return pressure + + @property + def humidity(self): + """Return the humidity.""" + return self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_HUMIDITY) + + @property + def wind_speed(self): + """Return the wind speed.""" + wind_speed = self._get_cc_value( + self.coordinator.data[CURRENT], CC_ATTR_WIND_SPEED + ) + if self.hass.config.units.is_metric and wind_speed: + return distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS) + return wind_speed + + @property + def wind_bearing(self): + """Return the wind bearing.""" + return self._get_cc_value( + self.coordinator.data[CURRENT], CC_ATTR_WIND_DIRECTION + ) + + @property + def ozone(self): + """Return the O3 (ozone) level.""" + return self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_OZONE) + + @property + def condition(self): + """Return the condition.""" + return _translate_condition( + self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_CONDITION), + is_up(self.hass), + ) + + @property + def visibility(self): + """Return the visibility.""" + visibility = self._get_cc_value( + self.coordinator.data[CURRENT], CC_ATTR_VISIBILITY + ) + if self.hass.config.units.is_metric and visibility: + return distance_convert(visibility, LENGTH_MILES, LENGTH_KILOMETERS) + return visibility + + @property + def forecast(self): + """Return the forecast.""" + # Check if forecasts are available + if not self.coordinator.data[FORECASTS].get(self.forecast_type): + return None + + forecasts = [] + + # Set default values (in cases where keys don't exist), None will be + # returned. Override properties per forecast type as needed + for forecast in self.coordinator.data[FORECASTS][self.forecast_type]: + timestamp = self._get_cc_value(forecast, CC_ATTR_TIMESTAMP) + use_datetime = True + condition = self._get_cc_value(forecast, CC_ATTR_CONDITION) + precipitation = self._get_cc_value(forecast, CC_ATTR_PRECIPITATION) + precipitation_probability = self._get_cc_value( + forecast, CC_ATTR_PRECIPITATION_PROBABILITY + ) + temp = self._get_cc_value(forecast, CC_ATTR_TEMPERATURE) + temp_low = None + wind_direction = self._get_cc_value(forecast, CC_ATTR_WIND_DIRECTION) + wind_speed = self._get_cc_value(forecast, CC_ATTR_WIND_SPEED) + + if self.forecast_type == DAILY: + use_datetime = False + precipitation = self._get_cc_value( + forecast, CC_ATTR_PRECIPITATION_DAILY + ) + temp = next( + ( + self._get_cc_value(item, CC_ATTR_TEMPERATURE_HIGH) + for item in forecast[CC_ATTR_TEMPERATURE] + if "max" in item + ), + temp, + ) + temp_low = next( + ( + self._get_cc_value(item, CC_ATTR_TEMPERATURE_LOW) + for item in forecast[CC_ATTR_TEMPERATURE] + if "max" in item + ), + temp_low, + ) + elif self.forecast_type == NOWCAST: + # Precipitation is forecasted in CONF_TIMESTEP increments but in a + # per hour rate, so value needs to be converted to an amount. + if precipitation: + precipitation = ( + precipitation / 60 * self._config_entry.options[CONF_TIMESTEP] + ) + + forecasts.append( + _forecast_dict( + self.hass, + timestamp, + use_datetime, + condition, + precipitation, + precipitation_probability, + temp, + temp_low, + wind_direction, + wind_speed, + ) + ) + + return forecasts diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index e8e06ed7a15..e0a4fd8cd57 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -40,6 +40,7 @@ FLOWS = [ "canary", "cast", "cert_expiry", + "climacell", "cloudflare", "control4", "coolmaster", diff --git a/requirements_all.txt b/requirements_all.txt index 3ac487c8fb9..60f7997a433 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1313,6 +1313,9 @@ pychromecast==8.1.0 # homeassistant.components.pocketcasts pycketcasts==1.0.0 +# homeassistant.components.climacell +pyclimacell==0.14.0 + # homeassistant.components.cmus pycmus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a434739be14..c21bec2a08f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -693,6 +693,9 @@ pycfdns==1.2.1 # homeassistant.components.cast pychromecast==8.1.0 +# homeassistant.components.climacell +pyclimacell==0.14.0 + # homeassistant.components.comfoconnect pycomfoconnect==0.4 diff --git a/tests/components/climacell/__init__.py b/tests/components/climacell/__init__.py new file mode 100644 index 00000000000..04ebc3c14c3 --- /dev/null +++ b/tests/components/climacell/__init__.py @@ -0,0 +1 @@ +"""Tests for the ClimaCell Weather API integration.""" diff --git a/tests/components/climacell/conftest.py b/tests/components/climacell/conftest.py new file mode 100644 index 00000000000..3666243b4b4 --- /dev/null +++ b/tests/components/climacell/conftest.py @@ -0,0 +1,42 @@ +"""Configure py.test.""" +from unittest.mock import patch + +import pytest + + +@pytest.fixture(name="skip_notifications", autouse=True) +def skip_notifications_fixture(): + """Skip notification calls.""" + with patch("homeassistant.components.persistent_notification.async_create"), patch( + "homeassistant.components.persistent_notification.async_dismiss" + ): + yield + + +@pytest.fixture(name="climacell_config_flow_connect", autouse=True) +def climacell_config_flow_connect(): + """Mock valid climacell config flow setup.""" + with patch( + "homeassistant.components.climacell.config_flow.ClimaCell.realtime", + return_value={}, + ): + yield + + +@pytest.fixture(name="climacell_config_entry_update") +def climacell_config_entry_update_fixture(): + """Mock valid climacell config entry setup.""" + with patch( + "homeassistant.components.climacell.ClimaCell.realtime", + return_value={}, + ), patch( + "homeassistant.components.climacell.ClimaCell.forecast_hourly", + return_value=[], + ), patch( + "homeassistant.components.climacell.ClimaCell.forecast_daily", + return_value=[], + ), patch( + "homeassistant.components.climacell.ClimaCell.forecast_nowcast", + return_value=[], + ): + yield diff --git a/tests/components/climacell/const.py b/tests/components/climacell/const.py new file mode 100644 index 00000000000..ada0ebd1eb5 --- /dev/null +++ b/tests/components/climacell/const.py @@ -0,0 +1,9 @@ +"""Constants for climacell tests.""" + +from homeassistant.const import CONF_API_KEY + +API_KEY = "aa" + +MIN_CONFIG = { + CONF_API_KEY: API_KEY, +} diff --git a/tests/components/climacell/test_config_flow.py b/tests/components/climacell/test_config_flow.py new file mode 100644 index 00000000000..a34bf6fd0fd --- /dev/null +++ b/tests/components/climacell/test_config_flow.py @@ -0,0 +1,167 @@ +"""Test the ClimaCell config flow.""" +import logging +from unittest.mock import patch + +from pyclimacell.exceptions import ( + CantConnectException, + InvalidAPIKeyException, + RateLimitedException, + UnknownException, +) + +from homeassistant import data_entry_flow +from homeassistant.components.climacell.config_flow import ( + _get_config_schema, + _get_unique_id, +) +from homeassistant.components.climacell.const import ( + CONF_TIMESTEP, + DEFAULT_NAME, + DEFAULT_TIMESTEP, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.helpers.typing import HomeAssistantType + +from .const import API_KEY, MIN_CONFIG + +from tests.common import MockConfigEntry + +_LOGGER = logging.getLogger(__name__) + + +async def test_user_flow_minimum_fields(hass: HomeAssistantType) -> None: + """Test user config flow with minimum fields.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=_get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"][CONF_NAME] == DEFAULT_NAME + assert result["data"][CONF_API_KEY] == API_KEY + assert result["data"][CONF_LATITUDE] == hass.config.latitude + assert result["data"][CONF_LONGITUDE] == hass.config.longitude + + +async def test_user_flow_same_unique_ids(hass: HomeAssistantType) -> None: + """Test user config flow with the same unique ID as an existing entry.""" + user_input = _get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG) + MockConfigEntry( + domain=DOMAIN, + data=user_input, + source=SOURCE_USER, + unique_id=_get_unique_id(hass, user_input), + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=user_input, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_user_flow_cannot_connect(hass: HomeAssistantType) -> None: + """Test user config flow when ClimaCell can't connect.""" + with patch( + "homeassistant.components.climacell.config_flow.ClimaCell.realtime", + side_effect=CantConnectException, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=_get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_user_flow_invalid_api(hass: HomeAssistantType) -> None: + """Test user config flow when API key is invalid.""" + with patch( + "homeassistant.components.climacell.config_flow.ClimaCell.realtime", + side_effect=InvalidAPIKeyException, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=_get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} + + +async def test_user_flow_rate_limited(hass: HomeAssistantType) -> None: + """Test user config flow when API key is rate limited.""" + with patch( + "homeassistant.components.climacell.config_flow.ClimaCell.realtime", + side_effect=RateLimitedException, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=_get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_API_KEY: "rate_limited"} + + +async def test_user_flow_unknown_exception(hass: HomeAssistantType) -> None: + """Test user config flow when unknown error occurs.""" + with patch( + "homeassistant.components.climacell.config_flow.ClimaCell.realtime", + side_effect=UnknownException, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=_get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "unknown"} + + +async def test_options_flow(hass: HomeAssistantType) -> None: + """Test options config flow for climacell.""" + user_config = _get_config_schema(hass)(MIN_CONFIG) + entry = MockConfigEntry( + domain=DOMAIN, + data=user_config, + source=SOURCE_USER, + unique_id=_get_unique_id(hass, user_config), + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + assert entry.options[CONF_TIMESTEP] == DEFAULT_TIMESTEP + assert CONF_TIMESTEP not in entry.data + + result = await hass.config_entries.options.async_init(entry.entry_id, data=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_TIMESTEP: 1} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "" + assert result["data"][CONF_TIMESTEP] == 1 + assert entry.options[CONF_TIMESTEP] == 1 diff --git a/tests/components/climacell/test_init.py b/tests/components/climacell/test_init.py new file mode 100644 index 00000000000..1456a068d77 --- /dev/null +++ b/tests/components/climacell/test_init.py @@ -0,0 +1,82 @@ +"""Tests for Climacell init.""" +from datetime import timedelta +import logging +from unittest.mock import patch + +import pytest + +from homeassistant.components.climacell.config_flow import ( + _get_config_schema, + _get_unique_id, +) +from homeassistant.components.climacell.const import DOMAIN +from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import dt as dt_util + +from .const import MIN_CONFIG + +from tests.common import MockConfigEntry, async_fire_time_changed + +_LOGGER = logging.getLogger(__name__) + + +async def test_load_and_unload( + hass: HomeAssistantType, + climacell_config_entry_update: pytest.fixture, +) -> None: + """Test loading and unloading entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=_get_config_schema(hass)(MIN_CONFIG), + unique_id=_get_unique_id(hass, _get_config_schema(hass)(MIN_CONFIG)), + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 1 + + assert await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 0 + + +async def test_update_interval( + hass: HomeAssistantType, + climacell_config_entry_update: pytest.fixture, +) -> None: + """Test that update_interval changes based on number of entries.""" + now = dt_util.utcnow() + async_fire_time_changed(hass, now) + config = _get_config_schema(hass)(MIN_CONFIG) + for i in range(1, 3): + config_entry = MockConfigEntry( + domain=DOMAIN, data=config, unique_id=_get_unique_id(hass, config) + str(i) + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + with patch("homeassistant.components.climacell.ClimaCell.realtime") as mock_api: + # First entry refresh will happen in 7 minutes due to original update interval. + # Next refresh for this entry will happen at 20 minutes due to the update interval + # change. + mock_api.return_value = {} + async_fire_time_changed(hass, now + timedelta(minutes=7)) + await hass.async_block_till_done() + assert mock_api.call_count == 1 + + # Second entry refresh will happen in 13 minutes due to the update interval set + # when it was set up. Next refresh for this entry will happen at 26 minutes due to the + # update interval change. + mock_api.reset_mock() + async_fire_time_changed(hass, now + timedelta(minutes=13)) + await hass.async_block_till_done() + assert not mock_api.call_count == 1 + + # 19 minutes should be after the first update for each config entry and before the + # second update for the first config entry + mock_api.reset_mock() + async_fire_time_changed(hass, now + timedelta(minutes=19)) + await hass.async_block_till_done() + assert not mock_api.call_count == 0 From 1c7c6163dd6c45b9db7e937190a4deac5cb04a02 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 24 Feb 2021 11:31:31 +0100 Subject: [PATCH 691/796] Save mysensors gateway type in config entry (#46981) --- .../components/mysensors/config_flow.py | 30 ++++++++++++------- .../components/mysensors/test_config_flow.py | 3 ++ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index 058b782d208..06ead121706 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -19,6 +19,7 @@ from homeassistant.components.mysensors import ( is_persistence_file, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from . import CONF_RETAIN, CONF_VERSION, DEFAULT_VERSION @@ -99,6 +100,10 @@ def _is_same_device( class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" + def __init__(self) -> None: + """Set up config flow.""" + self._gw_type: Optional[str] = None + async def async_step_import(self, user_input: Optional[Dict[str, str]] = None): """Import a config entry. @@ -130,7 +135,7 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): schema = vol.Schema(schema) if user_input is not None: - gw_type = user_input[CONF_GATEWAY_TYPE] + gw_type = self._gw_type = user_input[CONF_GATEWAY_TYPE] input_pass = user_input if CONF_DEVICE in user_input else None if gw_type == CONF_GATEWAY_TYPE_MQTT: return await self.async_step_gw_mqtt(input_pass) @@ -149,9 +154,7 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.validate_common(CONF_GATEWAY_TYPE_SERIAL, errors, user_input) ) if not errors: - return self.async_create_entry( - title=f"{user_input[CONF_DEVICE]}", data=user_input - ) + return self._async_create_entry(user_input) schema = _get_schema_common() schema[ @@ -177,9 +180,7 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.validate_common(CONF_GATEWAY_TYPE_TCP, errors, user_input) ) if not errors: - return self.async_create_entry( - title=f"{user_input[CONF_DEVICE]}", data=user_input - ) + return self._async_create_entry(user_input) schema = _get_schema_common() schema[vol.Required(CONF_DEVICE, default="127.0.0.1")] = str @@ -228,9 +229,8 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.validate_common(CONF_GATEWAY_TYPE_MQTT, errors, user_input) ) if not errors: - return self.async_create_entry( - title=f"{user_input[CONF_DEVICE]}", data=user_input - ) + return self._async_create_entry(user_input) + schema = _get_schema_common() schema[vol.Required(CONF_RETAIN, default=True)] = bool schema[vol.Required(CONF_TOPIC_IN_PREFIX)] = str @@ -241,6 +241,16 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="gw_mqtt", data_schema=schema, errors=errors ) + @callback + def _async_create_entry( + self, user_input: Optional[Dict[str, str]] = None + ) -> Dict[str, Any]: + """Create the config entry.""" + return self.async_create_entry( + title=f"{user_input[CONF_DEVICE]}", + data={**user_input, CONF_GATEWAY_TYPE: self._gw_type}, + ) + def _normalize_persistence_file(self, path: str) -> str: return os.path.realpath(os.path.normcase(self.hass.config.path(path))) diff --git a/tests/components/mysensors/test_config_flow.py b/tests/components/mysensors/test_config_flow.py index 6bfec3b102e..5fd9e3e7ea1 100644 --- a/tests/components/mysensors/test_config_flow.py +++ b/tests/components/mysensors/test_config_flow.py @@ -81,6 +81,7 @@ async def test_config_mqtt(hass: HomeAssistantType): CONF_TOPIC_IN_PREFIX: "bla", CONF_TOPIC_OUT_PREFIX: "blub", CONF_VERSION: "2.4", + CONF_GATEWAY_TYPE: "MQTT", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -120,6 +121,7 @@ async def test_config_serial(hass: HomeAssistantType): CONF_DEVICE: "/dev/ttyACM0", CONF_BAUD_RATE: 115200, CONF_VERSION: "2.4", + CONF_GATEWAY_TYPE: "Serial", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -156,6 +158,7 @@ async def test_config_tcp(hass: HomeAssistantType): CONF_DEVICE: "127.0.0.1", CONF_TCP_PORT: 5003, CONF_VERSION: "2.4", + CONF_GATEWAY_TYPE: "TCP", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 From 1a73cb4791980a4d31fff16b696c32a478384591 Mon Sep 17 00:00:00 2001 From: MeIchthys <10717998+meichthys@users.noreply.github.com> Date: Wed, 24 Feb 2021 06:04:38 -0500 Subject: [PATCH 692/796] Mullvad VPN (#44189) Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- .coveragerc | 3 + CODEOWNERS | 1 + homeassistant/components/mullvad/__init__.py | 63 +++++++++++++++++++ .../components/mullvad/binary_sensor.py | 52 +++++++++++++++ .../components/mullvad/config_flow.py | 25 ++++++++ homeassistant/components/mullvad/const.py | 3 + .../components/mullvad/manifest.json | 12 ++++ homeassistant/components/mullvad/strings.json | 22 +++++++ .../components/mullvad/translations/en.json | 22 +++++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/mullvad/__init__.py | 1 + tests/components/mullvad/test_config_flow.py | 46 ++++++++++++++ 14 files changed, 257 insertions(+) create mode 100644 homeassistant/components/mullvad/__init__.py create mode 100644 homeassistant/components/mullvad/binary_sensor.py create mode 100644 homeassistant/components/mullvad/config_flow.py create mode 100644 homeassistant/components/mullvad/const.py create mode 100644 homeassistant/components/mullvad/manifest.json create mode 100644 homeassistant/components/mullvad/strings.json create mode 100644 homeassistant/components/mullvad/translations/en.json create mode 100644 tests/components/mullvad/__init__.py create mode 100644 tests/components/mullvad/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index c0a28f70a4f..dfa742b490c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -586,6 +586,9 @@ omit = homeassistant/components/mpd/media_player.py homeassistant/components/mqtt_room/sensor.py homeassistant/components/msteams/notify.py + homeassistant/components/mullvad/__init__.py + homeassistant/components/mullvad/binary_sensor.py + homeassistant/components/nest/const.py homeassistant/components/mvglive/sensor.py homeassistant/components/mychevy/* homeassistant/components/mycroft/* diff --git a/CODEOWNERS b/CODEOWNERS index 2db89f09948..398e5b15f7f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -293,6 +293,7 @@ homeassistant/components/motion_blinds/* @starkillerOG homeassistant/components/mpd/* @fabaff homeassistant/components/mqtt/* @emontnemery homeassistant/components/msteams/* @peroyvind +homeassistant/components/mullvad/* @meichthys homeassistant/components/my/* @home-assistant/core homeassistant/components/myq/* @bdraco homeassistant/components/mysensors/* @MartinHjelmare @functionpointer diff --git a/homeassistant/components/mullvad/__init__.py b/homeassistant/components/mullvad/__init__.py new file mode 100644 index 00000000000..8d63ffd2221 --- /dev/null +++ b/homeassistant/components/mullvad/__init__.py @@ -0,0 +1,63 @@ +"""The Mullvad VPN integration.""" +import asyncio +from datetime import timedelta +import logging + +import async_timeout +from mullvad_api import MullvadAPI + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import update_coordinator + +from .const import DOMAIN + +PLATFORMS = ["binary_sensor"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Mullvad VPN integration.""" + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: dict): + """Set up Mullvad VPN integration.""" + + async def async_get_mullvad_api_data(): + with async_timeout.timeout(10): + api = await hass.async_add_executor_job(MullvadAPI) + return api.data + + hass.data[DOMAIN] = update_coordinator.DataUpdateCoordinator( + hass, + logging.getLogger(__name__), + name=DOMAIN, + update_method=async_get_mullvad_api_data, + update_interval=timedelta(minutes=1), + ) + await hass.data[DOMAIN].async_refresh() + + 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: + del hass.data[DOMAIN] + + return unload_ok diff --git a/homeassistant/components/mullvad/binary_sensor.py b/homeassistant/components/mullvad/binary_sensor.py new file mode 100644 index 00000000000..40b9ae5b1a8 --- /dev/null +++ b/homeassistant/components/mullvad/binary_sensor.py @@ -0,0 +1,52 @@ +"""Setup Mullvad VPN Binary Sensors.""" +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + BinarySensorEntity, +) +from homeassistant.const import CONF_DEVICE_CLASS, CONF_ID, CONF_NAME +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN + +BINARY_SENSORS = ( + { + CONF_ID: "mullvad_exit_ip", + CONF_NAME: "Mullvad Exit IP", + CONF_DEVICE_CLASS: DEVICE_CLASS_CONNECTIVITY, + }, +) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Defer sensor setup to the shared sensor module.""" + coordinator = hass.data[DOMAIN] + + async_add_entities( + MullvadBinarySensor(coordinator, sensor) for sensor in BINARY_SENSORS + ) + + +class MullvadBinarySensor(CoordinatorEntity, BinarySensorEntity): + """Represents a Mullvad binary sensor.""" + + def __init__(self, coordinator, sensor): # pylint: disable=super-init-not-called + """Initialize the Mullvad binary sensor.""" + super().__init__(coordinator) + self.id = sensor[CONF_ID] + self._name = sensor[CONF_NAME] + self._device_class = sensor[CONF_DEVICE_CLASS] + + @property + def device_class(self): + """Return the device class for this binary sensor.""" + return self._device_class + + @property + def name(self): + """Return the name for this binary sensor.""" + return self._name + + @property + def state(self): + """Return the state for this binary sensor.""" + return self.coordinator.data[self.id] diff --git a/homeassistant/components/mullvad/config_flow.py b/homeassistant/components/mullvad/config_flow.py new file mode 100644 index 00000000000..d7b6f92c445 --- /dev/null +++ b/homeassistant/components/mullvad/config_flow.py @@ -0,0 +1,25 @@ +"""Config flow for Mullvad VPN integration.""" +import logging + +from homeassistant import config_entries + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Mullvad VPN.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + if self.hass.config_entries.async_entries(DOMAIN): + return self.async_abort(reason="already_configured") + + if user_input is not None: + return self.async_create_entry(title="Mullvad VPN", data=user_input) + + return self.async_show_form(step_id="user") diff --git a/homeassistant/components/mullvad/const.py b/homeassistant/components/mullvad/const.py new file mode 100644 index 00000000000..4e3be28782c --- /dev/null +++ b/homeassistant/components/mullvad/const.py @@ -0,0 +1,3 @@ +"""Constants for the Mullvad VPN integration.""" + +DOMAIN = "mullvad" diff --git a/homeassistant/components/mullvad/manifest.json b/homeassistant/components/mullvad/manifest.json new file mode 100644 index 00000000000..1a440240d7e --- /dev/null +++ b/homeassistant/components/mullvad/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "mullvad", + "name": "Mullvad VPN", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/mullvad", + "requirements": [ + "mullvad-api==1.0.0" + ], + "codeowners": [ + "@meichthys" + ] +} diff --git a/homeassistant/components/mullvad/strings.json b/homeassistant/components/mullvad/strings.json new file mode 100644 index 00000000000..f522c12871f --- /dev/null +++ b/homeassistant/components/mullvad/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "step": { + "user": { + "description": "Set up the Mullvad VPN integration?", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]" + } + } + } + } +} diff --git a/homeassistant/components/mullvad/translations/en.json b/homeassistant/components/mullvad/translations/en.json new file mode 100644 index 00000000000..fcfa89ef082 --- /dev/null +++ b/homeassistant/components/mullvad/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "username": "Username" + }, + "description": "Set up the Mullvad VPN integration?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index e0a4fd8cd57..da16d32d45b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -143,6 +143,7 @@ FLOWS = [ "monoprice", "motion_blinds", "mqtt", + "mullvad", "myq", "mysensors", "neato", diff --git a/requirements_all.txt b/requirements_all.txt index 60f7997a433..102b474f3bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -955,6 +955,9 @@ mitemp_bt==0.0.3 # homeassistant.components.motion_blinds motionblinds==0.4.8 +# homeassistant.components.mullvad +mullvad-api==1.0.0 + # homeassistant.components.tts mutagen==1.45.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c21bec2a08f..7070b69162e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -500,6 +500,9 @@ minio==4.0.9 # homeassistant.components.motion_blinds motionblinds==0.4.8 +# homeassistant.components.mullvad +mullvad-api==1.0.0 + # homeassistant.components.tts mutagen==1.45.1 diff --git a/tests/components/mullvad/__init__.py b/tests/components/mullvad/__init__.py new file mode 100644 index 00000000000..dc940265eac --- /dev/null +++ b/tests/components/mullvad/__init__.py @@ -0,0 +1 @@ +"""Tests for the mullvad component.""" diff --git a/tests/components/mullvad/test_config_flow.py b/tests/components/mullvad/test_config_flow.py new file mode 100644 index 00000000000..01485da60a0 --- /dev/null +++ b/tests/components/mullvad/test_config_flow.py @@ -0,0 +1,46 @@ +"""Test the Mullvad config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries, setup +from homeassistant.components.mullvad.const import DOMAIN + +from tests.common import MockConfigEntry + + +async def test_form_user(hass): + """Test we can setup by the user.""" + await setup.async_setup_component(hass, DOMAIN, {}) + 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.mullvad.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.mullvad.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"] == "Mullvad VPN" + assert result2["data"] == {} + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_user_only_once(hass): + """Test we can setup by the user only once.""" + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + 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"] == "abort" + assert result["reason"] == "already_configured" From 17f4c9dd06815963cad19acf0d0deb82daf6f53a Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 24 Feb 2021 12:52:04 +0100 Subject: [PATCH 693/796] Improve mysensors config flow (#46984) --- .../components/mysensors/config_flow.py | 58 ++++++++++++++----- homeassistant/components/mysensors/gateway.py | 2 +- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index 06ead121706..d2cd7f3bccd 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -44,15 +44,17 @@ from .gateway import MQTT_COMPONENT, is_serial_port, is_socket_address, try_conn _LOGGER = logging.getLogger(__name__) -def _get_schema_common() -> dict: +def _get_schema_common(user_input: Dict[str, str]) -> dict: """Create a schema with options common to all gateway types.""" schema = { vol.Required( - CONF_VERSION, default="", description={"suggested_value": DEFAULT_VERSION} - ): str, - vol.Optional( - CONF_PERSISTENCE_FILE, + CONF_VERSION, + default="", + description={ + "suggested_value": user_input.get(CONF_VERSION, DEFAULT_VERSION) + }, ): str, + vol.Optional(CONF_PERSISTENCE_FILE): str, } return schema @@ -156,11 +158,19 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not errors: return self._async_create_entry(user_input) - schema = _get_schema_common() + user_input = user_input or {} + schema = _get_schema_common(user_input) schema[ - vol.Required(CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE) + vol.Required( + CONF_BAUD_RATE, + default=user_input.get(CONF_BAUD_RATE, DEFAULT_BAUD_RATE), + ) ] = cv.positive_int - schema[vol.Required(CONF_DEVICE, default="/dev/ttyACM0")] = str + schema[ + vol.Required( + CONF_DEVICE, default=user_input.get(CONF_DEVICE, "/dev/ttyACM0") + ) + ] = str schema = vol.Schema(schema) return self.async_show_form( @@ -182,10 +192,17 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not errors: return self._async_create_entry(user_input) - schema = _get_schema_common() - schema[vol.Required(CONF_DEVICE, default="127.0.0.1")] = str + user_input = user_input or {} + schema = _get_schema_common(user_input) + schema[ + vol.Required(CONF_DEVICE, default=user_input.get(CONF_DEVICE, "127.0.0.1")) + ] = str # Don't use cv.port as that would show a slider *facepalm* - schema[vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT)] = vol.Coerce(int) + schema[ + vol.Optional( + CONF_TCP_PORT, default=user_input.get(CONF_TCP_PORT, DEFAULT_TCP_PORT) + ) + ] = vol.Coerce(int) schema = vol.Schema(schema) return self.async_show_form(step_id="gw_tcp", data_schema=schema, errors=errors) @@ -231,10 +248,21 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not errors: return self._async_create_entry(user_input) - schema = _get_schema_common() - schema[vol.Required(CONF_RETAIN, default=True)] = bool - schema[vol.Required(CONF_TOPIC_IN_PREFIX)] = str - schema[vol.Required(CONF_TOPIC_OUT_PREFIX)] = str + user_input = user_input or {} + schema = _get_schema_common(user_input) + schema[ + vol.Required(CONF_RETAIN, default=user_input.get(CONF_RETAIN, True)) + ] = bool + schema[ + vol.Required( + CONF_TOPIC_IN_PREFIX, default=user_input.get(CONF_TOPIC_IN_PREFIX, "") + ) + ] = str + schema[ + vol.Required( + CONF_TOPIC_OUT_PREFIX, default=user_input.get(CONF_TOPIC_OUT_PREFIX, "") + ) + ] = str schema = vol.Schema(schema) return self.async_show_form( diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index b618004b622..4267ba5cbb3 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -96,7 +96,7 @@ async def try_connect(hass: HomeAssistantType, user_input: Dict[str, str]) -> bo connect_task = None try: connect_task = asyncio.create_task(gateway.start()) - with async_timeout.timeout(5): + with async_timeout.timeout(20): await gateway_ready return True except asyncio.TimeoutError: From b0d56970a54907a367490d4dfd2506febef68baf Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 24 Feb 2021 13:43:24 +0100 Subject: [PATCH 694/796] Fix TTS services name (#46988) --- homeassistant/components/tts/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 67bc933a530..f9b07a98595 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -209,7 +209,7 @@ async def async_setup(hass, config): # Register the service description service_desc = { - CONF_NAME: "Say an TTS message with {p_type}", + CONF_NAME: f"Say an TTS message with {p_type}", CONF_DESCRIPTION: f"Say something using text-to-speech on a media player with {p_type}.", CONF_FIELDS: services_dict[SERVICE_SAY][CONF_FIELDS], } From 45b50c53ad89d6093700fe3d05cdb1316e822256 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 24 Feb 2021 13:43:44 +0100 Subject: [PATCH 695/796] Mullvad integration improvements (#46987) --- homeassistant/components/mullvad/__init__.py | 11 +++- .../components/mullvad/binary_sensor.py | 2 +- .../components/mullvad/config_flow.py | 14 ++++- homeassistant/components/mullvad/strings.json | 8 +-- .../components/mullvad/translations/en.json | 6 -- tests/components/mullvad/test_config_flow.py | 56 +++++++++++++++++-- 6 files changed, 74 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/mullvad/__init__.py b/homeassistant/components/mullvad/__init__.py index 8d63ffd2221..20a7092d58a 100644 --- a/homeassistant/components/mullvad/__init__.py +++ b/homeassistant/components/mullvad/__init__.py @@ -8,6 +8,7 @@ from mullvad_api import MullvadAPI from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import update_coordinator from .const import DOMAIN @@ -17,7 +18,6 @@ PLATFORMS = ["binary_sensor"] async def async_setup(hass: HomeAssistant, config: dict): """Set up the Mullvad VPN integration.""" - return True @@ -29,14 +29,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: dict): api = await hass.async_add_executor_job(MullvadAPI) return api.data - hass.data[DOMAIN] = update_coordinator.DataUpdateCoordinator( + coordinator = update_coordinator.DataUpdateCoordinator( hass, logging.getLogger(__name__), name=DOMAIN, update_method=async_get_mullvad_api_data, update_interval=timedelta(minutes=1), ) - await hass.data[DOMAIN].async_refresh() + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + hass.data[DOMAIN] = coordinator for component in PLATFORMS: hass.async_create_task( diff --git a/homeassistant/components/mullvad/binary_sensor.py b/homeassistant/components/mullvad/binary_sensor.py index 40b9ae5b1a8..f85820cd7d0 100644 --- a/homeassistant/components/mullvad/binary_sensor.py +++ b/homeassistant/components/mullvad/binary_sensor.py @@ -47,6 +47,6 @@ class MullvadBinarySensor(CoordinatorEntity, BinarySensorEntity): return self._name @property - def state(self): + def is_on(self): """Return the state for this binary sensor.""" return self.coordinator.data[self.id] diff --git a/homeassistant/components/mullvad/config_flow.py b/homeassistant/components/mullvad/config_flow.py index d7b6f92c445..674308c1d1a 100644 --- a/homeassistant/components/mullvad/config_flow.py +++ b/homeassistant/components/mullvad/config_flow.py @@ -1,6 +1,8 @@ """Config flow for Mullvad VPN integration.""" import logging +from mullvad_api import MullvadAPI, MullvadAPIError + from homeassistant import config_entries from .const import DOMAIN # pylint:disable=unused-import @@ -19,7 +21,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self.hass.config_entries.async_entries(DOMAIN): return self.async_abort(reason="already_configured") + errors = {} if user_input is not None: - return self.async_create_entry(title="Mullvad VPN", data=user_input) + try: + await self.hass.async_add_executor_job(MullvadAPI) + except MullvadAPIError: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + errors["base"] = "unknown" + else: + return self.async_create_entry(title="Mullvad VPN", data=user_input) - return self.async_show_form(step_id="user") + return self.async_show_form(step_id="user", errors=errors) diff --git a/homeassistant/components/mullvad/strings.json b/homeassistant/components/mullvad/strings.json index f522c12871f..7910a40ec35 100644 --- a/homeassistant/components/mullvad/strings.json +++ b/homeassistant/components/mullvad/strings.json @@ -5,17 +5,11 @@ }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "step": { "user": { - "description": "Set up the Mullvad VPN integration?", - "data": { - "host": "[%key:common::config_flow::data::host%]", - "password": "[%key:common::config_flow::data::password%]", - "username": "[%key:common::config_flow::data::username%]" - } + "description": "Set up the Mullvad VPN integration?" } } } diff --git a/homeassistant/components/mullvad/translations/en.json b/homeassistant/components/mullvad/translations/en.json index fcfa89ef082..45664554aed 100644 --- a/homeassistant/components/mullvad/translations/en.json +++ b/homeassistant/components/mullvad/translations/en.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, "step": { "user": { - "data": { - "host": "Host", - "password": "Password", - "username": "Username" - }, "description": "Set up the Mullvad VPN integration?" } } diff --git a/tests/components/mullvad/test_config_flow.py b/tests/components/mullvad/test_config_flow.py index 01485da60a0..c101e5a7246 100644 --- a/tests/components/mullvad/test_config_flow.py +++ b/tests/components/mullvad/test_config_flow.py @@ -1,8 +1,11 @@ """Test the Mullvad config flow.""" from unittest.mock import patch +from mullvad_api import MullvadAPIError + from homeassistant import config_entries, setup from homeassistant.components.mullvad.const import DOMAIN +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM from tests.common import MockConfigEntry @@ -13,15 +16,17 @@ async def test_form_user(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" - assert result["errors"] is None + assert result["type"] == RESULT_TYPE_FORM + assert not result["errors"] with patch( "homeassistant.components.mullvad.async_setup", return_value=True ) as mock_setup, patch( "homeassistant.components.mullvad.async_setup_entry", return_value=True, - ) as mock_setup_entry: + ) as mock_setup_entry, patch( + "homeassistant.components.mullvad.config_flow.MullvadAPI" + ) as mock_mullvad_api: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, @@ -33,6 +38,7 @@ async def test_form_user(hass): assert result2["data"] == {} assert len(mock_setup.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_mullvad_api.mock_calls) == 1 async def test_form_user_only_once(hass): @@ -42,5 +48,47 @@ async def test_form_user_only_once(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" + + +async def test_connection_error(hass): + """Test we show an error when we have trouble connecting.""" + await setup.async_setup_component(hass, DOMAIN, {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.mullvad.config_flow.MullvadAPI", + side_effect=MullvadAPIError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_unknown_error(hass): + """Test we show an error when an unknown error occurs.""" + await setup.async_setup_component(hass, DOMAIN, {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.mullvad.config_flow.MullvadAPI", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} From b645b151f987dda47df390870c35cae8f8b010ea Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 24 Feb 2021 14:13:32 +0100 Subject: [PATCH 696/796] Catch AuthRequired exception in confirm discovery step for Shelly config flow (#46135) --- .../components/shelly/config_flow.py | 2 ++ tests/components/shelly/test_config_flow.py | 30 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index cd74b83a62a..dfb078ee9c7 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -192,6 +192,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): device_info = await validate_input(self.hass, self.host, {}) except HTTP_CONNECT_ERRORS: errors["base"] = "cannot_connect" + except aioshelly.AuthRequired: + return await self.async_step_credentials() except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 450bf8efb24..9dfbc19255b 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -512,6 +512,36 @@ async def test_zeroconf_confirm_error(hass, error): assert result2["errors"] == {"base": base_error} +async def test_zeroconf_confirm_auth_error(hass): + """Test we get credentials form after an auth error when confirming discovery.""" + 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=DISCOVERY_INFO, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "aioshelly.Device.create", + new=AsyncMock(side_effect=aioshelly.AuthRequired), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "credentials" + assert result2["errors"] == {} + + async def test_zeroconf_already_configured(hass): """Test we get the form.""" await setup.async_setup_component(hass, "persistent_notification", {}) From 470121e5b07a98cc34b68dd13bd97b4a81be76c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Wed, 24 Feb 2021 14:17:01 +0100 Subject: [PATCH 697/796] Restore Tado binary sensor attributes (#46069) --- .../components/tado/binary_sensor.py | 11 +++++++++ tests/components/tado/util.py | 23 +++++++++++++++++++ tests/fixtures/tado/zone_default_overlay.json | 5 ++++ 3 files changed, 39 insertions(+) create mode 100644 tests/fixtures/tado/zone_default_overlay.json diff --git a/homeassistant/components/tado/binary_sensor.py b/homeassistant/components/tado/binary_sensor.py index 71b52931013..068c3a7ce93 100644 --- a/homeassistant/components/tado/binary_sensor.py +++ b/homeassistant/components/tado/binary_sensor.py @@ -183,6 +183,7 @@ class TadoZoneBinarySensor(TadoZoneEntity, BinarySensorEntity): self._unique_id = f"{zone_variable} {zone_id} {tado.home_id}" self._state = None + self._state_attributes = None self._tado_zone_data = None async def async_added_to_hass(self): @@ -229,6 +230,11 @@ class TadoZoneBinarySensor(TadoZoneEntity, BinarySensorEntity): return DEVICE_CLASS_POWER return None + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._state_attributes + @callback def _async_update_callback(self): """Update and write state.""" @@ -251,6 +257,10 @@ class TadoZoneBinarySensor(TadoZoneEntity, BinarySensorEntity): elif self.zone_variable == "overlay": self._state = self._tado_zone_data.overlay_active + if self._tado_zone_data.overlay_active: + self._state_attributes = { + "termination": self._tado_zone_data.overlay_termination_type + } elif self.zone_variable == "early start": self._state = self._tado_zone_data.preparation @@ -260,3 +270,4 @@ class TadoZoneBinarySensor(TadoZoneEntity, BinarySensorEntity): self._tado_zone_data.open_window or self._tado_zone_data.open_window_detected ) + self._state_attributes = self._tado_zone_data.open_window_attr diff --git a/tests/components/tado/util.py b/tests/components/tado/util.py index d27ede47a63..c5bf8cf28a4 100644 --- a/tests/components/tado/util.py +++ b/tests/components/tado/util.py @@ -46,6 +46,9 @@ async def async_init_integration( # Device Temp Offset device_temp_offset = "tado/device_temp_offset.json" + # Zone Default Overlay + zone_def_overlay = "tado/zone_default_overlay.json" + with requests_mock.mock() as m: m.post("https://auth.tado.com/oauth/token", text=load_fixture(token_fixture)) m.get( @@ -92,6 +95,26 @@ async def async_init_integration( "https://my.tado.com/api/v2/homes/1/zones/1/capabilities", text=load_fixture(zone_1_capabilities_fixture), ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/1/defaultOverlay", + text=load_fixture(zone_def_overlay), + ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/2/defaultOverlay", + text=load_fixture(zone_def_overlay), + ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/3/defaultOverlay", + text=load_fixture(zone_def_overlay), + ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/4/defaultOverlay", + text=load_fixture(zone_def_overlay), + ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/5/defaultOverlay", + text=load_fixture(zone_def_overlay), + ) m.get( "https://my.tado.com/api/v2/homes/1/zones/5/state", text=load_fixture(zone_5_state_fixture), diff --git a/tests/fixtures/tado/zone_default_overlay.json b/tests/fixtures/tado/zone_default_overlay.json new file mode 100644 index 00000000000..092b2b25d4d --- /dev/null +++ b/tests/fixtures/tado/zone_default_overlay.json @@ -0,0 +1,5 @@ +{ + "terminationCondition": { + "type": "MANUAL" + } +} From 44293a37388aef33d1bb0c67b87a29c74b183a50 Mon Sep 17 00:00:00 2001 From: adrian-vlad Date: Wed, 24 Feb 2021 15:26:05 +0200 Subject: [PATCH 698/796] Add enable and disable services for recorder (#45778) --- homeassistant/components/recorder/__init__.py | 30 ++++ .../components/recorder/services.yaml | 6 + tests/components/recorder/test_init.py | 157 +++++++++++++++++- 3 files changed, 191 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 915e6b45181..3935aa97eb8 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -48,6 +48,8 @@ from .util import ( _LOGGER = logging.getLogger(__name__) SERVICE_PURGE = "purge" +SERVICE_ENABLE = "enable" +SERVICE_DISABLE = "disable" ATTR_KEEP_DAYS = "keep_days" ATTR_REPACK = "repack" @@ -58,6 +60,8 @@ SERVICE_PURGE_SCHEMA = vol.Schema( vol.Optional(ATTR_REPACK, default=False): cv.boolean, } ) +SERVICE_ENABLE_SCHEMA = vol.Schema({}) +SERVICE_DISABLE_SCHEMA = vol.Schema({}) DEFAULT_URL = "sqlite:///{hass_config_path}" DEFAULT_DB_FILE = "home-assistant_v2.db" @@ -199,6 +203,23 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, SERVICE_PURGE, async_handle_purge_service, schema=SERVICE_PURGE_SCHEMA ) + async def async_handle_enable_sevice(service): + instance.set_enable(True) + + hass.services.async_register( + DOMAIN, SERVICE_ENABLE, async_handle_enable_sevice, schema=SERVICE_ENABLE_SCHEMA + ) + + async def async_handle_disable_service(service): + instance.set_enable(False) + + hass.services.async_register( + DOMAIN, + SERVICE_DISABLE, + async_handle_disable_service, + schema=SERVICE_DISABLE_SCHEMA, + ) + return await instance.async_db_ready @@ -255,6 +276,12 @@ class Recorder(threading.Thread): self.get_session = None self._completed_database_setup = None + self.enabled = True + + def set_enable(self, enable): + """Enable or disable recording events and states.""" + self.enabled = enable + @callback def async_initialize(self): """Initialize the recorder.""" @@ -413,6 +440,9 @@ class Recorder(threading.Thread): self._commit_event_session_or_recover() return + if not self.enabled: + return + try: if event.event_type == EVENT_STATE_CHANGED: dbevent = Events.from_event(event, event_data="{}") diff --git a/homeassistant/components/recorder/services.yaml b/homeassistant/components/recorder/services.yaml index e3dea47f4f8..2be5b0e095e 100644 --- a/homeassistant/components/recorder/services.yaml +++ b/homeassistant/components/recorder/services.yaml @@ -24,3 +24,9 @@ purge: default: false selector: boolean: + +disable: + description: Stop the recording of events and state changes + +enabled: + description: Start the recording of events and state changes diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index dfa65944811..63f4b9887c6 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -8,13 +8,17 @@ from sqlalchemy.exc import OperationalError from homeassistant.components.recorder import ( CONF_DB_URL, CONFIG_SCHEMA, + DATA_INSTANCE, DOMAIN, + SERVICE_DISABLE, + SERVICE_ENABLE, + SERVICE_PURGE, + SQLITE_URL_PREFIX, Recorder, run_information, run_information_from_instance, run_information_with_session, ) -from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX from homeassistant.components.recorder.models import Events, RecorderRuns, States from homeassistant.components.recorder.util import session_scope from homeassistant.const import ( @@ -24,7 +28,7 @@ from homeassistant.const import ( STATE_UNLOCKED, ) from homeassistant.core import Context, CoreState, callback -from homeassistant.setup import async_setup_component +from homeassistant.setup import async_setup_component, setup_component from homeassistant.util import dt as dt_util from .common import async_wait_recording_done, corrupt_db_file, wait_recording_done @@ -518,6 +522,155 @@ def test_run_information(hass_recorder): assert run_info.closed_incorrect is False +def test_has_services(hass_recorder): + """Test the services exist.""" + hass = hass_recorder() + + assert hass.services.has_service(DOMAIN, SERVICE_DISABLE) + assert hass.services.has_service(DOMAIN, SERVICE_ENABLE) + assert hass.services.has_service(DOMAIN, SERVICE_PURGE) + + +def test_service_disable_events_not_recording(hass, hass_recorder): + """Test that events are not recorded when recorder is disabled using service.""" + hass = hass_recorder() + + assert hass.services.call( + DOMAIN, + SERVICE_DISABLE, + {}, + blocking=True, + ) + + event_type = "EVENT_TEST" + + events = [] + + @callback + def event_listener(event): + """Record events from eventbus.""" + if event.event_type == event_type: + events.append(event) + + hass.bus.listen(MATCH_ALL, event_listener) + + event_data1 = {"test_attr": 5, "test_attr_10": "nice"} + hass.bus.fire(event_type, event_data1) + wait_recording_done(hass) + + assert len(events) == 1 + event = events[0] + + with session_scope(hass=hass) as session: + db_events = list(session.query(Events).filter_by(event_type=event_type)) + assert len(db_events) == 0 + + assert hass.services.call( + DOMAIN, + SERVICE_ENABLE, + {}, + blocking=True, + ) + + event_data2 = {"attr_one": 5, "attr_two": "nice"} + hass.bus.fire(event_type, event_data2) + wait_recording_done(hass) + + assert len(events) == 2 + assert events[0] != events[1] + assert events[0].data != events[1].data + + with session_scope(hass=hass) as session: + db_events = list(session.query(Events).filter_by(event_type=event_type)) + assert len(db_events) == 1 + db_event = db_events[0].to_native() + + event = events[1] + + assert event.event_type == db_event.event_type + assert event.data == db_event.data + assert event.origin == db_event.origin + assert event.time_fired.replace(microsecond=0) == db_event.time_fired.replace( + microsecond=0 + ) + + +def test_service_disable_states_not_recording(hass, hass_recorder): + """Test that state changes are not recorded when recorder is disabled using service.""" + hass = hass_recorder() + + assert hass.services.call( + DOMAIN, + SERVICE_DISABLE, + {}, + blocking=True, + ) + + hass.states.set("test.one", "on", {}) + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + assert len(list(session.query(States))) == 0 + + assert hass.services.call( + DOMAIN, + SERVICE_ENABLE, + {}, + blocking=True, + ) + + hass.states.set("test.two", "off", {}) + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + db_states = list(session.query(States)) + assert len(db_states) == 1 + assert db_states[0].event_id > 0 + assert db_states[0].to_native() == _state_empty_context(hass, "test.two") + + +def test_service_disable_run_information_recorded(tmpdir): + """Test that runs are still recorded when recorder is disabled.""" + test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") + dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" + + hass = get_test_home_assistant() + setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) + hass.start() + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + db_run_info = list(session.query(RecorderRuns)) + assert len(db_run_info) == 1 + assert db_run_info[0].start is not None + assert db_run_info[0].end is None + + assert hass.services.call( + DOMAIN, + SERVICE_DISABLE, + {}, + blocking=True, + ) + + wait_recording_done(hass) + hass.stop() + + hass = get_test_home_assistant() + setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) + hass.start() + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + db_run_info = list(session.query(RecorderRuns)) + assert len(db_run_info) == 2 + assert db_run_info[0].start is not None + assert db_run_info[0].end is not None + assert db_run_info[1].start is not None + assert db_run_info[1].end is None + + hass.stop() + + class CannotSerializeMe: """A class that the JSONEncoder cannot serialize.""" From 424526db7eeec63a31cd162bf5983568236aee88 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 24 Feb 2021 09:41:10 -0500 Subject: [PATCH 699/796] Migrate zwave_js entities to use new unique ID format (#46979) * migrate zwave_js entities to use new unique ID format * remove extra param from helper * add comment to remove migration logic in the future * update comment * use instance attribute instead of calling functino on every state update --- homeassistant/components/zwave_js/__init__.py | 27 ++++++++++++++-- homeassistant/components/zwave_js/entity.py | 7 +++-- homeassistant/components/zwave_js/helpers.py | 17 ++++++++++ tests/components/zwave_js/test_init.py | 31 +++++++++++++++++++ 4 files changed, 78 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 062b28cf6a9..cc58e31066a 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -43,7 +43,7 @@ from .const import ( ZWAVE_JS_EVENT, ) from .discovery import async_discover_values -from .helpers import get_device_id +from .helpers import get_device_id, get_old_value_id, get_unique_id from .services import ZWaveServices LOGGER = logging.getLogger(__package__) @@ -83,6 +83,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Z-Wave JS from a config entry.""" client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass)) dev_reg = await device_registry.async_get_registry(hass) + ent_reg = entity_registry.async_get(hass) @callback def async_on_node_ready(node: ZwaveNode) -> None: @@ -95,6 +96,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # run discovery on all node values and create/update entities for disc_info in async_discover_values(node): LOGGER.debug("Discovered entity: %s", disc_info) + + # This migration logic was added in 2021.3 to handle breaking change to + # value_id format. Some time in the future, this code block + # (and get_old_value_id helper) can be removed. + old_value_id = get_old_value_id(disc_info.primary_value) + old_unique_id = get_unique_id( + client.driver.controller.home_id, old_value_id + ) + if entity_id := ent_reg.async_get_entity_id( + disc_info.platform, DOMAIN, old_unique_id + ): + LOGGER.debug( + "Entity %s is using old unique ID, migrating to new one", entity_id + ) + ent_reg.async_update_entity( + entity_id, + new_unique_id=get_unique_id( + client.driver.controller.home_id, + disc_info.primary_value.value_id, + ), + ) + async_dispatcher_send( hass, f"{DOMAIN}_{entry.entry_id}_add_{disc_info.platform}", disc_info ) @@ -193,7 +216,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_UNSUBSCRIBE: unsubscribe_callbacks, } - services = ZWaveServices(hass, entity_registry.async_get(hass)) + services = ZWaveServices(hass, ent_reg) services.async_register() # Set up websocket API diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index cb898e861e9..685fe50c9b6 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -13,7 +13,7 @@ from homeassistant.helpers.entity import Entity from .const import DOMAIN from .discovery import ZwaveDiscoveryInfo -from .helpers import get_device_id +from .helpers import get_device_id, get_unique_id LOGGER = logging.getLogger(__name__) @@ -31,6 +31,9 @@ class ZWaveBaseEntity(Entity): self.client = client self.info = info self._name = self.generate_name() + self._unique_id = get_unique_id( + self.client.driver.controller.home_id, self.info.value_id + ) # entities requiring additional values, can add extra ids to this list self.watched_value_ids = {self.info.primary_value.value_id} @@ -128,7 +131,7 @@ class ZWaveBaseEntity(Entity): @property def unique_id(self) -> str: """Return the unique_id of the entity.""" - return f"{self.client.driver.controller.home_id}.{self.info.value_id}" + return self._unique_id @property def available(self) -> bool: diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index cc00c39b747..9582b7ee054 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -3,6 +3,7 @@ from typing import List, Tuple, cast from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.node import Node as ZwaveNode +from zwave_js_server.model.value import Value as ZwaveValue from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -12,6 +13,22 @@ from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg from .const import DATA_CLIENT, DOMAIN +@callback +def get_old_value_id(value: ZwaveValue) -> str: + """Get old value ID so we can migrate entity unique ID.""" + command_class = value.command_class + endpoint = value.endpoint or "00" + property_ = value.property_ + property_key_name = value.property_key_name or "00" + return f"{value.node.node_id}-{command_class}-{endpoint}-{property_}-{property_key_name}" + + +@callback +def get_unique_id(home_id: str, value_id: str) -> str: + """Get unique ID from home ID and value ID.""" + return f"{home_id}.{value_id}" + + @callback def get_device_id(client: ZwaveClient, node: ZwaveNode) -> Tuple[str, str]: """Get device registry identifier for Z-Wave node.""" diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 6a255becf2d..3634454544f 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -124,6 +124,37 @@ async def test_on_node_added_ready( ) +async def test_unique_id_migration(hass, multisensor_6_state, client, integration): + """Test unique ID is migrated from old format to new.""" + ent_reg = entity_registry.async_get(hass) + entity_name = AIR_TEMPERATURE_SENSOR.split(".")[1] + + # Create entity RegistryEntry using old unique ID format + old_unique_id = f"{client.driver.controller.home_id}.52-49-00-Air temperature-00" + entity_entry = ent_reg.async_get_or_create( + "sensor", + DOMAIN, + old_unique_id, + suggested_object_id=entity_name, + config_entry=integration, + original_name=entity_name, + ) + assert entity_entry.entity_id == AIR_TEMPERATURE_SENSOR + assert entity_entry.unique_id == old_unique_id + + # Add a ready node, unique ID should be migrated + node = Node(client, multisensor_6_state) + event = {"node": node} + + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + # Check that new RegistryEntry is using new unique ID format + entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR) + new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature-00-00" + assert entity_entry.unique_id == new_unique_id + + async def test_on_node_added_not_ready( hass, multisensor_6_state, client, integration, device_registry ): From db8f597f1052dda14f5415423bcea571141047ea Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 24 Feb 2021 16:22:58 +0100 Subject: [PATCH 700/796] Fix zwave_js config flow server version timeout (#46990) --- .../components/zwave_js/config_flow.py | 15 ++++++----- tests/components/zwave_js/test_config_flow.py | 27 +++++++++++++++---- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index fe79796edf1..cc19fb85d3a 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -41,6 +41,7 @@ TITLE = "Z-Wave JS" ADDON_SETUP_TIMEOUT = 5 ADDON_SETUP_TIMEOUT_ROUNDS = 4 +SERVER_VERSION_TIMEOUT = 10 ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=True): bool}) STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_URL, default=DEFAULT_URL): str}) @@ -61,16 +62,16 @@ async def validate_input(hass: HomeAssistant, user_input: dict) -> VersionInfo: async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> VersionInfo: """Return Z-Wave JS version info.""" - async with timeout(10): - try: + try: + async with timeout(SERVER_VERSION_TIMEOUT): version_info: VersionInfo = await get_server_version( ws_address, async_get_clientsession(hass) ) - except (asyncio.TimeoutError, aiohttp.ClientError) as err: - # We don't want to spam the log if the add-on isn't started - # or takes a long time to start. - _LOGGER.debug("Failed to connect to Z-Wave JS server: %s", err) - raise CannotConnect from err + except (asyncio.TimeoutError, aiohttp.ClientError) as err: + # We don't want to spam the log if the add-on isn't started + # or takes a long time to start. + _LOGGER.debug("Failed to connect to Z-Wave JS server: %s", err) + raise CannotConnect from err return version_info diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 3c956d42a27..73057f3fe21 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -7,7 +7,7 @@ from zwave_js_server.version import VersionInfo from homeassistant import config_entries, setup from homeassistant.components.hassio.handler import HassioAPIError -from homeassistant.components.zwave_js.config_flow import TITLE +from homeassistant.components.zwave_js.config_flow import SERVER_VERSION_TIMEOUT, TITLE from homeassistant.components.zwave_js.const import DOMAIN from tests.common import MockConfigEntry @@ -138,7 +138,7 @@ def server_version_side_effect_fixture(): @pytest.fixture(name="get_server_version", autouse=True) -def mock_get_server_version(server_version_side_effect): +def mock_get_server_version(server_version_side_effect, server_version_timeout): """Mock server version.""" version_info = VersionInfo( driver_version="mock-driver-version", @@ -149,10 +149,19 @@ def mock_get_server_version(server_version_side_effect): "homeassistant.components.zwave_js.config_flow.get_server_version", side_effect=server_version_side_effect, return_value=version_info, - ) as mock_version: + ) as mock_version, patch( + "homeassistant.components.zwave_js.config_flow.SERVER_VERSION_TIMEOUT", + new=server_version_timeout, + ): yield mock_version +@pytest.fixture(name="server_version_timeout") +def mock_server_version_timeout(): + """Patch the timeout for getting server version.""" + return SERVER_VERSION_TIMEOUT + + @pytest.fixture(name="addon_setup_time", autouse=True) def mock_addon_setup_time(): """Mock add-on setup sleep time.""" @@ -198,22 +207,30 @@ async def test_manual(hass): assert result2["result"].unique_id == 1234 +async def slow_server_version(*args): + """Simulate a slow server version.""" + await asyncio.sleep(0.1) + + @pytest.mark.parametrize( - "url, server_version_side_effect, error", + "url, server_version_side_effect, server_version_timeout, error", [ ( "not-ws-url", None, + SERVER_VERSION_TIMEOUT, "invalid_ws_url", ), ( "ws://localhost:3000", - asyncio.TimeoutError, + slow_server_version, + 0, "cannot_connect", ), ( "ws://localhost:3000", Exception("Boom"), + SERVER_VERSION_TIMEOUT, "unknown", ), ], From 0ef16dd563c14fa95074a14eca21ff027a4df24f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 24 Feb 2021 16:43:09 +0100 Subject: [PATCH 701/796] Set awesomeversion to 21.2.3 (#46989) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fa5862b5c28..d867db5cef0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ astral==1.10.1 async-upnp-client==0.14.13 async_timeout==3.0.1 attrs==19.3.0 -awesomeversion==21.2.2 +awesomeversion==21.2.3 bcrypt==3.1.7 certifi>=2020.12.5 ciso8601==2.1.3 diff --git a/requirements.txt b/requirements.txt index d0b894bcb58..14ebf2708ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ aiohttp==3.7.3 astral==1.10.1 async_timeout==3.0.1 attrs==19.3.0 -awesomeversion==21.2.2 +awesomeversion==21.2.3 bcrypt==3.1.7 certifi>=2020.12.5 ciso8601==2.1.3 diff --git a/setup.py b/setup.py index 1dbc29f5537..6dbe35760a6 100755 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ REQUIRES = [ "astral==1.10.1", "async_timeout==3.0.1", "attrs==19.3.0", - "awesomeversion==21.2.2", + "awesomeversion==21.2.3", "bcrypt==3.1.7", "certifi>=2020.12.5", "ciso8601==2.1.3", From 11a89bc3ac54270b61993ce4588e16516664daf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 24 Feb 2021 17:02:48 +0100 Subject: [PATCH 702/796] Add addon selector (#46789) --- homeassistant/components/hassio/services.yaml | 101 ++++++++---------- homeassistant/helpers/selector.py | 7 ++ tests/helpers/test_selector.py | 9 ++ 3 files changed, 62 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/hassio/services.yaml b/homeassistant/components/hassio/services.yaml index 8afdcc633bf..3570a857c55 100644 --- a/homeassistant/components/hassio/services.yaml +++ b/homeassistant/components/hassio/services.yaml @@ -1,110 +1,101 @@ -addon_install: - description: Install a Hass.io docker add-on. - fields: - addon: - description: The add-on slug. - example: core_ssh - version: - description: Optional or it will be use the latest version. - example: "0.2" - addon_start: - description: Start a Hass.io docker add-on. + name: Start add-on + description: Start add-on. fields: addon: + name: Add-on + required: true description: The add-on slug. example: core_ssh + selector: + addon: addon_restart: - description: Restart a Hass.io docker add-on. + name: Restart add-on. + description: Restart add-on. fields: addon: + name: Add-on + required: true description: The add-on slug. example: core_ssh + selector: + addon: addon_stdin: - description: Write data to a Hass.io docker add-on stdin . + name: Write data to add-on stdin. + description: Write data to add-on stdin. fields: addon: + name: Add-on + required: true description: The add-on slug. example: core_ssh + selector: + addon: addon_stop: - description: Stop a Hass.io docker add-on. + name: Stop add-on. + description: Stop add-on. fields: addon: + name: Add-on + required: true description: The add-on slug. example: core_ssh - -addon_uninstall: - description: Uninstall a Hass.io docker add-on. - fields: - addon: - description: The add-on slug. - example: core_ssh - -addon_update: - description: Update a Hass.io docker add-on. - fields: - addon: - description: The add-on slug. - example: core_ssh - version: - description: Optional or it will be use the latest version. - example: "0.2" - -homeassistant_update: - description: Update the Home Assistant docker image. - fields: - version: - description: Optional or it will be use the latest version. - example: 0.40.1 + selector: + addon: host_reboot: + name: Reboot the host system. description: Reboot the host system. host_shutdown: + name: Poweroff the host system. description: Poweroff the host system. -host_update: - description: Update the host system. - fields: - version: - description: Optional or it will be use the latest version. - example: "0.3" - snapshot_full: + name: Create a full snapshot. description: Create a full snapshot. fields: name: + name: Name description: Optional or it will be the current date and time. example: "Snapshot 1" + selector: + text: password: + name: Password description: Optional password. example: "password" + selector: + text: snapshot_partial: + name: Create a partial snapshot. description: Create a partial snapshot. fields: addons: + name: Add-ons description: Optional list of addon slugs. example: ["core_ssh", "core_samba", "core_mosquitto"] + selector: + object: folders: + name: Folders description: Optional list of directories. example: ["homeassistant", "share"] + selector: + object: name: + name: Name description: Optional or it will be the current date and time. example: "Partial Snapshot 1" + selector: + text: password: + name: Password description: Optional password. example: "password" - -supervisor_reload: - description: Reload the Hass.io supervisor. - -supervisor_update: - description: Update the Hass.io supervisor. - fields: - version: - description: Optional or it will be use the latest version. - example: "0.3" + selector: + text: diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 68511a771d3..34befe9c37b 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -116,6 +116,13 @@ class NumberSelector(Selector): ) +@SELECTORS.register("addon") +class AddonSelector(Selector): + """Selector of a add-on.""" + + CONFIG_SCHEMA = vol.Schema({}) + + @SELECTORS.register("boolean") class BooleanSelector(Selector): """Selector of a boolean value.""" diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index c43ed4097e0..23d8200be23 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -118,6 +118,15 @@ def test_number_selector_schema(schema): selector.validate_selector({"number": schema}) +@pytest.mark.parametrize( + "schema", + ({},), +) +def test_addon_selector_schema(schema): + """Test add-on selector.""" + selector.validate_selector({"addon": schema}) + + @pytest.mark.parametrize( "schema", ({},), From 106ae184322c21cf820647261bc9caa70119f529 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 24 Feb 2021 11:15:01 -0500 Subject: [PATCH 703/796] Climacell fixes: Use common keys for strings, fix temp_low measurement, add windy condition (#46991) * use common keys for lat and long * additional fixes * Update homeassistant/components/climacell/strings.json Co-authored-by: Milan Meulemans Co-authored-by: Milan Meulemans --- homeassistant/components/climacell/const.py | 2 ++ homeassistant/components/climacell/strings.json | 8 ++++---- homeassistant/components/climacell/weather.py | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index 28117a164f8..f2d0a596121 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -12,6 +12,7 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SNOWY, ATTR_CONDITION_SNOWY_RAINY, ATTR_CONDITION_SUNNY, + ATTR_CONDITION_WINDY, ) CONF_TIMESTEP = "timestep" @@ -33,6 +34,7 @@ ATTRIBUTION = "Powered by ClimaCell" MAX_REQUESTS_PER_DAY = 1000 CONDITIONS = { + "breezy": ATTR_CONDITION_WINDY, "freezing_rain_heavy": ATTR_CONDITION_SNOWY_RAINY, "freezing_rain": ATTR_CONDITION_SNOWY_RAINY, "freezing_rain_light": ATTR_CONDITION_SNOWY_RAINY, diff --git a/homeassistant/components/climacell/strings.json b/homeassistant/components/climacell/strings.json index 45a1d5b7404..be80ac4e506 100644 --- a/homeassistant/components/climacell/strings.json +++ b/homeassistant/components/climacell/strings.json @@ -3,12 +3,12 @@ "config": { "step": { "user": { - "description": "If [%key:component::climacell::config::step::user::data::latitude%] and [%key:component::climacell::config::step::user::data::longitude%] are not provided, the default values in the Home Assistant configuration will be used. An entity will be created for each forecast type but only the ones you select will be enabled by default.", + "description": "If [%key:common::config_flow::data::latitude%] and [%key:common::config_flow::data::longitude%] are not provided, the default values in the Home Assistant configuration will be used. An entity will be created for each forecast type but only the ones you select will be enabled by default.", "data": { - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "api_key": "[%key:common::config_flow::data::api_key%]", - "latitude": "Latitude", - "longitude": "Longitude" + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]" } } }, diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index a4802586bf1..da3282108a5 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -275,7 +275,7 @@ class ClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): ( self._get_cc_value(item, CC_ATTR_TEMPERATURE_LOW) for item in forecast[CC_ATTR_TEMPERATURE] - if "max" in item + if "min" in item ), temp_low, ) From db0d815f9d95d841465c21b01392edc2450093cb Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Wed, 24 Feb 2021 18:44:12 +0100 Subject: [PATCH 704/796] Use location common key reference in totalconnect (#46995) --- homeassistant/components/totalconnect/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/totalconnect/strings.json b/homeassistant/components/totalconnect/strings.json index 41b0bf4648b..f284e4b86da 100644 --- a/homeassistant/components/totalconnect/strings.json +++ b/homeassistant/components/totalconnect/strings.json @@ -12,7 +12,7 @@ "title": "Location Usercodes", "description": "Enter the usercode for this user at this location", "data": { - "location": "Location" + "location": "[%key:common::config_flow::data::location%]" } }, "reauth_confirm": { From 722b1e8746176d800644f6fa3e9d373e5969cacb Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 24 Feb 2021 19:20:40 +0100 Subject: [PATCH 705/796] Add Netatmo device trigger (#45387) Co-authored-by: J. Nick Koston --- homeassistant/components/netatmo/climate.py | 6 + homeassistant/components/netatmo/const.py | 101 +++++- .../components/netatmo/device_trigger.py | 155 +++++++++ .../components/netatmo/netatmo_entity_base.py | 6 +- homeassistant/components/netatmo/strings.json | 24 +- .../components/netatmo/translations/en.json | 22 ++ homeassistant/components/netatmo/webhook.py | 40 ++- .../components/netatmo/test_device_trigger.py | 311 ++++++++++++++++++ 8 files changed, 632 insertions(+), 33 deletions(-) create mode 100644 homeassistant/components/netatmo/device_trigger.py create mode 100644 tests/components/netatmo/test_device_trigger.py diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 34a94df008a..91026c40c2f 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -26,12 +26,14 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.device_registry import async_get_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( ATTR_HEATING_POWER_REQUEST, ATTR_SCHEDULE_NAME, ATTR_SELECTED_SCHEDULE, + DATA_DEVICE_IDS, DATA_HANDLER, DATA_HOMES, DATA_SCHEDULES, @@ -237,6 +239,10 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): ) ) + registry = await async_get_registry(self.hass) + device = registry.async_get_device({(DOMAIN, self._id)}, set()) + self.hass.data[DOMAIN][DATA_DEVICE_IDS][self._home_id] = device.id + async def handle_event(self, event): """Handle webhook events.""" data = event["data"] diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 4c3650ef121..baee3e4035c 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -4,21 +4,36 @@ API = "api" DOMAIN = "netatmo" MANUFACTURER = "Netatmo" +MODEL_NAPLUG = "Relay" +MODEL_NATHERM1 = "Smart Thermostat" +MODEL_NRV = "Smart Radiator Valves" +MODEL_NOC = "Smart Outdoor Camera" +MODEL_NACAMERA = "Smart Indoor Camera" +MODEL_NSD = "Smart Smoke Alarm" +MODEL_NACAMDOORTAG = "Smart Door and Window Sensors" +MODEL_NHC = "Smart Indoor Air Quality Monitor" +MODEL_NAMAIN = "Smart Home Weather station – indoor module" +MODEL_NAMODULE1 = "Smart Home Weather station – outdoor module" +MODEL_NAMODULE4 = "Smart Additional Indoor module" +MODEL_NAMODULE3 = "Smart Rain Gauge" +MODEL_NAMODULE2 = "Smart Anemometer" +MODEL_PUBLIC = "Public Weather stations" + MODELS = { - "NAPlug": "Relay", - "NATherm1": "Smart Thermostat", - "NRV": "Smart Radiator Valves", - "NACamera": "Smart Indoor Camera", - "NOC": "Smart Outdoor Camera", - "NSD": "Smart Smoke Alarm", - "NACamDoorTag": "Smart Door and Window Sensors", - "NHC": "Smart Indoor Air Quality Monitor", - "NAMain": "Smart Home Weather station – indoor module", - "NAModule1": "Smart Home Weather station – outdoor module", - "NAModule4": "Smart Additional Indoor module", - "NAModule3": "Smart Rain Gauge", - "NAModule2": "Smart Anemometer", - "public": "Public Weather stations", + "NAPlug": MODEL_NAPLUG, + "NATherm1": MODEL_NATHERM1, + "NRV": MODEL_NRV, + "NACamera": MODEL_NACAMERA, + "NOC": MODEL_NOC, + "NSD": MODEL_NSD, + "NACamDoorTag": MODEL_NACAMDOORTAG, + "NHC": MODEL_NHC, + "NAMain": MODEL_NAMAIN, + "NAModule1": MODEL_NAMODULE1, + "NAModule4": MODEL_NAMODULE4, + "NAModule3": MODEL_NAMODULE3, + "NAModule2": MODEL_NAMODULE2, + "public": MODEL_PUBLIC, } AUTH = "netatmo_auth" @@ -76,12 +91,66 @@ SERVICE_SET_SCHEDULE = "set_schedule" SERVICE_SET_PERSONS_HOME = "set_persons_home" SERVICE_SET_PERSON_AWAY = "set_person_away" +# Climate events +EVENT_TYPE_SET_POINT = "set_point" EVENT_TYPE_CANCEL_SET_POINT = "cancel_set_point" +EVENT_TYPE_THERM_MODE = "therm_mode" +# Camera events EVENT_TYPE_LIGHT_MODE = "light_mode" +EVENT_TYPE_CAMERA_OUTDOOR = "outdoor" +EVENT_TYPE_CAMERA_ANIMAL = "animal" +EVENT_TYPE_CAMERA_HUMAN = "human" +EVENT_TYPE_CAMERA_VEHICLE = "vehicle" +EVENT_TYPE_CAMERA_MOVEMENT = "movement" +EVENT_TYPE_CAMERA_PERSON = "person" +EVENT_TYPE_CAMERA_PERSON_AWAY = "person_away" +# Door tags +EVENT_TYPE_DOOR_TAG_SMALL_MOVE = "tag_small_move" +EVENT_TYPE_DOOR_TAG_BIG_MOVE = "tag_big_move" +EVENT_TYPE_DOOR_TAG_OPEN = "tag_open" EVENT_TYPE_OFF = "off" EVENT_TYPE_ON = "on" -EVENT_TYPE_SET_POINT = "set_point" -EVENT_TYPE_THERM_MODE = "therm_mode" +EVENT_TYPE_ALARM_STARTED = "alarm_started" + +OUTDOOR_CAMERA_TRIGGERS = [ + EVENT_TYPE_CAMERA_ANIMAL, + EVENT_TYPE_CAMERA_HUMAN, + EVENT_TYPE_CAMERA_OUTDOOR, + EVENT_TYPE_CAMERA_VEHICLE, +] +INDOOR_CAMERA_TRIGGERS = [ + EVENT_TYPE_CAMERA_MOVEMENT, + EVENT_TYPE_CAMERA_PERSON, + EVENT_TYPE_CAMERA_PERSON_AWAY, + EVENT_TYPE_ALARM_STARTED, +] +DOOR_TAG_TRIGGERS = [ + EVENT_TYPE_DOOR_TAG_SMALL_MOVE, + EVENT_TYPE_DOOR_TAG_BIG_MOVE, + EVENT_TYPE_DOOR_TAG_OPEN, +] +CLIMATE_TRIGGERS = [ + EVENT_TYPE_SET_POINT, + EVENT_TYPE_CANCEL_SET_POINT, + EVENT_TYPE_THERM_MODE, +] +EVENT_ID_MAP = { + EVENT_TYPE_CAMERA_MOVEMENT: "device_id", + EVENT_TYPE_CAMERA_PERSON: "device_id", + EVENT_TYPE_CAMERA_PERSON_AWAY: "device_id", + EVENT_TYPE_CAMERA_ANIMAL: "device_id", + EVENT_TYPE_CAMERA_HUMAN: "device_id", + EVENT_TYPE_CAMERA_OUTDOOR: "device_id", + EVENT_TYPE_CAMERA_VEHICLE: "device_id", + EVENT_TYPE_DOOR_TAG_SMALL_MOVE: "device_id", + EVENT_TYPE_DOOR_TAG_BIG_MOVE: "device_id", + EVENT_TYPE_DOOR_TAG_OPEN: "device_id", + EVENT_TYPE_LIGHT_MODE: "device_id", + EVENT_TYPE_ALARM_STARTED: "device_id", + EVENT_TYPE_CANCEL_SET_POINT: "room_id", + EVENT_TYPE_SET_POINT: "room_id", + EVENT_TYPE_THERM_MODE: "home_id", +} MODE_LIGHT_ON = "on" MODE_LIGHT_OFF = "off" diff --git a/homeassistant/components/netatmo/device_trigger.py b/homeassistant/components/netatmo/device_trigger.py new file mode 100644 index 00000000000..38601e981db --- /dev/null +++ b/homeassistant/components/netatmo/device_trigger.py @@ -0,0 +1,155 @@ +"""Provides device automations for Netatmo.""" +from typing import List + +import voluptuous as vol + +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) +from homeassistant.components.homeassistant.triggers import event as event_trigger +from homeassistant.const import ( + ATTR_DEVICE_ID, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, +) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType + +from . import DOMAIN +from .climate import STATE_NETATMO_AWAY, STATE_NETATMO_HG, STATE_NETATMO_SCHEDULE +from .const import ( + CLIMATE_TRIGGERS, + EVENT_TYPE_THERM_MODE, + INDOOR_CAMERA_TRIGGERS, + MODEL_NACAMERA, + MODEL_NATHERM1, + MODEL_NOC, + MODEL_NRV, + NETATMO_EVENT, + OUTDOOR_CAMERA_TRIGGERS, +) + +CONF_SUBTYPE = "subtype" + +DEVICES = { + MODEL_NACAMERA: INDOOR_CAMERA_TRIGGERS, + MODEL_NOC: OUTDOOR_CAMERA_TRIGGERS, + MODEL_NATHERM1: CLIMATE_TRIGGERS, + MODEL_NRV: CLIMATE_TRIGGERS, +} + +SUBTYPES = { + EVENT_TYPE_THERM_MODE: [ + STATE_NETATMO_SCHEDULE, + STATE_NETATMO_HG, + STATE_NETATMO_AWAY, + ] +} + +TRIGGER_TYPES = OUTDOOR_CAMERA_TRIGGERS + INDOOR_CAMERA_TRIGGERS + CLIMATE_TRIGGERS + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + vol.Optional(CONF_SUBTYPE): str, + } +) + + +async def async_validate_trigger_config(hass, config): + """Validate config.""" + config = TRIGGER_SCHEMA(config) + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(config[CONF_DEVICE_ID]) + + trigger = config[CONF_TYPE] + + if ( + not device + or device.model not in DEVICES + or trigger not in DEVICES[device.model] + ): + raise InvalidDeviceAutomationConfig(f"Unsupported model {device.model}") + + return config + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for Netatmo devices.""" + registry = await entity_registry.async_get_registry(hass) + device_registry = await hass.helpers.device_registry.async_get_registry() + triggers = [] + + for entry in entity_registry.async_entries_for_device(registry, device_id): + device = device_registry.async_get(device_id) + + for trigger in DEVICES.get(device.model, []): + if trigger in SUBTYPES: + for subtype in SUBTYPES[trigger]: + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: trigger, + CONF_SUBTYPE: subtype, + } + ) + else: + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: trigger, + } + ) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(config[CONF_DEVICE_ID]) + + if not device: + return + + if device.model not in DEVICES: + return + + event_config = { + event_trigger.CONF_PLATFORM: "event", + event_trigger.CONF_EVENT_TYPE: NETATMO_EVENT, + event_trigger.CONF_EVENT_DATA: { + "type": config[CONF_TYPE], + ATTR_DEVICE_ID: config[ATTR_DEVICE_ID], + }, + } + if config[CONF_TYPE] in SUBTYPES: + event_config[event_trigger.CONF_EVENT_DATA]["data"] = { + "mode": config[CONF_SUBTYPE] + } + + event_config = event_trigger.TRIGGER_SCHEMA(event_config) + return await event_trigger.async_attach_trigger( + hass, event_config, action, automation_info, platform_type="device" + ) diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index d0753613555..1845cbe76e9 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -5,7 +5,7 @@ from typing import Dict, List from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers.entity import Entity -from .const import DOMAIN, MANUFACTURER, MODELS, SIGNAL_NAME +from .const import DATA_DEVICE_IDS, DOMAIN, MANUFACTURER, MODELS, SIGNAL_NAME from .data_handler import NetatmoDataHandler _LOGGER = logging.getLogger(__name__) @@ -58,6 +58,10 @@ class NetatmoBase(Entity): await self.data_handler.unregister_data_class(signal_name, None) + registry = await self.hass.helpers.device_registry.async_get_registry() + device = registry.async_get_device({(DOMAIN, self._id)}, set()) + self.hass.data[DOMAIN][DATA_DEVICE_IDS][self._id] = device.id + self.async_update_callback() async def async_will_remove_from_hass(self): diff --git a/homeassistant/components/netatmo/strings.json b/homeassistant/components/netatmo/strings.json index 60fdab5f22c..c65001b2e8f 100644 --- a/homeassistant/components/netatmo/strings.json +++ b/homeassistant/components/netatmo/strings.json @@ -39,5 +39,27 @@ "title": "Netatmo public weather sensor" } } + }, + "device_automation": { + "trigger_subtype": { + "away": "away", + "schedule": "schedule", + "hg": "frost guard" + }, + "trigger_type": { + "turned_off": "{entity_name} turned off", + "turned_on": "{entity_name} turned on", + "human": "{entity_name} detected a human", + "movement": "{entity_name} detected movement", + "person": "{entity_name} detected a person", + "person_away": "{entity_name} detected a person has left", + "animal": "{entity_name} detected an animal", + "outdoor": "{entity_name} detected an outdoor event", + "vehicle": "{entity_name} detected a vehicle", + "alarm_started": "{entity_name} detected an alarm", + "set_point": "Target temperature {entity_name} set manually", + "cancel_set_point": "{entity_name} has resumed its schedule", + "therm_mode": "{entity_name} switched to \"{subtype}\"" + } } -} +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/en.json b/homeassistant/components/netatmo/translations/en.json index e31d801b7a0..7e230374720 100644 --- a/homeassistant/components/netatmo/translations/en.json +++ b/homeassistant/components/netatmo/translations/en.json @@ -15,6 +15,28 @@ } } }, + "device_automation": { + "trigger_subtype": { + "away": "away", + "hg": "frost guard", + "schedule": "schedule" + }, + "trigger_type": { + "alarm_started": "{entity_name} detected an alarm", + "animal": "{entity_name} detected an animal", + "cancel_set_point": "{entity_name} has resumed its schedule", + "human": "{entity_name} detected a human", + "movement": "{entity_name} detected movement", + "outdoor": "{entity_name} detected an outdoor event", + "person": "{entity_name} detected a person", + "person_away": "{entity_name} detected a person has left", + "set_point": "Target temperature {entity_name} set manually", + "therm_mode": "{entity_name} switched to \"{subtype}\"", + "turned_off": "{entity_name} turned off", + "turned_on": "{entity_name} turned on", + "vehicle": "{entity_name} detected a vehicle" + } + }, "options": { "step": { "public_weather": { diff --git a/homeassistant/components/netatmo/webhook.py b/homeassistant/components/netatmo/webhook.py index 309451fd982..1fe7302038e 100644 --- a/homeassistant/components/netatmo/webhook.py +++ b/homeassistant/components/netatmo/webhook.py @@ -1,8 +1,7 @@ """The Netatmo integration.""" import logging -from homeassistant.const import ATTR_ID -from homeassistant.core import callback +from homeassistant.const import ATTR_DEVICE_ID, ATTR_ID from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( @@ -11,9 +10,11 @@ from .const import ( ATTR_IS_KNOWN, ATTR_NAME, ATTR_PERSONS, + DATA_DEVICE_IDS, DATA_PERSONS, DEFAULT_PERSON, DOMAIN, + EVENT_ID_MAP, NETATMO_EVENT, ) @@ -38,17 +39,16 @@ async def handle_webhook(hass, webhook_id, request): event_type = data.get(ATTR_EVENT_TYPE) if event_type in EVENT_TYPE_MAP: - async_send_event(hass, event_type, data) + await async_send_event(hass, event_type, data) for event_data in data.get(EVENT_TYPE_MAP[event_type], []): - async_evaluate_event(hass, event_data) + await async_evaluate_event(hass, event_data) else: - async_evaluate_event(hass, data) + await async_evaluate_event(hass, data) -@callback -def async_evaluate_event(hass, event_data): +async def async_evaluate_event(hass, event_data): """Evaluate events from webhook.""" event_type = event_data.get(ATTR_EVENT_TYPE) @@ -62,21 +62,31 @@ def async_evaluate_event(hass, event_data): person_event_data[ATTR_IS_KNOWN] = person.get(ATTR_IS_KNOWN) person_event_data[ATTR_FACE_URL] = person.get(ATTR_FACE_URL) - async_send_event(hass, event_type, person_event_data) + await async_send_event(hass, event_type, person_event_data) else: - _LOGGER.debug("%s: %s", event_type, event_data) - async_send_event(hass, event_type, event_data) + await async_send_event(hass, event_type, event_data) -@callback -def async_send_event(hass, event_type, data): +async def async_send_event(hass, event_type, data): """Send events.""" - hass.bus.async_fire( - event_type=NETATMO_EVENT, event_data={"type": event_type, "data": data} - ) + _LOGGER.debug("%s: %s", event_type, data) async_dispatcher_send( hass, f"signal-{DOMAIN}-webhook-{event_type}", {"type": event_type, "data": data}, ) + + if event_type not in EVENT_ID_MAP: + return + + data_device_id = data[EVENT_ID_MAP[event_type]] + + hass.bus.async_fire( + event_type=NETATMO_EVENT, + event_data={ + "type": event_type, + "data": data, + ATTR_DEVICE_ID: hass.data[DOMAIN][DATA_DEVICE_IDS].get(data_device_id), + }, + ) diff --git a/tests/components/netatmo/test_device_trigger.py b/tests/components/netatmo/test_device_trigger.py new file mode 100644 index 00000000000..7e014d2648f --- /dev/null +++ b/tests/components/netatmo/test_device_trigger.py @@ -0,0 +1,311 @@ +"""The tests for Netatmo device triggers.""" +import pytest + +import homeassistant.components.automation as automation +from homeassistant.components.netatmo import DOMAIN as NETATMO_DOMAIN +from homeassistant.components.netatmo.const import ( + CLIMATE_TRIGGERS, + INDOOR_CAMERA_TRIGGERS, + MODEL_NACAMERA, + MODEL_NAPLUG, + MODEL_NATHERM1, + MODEL_NOC, + MODEL_NRV, + NETATMO_EVENT, + OUTDOOR_CAMERA_TRIGGERS, +) +from homeassistant.components.netatmo.device_trigger import SUBTYPES +from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_capture_events, + async_get_device_automations, + async_mock_service, + mock_device_registry, + mock_registry, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +@pytest.mark.parametrize( + "platform,device_type,event_types", + [ + ("camera", MODEL_NOC, OUTDOOR_CAMERA_TRIGGERS), + ("camera", MODEL_NACAMERA, INDOOR_CAMERA_TRIGGERS), + ("climate", MODEL_NRV, CLIMATE_TRIGGERS), + ("climate", MODEL_NATHERM1, CLIMATE_TRIGGERS), + ], +) +async def test_get_triggers( + hass, device_reg, entity_reg, platform, device_type, event_types +): + """Test we get the expected triggers from a netatmo devices.""" + config_entry = MockConfigEntry(domain=NETATMO_DOMAIN, data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + model=device_type, + ) + entity_reg.async_get_or_create( + platform, NETATMO_DOMAIN, "5678", device_id=device_entry.id + ) + expected_triggers = [] + for event_type in event_types: + if event_type in SUBTYPES: + for subtype in SUBTYPES[event_type]: + expected_triggers.append( + { + "platform": "device", + "domain": NETATMO_DOMAIN, + "type": event_type, + "subtype": subtype, + "device_id": device_entry.id, + "entity_id": f"{platform}.{NETATMO_DOMAIN}_5678", + } + ) + else: + expected_triggers.append( + { + "platform": "device", + "domain": NETATMO_DOMAIN, + "type": event_type, + "device_id": device_entry.id, + "entity_id": f"{platform}.{NETATMO_DOMAIN}_5678", + } + ) + triggers = [ + trigger + for trigger in await async_get_device_automations( + hass, "trigger", device_entry.id + ) + if trigger["domain"] == NETATMO_DOMAIN + ] + assert_lists_same(triggers, expected_triggers) + + +@pytest.mark.parametrize( + "platform,camera_type,event_type", + [("camera", MODEL_NOC, trigger) for trigger in OUTDOOR_CAMERA_TRIGGERS] + + [("camera", MODEL_NACAMERA, trigger) for trigger in INDOOR_CAMERA_TRIGGERS] + + [ + ("climate", MODEL_NRV, trigger) + for trigger in CLIMATE_TRIGGERS + if trigger not in SUBTYPES + ] + + [ + ("climate", MODEL_NATHERM1, trigger) + for trigger in CLIMATE_TRIGGERS + if trigger not in SUBTYPES + ], +) +async def test_if_fires_on_event( + hass, calls, device_reg, entity_reg, platform, camera_type, event_type +): + """Test for event triggers firing.""" + mac_address = "12:34:56:AB:CD:EF" + connection = (device_registry.CONNECTION_NETWORK_MAC, mac_address) + config_entry = MockConfigEntry(domain=NETATMO_DOMAIN, data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={connection}, + identifiers={(NETATMO_DOMAIN, mac_address)}, + model=camera_type, + ) + entity_reg.async_get_or_create( + platform, NETATMO_DOMAIN, "5678", device_id=device_entry.id + ) + events = async_capture_events(hass, "netatmo_event") + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": NETATMO_DOMAIN, + "device_id": device_entry.id, + "entity_id": f"{platform}.{NETATMO_DOMAIN}_5678", + "type": event_type, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "{{trigger.event.data.type}} - {{trigger.platform}} - {{trigger.event.data.device_id}}" + ) + }, + }, + }, + ] + }, + ) + + device = device_reg.async_get_device(set(), {connection}) + assert device is not None + + # Fake that the entity is turning on. + hass.bus.async_fire( + event_type=NETATMO_EVENT, + event_data={ + "type": event_type, + ATTR_DEVICE_ID: device.id, + }, + ) + await hass.async_block_till_done() + assert len(events) == 1 + assert len(calls) == 1 + assert calls[0].data["some"] == f"{event_type} - device - {device.id}" + + +@pytest.mark.parametrize( + "platform,camera_type,event_type,sub_type", + [ + ("climate", MODEL_NRV, trigger, subtype) + for trigger in SUBTYPES + for subtype in SUBTYPES[trigger] + ] + + [ + ("climate", MODEL_NATHERM1, trigger, subtype) + for trigger in SUBTYPES + for subtype in SUBTYPES[trigger] + ], +) +async def test_if_fires_on_event_with_subtype( + hass, calls, device_reg, entity_reg, platform, camera_type, event_type, sub_type +): + """Test for event triggers firing.""" + mac_address = "12:34:56:AB:CD:EF" + connection = (device_registry.CONNECTION_NETWORK_MAC, mac_address) + config_entry = MockConfigEntry(domain=NETATMO_DOMAIN, data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={connection}, + identifiers={(NETATMO_DOMAIN, mac_address)}, + model=camera_type, + ) + entity_reg.async_get_or_create( + platform, NETATMO_DOMAIN, "5678", device_id=device_entry.id + ) + events = async_capture_events(hass, "netatmo_event") + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": NETATMO_DOMAIN, + "device_id": device_entry.id, + "entity_id": f"{platform}.{NETATMO_DOMAIN}_5678", + "type": event_type, + "subtype": sub_type, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "{{trigger.event.data.type}} - {{trigger.event.data.data.mode}} - " + "{{trigger.platform}} - {{trigger.event.data.device_id}}" + ) + }, + }, + }, + ] + }, + ) + + device = device_reg.async_get_device(set(), {connection}) + assert device is not None + + # Fake that the entity is turning on. + hass.bus.async_fire( + event_type=NETATMO_EVENT, + event_data={ + "type": event_type, + "data": { + "mode": sub_type, + }, + ATTR_DEVICE_ID: device.id, + }, + ) + await hass.async_block_till_done() + assert len(events) == 1 + assert len(calls) == 1 + assert calls[0].data["some"] == f"{event_type} - {sub_type} - device - {device.id}" + + +@pytest.mark.parametrize( + "platform,device_type,event_type", + [("climate", MODEL_NAPLUG, trigger) for trigger in CLIMATE_TRIGGERS], +) +async def test_if_invalid_device( + hass, device_reg, entity_reg, platform, device_type, event_type +): + """Test for event triggers firing.""" + mac_address = "12:34:56:AB:CD:EF" + connection = (device_registry.CONNECTION_NETWORK_MAC, mac_address) + config_entry = MockConfigEntry(domain=NETATMO_DOMAIN, data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={connection}, + identifiers={(NETATMO_DOMAIN, mac_address)}, + model=device_type, + ) + entity_reg.async_get_or_create( + platform, NETATMO_DOMAIN, "5678", device_id=device_entry.id + ) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": NETATMO_DOMAIN, + "device_id": device_entry.id, + "entity_id": f"{platform}.{NETATMO_DOMAIN}_5678", + "type": event_type, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "{{trigger.event.data.type}} - {{trigger.platform}} - {{trigger.event.data.device_id}}" + ) + }, + }, + }, + ] + }, + ) From 0eb8951aed07f18f641a8061ccb5602292314be7 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Wed, 24 Feb 2021 19:29:53 +0100 Subject: [PATCH 706/796] Remove recursive key reference (#46999) --- homeassistant/components/syncthru/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/syncthru/strings.json b/homeassistant/components/syncthru/strings.json index 0164fdf6ddc..67f50e84a98 100644 --- a/homeassistant/components/syncthru/strings.json +++ b/homeassistant/components/syncthru/strings.json @@ -12,7 +12,7 @@ "step": { "confirm": { "data": { - "name": "[%key:component::syncthru::config::step::user::data::name%]", + "name": "[%key:common::config_flow::data::name%]", "url": "[%key:component::syncthru::config::step::user::data::url%]" } }, From 868a536d8175d0a56fb8766d1fb38571dfc43b52 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 24 Feb 2021 19:39:35 +0100 Subject: [PATCH 707/796] Add number platform to Z-Wave JS (#46956) * add number platform to zwave_js integration * add discovery scheme for thermostat valve control, using number platform Co-authored-by: kpine --- homeassistant/components/zwave_js/const.py | 1 + .../components/zwave_js/discovery.py | 8 + homeassistant/components/zwave_js/number.py | 84 +++ tests/components/zwave_js/conftest.py | 14 + tests/components/zwave_js/test_number.py | 69 ++ .../aeotec_radiator_thermostat_state.json | 626 ++++++++++++++++++ 6 files changed, 802 insertions(+) create mode 100644 homeassistant/components/zwave_js/number.py create mode 100644 tests/components/zwave_js/test_number.py create mode 100644 tests/fixtures/zwave_js/aeotec_radiator_thermostat_state.json diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index dba4e6d33a3..19e6fc3db14 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -9,6 +9,7 @@ PLATFORMS = [ "fan", "light", "lock", + "number", "sensor", "switch", ] diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index f4f5c359e22..77709f84e58 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -365,6 +365,14 @@ DISCOVERY_SCHEMAS = [ device_class_specific={"Fan Switch"}, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, ), + # number platform + # valve control for thermostats + ZWaveDiscoverySchema( + platform="number", + hint="Valve control", + device_class_generic={"Thermostat"}, + primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, + ), # lights # primary value is the currentValue (brightness) # catch any device with multilevel CC as light diff --git a/homeassistant/components/zwave_js/number.py b/homeassistant/components/zwave_js/number.py new file mode 100644 index 00000000000..8f8e894cda2 --- /dev/null +++ b/homeassistant/components/zwave_js/number.py @@ -0,0 +1,84 @@ +"""Support for Z-Wave controls using the number platform.""" +from typing import Callable, List, Optional + +from zwave_js_server.client import Client as ZwaveClient + +from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .discovery import ZwaveDiscoveryInfo +from .entity import ZWaveBaseEntity + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up Z-Wave Number entity from Config Entry.""" + client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + + @callback + def async_add_number(info: ZwaveDiscoveryInfo) -> None: + """Add Z-Wave number entity.""" + entities: List[ZWaveBaseEntity] = [] + entities.append(ZwaveNumberEntity(config_entry, client, info)) + async_add_entities(entities) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect( + hass, + f"{DOMAIN}_{config_entry.entry_id}_add_{NUMBER_DOMAIN}", + async_add_number, + ) + ) + + +class ZwaveNumberEntity(ZWaveBaseEntity, NumberEntity): + """Representation of a Z-Wave number entity.""" + + def __init__( + self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize a ZwaveNumberEntity entity.""" + super().__init__(config_entry, client, info) + self._name = self.generate_name( + include_value_name=True, alternate_value_name=info.platform_hint + ) + if self.info.primary_value.metadata.writeable: + self._target_value = self.info.primary_value + else: + self._target_value = self.get_zwave_value("targetValue") + + @property + def min_value(self) -> float: + """Return the minimum value.""" + if self.info.primary_value.metadata.min is None: + return 0 + return float(self.info.primary_value.metadata.min) + + @property + def max_value(self) -> float: + """Return the maximum value.""" + if self.info.primary_value.metadata.max is None: + return 255 + return float(self.info.primary_value.metadata.max) + + @property + def value(self) -> Optional[float]: # type: ignore + """Return the entity value.""" + if self.info.primary_value.value is None: + return None + return float(self.info.primary_value.value) + + @property + def unit_of_measurement(self) -> Optional[str]: + """Return the unit of measurement of this entity, if any.""" + if self.info.primary_value.metadata.unit is None: + return None + return str(self.info.primary_value.metadata.unit) + + async def async_set_value(self, value: float) -> None: + """Set new value.""" + await self.info.node.async_set_value(self._target_value, value) diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index a9618acc64d..e0bc588abf4 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -160,6 +160,12 @@ def ge_12730_state_fixture(): return json.loads(load_fixture("zwave_js/fan_ge_12730_state.json")) +@pytest.fixture(name="aeotec_radiator_thermostat_state", scope="session") +def aeotec_radiator_thermostat_state_fixture(): + """Load the Aeotec Radiator Thermostat node state fixture data.""" + return json.loads(load_fixture("zwave_js/aeotec_radiator_thermostat_state.json")) + + @pytest.fixture(name="client") def mock_client_fixture(controller_state, version_state): """Mock a client.""" @@ -295,6 +301,14 @@ def nortek_thermostat_fixture(client, nortek_thermostat_state): return node +@pytest.fixture(name="aeotec_radiator_thermostat") +def aeotec_radiator_thermostat_fixture(client, aeotec_radiator_thermostat_state): + """Mock a Aeotec thermostat node.""" + node = Node(client, aeotec_radiator_thermostat_state) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="nortek_thermostat_added_event") def nortek_thermostat_added_event_fixture(client): """Mock a Nortek thermostat node added event.""" diff --git a/tests/components/zwave_js/test_number.py b/tests/components/zwave_js/test_number.py new file mode 100644 index 00000000000..b7d83068bea --- /dev/null +++ b/tests/components/zwave_js/test_number.py @@ -0,0 +1,69 @@ +"""Test the Z-Wave JS number platform.""" +from zwave_js_server.event import Event + +NUMBER_ENTITY = "number.thermostat_hvac_valve_control" + + +async def test_number(hass, client, aeotec_radiator_thermostat, integration): + """Test the number entity.""" + node = aeotec_radiator_thermostat + state = hass.states.get(NUMBER_ENTITY) + + assert state + assert state.state == "75.0" + + # Test turn on setting value + await hass.services.async_call( + "number", + "set_value", + {"entity_id": NUMBER_ENTITY, "value": 30}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 4 + assert args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "ccVersion": 1, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "label": "Target value", + "max": 99, + "min": 0, + "type": "number", + "readable": True, + "writeable": True, + "label": "Target value", + }, + } + assert args["value"] == 30.0 + + client.async_send_command.reset_mock() + + # Test value update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 4, + "args": { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "currentValue", + "newValue": 99, + "prevValue": 0, + "propertyName": "currentValue", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(NUMBER_ENTITY) + assert state.state == "99.0" diff --git a/tests/fixtures/zwave_js/aeotec_radiator_thermostat_state.json b/tests/fixtures/zwave_js/aeotec_radiator_thermostat_state.json new file mode 100644 index 00000000000..8cd6fe78201 --- /dev/null +++ b/tests/fixtures/zwave_js/aeotec_radiator_thermostat_state.json @@ -0,0 +1,626 @@ +{ + "nodeId": 4, + "index": 0, + "installerIcon": 4608, + "userIcon": 4608, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Routing Slave", + "generic": "Thermostat", + "specific": "Thermostat General V2", + "mandatorySupportedCCs": [ + "Basic", + "Manufacturer Specific", + "Thermostat Mode", + "Thermostat Setpoint", + "Version" + ], + "mandatoryControlCCs": [] + }, + "isListening": false, + "isFrequentListening": true, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 881, + "productId": 21, + "productType": 2, + "firmwareVersion": "0.16", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 7, + "deviceConfig": { + "manufacturerId": 881, + "manufacturer": "Aeotec Ltd.", + "label": "Radiator Thermostat", + "description": "Thermostat - HVAC", + "devices": [{ "productType": "0x0002", "productId": "0x0015" }], + "firmwareVersion": { "min": "0.0", "max": "255.255" }, + "paramInformation": { "_map": {} } + }, + "label": "Radiator Thermostat", + "neighbors": [6, 7, 45, 67], + "interviewAttempts": 1, + "endpoints": [ + { "nodeId": 4, "index": 0, "installerIcon": 4608, "userIcon": 4608 } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 99, + "label": "Target value" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Transition duration" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 99, + "label": "Current value" + }, + "value": 75 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { "switchType": 2 } + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { "switchType": 2 } + } + }, + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 5, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "\u00b0C", + "label": "Air temperature", + "ccSpecific": { "sensorType": 1, "scale": 0 } + }, + "value": 19.37 + }, + { + "endpoint": 0, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "mode", + "propertyName": "mode", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 31, + "label": "Thermostat mode", + "states": { + "0": "Off", + "1": "Heat", + "11": "Energy heat", + "15": "Full power" + } + }, + "value": 31 + }, + { + "endpoint": 0, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "manufacturerData", + "propertyName": "manufacturerData", + "ccVersion": 3, + "metadata": { "type": "any", "readable": true, "writeable": true } + }, + { + "endpoint": 0, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 8, + "max": 28, + "unit": "\u00b0C", + "ccSpecific": { "setpointType": 1 } + }, + "value": 24 + }, + { + "endpoint": 0, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyName": "setpoint", + "propertyKeyName": "Energy Save Heating", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 8, + "max": 28, + "unit": "\u00b0C", + "ccSpecific": { "setpointType": 11 } + }, + "value": 18 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "Invert LCD orientation", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Normal orientation", + "1": "LCD content inverted" + }, + "label": "Invert LCD orientation", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "LCD Timeout", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 30, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "LCD Timeout", + "description": "LCD Timeout in seconds", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Backlight", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Backlight disabled", + "1": "Backlight enabled" + }, + "label": "Backlight", + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Battery report", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Battery reporting disabled", + "1": "Battery reporting enabled" + }, + "label": "Battery report", + "description": "Battery reporting", + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "Measured Temperature", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 50, + "default": 5, + "format": 0, + "allowManualEntry": true, + "label": "Measured Temperature", + "description": "Measured Temperature report. Reporting Delta in 1/10 Celsius. '0' to disable reporting.", + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyName": "Valve position", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Valve position", + "description": "Valve position report. Reporting delta in percent. '0' to disable reporting.", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "Window open detection", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 3, + "default": 2, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Detection Disabled", + "1": "Sensitivity low", + "2": "Sensitivity medium", + "3": "Sensitivity high" + }, + "label": "Window open detection", + "description": "Control 'Window open detection' sensitivity", + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyName": "Temperature Offset", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": -128, + "max": 50, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Temperature Offset", + "description": "Measured Temperature offset", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyName": "Power Management", + "propertyKeyName": "Battery maintenance status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Battery maintenance status", + "states": { + "0": "idle", + "10": "Replace battery soon", + "11": "Replace battery now" + }, + "ccSpecific": { "notificationType": 8 } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "System", + "propertyName": "System", + "propertyKeyName": "Hardware status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Hardware status", + "states": { + "0": "idle", + "3": "System hardware failure (with failure code)" + }, + "ccSpecific": { "notificationType": 9 } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 881 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 21 + }, + { + "endpoint": 0, + "commandClass": 117, + "commandClassName": "Protection", + "property": "local", + "propertyName": "local", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Local protection state", + "states": { + "0": "Unprotected", + "1": "ProtectedBySequence", + "2": "NoOperationPossible" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 100, + "unit": "%", + "label": "Battery level" + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.61" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["0.16"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + } + ] + } \ No newline at end of file From 783e0f9a143181aae27c67e4c67cec14d2f35352 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 24 Feb 2021 14:15:47 -0500 Subject: [PATCH 708/796] Setup config entry even if vizio device is unreachable (#46864) --- .../components/vizio/media_player.py | 17 +++------ tests/components/vizio/conftest.py | 3 ++ tests/components/vizio/test_media_player.py | 37 +++++++++---------- 3 files changed, 27 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 4c06c89692a..53c8a2bba88 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -25,7 +25,6 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import callback -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import entity_platform from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( @@ -115,10 +114,6 @@ async def async_setup_entry( timeout=DEFAULT_TIMEOUT, ) - if not await device.can_connect_with_auth_check(): - _LOGGER.warning("Failed to connect to %s", host) - raise PlatformNotReady - apps_coordinator = hass.data[DOMAIN].get(CONF_APPS) entity = VizioDevice(config_entry, device, name, device_class, apps_coordinator) @@ -183,12 +178,6 @@ 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(log_api_exception=False) - - if not self._sw_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) if is_on is None: @@ -205,6 +194,12 @@ class VizioDevice(MediaPlayerEntity): ) self._available = True + if not self._model: + 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(log_api_exception=False) + if not is_on: self._state = STATE_OFF self._volume_level = None diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py index 917e6f7f291..8124827dbf0 100644 --- a/tests/components/vizio/conftest.py +++ b/tests/components/vizio/conftest.py @@ -157,6 +157,9 @@ def vizio_cant_connect_fixture(): with patch( "homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config", AsyncMock(return_value=False), + ), patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_power_state", + return_value=None, ): yield diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index 78976032b00..6d0ba2781e6 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -239,17 +239,6 @@ def _assert_source_list_with_apps( assert attr["source_list"] == list_to_test -async def _test_setup_failure(hass: HomeAssistantType, config: str) -> None: - """Test generic Vizio entity setup failure.""" - with patch( - "homeassistant.components.vizio.media_player.VizioAsync.can_connect_with_auth_check", - return_value=False, - ): - config_entry = MockConfigEntry(domain=DOMAIN, data=config, unique_id=UNIQUE_ID) - await _add_config_entry_to_hass(hass, config_entry) - assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0 - - async def _test_service( hass: HomeAssistantType, domain: str, @@ -334,18 +323,28 @@ async def test_init_tv_unavailable( await _test_setup_tv(hass, None) -async def test_setup_failure_speaker( - hass: HomeAssistantType, vizio_connect: pytest.fixture +async def test_setup_unavailable_speaker( + hass: HomeAssistantType, vizio_cant_connect: pytest.fixture ) -> None: - """Test speaker entity setup failure.""" - await _test_setup_failure(hass, MOCK_SPEAKER_CONFIG) + """Test speaker entity sets up as unavailable.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, unique_id=UNIQUE_ID + ) + await _add_config_entry_to_hass(hass, config_entry) + assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1 + assert hass.states.get("media_player.vizio").state == STATE_UNAVAILABLE -async def test_setup_failure_tv( - hass: HomeAssistantType, vizio_connect: pytest.fixture +async def test_setup_unavailable_tv( + hass: HomeAssistantType, vizio_cant_connect: pytest.fixture ) -> None: - """Test TV entity setup failure.""" - await _test_setup_failure(hass, MOCK_USER_VALID_TV_CONFIG) + """Test TV entity sets up as unavailable.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG, unique_id=UNIQUE_ID + ) + await _add_config_entry_to_hass(hass, config_entry) + assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1 + assert hass.states.get("media_player.vizio").state == STATE_UNAVAILABLE async def test_services( From daf7595ca64405572488b3e184dbf5eafbe97317 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 24 Feb 2021 20:16:36 +0100 Subject: [PATCH 709/796] Support value_template in MQTT triggers (#46891) * Support value_template in MQTT triggers * Rename value_template to payload_template * Revert "Rename value_template to payload_template" This reverts commit 902094eefc6612e6b5c3bdb7440520af050c7f20. --- homeassistant/components/mqtt/trigger.py | 26 ++++++++++++++------ tests/components/mqtt/test_trigger.py | 31 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/mqtt/trigger.py b/homeassistant/components/mqtt/trigger.py index a82ea355343..459adabd418 100644 --- a/homeassistant/components/mqtt/trigger.py +++ b/homeassistant/components/mqtt/trigger.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_PAYLOAD, CONF_PLATFORM +from homeassistant.const import CONF_PAYLOAD, CONF_PLATFORM, CONF_VALUE_TEMPLATE from homeassistant.core import HassJob, callback from homeassistant.helpers import config_validation as cv, template @@ -23,6 +23,7 @@ TRIGGER_SCHEMA = vol.Schema( vol.Required(CONF_PLATFORM): mqtt.DOMAIN, vol.Required(CONF_TOPIC): mqtt.util.valid_subscribe_topic_template, vol.Optional(CONF_PAYLOAD): cv.template, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, vol.Optional(CONF_QOS, default=DEFAULT_QOS): vol.All( vol.Coerce(int), vol.In([0, 1, 2]) @@ -36,7 +37,8 @@ _LOGGER = logging.getLogger(__name__) async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" topic = config[CONF_TOPIC] - payload = config.get(CONF_PAYLOAD) + wanted_payload = config.get(CONF_PAYLOAD) + value_template = config.get(CONF_VALUE_TEMPLATE) encoding = config[CONF_ENCODING] or None qos = config[CONF_QOS] job = HassJob(action) @@ -44,19 +46,29 @@ async def async_attach_trigger(hass, config, action, automation_info): if automation_info: variables = automation_info.get("variables") - template.attach(hass, payload) - if payload: - payload = payload.async_render(variables, limited=True) + template.attach(hass, wanted_payload) + if wanted_payload: + wanted_payload = wanted_payload.async_render(variables, limited=True) template.attach(hass, topic) if isinstance(topic, template.Template): topic = topic.async_render(variables, limited=True) topic = mqtt.util.valid_subscribe_topic(topic) + template.attach(hass, value_template) + @callback def mqtt_automation_listener(mqttmsg): """Listen for MQTT messages.""" - if payload is None or payload == mqttmsg.payload: + payload = mqttmsg.payload + + if value_template is not None: + payload = value_template.async_render_with_possible_json_value( + payload, + error_value=None, + ) + + if wanted_payload is None or wanted_payload == payload: data = { "platform": "mqtt", "topic": mqttmsg.topic, @@ -73,7 +85,7 @@ async def async_attach_trigger(hass, config, action, automation_info): hass.async_run_hass_job(job, {"trigger": data}) _LOGGER.debug( - "Attaching MQTT trigger for topic: '%s', payload: '%s'", topic, payload + "Attaching MQTT trigger for topic: '%s', payload: '%s'", topic, wanted_payload ) remove = await mqtt.async_subscribe( diff --git a/tests/components/mqtt/test_trigger.py b/tests/components/mqtt/test_trigger.py index 537a4f8dc64..23078b9ba23 100644 --- a/tests/components/mqtt/test_trigger.py +++ b/tests/components/mqtt/test_trigger.py @@ -111,6 +111,37 @@ async def test_if_fires_on_templated_topic_and_payload_match(hass, calls): assert len(calls) == 1 +async def test_if_fires_on_payload_template(hass, calls): + """Test if message is fired on templated topic and payload match.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "mqtt", + "topic": "test-topic", + "payload": "hello", + "value_template": "{{ value_json.wanted_key }}", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + async_fire_mqtt_message(hass, "test-topic", "hello") + await hass.async_block_till_done() + assert len(calls) == 0 + + async_fire_mqtt_message(hass, "test-topic", '{"unwanted_key":"hello"}') + await hass.async_block_till_done() + assert len(calls) == 0 + + async_fire_mqtt_message(hass, "test-topic", '{"wanted_key":"hello"}') + await hass.async_block_till_done() + assert len(calls) == 1 + + async def test_non_allowed_templates(hass, calls, caplog): """Test non allowed function in template.""" assert await async_setup_component( From 39baeb62f20e68a85a9d2963ccb4af26ff4e271e Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 24 Feb 2021 20:51:12 +0100 Subject: [PATCH 710/796] Add Sonos media browser image proxy (#46902) --- homeassistant/components/sonos/const.py | 111 ++++++ homeassistant/components/sonos/exception.py | 6 + .../components/sonos/media_browser.py | 218 +++++++++++ .../components/sonos/media_player.py | 352 +++--------------- 4 files changed, 382 insertions(+), 305 deletions(-) create mode 100644 homeassistant/components/sonos/exception.py create mode 100644 homeassistant/components/sonos/media_browser.py diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index da397b3e5e7..63d5745da21 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -1,4 +1,20 @@ """Const for Sonos.""" +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_ALBUM, + MEDIA_CLASS_ARTIST, + MEDIA_CLASS_COMPOSER, + MEDIA_CLASS_CONTRIBUTING_ARTIST, + MEDIA_CLASS_GENRE, + MEDIA_CLASS_PLAYLIST, + MEDIA_CLASS_TRACK, + MEDIA_TYPE_ALBUM, + MEDIA_TYPE_ARTIST, + MEDIA_TYPE_COMPOSER, + MEDIA_TYPE_CONTRIBUTING_ARTIST, + MEDIA_TYPE_GENRE, + MEDIA_TYPE_PLAYLIST, + MEDIA_TYPE_TRACK, +) DOMAIN = "sonos" DATA_SONOS = "sonos_media_player" @@ -10,3 +26,98 @@ SONOS_GENRE = "genres" SONOS_ALBUM_ARTIST = "album_artists" SONOS_TRACKS = "tracks" SONOS_COMPOSER = "composers" + +EXPANDABLE_MEDIA_TYPES = [ + MEDIA_TYPE_ALBUM, + MEDIA_TYPE_ARTIST, + MEDIA_TYPE_COMPOSER, + MEDIA_TYPE_GENRE, + MEDIA_TYPE_PLAYLIST, + SONOS_ALBUM, + SONOS_ALBUM_ARTIST, + SONOS_ARTIST, + SONOS_GENRE, + SONOS_COMPOSER, + SONOS_PLAYLISTS, +] + +SONOS_TO_MEDIA_CLASSES = { + SONOS_ALBUM: MEDIA_CLASS_ALBUM, + SONOS_ALBUM_ARTIST: MEDIA_CLASS_ARTIST, + SONOS_ARTIST: MEDIA_CLASS_CONTRIBUTING_ARTIST, + SONOS_COMPOSER: MEDIA_CLASS_COMPOSER, + SONOS_GENRE: MEDIA_CLASS_GENRE, + SONOS_PLAYLISTS: MEDIA_CLASS_PLAYLIST, + SONOS_TRACKS: MEDIA_CLASS_TRACK, + "object.container.album.musicAlbum": MEDIA_CLASS_ALBUM, + "object.container.genre.musicGenre": MEDIA_CLASS_PLAYLIST, + "object.container.person.composer": MEDIA_CLASS_PLAYLIST, + "object.container.person.musicArtist": MEDIA_CLASS_ARTIST, + "object.container.playlistContainer.sameArtist": MEDIA_CLASS_ARTIST, + "object.container.playlistContainer": MEDIA_CLASS_PLAYLIST, + "object.item.audioItem.musicTrack": MEDIA_CLASS_TRACK, +} + +SONOS_TO_MEDIA_TYPES = { + SONOS_ALBUM: MEDIA_TYPE_ALBUM, + SONOS_ALBUM_ARTIST: MEDIA_TYPE_ARTIST, + SONOS_ARTIST: MEDIA_TYPE_CONTRIBUTING_ARTIST, + SONOS_COMPOSER: MEDIA_TYPE_COMPOSER, + SONOS_GENRE: MEDIA_TYPE_GENRE, + SONOS_PLAYLISTS: MEDIA_TYPE_PLAYLIST, + SONOS_TRACKS: MEDIA_TYPE_TRACK, + "object.container.album.musicAlbum": MEDIA_TYPE_ALBUM, + "object.container.genre.musicGenre": MEDIA_TYPE_PLAYLIST, + "object.container.person.composer": MEDIA_TYPE_PLAYLIST, + "object.container.person.musicArtist": MEDIA_TYPE_ARTIST, + "object.container.playlistContainer.sameArtist": MEDIA_TYPE_ARTIST, + "object.container.playlistContainer": MEDIA_TYPE_PLAYLIST, + "object.item.audioItem.musicTrack": MEDIA_TYPE_TRACK, +} + +MEDIA_TYPES_TO_SONOS = { + MEDIA_TYPE_ALBUM: SONOS_ALBUM, + MEDIA_TYPE_ARTIST: SONOS_ALBUM_ARTIST, + MEDIA_TYPE_CONTRIBUTING_ARTIST: SONOS_ARTIST, + MEDIA_TYPE_COMPOSER: SONOS_COMPOSER, + MEDIA_TYPE_GENRE: SONOS_GENRE, + MEDIA_TYPE_PLAYLIST: SONOS_PLAYLISTS, + MEDIA_TYPE_TRACK: SONOS_TRACKS, +} + +SONOS_TYPES_MAPPING = { + "A:ALBUM": SONOS_ALBUM, + "A:ALBUMARTIST": SONOS_ALBUM_ARTIST, + "A:ARTIST": SONOS_ARTIST, + "A:COMPOSER": SONOS_COMPOSER, + "A:GENRE": SONOS_GENRE, + "A:PLAYLISTS": SONOS_PLAYLISTS, + "A:TRACKS": SONOS_TRACKS, + "object.container.album.musicAlbum": SONOS_ALBUM, + "object.container.genre.musicGenre": SONOS_GENRE, + "object.container.person.composer": SONOS_COMPOSER, + "object.container.person.musicArtist": SONOS_ALBUM_ARTIST, + "object.container.playlistContainer.sameArtist": SONOS_ARTIST, + "object.container.playlistContainer": SONOS_PLAYLISTS, + "object.item.audioItem.musicTrack": SONOS_TRACKS, +} + +LIBRARY_TITLES_MAPPING = { + "A:ALBUM": "Albums", + "A:ALBUMARTIST": "Artists", + "A:ARTIST": "Contributing Artists", + "A:COMPOSER": "Composers", + "A:GENRE": "Genres", + "A:PLAYLISTS": "Playlists", + "A:TRACKS": "Tracks", +} + +PLAYABLE_MEDIA_TYPES = [ + MEDIA_TYPE_ALBUM, + MEDIA_TYPE_ARTIST, + MEDIA_TYPE_COMPOSER, + MEDIA_TYPE_CONTRIBUTING_ARTIST, + MEDIA_TYPE_GENRE, + MEDIA_TYPE_PLAYLIST, + MEDIA_TYPE_TRACK, +] diff --git a/homeassistant/components/sonos/exception.py b/homeassistant/components/sonos/exception.py new file mode 100644 index 00000000000..3d5a1230bcb --- /dev/null +++ b/homeassistant/components/sonos/exception.py @@ -0,0 +1,6 @@ +"""Sonos specific exceptions.""" +from homeassistant.components.media_player.errors import BrowseError + + +class UnknownMediaType(BrowseError): + """Unknown media type.""" diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py new file mode 100644 index 00000000000..6b6c927ca1c --- /dev/null +++ b/homeassistant/components/sonos/media_browser.py @@ -0,0 +1,218 @@ +"""Support for media browsing.""" +import logging +import urllib.parse + +from homeassistant.components.media_player import BrowseMedia +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_DIRECTORY, + MEDIA_TYPE_ALBUM, +) +from homeassistant.components.media_player.errors import BrowseError + +from .const import ( + EXPANDABLE_MEDIA_TYPES, + LIBRARY_TITLES_MAPPING, + MEDIA_TYPES_TO_SONOS, + PLAYABLE_MEDIA_TYPES, + SONOS_ALBUM, + SONOS_ALBUM_ARTIST, + SONOS_GENRE, + SONOS_TO_MEDIA_CLASSES, + SONOS_TO_MEDIA_TYPES, + SONOS_TRACKS, + SONOS_TYPES_MAPPING, +) +from .exception import UnknownMediaType + +_LOGGER = logging.getLogger(__name__) + + +def build_item_response(media_library, payload, get_thumbnail_url=None): + """Create response payload for the provided media query.""" + if payload["search_type"] == MEDIA_TYPE_ALBUM and payload["idstring"].startswith( + ("A:GENRE", "A:COMPOSER") + ): + payload["idstring"] = "A:ALBUMARTIST/" + "/".join( + payload["idstring"].split("/")[2:] + ) + + media = media_library.browse_by_idstring( + MEDIA_TYPES_TO_SONOS[payload["search_type"]], + payload["idstring"], + full_album_art_uri=True, + max_items=0, + ) + + if media is None: + return + + thumbnail = None + title = None + + # Fetch album info for titles and thumbnails + # Can't be extracted from track info + if ( + payload["search_type"] == MEDIA_TYPE_ALBUM + and media[0].item_class == "object.item.audioItem.musicTrack" + ): + item = get_media(media_library, payload["idstring"], SONOS_ALBUM_ARTIST) + title = getattr(item, "title", None) + thumbnail = get_thumbnail_url(SONOS_ALBUM_ARTIST, payload["idstring"]) + + if not title: + try: + title = urllib.parse.unquote(payload["idstring"].split("/")[1]) + except IndexError: + title = LIBRARY_TITLES_MAPPING[payload["idstring"]] + + try: + media_class = SONOS_TO_MEDIA_CLASSES[ + MEDIA_TYPES_TO_SONOS[payload["search_type"]] + ] + except KeyError: + _LOGGER.debug("Unknown media type received %s", payload["search_type"]) + return None + + children = [] + for item in media: + try: + children.append(item_payload(item, get_thumbnail_url)) + except UnknownMediaType: + pass + + return BrowseMedia( + title=title, + thumbnail=thumbnail, + media_class=media_class, + media_content_id=payload["idstring"], + media_content_type=payload["search_type"], + children=children, + can_play=can_play(payload["search_type"]), + can_expand=can_expand(payload["search_type"]), + ) + + +def item_payload(item, get_thumbnail_url=None): + """ + Create response payload for a single media item. + + Used by async_browse_media. + """ + media_type = get_media_type(item) + try: + media_class = SONOS_TO_MEDIA_CLASSES[media_type] + except KeyError as err: + _LOGGER.debug("Unknown media type received %s", media_type) + raise UnknownMediaType from err + + content_id = get_content_id(item) + thumbnail = None + if getattr(item, "album_art_uri", None): + thumbnail = get_thumbnail_url(media_class, content_id) + + return BrowseMedia( + title=item.title, + thumbnail=thumbnail, + media_class=media_class, + media_content_id=content_id, + media_content_type=SONOS_TO_MEDIA_TYPES[media_type], + can_play=can_play(item.item_class), + can_expand=can_expand(item), + ) + + +def library_payload(media_library, get_thumbnail_url=None): + """ + Create response payload to describe contents of a specific library. + + Used by async_browse_media. + """ + if not media_library.browse_by_idstring( + "tracks", + "", + max_items=1, + ): + raise BrowseError("Local library not found") + + children = [] + for item in media_library.browse(): + try: + children.append(item_payload(item, get_thumbnail_url)) + except UnknownMediaType: + pass + + return BrowseMedia( + title="Music Library", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_id="library", + media_content_type="library", + can_play=False, + can_expand=True, + children=children, + ) + + +def get_media_type(item): + """Extract media type of item.""" + if item.item_class == "object.item.audioItem.musicTrack": + return SONOS_TRACKS + + if ( + item.item_class == "object.container.album.musicAlbum" + and SONOS_TYPES_MAPPING.get(item.item_id.split("/")[0]) + in [ + SONOS_ALBUM_ARTIST, + SONOS_GENRE, + ] + ): + return SONOS_TYPES_MAPPING[item.item_class] + + return SONOS_TYPES_MAPPING.get(item.item_id.split("/")[0], item.item_class) + + +def can_play(item): + """ + Test if playable. + + Used by async_browse_media. + """ + return SONOS_TO_MEDIA_TYPES.get(item) in PLAYABLE_MEDIA_TYPES + + +def can_expand(item): + """ + Test if expandable. + + Used by async_browse_media. + """ + if isinstance(item, str): + return SONOS_TYPES_MAPPING.get(item) in EXPANDABLE_MEDIA_TYPES + + if SONOS_TO_MEDIA_TYPES.get(item.item_class) in EXPANDABLE_MEDIA_TYPES: + return True + + return SONOS_TYPES_MAPPING.get(item.item_id) in EXPANDABLE_MEDIA_TYPES + + +def get_content_id(item): + """Extract content id or uri.""" + if item.item_class == "object.item.audioItem.musicTrack": + return item.get_uri() + return item.item_id + + +def get_media(media_library, item_id, search_type): + """Fetch media/album.""" + search_type = MEDIA_TYPES_TO_SONOS.get(search_type, search_type) + + if not item_id.startswith("A:ALBUM") and search_type == SONOS_ALBUM: + item_id = "A:ALBUMARTIST/" + "/".join(item_id.split("/")[2:]) + + for item in media_library.browse_by_idstring( + search_type, + "/".join(item_id.split("/")[:-1]), + full_album_art_uri=True, + max_items=0, + ): + if item.item_id == item_id: + return item diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 363e499292e..e6ee45e7a57 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -21,22 +21,11 @@ import pysonos.music_library import pysonos.snapshot import voluptuous as vol -from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, - MEDIA_CLASS_ALBUM, - MEDIA_CLASS_ARTIST, - MEDIA_CLASS_COMPOSER, - MEDIA_CLASS_CONTRIBUTING_ARTIST, - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_GENRE, - MEDIA_CLASS_PLAYLIST, - MEDIA_CLASS_TRACK, MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, - MEDIA_TYPE_COMPOSER, - MEDIA_TYPE_CONTRIBUTING_ARTIST, - MEDIA_TYPE_GENRE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_TRACK, @@ -71,20 +60,17 @@ from homeassistant.const import ( from homeassistant.core import ServiceCall, callback from homeassistant.helpers import config_validation as cv, entity_platform, service import homeassistant.helpers.device_registry as dr +from homeassistant.helpers.network import is_internal_request from homeassistant.util.dt import utcnow from . import CONF_ADVERTISE_ADDR, CONF_HOSTS, CONF_INTERFACE_ADDR from .const import ( DATA_SONOS, DOMAIN as SONOS_DOMAIN, - SONOS_ALBUM, - SONOS_ALBUM_ARTIST, - SONOS_ARTIST, - SONOS_COMPOSER, - SONOS_GENRE, - SONOS_PLAYLISTS, - SONOS_TRACKS, + MEDIA_TYPES_TO_SONOS, + PLAYABLE_MEDIA_TYPES, ) +from .media_browser import build_item_response, get_media, library_payload _LOGGER = logging.getLogger(__name__) @@ -111,101 +97,6 @@ SUPPORT_SONOS = ( SOURCE_LINEIN = "Line-in" SOURCE_TV = "TV" -EXPANDABLE_MEDIA_TYPES = [ - MEDIA_TYPE_ALBUM, - MEDIA_TYPE_ARTIST, - MEDIA_TYPE_COMPOSER, - MEDIA_TYPE_GENRE, - MEDIA_TYPE_PLAYLIST, - SONOS_ALBUM, - SONOS_ALBUM_ARTIST, - SONOS_ARTIST, - SONOS_GENRE, - SONOS_COMPOSER, - SONOS_PLAYLISTS, -] - -SONOS_TO_MEDIA_CLASSES = { - SONOS_ALBUM: MEDIA_CLASS_ALBUM, - SONOS_ALBUM_ARTIST: MEDIA_CLASS_ARTIST, - SONOS_ARTIST: MEDIA_CLASS_CONTRIBUTING_ARTIST, - SONOS_COMPOSER: MEDIA_CLASS_COMPOSER, - SONOS_GENRE: MEDIA_CLASS_GENRE, - SONOS_PLAYLISTS: MEDIA_CLASS_PLAYLIST, - SONOS_TRACKS: MEDIA_CLASS_TRACK, - "object.container.album.musicAlbum": MEDIA_CLASS_ALBUM, - "object.container.genre.musicGenre": MEDIA_CLASS_PLAYLIST, - "object.container.person.composer": MEDIA_CLASS_PLAYLIST, - "object.container.person.musicArtist": MEDIA_CLASS_ARTIST, - "object.container.playlistContainer.sameArtist": MEDIA_CLASS_ARTIST, - "object.container.playlistContainer": MEDIA_CLASS_PLAYLIST, - "object.item.audioItem.musicTrack": MEDIA_CLASS_TRACK, -} - -SONOS_TO_MEDIA_TYPES = { - SONOS_ALBUM: MEDIA_TYPE_ALBUM, - SONOS_ALBUM_ARTIST: MEDIA_TYPE_ARTIST, - SONOS_ARTIST: MEDIA_TYPE_CONTRIBUTING_ARTIST, - SONOS_COMPOSER: MEDIA_TYPE_COMPOSER, - SONOS_GENRE: MEDIA_TYPE_GENRE, - SONOS_PLAYLISTS: MEDIA_TYPE_PLAYLIST, - SONOS_TRACKS: MEDIA_TYPE_TRACK, - "object.container.album.musicAlbum": MEDIA_TYPE_ALBUM, - "object.container.genre.musicGenre": MEDIA_TYPE_PLAYLIST, - "object.container.person.composer": MEDIA_TYPE_PLAYLIST, - "object.container.person.musicArtist": MEDIA_TYPE_ARTIST, - "object.container.playlistContainer.sameArtist": MEDIA_TYPE_ARTIST, - "object.container.playlistContainer": MEDIA_TYPE_PLAYLIST, - "object.item.audioItem.musicTrack": MEDIA_TYPE_TRACK, -} - -MEDIA_TYPES_TO_SONOS = { - MEDIA_TYPE_ALBUM: SONOS_ALBUM, - MEDIA_TYPE_ARTIST: SONOS_ALBUM_ARTIST, - MEDIA_TYPE_CONTRIBUTING_ARTIST: SONOS_ARTIST, - MEDIA_TYPE_COMPOSER: SONOS_COMPOSER, - MEDIA_TYPE_GENRE: SONOS_GENRE, - MEDIA_TYPE_PLAYLIST: SONOS_PLAYLISTS, - MEDIA_TYPE_TRACK: SONOS_TRACKS, -} - -SONOS_TYPES_MAPPING = { - "A:ALBUM": SONOS_ALBUM, - "A:ALBUMARTIST": SONOS_ALBUM_ARTIST, - "A:ARTIST": SONOS_ARTIST, - "A:COMPOSER": SONOS_COMPOSER, - "A:GENRE": SONOS_GENRE, - "A:PLAYLISTS": SONOS_PLAYLISTS, - "A:TRACKS": SONOS_TRACKS, - "object.container.album.musicAlbum": SONOS_ALBUM, - "object.container.genre.musicGenre": SONOS_GENRE, - "object.container.person.composer": SONOS_COMPOSER, - "object.container.person.musicArtist": SONOS_ALBUM_ARTIST, - "object.container.playlistContainer.sameArtist": SONOS_ARTIST, - "object.container.playlistContainer": SONOS_PLAYLISTS, - "object.item.audioItem.musicTrack": SONOS_TRACKS, -} - -LIBRARY_TITLES_MAPPING = { - "A:ALBUM": "Albums", - "A:ALBUMARTIST": "Artists", - "A:ARTIST": "Contributing Artists", - "A:COMPOSER": "Composers", - "A:GENRE": "Genres", - "A:PLAYLISTS": "Playlists", - "A:TRACKS": "Tracks", -} - -PLAYABLE_MEDIA_TYPES = [ - MEDIA_TYPE_ALBUM, - MEDIA_TYPE_ARTIST, - MEDIA_TYPE_COMPOSER, - MEDIA_TYPE_CONTRIBUTING_ARTIST, - MEDIA_TYPE_GENRE, - MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_TRACK, -] - REPEAT_TO_SONOS = { REPEAT_MODE_OFF: False, REPEAT_MODE_ALL: True, @@ -244,10 +135,6 @@ ATTR_STATUS_LIGHT = "status_light" UNAVAILABLE_VALUES = {"", "NOT_IMPLEMENTED", None} -class UnknownMediaType(BrowseError): - """Unknown media type.""" - - class SonosData: """Storage class for platform global data.""" @@ -1491,11 +1378,51 @@ class SonosEntity(MediaPlayerEntity): return attributes + async def async_get_browse_image( + self, media_content_type, media_content_id, media_image_id=None + ): + """Fetch media browser image to serve via proxy.""" + if ( + media_content_type in [MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST] + and media_content_id + ): + item = await self.hass.async_add_executor_job( + get_media, + self._media_library, + media_content_id, + MEDIA_TYPES_TO_SONOS[media_content_type], + ) + image_url = getattr(item, "album_art_uri", None) + if image_url: + result = await self._async_fetch_image(image_url) + return result + + return (None, None) + async def async_browse_media(self, media_content_type=None, media_content_id=None): """Implement the websocket media browsing helper.""" + is_internal = is_internal_request(self.hass) + + def _get_thumbnail_url( + media_content_type, media_content_id, media_image_id=None + ): + if is_internal: + item = get_media( + self._media_library, + media_content_id, + media_content_type, + ) + return getattr(item, "album_art_uri", None) + + return self.get_browse_image_url( + media_content_type, + urllib.parse.quote_plus(media_content_id), + media_image_id, + ) + if media_content_type in [None, "library"]: return await self.hass.async_add_executor_job( - library_payload, self._media_library + library_payload, self._media_library, _get_thumbnail_url ) payload = { @@ -1503,195 +1430,10 @@ class SonosEntity(MediaPlayerEntity): "idstring": media_content_id, } response = await self.hass.async_add_executor_job( - build_item_response, self._media_library, payload + build_item_response, self._media_library, payload, _get_thumbnail_url ) if response is None: raise BrowseError( f"Media not found: {media_content_type} / {media_content_id}" ) return response - - -def build_item_response(media_library, payload): - """Create response payload for the provided media query.""" - if payload["search_type"] == MEDIA_TYPE_ALBUM and payload["idstring"].startswith( - ("A:GENRE", "A:COMPOSER") - ): - payload["idstring"] = "A:ALBUMARTIST/" + "/".join( - payload["idstring"].split("/")[2:] - ) - - media = media_library.browse_by_idstring( - MEDIA_TYPES_TO_SONOS[payload["search_type"]], - payload["idstring"], - full_album_art_uri=True, - max_items=0, - ) - - if media is None: - return - - thumbnail = None - title = None - - # Fetch album info for titles and thumbnails - # Can't be extracted from track info - if ( - payload["search_type"] == MEDIA_TYPE_ALBUM - and media[0].item_class == "object.item.audioItem.musicTrack" - ): - item = get_media(media_library, payload["idstring"], SONOS_ALBUM_ARTIST) - title = getattr(item, "title", None) - thumbnail = getattr(item, "album_art_uri", media[0].album_art_uri) - - if not title: - try: - title = urllib.parse.unquote(payload["idstring"].split("/")[1]) - except IndexError: - title = LIBRARY_TITLES_MAPPING[payload["idstring"]] - - try: - media_class = SONOS_TO_MEDIA_CLASSES[ - MEDIA_TYPES_TO_SONOS[payload["search_type"]] - ] - except KeyError: - _LOGGER.debug("Unknown media type received %s", payload["search_type"]) - return None - - children = [] - for item in media: - try: - children.append(item_payload(item)) - except UnknownMediaType: - pass - - return BrowseMedia( - title=title, - thumbnail=thumbnail, - media_class=media_class, - media_content_id=payload["idstring"], - media_content_type=payload["search_type"], - children=children, - can_play=can_play(payload["search_type"]), - can_expand=can_expand(payload["search_type"]), - ) - - -def item_payload(item): - """ - Create response payload for a single media item. - - Used by async_browse_media. - """ - media_type = get_media_type(item) - try: - media_class = SONOS_TO_MEDIA_CLASSES[media_type] - except KeyError as err: - _LOGGER.debug("Unknown media type received %s", media_type) - raise UnknownMediaType from err - return BrowseMedia( - title=item.title, - thumbnail=getattr(item, "album_art_uri", None), - media_class=media_class, - media_content_id=get_content_id(item), - media_content_type=SONOS_TO_MEDIA_TYPES[media_type], - can_play=can_play(item.item_class), - can_expand=can_expand(item), - ) - - -def library_payload(media_library): - """ - Create response payload to describe contents of a specific library. - - Used by async_browse_media. - """ - if not media_library.browse_by_idstring( - "tracks", - "", - max_items=1, - ): - raise BrowseError("Local library not found") - - children = [] - for item in media_library.browse(): - try: - children.append(item_payload(item)) - except UnknownMediaType: - pass - - return BrowseMedia( - title="Music Library", - media_class=MEDIA_CLASS_DIRECTORY, - media_content_id="library", - media_content_type="library", - can_play=False, - can_expand=True, - children=children, - ) - - -def get_media_type(item): - """Extract media type of item.""" - if item.item_class == "object.item.audioItem.musicTrack": - return SONOS_TRACKS - - if ( - item.item_class == "object.container.album.musicAlbum" - and SONOS_TYPES_MAPPING.get(item.item_id.split("/")[0]) - in [ - SONOS_ALBUM_ARTIST, - SONOS_GENRE, - ] - ): - return SONOS_TYPES_MAPPING[item.item_class] - - return SONOS_TYPES_MAPPING.get(item.item_id.split("/")[0], item.item_class) - - -def can_play(item): - """ - Test if playable. - - Used by async_browse_media. - """ - return SONOS_TO_MEDIA_TYPES.get(item) in PLAYABLE_MEDIA_TYPES - - -def can_expand(item): - """ - Test if expandable. - - Used by async_browse_media. - """ - if isinstance(item, str): - return SONOS_TYPES_MAPPING.get(item) in EXPANDABLE_MEDIA_TYPES - - if SONOS_TO_MEDIA_TYPES.get(item.item_class) in EXPANDABLE_MEDIA_TYPES: - return True - - return SONOS_TYPES_MAPPING.get(item.item_id) in EXPANDABLE_MEDIA_TYPES - - -def get_content_id(item): - """Extract content id or uri.""" - if item.item_class == "object.item.audioItem.musicTrack": - return item.get_uri() - return item.item_id - - -def get_media(media_library, item_id, search_type): - """Fetch media/album.""" - search_type = MEDIA_TYPES_TO_SONOS.get(search_type, search_type) - - if not item_id.startswith("A:ALBUM") and search_type == SONOS_ALBUM: - item_id = "A:ALBUMARTIST/" + "/".join(item_id.split("/")[2:]) - - for item in media_library.browse_by_idstring( - search_type, - "/".join(item_id.split("/")[:-1]), - full_album_art_uri=True, - max_items=0, - ): - if item.item_id == item_id: - return item From 2f1dba74d1254e2e575359f33e938fe070c801cc Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 24 Feb 2021 13:57:02 -0600 Subject: [PATCH 711/796] Use Plex server URL as config entry title (#47010) --- homeassistant/components/plex/config_flow.py | 2 +- tests/components/plex/test_config_flow.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index e52e4597bf9..d611c09c43e 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -240,7 +240,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.debug("Valid config created for %s", plex_server.friendly_name) - return self.async_create_entry(title=plex_server.friendly_name, data=data) + return self.async_create_entry(title=url, data=data) async def async_step_select_server(self, user_input=None): """Use selected Plex server.""" diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index bc0e59e658f..bdd78131800 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -203,7 +203,7 @@ async def test_single_available_server(hass, mock_plex_calls): server_id = result["data"][CONF_SERVER_IDENTIFIER] mock_plex_server = hass.data[DOMAIN][SERVERS][server_id] - assert result["title"] == mock_plex_server.friendly_name + assert result["title"] == mock_plex_server.url_in_use assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name assert ( result["data"][CONF_SERVER_IDENTIFIER] @@ -259,7 +259,7 @@ async def test_multiple_servers_with_selection( server_id = result["data"][CONF_SERVER_IDENTIFIER] mock_plex_server = hass.data[DOMAIN][SERVERS][server_id] - assert result["title"] == mock_plex_server.friendly_name + assert result["title"] == mock_plex_server.url_in_use assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name assert ( result["data"][CONF_SERVER_IDENTIFIER] @@ -317,7 +317,7 @@ async def test_adding_last_unconfigured_server( server_id = result["data"][CONF_SERVER_IDENTIFIER] mock_plex_server = hass.data[DOMAIN][SERVERS][server_id] - assert result["title"] == mock_plex_server.friendly_name + assert result["title"] == mock_plex_server.url_in_use assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name assert ( result["data"][CONF_SERVER_IDENTIFIER] @@ -656,7 +656,7 @@ async def test_manual_config(hass, mock_plex_calls): server_id = result["data"][CONF_SERVER_IDENTIFIER] mock_plex_server = hass.data[DOMAIN][SERVERS][server_id] - assert result["title"] == mock_plex_server.friendly_name + assert result["title"] == mock_plex_server.url_in_use assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name assert result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machine_identifier assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server.url_in_use @@ -692,7 +692,7 @@ async def test_manual_config_with_token(hass, mock_plex_calls): server_id = result["data"][CONF_SERVER_IDENTIFIER] mock_plex_server = hass.data[DOMAIN][SERVERS][server_id] - assert result["title"] == mock_plex_server.friendly_name + assert result["title"] == mock_plex_server.url_in_use assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name assert result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machine_identifier assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server.url_in_use From ba51ada4944c247f853628fb70039c466b40a6e2 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 24 Feb 2021 14:00:58 -0600 Subject: [PATCH 712/796] Bump plexapi to 4.4.0 (#47007) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 913f405cfcd..49388bdfdb6 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==4.3.1", + "plexapi==4.4.0", "plexauth==0.0.6", "plexwebsocket==0.0.12" ], diff --git a/requirements_all.txt b/requirements_all.txt index 102b474f3bf..bcef556ae38 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1141,7 +1141,7 @@ pillow==8.1.0 pizzapi==0.0.3 # homeassistant.components.plex -plexapi==4.3.1 +plexapi==4.4.0 # homeassistant.components.plex plexauth==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7070b69162e..155aaa269e5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -587,7 +587,7 @@ pilight==0.1.1 pillow==8.1.0 # homeassistant.components.plex -plexapi==4.3.1 +plexapi==4.4.0 # homeassistant.components.plex plexauth==0.0.6 From 8d2606134d3918d9210ccaf0a24cbbf3ef65c5a7 Mon Sep 17 00:00:00 2001 From: Nathan Tilley Date: Wed, 24 Feb 2021 15:11:20 -0500 Subject: [PATCH 713/796] Add FAA Delays Integration (#41347) Co-authored-by: Franck Nijhof Co-authored-by: Martin Hjelmare --- .coveragerc | 2 + CODEOWNERS | 1 + .../components/faa_delays/__init__.py | 84 ++++++++++++ .../components/faa_delays/binary_sensor.py | 93 ++++++++++++++ .../components/faa_delays/config_flow.py | 62 +++++++++ homeassistant/components/faa_delays/const.py | 28 ++++ .../components/faa_delays/manifest.json | 8 ++ .../components/faa_delays/strings.json | 21 +++ .../faa_delays/translations/en.json | 19 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/faa_delays/__init__.py | 1 + .../components/faa_delays/test_config_flow.py | 120 ++++++++++++++++++ 14 files changed, 446 insertions(+) create mode 100644 homeassistant/components/faa_delays/__init__.py create mode 100644 homeassistant/components/faa_delays/binary_sensor.py create mode 100644 homeassistant/components/faa_delays/config_flow.py create mode 100644 homeassistant/components/faa_delays/const.py create mode 100644 homeassistant/components/faa_delays/manifest.json create mode 100644 homeassistant/components/faa_delays/strings.json create mode 100644 homeassistant/components/faa_delays/translations/en.json create mode 100644 tests/components/faa_delays/__init__.py create mode 100644 tests/components/faa_delays/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index dfa742b490c..50fcf151821 100644 --- a/.coveragerc +++ b/.coveragerc @@ -272,6 +272,8 @@ omit = homeassistant/components/evohome/* homeassistant/components/ezviz/* homeassistant/components/familyhub/camera.py + homeassistant/components/faa_delays/__init__.py + homeassistant/components/faa_delays/binary_sensor.py homeassistant/components/fastdotcom/* homeassistant/components/ffmpeg/camera.py homeassistant/components/fibaro/* diff --git a/CODEOWNERS b/CODEOWNERS index 398e5b15f7f..b0a31203009 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -146,6 +146,7 @@ homeassistant/components/esphome/* @OttoWinter homeassistant/components/essent/* @TheLastProject homeassistant/components/evohome/* @zxdavb homeassistant/components/ezviz/* @baqs +homeassistant/components/faa_delays/* @ntilley905 homeassistant/components/fastdotcom/* @rohankapoorcom homeassistant/components/file/* @fabaff homeassistant/components/filter/* @dgomes diff --git a/homeassistant/components/faa_delays/__init__.py b/homeassistant/components/faa_delays/__init__.py new file mode 100644 index 00000000000..b9def765123 --- /dev/null +++ b/homeassistant/components/faa_delays/__init__.py @@ -0,0 +1,84 @@ +"""The FAA Delays integration.""" +import asyncio +from datetime import timedelta +import logging + +from aiohttp import ClientConnectionError +from async_timeout import timeout +from faadelays import Airport + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ID +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["binary_sensor"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the FAA Delays component.""" + hass.data[DOMAIN] = {} + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up FAA Delays from a config entry.""" + code = entry.data[CONF_ID] + + coordinator = FAADataUpdateCoordinator(hass, code) + 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) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class FAADataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching FAA API data from a single endpoint.""" + + def __init__(self, hass, code): + """Initialize the coordinator.""" + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=timedelta(minutes=1) + ) + self.session = aiohttp_client.async_get_clientsession(hass) + self.data = Airport(code, self.session) + self.code = code + + async def _async_update_data(self): + try: + with timeout(10): + await self.data.update() + except ClientConnectionError as err: + raise UpdateFailed(err) from err + return self.data diff --git a/homeassistant/components/faa_delays/binary_sensor.py b/homeassistant/components/faa_delays/binary_sensor.py new file mode 100644 index 00000000000..6c5876b7017 --- /dev/null +++ b/homeassistant/components/faa_delays/binary_sensor.py @@ -0,0 +1,93 @@ +"""Platform for FAA Delays sensor component.""" +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.const import ATTR_ICON, ATTR_NAME +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, FAA_BINARY_SENSORS + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up a FAA sensor based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + binary_sensors = [] + for kind, attrs in FAA_BINARY_SENSORS.items(): + name = attrs[ATTR_NAME] + icon = attrs[ATTR_ICON] + + binary_sensors.append( + FAABinarySensor(coordinator, kind, name, icon, entry.entry_id) + ) + + async_add_entities(binary_sensors) + + +class FAABinarySensor(CoordinatorEntity, BinarySensorEntity): + """Define a binary sensor for FAA Delays.""" + + def __init__(self, coordinator, sensor_type, name, icon, entry_id): + """Initialize the sensor.""" + super().__init__(coordinator) + + self.coordinator = coordinator + self._entry_id = entry_id + self._icon = icon + self._name = name + self._sensor_type = sensor_type + self._id = self.coordinator.data.iata + self._attrs = {} + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self._id} {self._name}" + + @property + def icon(self): + """Return the icon.""" + return self._icon + + @property + def is_on(self): + """Return the status of the sensor.""" + if self._sensor_type == "GROUND_DELAY": + return self.coordinator.data.ground_delay.status + if self._sensor_type == "GROUND_STOP": + return self.coordinator.data.ground_stop.status + if self._sensor_type == "DEPART_DELAY": + return self.coordinator.data.depart_delay.status + if self._sensor_type == "ARRIVE_DELAY": + return self.coordinator.data.arrive_delay.status + if self._sensor_type == "CLOSURE": + return self.coordinator.data.closure.status + return None + + @property + def unique_id(self): + """Return a unique, Home Assistant friendly identifier for this entity.""" + return f"{self._id}_{self._sensor_type}" + + @property + def device_state_attributes(self): + """Return attributes for sensor.""" + if self._sensor_type == "GROUND_DELAY": + self._attrs["average"] = self.coordinator.data.ground_delay.average + self._attrs["reason"] = self.coordinator.data.ground_delay.reason + elif self._sensor_type == "GROUND_STOP": + self._attrs["endtime"] = self.coordinator.data.ground_stop.endtime + self._attrs["reason"] = self.coordinator.data.ground_stop.reason + elif self._sensor_type == "DEPART_DELAY": + self._attrs["minimum"] = self.coordinator.data.depart_delay.minimum + self._attrs["maximum"] = self.coordinator.data.depart_delay.maximum + self._attrs["trend"] = self.coordinator.data.depart_delay.trend + self._attrs["reason"] = self.coordinator.data.depart_delay.reason + elif self._sensor_type == "ARRIVE_DELAY": + self._attrs["minimum"] = self.coordinator.data.arrive_delay.minimum + self._attrs["maximum"] = self.coordinator.data.arrive_delay.maximum + self._attrs["trend"] = self.coordinator.data.arrive_delay.trend + self._attrs["reason"] = self.coordinator.data.arrive_delay.reason + elif self._sensor_type == "CLOSURE": + self._attrs["begin"] = self.coordinator.data.closure.begin + self._attrs["end"] = self.coordinator.data.closure.end + self._attrs["reason"] = self.coordinator.data.closure.reason + return self._attrs diff --git a/homeassistant/components/faa_delays/config_flow.py b/homeassistant/components/faa_delays/config_flow.py new file mode 100644 index 00000000000..46d917cc92f --- /dev/null +++ b/homeassistant/components/faa_delays/config_flow.py @@ -0,0 +1,62 @@ +"""Config flow for FAA Delays integration.""" +import logging + +from aiohttp import ClientConnectionError +import faadelays +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_ID +from homeassistant.helpers import aiohttp_client + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_ID): str}) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for FAA Delays.""" + + 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: + + await self.async_set_unique_id(user_input[CONF_ID]) + self._abort_if_unique_id_configured() + + websession = aiohttp_client.async_get_clientsession(self.hass) + + data = faadelays.Airport(user_input[CONF_ID], websession) + + try: + await data.update() + + except faadelays.InvalidAirport: + _LOGGER.error("Airport code %s is invalid", user_input[CONF_ID]) + errors[CONF_ID] = "invalid_airport" + + except ClientConnectionError: + _LOGGER.error("Error connecting to FAA API") + errors["base"] = "cannot_connect" + + except Exception as error: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception: %s", error) + errors["base"] = "unknown" + + if not errors: + _LOGGER.debug( + "Creating entry with id: %s, name: %s", + user_input[CONF_ID], + data.name, + ) + return self.async_create_entry(title=data.name, data=user_input) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/faa_delays/const.py b/homeassistant/components/faa_delays/const.py new file mode 100644 index 00000000000..c725be88106 --- /dev/null +++ b/homeassistant/components/faa_delays/const.py @@ -0,0 +1,28 @@ +"""Constants for the FAA Delays integration.""" + +from homeassistant.const import ATTR_ICON, ATTR_NAME + +DOMAIN = "faa_delays" + +FAA_BINARY_SENSORS = { + "GROUND_DELAY": { + ATTR_NAME: "Ground Delay", + ATTR_ICON: "mdi:airport", + }, + "GROUND_STOP": { + ATTR_NAME: "Ground Stop", + ATTR_ICON: "mdi:airport", + }, + "DEPART_DELAY": { + ATTR_NAME: "Departure Delay", + ATTR_ICON: "mdi:airplane-takeoff", + }, + "ARRIVE_DELAY": { + ATTR_NAME: "Arrival Delay", + ATTR_ICON: "mdi:airplane-landing", + }, + "CLOSURE": { + ATTR_NAME: "Closure", + ATTR_ICON: "mdi:airplane:off", + }, +} diff --git a/homeassistant/components/faa_delays/manifest.json b/homeassistant/components/faa_delays/manifest.json new file mode 100644 index 00000000000..4148e7b956f --- /dev/null +++ b/homeassistant/components/faa_delays/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "faa_delays", + "name": "FAA Delays", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/faadelays", + "requirements": ["faadelays==0.0.6"], + "codeowners": ["@ntilley905"] +} diff --git a/homeassistant/components/faa_delays/strings.json b/homeassistant/components/faa_delays/strings.json new file mode 100644 index 00000000000..92a9dafb4da --- /dev/null +++ b/homeassistant/components/faa_delays/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "title": "FAA Delays", + "description": "Enter a US Airport Code in IATA Format", + "data": { + "id": "Airport" + } + } + }, + "error": { + "invalid_airport": "Airport code is not valid", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "This airport is already configured." + } + } +} diff --git a/homeassistant/components/faa_delays/translations/en.json b/homeassistant/components/faa_delays/translations/en.json new file mode 100644 index 00000000000..48e9e1c8993 --- /dev/null +++ b/homeassistant/components/faa_delays/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "This airport is already configured." + }, + "error": { + "invalid_airport": "Airport code is not valid" + }, + "step": { + "user": { + "title": "FAA Delays", + "description": "Enter a US Airport Code in IATA Format", + "data": { + "id": "Airport" + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index da16d32d45b..c3d629ebe29 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -64,6 +64,7 @@ FLOWS = [ "enocean", "epson", "esphome", + "faa_delays", "fireservicerota", "flick_electric", "flo", diff --git a/requirements_all.txt b/requirements_all.txt index bcef556ae38..21bcd4d604a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -574,6 +574,9 @@ eternalegypt==0.0.12 # homeassistant.components.evohome evohome-async==0.3.5.post1 +# homeassistant.components.faa_delays +faadelays==0.0.6 + # homeassistant.components.dlib_face_detect # homeassistant.components.dlib_face_identify # face_recognition==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 155aaa269e5..ae06051a774 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -302,6 +302,9 @@ ephem==3.7.7.0 # homeassistant.components.epson epson-projector==0.2.3 +# homeassistant.components.faa_delays +faadelays==0.0.6 + # homeassistant.components.feedreader feedparser==6.0.2 diff --git a/tests/components/faa_delays/__init__.py b/tests/components/faa_delays/__init__.py new file mode 100644 index 00000000000..2bb5194605d --- /dev/null +++ b/tests/components/faa_delays/__init__.py @@ -0,0 +1 @@ +"""Tests for the FAA Delays integration.""" diff --git a/tests/components/faa_delays/test_config_flow.py b/tests/components/faa_delays/test_config_flow.py new file mode 100644 index 00000000000..c289f154415 --- /dev/null +++ b/tests/components/faa_delays/test_config_flow.py @@ -0,0 +1,120 @@ +"""Test the FAA Delays config flow.""" +from unittest.mock import patch + +from aiohttp import ClientConnectionError +import faadelays + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.faa_delays.const import DOMAIN +from homeassistant.const import CONF_ID +from homeassistant.exceptions import HomeAssistantError + +from tests.common import MockConfigEntry + + +async def mock_valid_airport(self, *args, **kwargs): + """Return a valid airport.""" + self.name = "Test airport" + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch.object(faadelays.Airport, "update", new=mock_valid_airport), patch( + "homeassistant.components.faa_delays.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.faa_delays.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "id": "test", + }, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "Test airport" + assert result2["data"] == { + "id": "test", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_duplicate_error(hass): + """Test that we handle a duplicate configuration.""" + conf = {CONF_ID: "test"} + + MockConfigEntry(domain=DOMAIN, unique_id="test", data=conf).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_form_invalid_airport(hass): + """Test we handle invalid airport.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "faadelays.Airport.update", + side_effect=faadelays.InvalidAirport, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "id": "test", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {CONF_ID: "invalid_airport"} + + +async def test_form_cannot_connect(hass): + """Test we handle a connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("faadelays.Airport.update", side_effect=ClientConnectionError): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "id": "test", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_unexpected_exception(hass): + """Test we handle an unexpected exception.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("faadelays.Airport.update", side_effect=HomeAssistantError): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "id": "test", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} From 4c294adfe88bc81384101e3db0da08820b1b12d0 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 24 Feb 2021 21:18:14 +0100 Subject: [PATCH 714/796] Add missing tilt icon to Shelly tilt sensor (#46993) --- homeassistant/components/shelly/sensor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 36656740b92..32fb33877d3 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -149,7 +149,11 @@ SENSORS = { unit=LIGHT_LUX, device_class=sensor.DEVICE_CLASS_ILLUMINANCE, ), - ("sensor", "tilt"): BlockAttributeDescription(name="Tilt", unit=DEGREE), + ("sensor", "tilt"): BlockAttributeDescription( + name="Tilt", + unit=DEGREE, + icon="mdi:angle-acute", + ), ("relay", "totalWorkTime"): BlockAttributeDescription( name="Lamp life", unit=PERCENTAGE, From afae2534328fcc9acfe24bee480c8b7c7b323fce Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 24 Feb 2021 21:18:24 +0100 Subject: [PATCH 715/796] Update frontend to 20210224.0 (#47013) --- 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 ea46e0b2e07..623aaf42ca5 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210222.0" + "home-assistant-frontend==20210224.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d867db5cef0..e4f84f0c8ef 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.41.0 -home-assistant-frontend==20210222.0 +home-assistant-frontend==20210224.0 httpx==0.16.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 21bcd4d604a..f406c042d52 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -766,7 +766,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210222.0 +home-assistant-frontend==20210224.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae06051a774..5d9df811084 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -415,7 +415,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210222.0 +home-assistant-frontend==20210224.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 23cbd2dda3076105167b42c509a47eb91c857f76 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 24 Feb 2021 21:59:06 +0100 Subject: [PATCH 716/796] Change Z-Wave JS discovery logic to adopt changes to DeviceClass (#46983) Co-authored-by: raman325 <7243222+raman325@users.noreply.github.com> --- homeassistant/components/zwave_js/api.py | 3 +- .../components/zwave_js/binary_sensor.py | 2 +- .../components/zwave_js/discovery.py | 37 +++++++---- .../components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/test_api.py | 3 - tests/components/zwave_js/test_climate.py | 50 +++++++-------- tests/components/zwave_js/test_cover.py | 52 ++++++++-------- tests/components/zwave_js/test_discovery.py | 4 +- tests/components/zwave_js/test_fan.py | 22 +++---- tests/components/zwave_js/test_light.py | 62 ++++++++++--------- tests/components/zwave_js/test_lock.py | 24 +++---- tests/components/zwave_js/test_number.py | 6 +- tests/components/zwave_js/test_services.py | 12 ++-- tests/components/zwave_js/test_switch.py | 14 ++--- .../zwave_js/aeon_smart_switch_6_state.json | 9 +-- .../aeotec_radiator_thermostat_state.json | 14 ++--- .../zwave_js/bulb_6_multi_color_state.json | 13 ++-- .../zwave_js/chain_actuator_zws12_state.json | 15 ++--- .../zwave_js/climate_danfoss_lc_13_state.json | 14 ++--- .../zwave_js/climate_heatit_z_trm3_state.json | 15 ++--- ..._ct100_plus_different_endpoints_state.json | 15 ++--- ...ate_radio_thermostat_ct100_plus_state.json | 15 ++--- .../zwave_js/cover_iblinds_v2_state.json | 12 ++-- .../fixtures/zwave_js/cover_zw062_state.json | 25 ++------ .../zwave_js/eaton_rf9640_dimmer_state.json | 13 ++-- .../zwave_js/ecolink_door_sensor_state.json | 16 ++--- .../fixtures/zwave_js/fan_ge_12730_state.json | 13 ++-- .../zwave_js/hank_binary_switch_state.json | 13 ++-- .../in_wall_smart_fan_control_state.json | 12 ++-- .../zwave_js/lock_august_asl03_state.json | 16 ++--- .../zwave_js/lock_schlage_be469_state.json | 16 ++--- .../zwave_js/multisensor_6_state.json | 12 ++-- .../nortek_thermostat_added_event.json | 15 ++--- .../nortek_thermostat_removed_event.json | 15 ++--- .../zwave_js/nortek_thermostat_state.json | 15 ++--- 37 files changed, 261 insertions(+), 339 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 8358b93aae5..a48eadfad1d 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -429,10 +429,9 @@ async def websocket_update_log_config( """Update the driver log config.""" entry_id = msg[ENTRY_ID] client = hass.data[DOMAIN][entry_id][DATA_CLIENT] - result = await client.driver.async_update_log_config(LogConfig(**msg[CONFIG])) + await client.driver.async_update_log_config(LogConfig(**msg[CONFIG])) connection.send_result( msg[ID], - result, ) diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index 8c56869449a..8d266c83f22 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -287,7 +287,7 @@ class ZWaveBooleanBinarySensor(ZWaveBaseEntity, BinarySensorEntity): if self.info.primary_value.command_class == CommandClass.SENSOR_BINARY: # Legacy binary sensors are phased out (replaced by notification sensors) # Disable by default to not confuse users - if self.info.node.device_class.generic != "Binary Sensor": + if self.info.node.device_class.generic.key != 0x20: return False return True diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 77709f84e58..248a34547b5 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -4,6 +4,7 @@ from dataclasses import dataclass from typing import Generator, List, Optional, Set, Union from zwave_js_server.const import CommandClass +from zwave_js_server.model.device_class import DeviceClassItem from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.value import Value as ZwaveValue @@ -72,11 +73,11 @@ class ZWaveDiscoverySchema: # [optional] the node's firmware_version must match ANY of these values firmware_version: Optional[Set[str]] = None # [optional] the node's basic device class must match ANY of these values - device_class_basic: Optional[Set[str]] = None + device_class_basic: Optional[Set[Union[str, int]]] = None # [optional] the node's generic device class must match ANY of these values - device_class_generic: Optional[Set[str]] = None + device_class_generic: Optional[Set[Union[str, int]]] = None # [optional] the node's specific device class must match ANY of these values - device_class_specific: Optional[Set[str]] = None + device_class_specific: Optional[Set[Union[str, int]]] = None # [optional] additional values that ALL need to be present on the node for this scheme to pass required_values: Optional[List[ZWaveValueDiscoverySchema]] = None # [optional] bool to specify if this primary value may be discovered by multiple platforms @@ -416,21 +417,18 @@ def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None ): continue # check device_class_basic - if ( - schema.device_class_basic is not None - and value.node.device_class.basic not in schema.device_class_basic + if not check_device_class( + value.node.device_class.basic, schema.device_class_basic ): continue # check device_class_generic - if ( - schema.device_class_generic is not None - and value.node.device_class.generic not in schema.device_class_generic + if not check_device_class( + value.node.device_class.generic, schema.device_class_generic ): continue # check device_class_specific - if ( - schema.device_class_specific is not None - and value.node.device_class.specific not in schema.device_class_specific + if not check_device_class( + value.node.device_class.specific, schema.device_class_specific ): continue # check primary value @@ -474,3 +472,18 @@ def check_value(value: ZwaveValue, schema: ZWaveValueDiscoverySchema) -> bool: if schema.type is not None and value.metadata.type not in schema.type: return False return True + + +@callback +def check_device_class( + device_class: DeviceClassItem, required_value: Optional[Set[Union[str, int]]] +) -> bool: + """Check if device class id or label matches.""" + if required_value is None: + return True + for val in required_value: + if isinstance(val, str) and device_class.label == val: + return True + if isinstance(val, int) and device_class.key == val: + return True + return False diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 56e60fc322c..f5d9461e9e0 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.18.0"], + "requirements": ["zwave-js-server-python==0.19.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"] } diff --git a/requirements_all.txt b/requirements_all.txt index f406c042d52..677ce0d0c2f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2406,4 +2406,4 @@ zigpy==0.32.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.18.0 +zwave-js-server-python==0.19.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d9df811084..28478c4349a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1243,4 +1243,4 @@ zigpy-znp==0.4.0 zigpy==0.32.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.18.0 +zwave-js-server-python==0.19.0 diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index d2a6215575f..dcbd924c86e 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -345,7 +345,6 @@ async def test_update_log_config(hass, client, integration, hass_ws_client): } ) msg = await ws_client.receive_json() - assert msg["result"] assert msg["success"] assert len(client.async_send_command.call_args_list) == 1 @@ -366,7 +365,6 @@ async def test_update_log_config(hass, client, integration, hass_ws_client): } ) msg = await ws_client.receive_json() - assert msg["result"] assert msg["success"] assert len(client.async_send_command.call_args_list) == 1 @@ -393,7 +391,6 @@ async def test_update_log_config(hass, client, integration, hass_ws_client): } ) msg = await ws_client.receive_json() - assert msg["result"] assert msg["success"] assert len(client.async_send_command.call_args_list) == 1 diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index 1ccf6f82017..44804825885 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -69,8 +69,8 @@ async def test_thermostat_v2( blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 13 assert args["valueId"] == { @@ -92,7 +92,7 @@ async def test_thermostat_v2( } assert args["value"] == 1 - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test setting hvac mode await hass.services.async_call( @@ -105,8 +105,8 @@ async def test_thermostat_v2( blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 13 assert args["valueId"] == { @@ -128,7 +128,7 @@ async def test_thermostat_v2( } assert args["value"] == 2 - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test setting temperature await hass.services.async_call( @@ -142,8 +142,8 @@ async def test_thermostat_v2( blocking=True, ) - assert len(client.async_send_command.call_args_list) == 2 - args = client.async_send_command.call_args_list[0][0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 2 + args = client.async_send_command_no_wait.call_args_list[0][0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 13 assert args["valueId"] == { @@ -164,7 +164,7 @@ async def test_thermostat_v2( "value": 1, } assert args["value"] == 2 - args = client.async_send_command.call_args_list[1][0][0] + args = client.async_send_command_no_wait.call_args_list[1][0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 13 assert args["valueId"] == { @@ -186,7 +186,7 @@ async def test_thermostat_v2( } assert args["value"] == 77 - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test cool mode update from value updated event event = Event( @@ -237,7 +237,7 @@ async def test_thermostat_v2( assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 22.8 assert state.attributes[ATTR_TARGET_TEMP_LOW] == 22.2 - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test setting temperature with heat_cool await hass.services.async_call( @@ -251,8 +251,8 @@ async def test_thermostat_v2( blocking=True, ) - assert len(client.async_send_command.call_args_list) == 2 - args = client.async_send_command.call_args_list[0][0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 2 + args = client.async_send_command_no_wait.call_args_list[0][0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 13 assert args["valueId"] == { @@ -274,7 +274,7 @@ async def test_thermostat_v2( } assert args["value"] == 77 - args = client.async_send_command.call_args_list[1][0][0] + args = client.async_send_command_no_wait.call_args_list[1][0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 13 assert args["valueId"] == { @@ -296,7 +296,7 @@ async def test_thermostat_v2( } assert args["value"] == 86 - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() with pytest.raises(ValueError): # Test setting unknown preset mode @@ -310,7 +310,7 @@ async def test_thermostat_v2( blocking=True, ) - assert len(client.async_send_command.call_args_list) == 0 + assert len(client.async_send_command_no_wait.call_args_list) == 0 # Test setting invalid hvac mode with pytest.raises(ValueError): @@ -336,7 +336,7 @@ async def test_thermostat_v2( blocking=True, ) - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test setting fan mode await hass.services.async_call( @@ -349,8 +349,8 @@ async def test_thermostat_v2( blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 13 assert args["valueId"] == { @@ -373,7 +373,7 @@ async def test_thermostat_v2( } assert args["value"] == 1 - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test setting invalid fan mode with pytest.raises(ValueError): @@ -408,7 +408,7 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat assert state.attributes[ATTR_HVAC_MODES] == [HVAC_MODE_HEAT] assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test setting temperature await hass.services.async_call( @@ -421,8 +421,8 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args_list[0][0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args_list[0][0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 5 assert args["valueId"] == { @@ -444,7 +444,7 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat } assert args["value"] == 21.5 - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test setpoint mode update from value updated event event = Event( @@ -471,7 +471,7 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat assert state.state == HVAC_MODE_HEAT assert state.attributes[ATTR_TEMPERATURE] == 23 - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() async def test_thermostat_heatit(hass, client, climate_heatit_z_trm3, integration): diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index 2378453e31a..e6118f9b37d 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -38,8 +38,8 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 6 assert args["valueId"] == { @@ -60,7 +60,7 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): } assert args["value"] == 50 - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test setting position await hass.services.async_call( @@ -70,8 +70,8 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 6 assert args["valueId"] == { @@ -92,7 +92,7 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): } assert args["value"] == 0 - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test opening await hass.services.async_call( @@ -102,8 +102,8 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 6 assert args["valueId"] == { @@ -124,7 +124,7 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): } assert args["value"] - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test stop after opening await hass.services.async_call( "cover", @@ -133,8 +133,8 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 2 - open_args = client.async_send_command.call_args_list[0][0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 2 + open_args = client.async_send_command_no_wait.call_args_list[0][0][0] assert open_args["command"] == "node.set_value" assert open_args["nodeId"] == 6 assert open_args["valueId"] == { @@ -153,7 +153,7 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): } assert not open_args["value"] - close_args = client.async_send_command.call_args_list[1][0][0] + close_args = client.async_send_command_no_wait.call_args_list[1][0][0] assert close_args["command"] == "node.set_value" assert close_args["nodeId"] == 6 assert close_args["valueId"] == { @@ -191,7 +191,7 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): }, ) node.receive_event(event) - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() state = hass.states.get(WINDOW_COVER_ENTITY) assert state.state == "open" @@ -203,8 +203,8 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): {"entity_id": WINDOW_COVER_ENTITY}, blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 6 assert args["valueId"] == { @@ -225,7 +225,7 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): } assert args["value"] == 0 - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test stop after closing await hass.services.async_call( @@ -235,8 +235,8 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 2 - open_args = client.async_send_command.call_args_list[0][0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 2 + open_args = client.async_send_command_no_wait.call_args_list[0][0][0] assert open_args["command"] == "node.set_value" assert open_args["nodeId"] == 6 assert open_args["valueId"] == { @@ -255,7 +255,7 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): } assert not open_args["value"] - close_args = client.async_send_command.call_args_list[1][0][0] + close_args = client.async_send_command_no_wait.call_args_list[1][0][0] assert close_args["command"] == "node.set_value" assert close_args["nodeId"] == 6 assert close_args["valueId"] == { @@ -274,7 +274,7 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): } assert not close_args["value"] - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() event = Event( type="value updated", @@ -314,8 +314,8 @@ async def test_motor_barrier_cover(hass, client, gdc_zw062, integration): DOMAIN, SERVICE_OPEN_COVER, {"entity_id": GDC_COVER_ENTITY}, blocking=True ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 12 assert args["value"] == 255 @@ -341,15 +341,15 @@ async def test_motor_barrier_cover(hass, client, gdc_zw062, integration): state = hass.states.get(GDC_COVER_ENTITY) assert state.state == STATE_CLOSED - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test close await hass.services.async_call( DOMAIN, SERVICE_CLOSE_COVER, {"entity_id": GDC_COVER_ENTITY}, blocking=True ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 12 assert args["value"] == 0 @@ -375,7 +375,7 @@ async def test_motor_barrier_cover(hass, client, gdc_zw062, integration): state = hass.states.get(GDC_COVER_ENTITY) assert state.state == STATE_CLOSED - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Barrier sends an opening state event = Event( diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index 8f3dbce8dca..e28c8ae1563 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -4,7 +4,7 @@ async def test_iblinds_v2(hass, client, iblinds_v2, integration): """Test that an iBlinds v2.0 multilevel switch value is discovered as a cover.""" node = iblinds_v2 - assert node.device_class.specific == "Unused" + assert node.device_class.specific.label == "Unused" state = hass.states.get("light.window_blind_controller") assert not state @@ -16,7 +16,7 @@ async def test_iblinds_v2(hass, client, iblinds_v2, integration): async def test_ge_12730(hass, client, ge_12730, integration): """Test GE 12730 Fan Controller v2.0 multilevel switch is discovered as a fan.""" node = ge_12730 - assert node.device_class.specific == "Multilevel Power Switch" + assert node.device_class.specific.label == "Multilevel Power Switch" state = hass.states.get("light.in_wall_smart_fan_control") assert not state diff --git a/tests/components/zwave_js/test_fan.py b/tests/components/zwave_js/test_fan.py index 5bd856c664a..0ee007aab35 100644 --- a/tests/components/zwave_js/test_fan.py +++ b/tests/components/zwave_js/test_fan.py @@ -23,8 +23,8 @@ async def test_fan(hass, client, in_wall_smart_fan_control, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 17 assert args["valueId"] == { @@ -45,7 +45,7 @@ async def test_fan(hass, client, in_wall_smart_fan_control, integration): } assert args["value"] == 66 - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test setting unknown speed with pytest.raises(ValueError): @@ -56,7 +56,7 @@ async def test_fan(hass, client, in_wall_smart_fan_control, integration): blocking=True, ) - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test turn on no speed await hass.services.async_call( @@ -66,8 +66,8 @@ async def test_fan(hass, client, in_wall_smart_fan_control, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 17 assert args["valueId"] == { @@ -88,7 +88,7 @@ async def test_fan(hass, client, in_wall_smart_fan_control, integration): } assert args["value"] == 255 - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test turning off await hass.services.async_call( @@ -98,8 +98,8 @@ async def test_fan(hass, client, in_wall_smart_fan_control, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 17 assert args["valueId"] == { @@ -120,7 +120,7 @@ async def test_fan(hass, client, in_wall_smart_fan_control, integration): } assert args["value"] == 0 - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test speed update from value updated event event = Event( @@ -146,7 +146,7 @@ async def test_fan(hass, client, in_wall_smart_fan_control, integration): assert state.state == "on" assert state.attributes[ATTR_SPEED] == "high" - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() event = Event( type="value updated", diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py index f48b02223d0..ca36ea35393 100644 --- a/tests/components/zwave_js/test_light.py +++ b/tests/components/zwave_js/test_light.py @@ -36,8 +36,8 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 39 assert args["valueId"] == { @@ -58,7 +58,7 @@ async def test_light(hass, client, bulb_6_multi_color, integration): } assert args["value"] == 255 - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test brightness update from value updated event event = Event( @@ -93,9 +93,9 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 + assert len(client.async_send_command_no_wait.call_args_list) == 1 - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test turning on with brightness await hass.services.async_call( @@ -105,8 +105,8 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 39 assert args["valueId"] == { @@ -127,7 +127,7 @@ async def test_light(hass, client, bulb_6_multi_color, integration): } assert args["value"] == 50 - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test turning on with rgb color await hass.services.async_call( @@ -137,8 +137,10 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 5 - warm_args = client.async_send_command.call_args_list[0][0][0] # warm white 0 + assert len(client.async_send_command_no_wait.call_args_list) == 5 + warm_args = client.async_send_command_no_wait.call_args_list[0][0][ + 0 + ] # warm white 0 assert warm_args["command"] == "node.set_value" assert warm_args["nodeId"] == 39 assert warm_args["valueId"]["commandClassName"] == "Color Switch" @@ -149,7 +151,9 @@ async def test_light(hass, client, bulb_6_multi_color, integration): assert warm_args["valueId"]["propertyName"] == "targetColor" assert warm_args["value"] == 0 - cold_args = client.async_send_command.call_args_list[1][0][0] # cold white 0 + cold_args = client.async_send_command_no_wait.call_args_list[1][0][ + 0 + ] # cold white 0 assert cold_args["command"] == "node.set_value" assert cold_args["nodeId"] == 39 assert cold_args["valueId"]["commandClassName"] == "Color Switch" @@ -159,7 +163,7 @@ async def test_light(hass, client, bulb_6_multi_color, integration): assert cold_args["valueId"]["property"] == "targetColor" assert cold_args["valueId"]["propertyName"] == "targetColor" assert cold_args["value"] == 0 - red_args = client.async_send_command.call_args_list[2][0][0] # red 255 + red_args = client.async_send_command_no_wait.call_args_list[2][0][0] # red 255 assert red_args["command"] == "node.set_value" assert red_args["nodeId"] == 39 assert red_args["valueId"]["commandClassName"] == "Color Switch" @@ -169,7 +173,7 @@ async def test_light(hass, client, bulb_6_multi_color, integration): assert red_args["valueId"]["property"] == "targetColor" assert red_args["valueId"]["propertyName"] == "targetColor" assert red_args["value"] == 255 - green_args = client.async_send_command.call_args_list[3][0][0] # green 76 + green_args = client.async_send_command_no_wait.call_args_list[3][0][0] # green 76 assert green_args["command"] == "node.set_value" assert green_args["nodeId"] == 39 assert green_args["valueId"]["commandClassName"] == "Color Switch" @@ -179,7 +183,7 @@ async def test_light(hass, client, bulb_6_multi_color, integration): assert green_args["valueId"]["property"] == "targetColor" assert green_args["valueId"]["propertyName"] == "targetColor" assert green_args["value"] == 76 - blue_args = client.async_send_command.call_args_list[4][0][0] # blue 255 + blue_args = client.async_send_command_no_wait.call_args_list[4][0][0] # blue 255 assert blue_args["command"] == "node.set_value" assert blue_args["nodeId"] == 39 assert blue_args["valueId"]["commandClassName"] == "Color Switch" @@ -231,7 +235,7 @@ async def test_light(hass, client, bulb_6_multi_color, integration): assert state.attributes[ATTR_COLOR_TEMP] == 370 assert state.attributes[ATTR_RGB_COLOR] == (255, 76, 255) - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test turning on with same rgb color await hass.services.async_call( @@ -241,9 +245,9 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 5 + assert len(client.async_send_command_no_wait.call_args_list) == 5 - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test turning on with color temp await hass.services.async_call( @@ -253,8 +257,8 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 5 - red_args = client.async_send_command.call_args_list[0][0][0] # red 0 + assert len(client.async_send_command_no_wait.call_args_list) == 5 + red_args = client.async_send_command_no_wait.call_args_list[0][0][0] # red 0 assert red_args["command"] == "node.set_value" assert red_args["nodeId"] == 39 assert red_args["valueId"]["commandClassName"] == "Color Switch" @@ -264,7 +268,7 @@ async def test_light(hass, client, bulb_6_multi_color, integration): assert red_args["valueId"]["property"] == "targetColor" assert red_args["valueId"]["propertyName"] == "targetColor" assert red_args["value"] == 0 - red_args = client.async_send_command.call_args_list[1][0][0] # green 0 + red_args = client.async_send_command_no_wait.call_args_list[1][0][0] # green 0 assert red_args["command"] == "node.set_value" assert red_args["nodeId"] == 39 assert red_args["valueId"]["commandClassName"] == "Color Switch" @@ -274,7 +278,7 @@ async def test_light(hass, client, bulb_6_multi_color, integration): assert red_args["valueId"]["property"] == "targetColor" assert red_args["valueId"]["propertyName"] == "targetColor" assert red_args["value"] == 0 - red_args = client.async_send_command.call_args_list[2][0][0] # blue 0 + red_args = client.async_send_command_no_wait.call_args_list[2][0][0] # blue 0 assert red_args["command"] == "node.set_value" assert red_args["nodeId"] == 39 assert red_args["valueId"]["commandClassName"] == "Color Switch" @@ -284,7 +288,9 @@ async def test_light(hass, client, bulb_6_multi_color, integration): assert red_args["valueId"]["property"] == "targetColor" assert red_args["valueId"]["propertyName"] == "targetColor" assert red_args["value"] == 0 - warm_args = client.async_send_command.call_args_list[3][0][0] # warm white 0 + warm_args = client.async_send_command_no_wait.call_args_list[3][0][ + 0 + ] # warm white 0 assert warm_args["command"] == "node.set_value" assert warm_args["nodeId"] == 39 assert warm_args["valueId"]["commandClassName"] == "Color Switch" @@ -294,7 +300,7 @@ async def test_light(hass, client, bulb_6_multi_color, integration): assert warm_args["valueId"]["property"] == "targetColor" assert warm_args["valueId"]["propertyName"] == "targetColor" assert warm_args["value"] == 20 - red_args = client.async_send_command.call_args_list[4][0][0] # cold white + red_args = client.async_send_command_no_wait.call_args_list[4][0][0] # cold white assert red_args["command"] == "node.set_value" assert red_args["nodeId"] == 39 assert red_args["valueId"]["commandClassName"] == "Color Switch" @@ -305,7 +311,7 @@ async def test_light(hass, client, bulb_6_multi_color, integration): assert red_args["valueId"]["propertyName"] == "targetColor" assert red_args["value"] == 235 - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test color temp update from value updated event red_event = Event( @@ -361,9 +367,9 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 5 + assert len(client.async_send_command_no_wait.call_args_list) == 5 - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test turning off await hass.services.async_call( @@ -373,8 +379,8 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 39 assert args["valueId"] == { diff --git a/tests/components/zwave_js/test_lock.py b/tests/components/zwave_js/test_lock.py index 9ddc7abdd88..e4032cf42ed 100644 --- a/tests/components/zwave_js/test_lock.py +++ b/tests/components/zwave_js/test_lock.py @@ -33,8 +33,8 @@ async def test_door_lock(hass, client, lock_schlage_be469, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 20 assert args["valueId"] == { @@ -64,7 +64,7 @@ async def test_door_lock(hass, client, lock_schlage_be469, integration): } assert args["value"] == 255 - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test locked update from value updated event event = Event( @@ -88,7 +88,7 @@ async def test_door_lock(hass, client, lock_schlage_be469, integration): assert hass.states.get(SCHLAGE_BE469_LOCK_ENTITY).state == STATE_LOCKED - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test unlocking await hass.services.async_call( @@ -98,8 +98,8 @@ async def test_door_lock(hass, client, lock_schlage_be469, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 20 assert args["valueId"] == { @@ -129,7 +129,7 @@ async def test_door_lock(hass, client, lock_schlage_be469, integration): } assert args["value"] == 0 - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test set usercode service await hass.services.async_call( @@ -143,8 +143,8 @@ async def test_door_lock(hass, client, lock_schlage_be469, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 20 assert args["valueId"] == { @@ -167,7 +167,7 @@ async def test_door_lock(hass, client, lock_schlage_be469, integration): } assert args["value"] == "1234" - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test clear usercode await hass.services.async_call( @@ -177,8 +177,8 @@ async def test_door_lock(hass, client, lock_schlage_be469, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 20 assert args["valueId"] == { diff --git a/tests/components/zwave_js/test_number.py b/tests/components/zwave_js/test_number.py index b7d83068bea..136e62c5405 100644 --- a/tests/components/zwave_js/test_number.py +++ b/tests/components/zwave_js/test_number.py @@ -20,8 +20,8 @@ async def test_number(hass, client, aeotec_radiator_thermostat, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 4 assert args["valueId"] == { @@ -43,7 +43,7 @@ async def test_number(hass, client, aeotec_radiator_thermostat, integration): } assert args["value"] == 30.0 - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test value update from value updated event event = Event( diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 8e882b9547c..d0bf08c1b7a 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -302,15 +302,15 @@ async def test_poll_value( ): """Test the poll_value service.""" # Test polling the primary value - client.async_send_command.return_value = {"result": 2} + client.async_send_command_no_wait.return_value = {"result": 2} await hass.services.async_call( DOMAIN, SERVICE_REFRESH_VALUE, {ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY}, blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.poll_value" assert args["nodeId"] == 26 assert args["valueId"] == { @@ -339,10 +339,10 @@ async def test_poll_value( "ccVersion": 2, } - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test polling all watched values - client.async_send_command.return_value = {"result": 2} + client.async_send_command_no_wait.return_value = {"result": 2} await hass.services.async_call( DOMAIN, SERVICE_REFRESH_VALUE, @@ -352,7 +352,7 @@ async def test_poll_value( }, blocking=True, ) - assert len(client.async_send_command.call_args_list) == 8 + assert len(client.async_send_command_no_wait.call_args_list) == 8 # Test polling against an invalid entity raises ValueError with pytest.raises(ValueError): diff --git a/tests/components/zwave_js/test_switch.py b/tests/components/zwave_js/test_switch.py index ea6e27d9b72..dceaa17a816 100644 --- a/tests/components/zwave_js/test_switch.py +++ b/tests/components/zwave_js/test_switch.py @@ -21,7 +21,7 @@ async def test_switch(hass, hank_binary_switch, integration, client): "switch", "turn_on", {"entity_id": SWITCH_ENTITY}, blocking=True ) - args = client.async_send_command.call_args[0][0] + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 32 assert args["valueId"] == { @@ -68,7 +68,7 @@ async def test_switch(hass, hank_binary_switch, integration, client): "switch", "turn_off", {"entity_id": SWITCH_ENTITY}, blocking=True ) - args = client.async_send_command.call_args[0][0] + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 32 assert args["valueId"] == { @@ -102,8 +102,8 @@ async def test_barrier_signaling_switch(hass, gdc_zw062, integration, client): DOMAIN, SERVICE_TURN_OFF, {"entity_id": entity}, blocking=True ) - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 12 assert args["value"] == 0 @@ -134,7 +134,7 @@ async def test_barrier_signaling_switch(hass, gdc_zw062, integration, client): state = hass.states.get(entity) assert state.state == STATE_OFF - client.async_send_command.reset_mock() + client.async_send_command_no_wait.reset_mock() # Test turning on await hass.services.async_call( @@ -143,8 +143,8 @@ async def test_barrier_signaling_switch(hass, gdc_zw062, integration, client): # Note: the valueId's value is still 255 because we never # received an updated value - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 12 assert args["value"] == 255 diff --git a/tests/fixtures/zwave_js/aeon_smart_switch_6_state.json b/tests/fixtures/zwave_js/aeon_smart_switch_6_state.json index 2da1c203561..36db78faace 100644 --- a/tests/fixtures/zwave_js/aeon_smart_switch_6_state.json +++ b/tests/fixtures/zwave_js/aeon_smart_switch_6_state.json @@ -6,10 +6,10 @@ "status": 4, "ready": true, "deviceClass": { - "basic": "Routing Slave", - "generic": "Binary Switch", - "specific": "Binary Power Switch", - "mandatorySupportedCCs": ["Basic", "Binary Switch", "All Switch"], + "basic": {"key": 4, "label": "Routing Slave"}, + "generic": {"key": 16, "label":"Binary Switch"}, + "specific": {"key": 1, "label":"Binary Power Switch"}, + "mandatorySupportedCCs": [], "mandatoryControlCCs": [] }, "isListening": true, @@ -53,6 +53,7 @@ "userIcon": 1792 } ], + "commandClasses": [], "values": [ { "endpoint": 0, diff --git a/tests/fixtures/zwave_js/aeotec_radiator_thermostat_state.json b/tests/fixtures/zwave_js/aeotec_radiator_thermostat_state.json index 8cd6fe78201..27c3f991d33 100644 --- a/tests/fixtures/zwave_js/aeotec_radiator_thermostat_state.json +++ b/tests/fixtures/zwave_js/aeotec_radiator_thermostat_state.json @@ -6,16 +6,10 @@ "status": 4, "ready": true, "deviceClass": { - "basic": "Routing Slave", - "generic": "Thermostat", - "specific": "Thermostat General V2", - "mandatorySupportedCCs": [ - "Basic", - "Manufacturer Specific", - "Thermostat Mode", - "Thermostat Setpoint", - "Version" - ], + "basic": {"key": 4, "label":"Routing Slave"}, + "generic": {"key": 8, "label":"Thermostat"}, + "specific": {"key": 6, "label":"Thermostat General V2"}, + "mandatorySupportedCCs": [], "mandatoryControlCCs": [] }, "isListening": false, diff --git a/tests/fixtures/zwave_js/bulb_6_multi_color_state.json b/tests/fixtures/zwave_js/bulb_6_multi_color_state.json index b7c422121c9..64bfecfb20b 100644 --- a/tests/fixtures/zwave_js/bulb_6_multi_color_state.json +++ b/tests/fixtures/zwave_js/bulb_6_multi_color_state.json @@ -6,14 +6,10 @@ "status": 4, "ready": true, "deviceClass": { - "basic": "Static Controller", - "generic": "Multilevel Switch", - "specific": "Multilevel Power Switch", - "mandatorySupportedCCs": [ - "Basic", - "Multilevel Switch", - "All Switch" - ], + "basic": {"key": 2, "label": "Static Controller"}, + "generic": {"key": 17, "label":"Multilevel Switch"}, + "specific": {"key": 1, "label":"Multilevel Power Switch"}, + "mandatorySupportedCCs": [], "mandatoryControlCCs": [] }, "isListening": true, @@ -67,6 +63,7 @@ "userIcon": 1536 } ], + "commandClasses": [], "values": [ { "commandClassName": "Multilevel Switch", diff --git a/tests/fixtures/zwave_js/chain_actuator_zws12_state.json b/tests/fixtures/zwave_js/chain_actuator_zws12_state.json index dbae35e04d0..cf7adddc21e 100644 --- a/tests/fixtures/zwave_js/chain_actuator_zws12_state.json +++ b/tests/fixtures/zwave_js/chain_actuator_zws12_state.json @@ -6,16 +6,10 @@ "status": 4, "ready": true, "deviceClass": { - "basic": "Routing Slave", - "generic": "Multilevel Switch", - "specific": "Motor Control Class C", - "mandatorySupportedCCs": [ - "Basic", - "Multilevel Switch", - "Binary Switch", - "Manufacturer Specific", - "Version" - ], + "basic": {"key": 4, "label":"Routing Slave"}, + "generic": {"key": 17, "label":"Multilevel Switch"}, + "specific": {"key": 7, "label":"Motor Control Class C"}, + "mandatorySupportedCCs": [], "mandatoryControlCCs": [] }, "isListening": true, @@ -52,6 +46,7 @@ "endpoints": [ { "nodeId": 6, "index": 0, "installerIcon": 6656, "userIcon": 6656 } ], + "commandClasses": [], "values": [ { "commandClassName": "Multilevel Switch", diff --git a/tests/fixtures/zwave_js/climate_danfoss_lc_13_state.json b/tests/fixtures/zwave_js/climate_danfoss_lc_13_state.json index e218d3b6a0e..90410998597 100644 --- a/tests/fixtures/zwave_js/climate_danfoss_lc_13_state.json +++ b/tests/fixtures/zwave_js/climate_danfoss_lc_13_state.json @@ -4,15 +4,10 @@ "status": 1, "ready": true, "deviceClass": { - "basic": "Routing Slave", - "generic": "Thermostat", - "specific": "Setpoint Thermostat", - "mandatorySupportedCCs": [ - "Manufacturer Specific", - "Multi Command", - "Thermostat Setpoint", - "Version" - ], + "basic": {"key": 4, "label":"Routing Slave"}, + "generic": {"key": 8, "label":"Thermostat"}, + "specific": {"key": 4, "label":"Setpoint Thermostat"}, + "mandatorySupportedCCs": [], "mandatoryControlCCs": [] }, "isListening": false, @@ -77,6 +72,7 @@ "index": 0 } ], + "commandClasses": [], "values": [ { "endpoint": 0, diff --git a/tests/fixtures/zwave_js/climate_heatit_z_trm3_state.json b/tests/fixtures/zwave_js/climate_heatit_z_trm3_state.json index 066811c7374..0dc040c6cb2 100644 --- a/tests/fixtures/zwave_js/climate_heatit_z_trm3_state.json +++ b/tests/fixtures/zwave_js/climate_heatit_z_trm3_state.json @@ -6,16 +6,10 @@ "status": 4, "ready": true, "deviceClass": { - "basic": "Routing Slave", - "generic": "Thermostat", - "specific": "Thermostat General V2", - "mandatorySupportedCCs": [ - "Basic", - "Manufacturer Specific", - "Thermostat Mode", - "Thermostat Setpoint", - "Version" - ], + "basic": {"key": 4, "label":"Routing Slave"}, + "generic": {"key": 8, "label":"Thermostat"}, + "specific": {"key": 6, "label":"Thermostat General V2"}, + "mandatorySupportedCCs": [], "mandatoryControlCCs": [] }, "isListening": true, @@ -116,6 +110,7 @@ "userIcon": 3329 } ], + "commandClasses": [], "values": [ { "endpoint": 0, diff --git a/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json b/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json index ea38dfd9d6b..fcdd57e981b 100644 --- a/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json +++ b/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json @@ -6,16 +6,10 @@ "status": 4, "ready": true, "deviceClass": { - "basic": "Routing Slave", - "generic": "Thermostat", - "specific": "Thermostat General V2", - "mandatorySupportedCCs": [ - "Basic", - "Manufacturer Specific", - "Thermostat Mode", - "Thermostat Setpoint", - "Version" - ], + "basic": {"key": 4, "label":"Routing Slave"}, + "generic": {"key": 8, "label":"Thermostat"}, + "specific": {"key": 6, "label":"Thermostat General V2"}, + "mandatorySupportedCCs": [], "mandatoryControlCCs": [] }, "isListening": true, @@ -63,6 +57,7 @@ "userIcon": 3333 } ], + "commandClasses": [], "values": [ { "commandClassName": "Manufacturer Specific", diff --git a/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_state.json b/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_state.json index caad22aac36..34df415301e 100644 --- a/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_state.json +++ b/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_state.json @@ -6,16 +6,10 @@ "status": 4, "ready": true, "deviceClass": { - "basic": "Static Controller", - "generic": "Thermostat", - "specific": "Thermostat General V2", - "mandatorySupportedCCs": [ - "Basic", - "Manufacturer Specific", - "Thermostat Mode", - "Thermostat Setpoint", - "Version" - ], + "basic": {"key": 2, "label":"Static Controller"}, + "generic": {"key": 8, "label":"Thermostat"}, + "specific": {"key": 6, "label":"Thermostat General V2"}, + "mandatorySupportedCCs": [], "mandatoryControlCCs": [] }, "isListening": true, @@ -63,6 +57,7 @@ }, { "nodeId": 13, "index": 2 } ], + "commandClasses": [], "values": [ { "commandClassName": "Manufacturer Specific", diff --git a/tests/fixtures/zwave_js/cover_iblinds_v2_state.json b/tests/fixtures/zwave_js/cover_iblinds_v2_state.json index 7cb6f94a6f0..35ce70f617a 100644 --- a/tests/fixtures/zwave_js/cover_iblinds_v2_state.json +++ b/tests/fixtures/zwave_js/cover_iblinds_v2_state.json @@ -6,13 +6,10 @@ "status": 4, "ready": true, "deviceClass": { - "basic": "Routing Slave", - "generic": "Multilevel Switch", - "specific": "Unused", - "mandatorySupportedCCs": [ - "Basic", - "Multilevel Switch" - ], + "basic": {"key": 4, "label":"Routing Slave"}, + "generic": {"key": 17, "label":"Routing Slave"}, + "specific": {"key": 0, "label":"Unused"}, + "mandatorySupportedCCs": [], "mandatoryControlCCs": [] }, "isListening": false, @@ -75,6 +72,7 @@ "userIcon": 6400 } ], + "commandClasses": [], "values": [ { "endpoint": 0, diff --git a/tests/fixtures/zwave_js/cover_zw062_state.json b/tests/fixtures/zwave_js/cover_zw062_state.json index 107225e0dcc..9e7b05adc34 100644 --- a/tests/fixtures/zwave_js/cover_zw062_state.json +++ b/tests/fixtures/zwave_js/cover_zw062_state.json @@ -6,26 +6,10 @@ "status": 4, "ready": true, "deviceClass": { - "basic": "Routing Slave", - "generic": "Entry Control", - "specific": "Secure Barrier Add-on", - "mandatorySupportedCCs": [ - "Application Status", - "Association", - "Association Group Information", - "Barrier Operator", - "Battery", - "Device Reset Locally", - "Manufacturer Specific", - "Notification", - "Powerlevel", - "Security", - "Security 2", - "Supervision", - "Transport Service", - "Version", - "Z-Wave Plus Info" - ], + "basic": {"key": 4, "label":"Routing Slave"}, + "generic": {"key": 64, "label":"Entry Control"}, + "specific": {"key": 7, "label":"Secure Barrier Add-on"}, + "mandatorySupportedCCs": [], "mandatoryControlCCs": [] }, "isListening": true, @@ -94,6 +78,7 @@ "userIcon": 7680 } ], + "commandClasses": [], "values": [ { "endpoint": 0, diff --git a/tests/fixtures/zwave_js/eaton_rf9640_dimmer_state.json b/tests/fixtures/zwave_js/eaton_rf9640_dimmer_state.json index 0f2f45d01e3..db815506a6b 100644 --- a/tests/fixtures/zwave_js/eaton_rf9640_dimmer_state.json +++ b/tests/fixtures/zwave_js/eaton_rf9640_dimmer_state.json @@ -6,14 +6,10 @@ "status": 4, "ready": true, "deviceClass": { - "basic": "Routing Slave", - "generic": "Multilevel Switch", - "specific": "Multilevel Power Switch", - "mandatorySupportedCCs": [ - "Basic", - "Multilevel Switch", - "All Switch" - ], + "basic": {"key": 4, "label":"Routing Slave"}, + "generic": {"key": 17, "label":"Routing Slave"}, + "specific": {"key": 1, "label":"Multilevel Power Switch"}, + "mandatorySupportedCCs": [], "mandatoryControlCCs": [] }, "isListening": true, @@ -74,6 +70,7 @@ "userIcon": 1536 } ], + "commandClasses": [], "values": [ { "commandClassName": "Multilevel Switch", diff --git a/tests/fixtures/zwave_js/ecolink_door_sensor_state.json b/tests/fixtures/zwave_js/ecolink_door_sensor_state.json index bd5f2c6b466..9c2befdf5e8 100644 --- a/tests/fixtures/zwave_js/ecolink_door_sensor_state.json +++ b/tests/fixtures/zwave_js/ecolink_door_sensor_state.json @@ -4,16 +4,11 @@ "status": 1, "ready": true, "deviceClass": { - "basic": "Static Controller", - "generic": "Binary Sensor", - "specific": "Routing Binary Sensor", - "mandatorySupportedCCs": [ - "Basic", - "Binary Sensor" - ], - "mandatoryControlCCs": [ - - ] + "basic": {"key": 2, "label":"Static Controller"}, + "generic": {"key": 32, "label":"Binary Sensor"}, + "specific": {"key": 1, "label":"Routing Binary Sensor"}, + "mandatorySupportedCCs": [], + "mandatoryControlCCs": [] }, "isListening": false, "isFrequentListening": false, @@ -61,6 +56,7 @@ "index": 0 } ], + "commandClasses": [], "values": [ { "commandClassName": "Basic", diff --git a/tests/fixtures/zwave_js/fan_ge_12730_state.json b/tests/fixtures/zwave_js/fan_ge_12730_state.json index 692cc75fe99..b6cf59b4226 100644 --- a/tests/fixtures/zwave_js/fan_ge_12730_state.json +++ b/tests/fixtures/zwave_js/fan_ge_12730_state.json @@ -4,14 +4,10 @@ "status": 4, "ready": true, "deviceClass": { - "basic": "Routing Slave", - "generic": "Multilevel Switch", - "specific": "Multilevel Power Switch", - "mandatorySupportedCCs": [ - "Basic", - "Multilevel Switch", - "All Switch" - ], + "basic": {"key": 4, "label":"Routing Slave"}, + "generic": {"key": 17, "label":"Multilevel Switch"}, + "specific": {"key": 1, "label":"Multilevel Power Switch"}, + "mandatorySupportedCCs": [], "mandatoryControlCCs": [] }, "isListening": true, @@ -57,6 +53,7 @@ "index": 0 } ], + "commandClasses": [], "values": [ { "endpoint": 0, diff --git a/tests/fixtures/zwave_js/hank_binary_switch_state.json b/tests/fixtures/zwave_js/hank_binary_switch_state.json index 0c629b3cf99..e5f739d63a5 100644 --- a/tests/fixtures/zwave_js/hank_binary_switch_state.json +++ b/tests/fixtures/zwave_js/hank_binary_switch_state.json @@ -6,14 +6,10 @@ "status": 4, "ready": true, "deviceClass": { - "basic": "Static Controller", - "generic": "Binary Switch", - "specific": "Binary Power Switch", - "mandatorySupportedCCs": [ - "Basic", - "Binary Switch", - "All Switch" - ], + "basic": {"key": 2, "label":"Static Controller"}, + "generic": {"key": 16, "label":"Binary Switch"}, + "specific": {"key": 1, "label":"Binary Power Switch"}, + "mandatorySupportedCCs": [], "mandatoryControlCCs": [] }, "isListening": true, @@ -67,6 +63,7 @@ "userIcon": 1792 } ], + "commandClasses": [], "values": [ { "commandClassName": "Binary Switch", diff --git a/tests/fixtures/zwave_js/in_wall_smart_fan_control_state.json b/tests/fixtures/zwave_js/in_wall_smart_fan_control_state.json index fe5550a5424..74467664955 100644 --- a/tests/fixtures/zwave_js/in_wall_smart_fan_control_state.json +++ b/tests/fixtures/zwave_js/in_wall_smart_fan_control_state.json @@ -6,13 +6,10 @@ "status": 4, "ready": true, "deviceClass": { - "basic": "Routing Slave", - "generic": "Multilevel Switch", - "specific": "Fan Switch", - "mandatorySupportedCCs": [ - "Basic", - "Multilevel Switch" - ], + "basic": {"key": 4, "label":"Routing Slave"}, + "generic": {"key": 17, "label":"Multilevel Switch"}, + "specific": {"key": 8, "label":"Fan Switch"}, + "mandatorySupportedCCs": [], "mandatoryControlCCs": [] }, "isListening": true, @@ -87,6 +84,7 @@ "userIcon": 1024 } ], + "commandClasses": [], "values": [ { "commandClassName": "Multilevel Switch", diff --git a/tests/fixtures/zwave_js/lock_august_asl03_state.json b/tests/fixtures/zwave_js/lock_august_asl03_state.json index b6d44341853..2b218cd915b 100644 --- a/tests/fixtures/zwave_js/lock_august_asl03_state.json +++ b/tests/fixtures/zwave_js/lock_august_asl03_state.json @@ -6,17 +6,10 @@ "status": 4, "ready": true, "deviceClass": { - "basic": "Routing Slave", - "generic": "Entry Control", - "specific": "Secure Keypad Door Lock", - "mandatorySupportedCCs": [ - "Basic", - "Door Lock", - "User Code", - "Manufacturer Specific", - "Security", - "Version" - ], + "basic": {"key": 4, "label":"Routing Slave"}, + "generic": {"key": 64, "label":"Entry Control"}, + "specific": {"key": 3, "label":"Secure Keypad Door Lock"}, + "mandatorySupportedCCs": [], "mandatoryControlCCs": [] }, "isListening": false, @@ -73,6 +66,7 @@ "userIcon": 768 } ], + "commandClasses": [], "values": [ { "commandClassName": "Door Lock", diff --git a/tests/fixtures/zwave_js/lock_schlage_be469_state.json b/tests/fixtures/zwave_js/lock_schlage_be469_state.json index af1fc92a206..be1ddb9c3f0 100644 --- a/tests/fixtures/zwave_js/lock_schlage_be469_state.json +++ b/tests/fixtures/zwave_js/lock_schlage_be469_state.json @@ -4,17 +4,10 @@ "status": 4, "ready": true, "deviceClass": { - "basic": "Static Controller", - "generic": "Entry Control", - "specific": "Secure Keypad Door Lock", - "mandatorySupportedCCs": [ - "Basic", - "Door Lock", - "User Code", - "Manufacturer Specific", - "Security", - "Version" - ], + "basic": {"key": 2, "label":"Static Controller"}, + "generic": {"key": 64, "label":"Entry Control"}, + "specific": {"key": 3, "label":"Secure Keypad Door Lock"}, + "mandatorySupportedCCs": [], "mandatoryControlCCs": [] }, "isListening": false, @@ -57,6 +50,7 @@ "index": 0 } ], + "commandClasses": [], "values": [ { "commandClassName": "Door Lock", diff --git a/tests/fixtures/zwave_js/multisensor_6_state.json b/tests/fixtures/zwave_js/multisensor_6_state.json index 3c508ffd3ff..131a5aa026f 100644 --- a/tests/fixtures/zwave_js/multisensor_6_state.json +++ b/tests/fixtures/zwave_js/multisensor_6_state.json @@ -6,13 +6,10 @@ "status": 1, "ready": true, "deviceClass": { - "basic": "Static Controller", - "generic": "Multilevel Sensor", - "specific": "Routing Multilevel Sensor", - "mandatorySupportedCCs": [ - "Basic", - "Multilevel Sensor" - ], + "basic": {"key": 2, "label":"Static Controller"}, + "generic": {"key": 21, "label":"Multilevel Sensor"}, + "specific": {"key": 1, "label":"Routing Multilevel Sensor"}, + "mandatorySupportedCCs": [], "mandatoryControlCCs": [] }, "isListening": true, @@ -70,6 +67,7 @@ "userIcon": 3079 } ], + "commandClasses": [], "values": [ { "commandClassName": "Basic", diff --git a/tests/fixtures/zwave_js/nortek_thermostat_added_event.json b/tests/fixtures/zwave_js/nortek_thermostat_added_event.json index d778f77ce24..60078100caf 100644 --- a/tests/fixtures/zwave_js/nortek_thermostat_added_event.json +++ b/tests/fixtures/zwave_js/nortek_thermostat_added_event.json @@ -7,16 +7,10 @@ "status": 0, "ready": false, "deviceClass": { - "basic": "Static Controller", - "generic": "Thermostat", - "specific": "Thermostat General V2", - "mandatorySupportedCCs": [ - "Basic", - "Manufacturer Specific", - "Thermostat Mode", - "Thermostat Setpoint", - "Version" - ], + "basic": {"key": 2, "label":"Static Controller"}, + "generic": {"key": 8, "label":"Thermostat"}, + "specific": {"key": 6, "label":"Thermostat General V2"}, + "mandatorySupportedCCs": [], "mandatoryControlCCs": [] }, "neighbors": [], @@ -27,6 +21,7 @@ "index": 0 } ], + "commandClasses": [], "values": [ { "commandClassName": "Basic", diff --git a/tests/fixtures/zwave_js/nortek_thermostat_removed_event.json b/tests/fixtures/zwave_js/nortek_thermostat_removed_event.json index ed25a650543..01bad6c4a8f 100644 --- a/tests/fixtures/zwave_js/nortek_thermostat_removed_event.json +++ b/tests/fixtures/zwave_js/nortek_thermostat_removed_event.json @@ -7,16 +7,10 @@ "status": 4, "ready": true, "deviceClass": { - "basic": "Static Controller", - "generic": "Thermostat", - "specific": "Thermostat General V2", - "mandatorySupportedCCs": [ - "Basic", - "Manufacturer Specific", - "Thermostat Mode", - "Thermostat Setpoint", - "Version" - ], + "basic": {"key": 2, "label":"Static Controller"}, + "generic": {"key": 8, "label":"Thermostat"}, + "specific": {"key": 6, "label":"Thermostat General V2"}, + "mandatorySupportedCCs": [], "mandatoryControlCCs": [] }, "isListening": false, @@ -67,6 +61,7 @@ "index": 0 } ], + "commandClasses": [], "values": [ { "commandClassName": "Manufacturer Specific", diff --git a/tests/fixtures/zwave_js/nortek_thermostat_state.json b/tests/fixtures/zwave_js/nortek_thermostat_state.json index 62a08999cda..4e6ca17e013 100644 --- a/tests/fixtures/zwave_js/nortek_thermostat_state.json +++ b/tests/fixtures/zwave_js/nortek_thermostat_state.json @@ -6,16 +6,10 @@ "status": 4, "ready": true, "deviceClass": { - "basic": "Static Controller", - "generic": "Thermostat", - "specific": "Thermostat General V2", - "mandatorySupportedCCs": [ - "Basic", - "Manufacturer Specific", - "Thermostat Mode", - "Thermostat Setpoint", - "Version" - ], + "basic": {"key": 2, "label":"Static Controller"}, + "generic": {"key": 8, "label":"Thermostat"}, + "specific": {"key": 6, "label":"Thermostat General V2"}, + "mandatorySupportedCCs": [], "mandatoryControlCCs": [] }, "isListening": false, @@ -75,6 +69,7 @@ "userIcon": 4608 } ], + "commandClasses": [], "values": [ { "commandClassName": "Manufacturer Specific", From 567ec26c4813547cb2d09dfb13afb8bb186da07f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 24 Feb 2021 22:01:28 +0100 Subject: [PATCH 717/796] Bumped version to 2021.3.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a0aafaad3ce..f9a8e3e99b3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 3 -PATCH_VERSION = "0.dev0" +PATCH_VERSION = "0b0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From 2fef4c4eef76fd70f2ccffeda4f5e24f51dd1fcc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Feb 2021 01:16:20 -0600 Subject: [PATCH 718/796] Ensure doorbird events are re-registered when changing options (#46860) - Fixed the update listener not being unsubscribed - DRY up some of the code - Fix sync code being called in async - Reduce executor jumps --- homeassistant/components/doorbird/__init__.py | 43 ++++++++++--------- homeassistant/components/doorbird/const.py | 2 + homeassistant/components/doorbird/switch.py | 5 ++- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 1dc5bf56c86..22db3c76273 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -34,6 +34,7 @@ from .const import ( DOOR_STATION_EVENT_ENTITY_IDS, DOOR_STATION_INFO, PLATFORMS, + UNDO_UPDATE_LISTENER, ) from .util import get_doorstation_by_token @@ -128,8 +129,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): device = DoorBird(device_ip, username, password) try: - status = await hass.async_add_executor_job(device.ready) - info = await hass.async_add_executor_job(device.info) + status, info = await hass.async_add_executor_job(_init_doorbird_device, device) except urllib.error.HTTPError as err: if err.code == HTTP_UNAUTHORIZED: _LOGGER.error( @@ -154,18 +154,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): custom_url = doorstation_config.get(CONF_CUSTOM_URL) name = doorstation_config.get(CONF_NAME) events = doorstation_options.get(CONF_EVENTS, []) - doorstation = ConfiguredDoorBird(device, name, events, custom_url, token) + doorstation = ConfiguredDoorBird(device, name, custom_url, token) + doorstation.update_events(events) # Subscribe to doorbell or motion events if not await _async_register_events(hass, doorstation): raise ConfigEntryNotReady + undo_listener = entry.add_update_listener(_update_listener) + hass.data[DOMAIN][config_entry_id] = { DOOR_STATION: doorstation, DOOR_STATION_INFO: info, + UNDO_UPDATE_LISTENER: undo_listener, } - entry.add_update_listener(_update_listener) - for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) @@ -174,9 +176,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return True +def _init_doorbird_device(device): + return device.ready(), device.info() + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" + hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() + unload_ok = all( await asyncio.gather( *[ @@ -195,7 +203,7 @@ async def _async_register_events(hass, doorstation): try: await hass.async_add_executor_job(doorstation.register_events, hass) except HTTPError: - hass.components.persistent_notification.create( + hass.components.persistent_notification.async_create( "Doorbird configuration failed. Please verify that API " "Operator permission is enabled for the Doorbird user. " "A restart will be required once permissions have been " @@ -212,8 +220,7 @@ async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): """Handle options update.""" config_entry_id = entry.entry_id doorstation = hass.data[DOMAIN][config_entry_id][DOOR_STATION] - - doorstation.events = entry.options[CONF_EVENTS] + doorstation.update_events(entry.options[CONF_EVENTS]) # Subscribe to doorbell or motion events await _async_register_events(hass, doorstation) @@ -234,14 +241,19 @@ def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: Confi class ConfiguredDoorBird: """Attach additional information to pass along with configured device.""" - def __init__(self, device, name, events, custom_url, token): + def __init__(self, device, name, custom_url, token): """Initialize configured device.""" self._name = name self._device = device self._custom_url = custom_url + self.events = None + self.doorstation_events = None + self._token = token + + def update_events(self, events): + """Update the doorbird events.""" self.events = events self.doorstation_events = [self._get_event_name(event) for event in self.events] - self._token = token @property def name(self): @@ -305,16 +317,7 @@ class ConfiguredDoorBird: def webhook_is_registered(self, url, favs=None) -> bool: """Return whether the given URL is registered as a device favorite.""" - favs = favs if favs else self.device.favorites() - - if "http" not in favs: - return False - - for fav in favs["http"].values(): - if fav["value"] == url: - return True - - return False + return self.get_webhook_id(url, favs) is not None def get_webhook_id(self, url, favs=None) -> str or None: """ diff --git a/homeassistant/components/doorbird/const.py b/homeassistant/components/doorbird/const.py index af847dac673..46a95f0d500 100644 --- a/homeassistant/components/doorbird/const.py +++ b/homeassistant/components/doorbird/const.py @@ -17,3 +17,5 @@ DOORBIRD_INFO_KEY_DEVICE_TYPE = "DEVICE-TYPE" DOORBIRD_INFO_KEY_RELAYS = "RELAYS" DOORBIRD_INFO_KEY_PRIMARY_MAC_ADDR = "PRIMARY_MAC_ADDR" DOORBIRD_INFO_KEY_WIFI_MAC_ADDR = "WIFI_MAC_ADDR" + +UNDO_UPDATE_LISTENER = "undo_update_listener" diff --git a/homeassistant/components/doorbird/switch.py b/homeassistant/components/doorbird/switch.py index f1f146aebb9..424bb79092f 100644 --- a/homeassistant/components/doorbird/switch.py +++ b/homeassistant/components/doorbird/switch.py @@ -17,8 +17,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] config_entry_id = config_entry.entry_id - doorstation = hass.data[DOMAIN][config_entry_id][DOOR_STATION] - doorstation_info = hass.data[DOMAIN][config_entry_id][DOOR_STATION_INFO] + data = hass.data[DOMAIN][config_entry_id] + doorstation = data[DOOR_STATION] + doorstation_info = data[DOOR_STATION_INFO] relays = doorstation_info["RELAYS"] relays.append(IR_RELAY) From a58931280ab40cf9b27ff2bb2470eea2aef6eef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 25 Feb 2021 19:52:11 +0100 Subject: [PATCH 719/796] Use dispatch instead of eventbus for supervisor events (#46986) Co-authored-by: Martin Hjelmare Co-authored-by: Paulus Schoutsen --- homeassistant/components/hassio/const.py | 3 +- .../components/hassio/websocket_api.py | 27 ++++- tests/components/hassio/__init__.py | 45 +++++++ tests/components/hassio/test_init.py | 114 +----------------- tests/components/hassio/test_websocket_api.py | 90 ++++++++++++++ 5 files changed, 164 insertions(+), 115 deletions(-) create mode 100644 tests/components/hassio/test_websocket_api.py diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index a3e4451312a..b2878c8143f 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -33,7 +33,8 @@ X_HASS_IS_ADMIN = "X-Hass-Is-Admin" WS_TYPE = "type" WS_ID = "id" -WS_TYPE_EVENT = "supervisor/event" WS_TYPE_API = "supervisor/api" +WS_TYPE_EVENT = "supervisor/event" +WS_TYPE_SUBSCRIBE = "supervisor/subscribe" EVENT_SUPERVISOR_EVENT = "supervisor_event" diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py index d2c0bc9ed10..387aa926489 100644 --- a/homeassistant/components/hassio/websocket_api.py +++ b/homeassistant/components/hassio/websocket_api.py @@ -7,6 +7,10 @@ from homeassistant.components import websocket_api from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from .const import ( ATTR_DATA, @@ -20,6 +24,7 @@ from .const import ( WS_TYPE, WS_TYPE_API, WS_TYPE_EVENT, + WS_TYPE_SUBSCRIBE, ) from .handler import HassIO @@ -36,6 +41,26 @@ def async_load_websocket_api(hass: HomeAssistant): """Set up the websocket API.""" websocket_api.async_register_command(hass, websocket_supervisor_event) websocket_api.async_register_command(hass, websocket_supervisor_api) + websocket_api.async_register_command(hass, websocket_subscribe) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command({vol.Required(WS_TYPE): WS_TYPE_SUBSCRIBE}) +async def websocket_subscribe( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +): + """Subscribe to supervisor events.""" + + @callback + def forward_messages(data): + """Forward events to websocket.""" + connection.send_message(websocket_api.event_message(msg[WS_ID], data)) + + connection.subscriptions[msg[WS_ID]] = async_dispatcher_connect( + hass, EVENT_SUPERVISOR_EVENT, forward_messages + ) + connection.send_message(websocket_api.result_message(msg[WS_ID])) @websocket_api.async_response @@ -49,7 +74,7 @@ async def websocket_supervisor_event( hass: HomeAssistant, connection: ActiveConnection, msg: dict ): """Publish events from the Supervisor.""" - hass.bus.async_fire(EVENT_SUPERVISOR_EVENT, msg[ATTR_DATA]) + async_dispatcher_send(hass, EVENT_SUPERVISOR_EVENT, msg[ATTR_DATA]) connection.send_result(msg[WS_ID]) diff --git a/tests/components/hassio/__init__.py b/tests/components/hassio/__init__.py index ad9829f17ff..f3f35b62562 100644 --- a/tests/components/hassio/__init__.py +++ b/tests/components/hassio/__init__.py @@ -1,3 +1,48 @@ """Tests for Hass.io component.""" +import pytest HASSIO_TOKEN = "123456" + + +@pytest.fixture(autouse=True) +def mock_all(aioclient_mock): + """Mock all setup requests.""" + aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) + aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) + aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/info", + json={ + "result": "ok", + "data": {"supervisor": "222", "homeassistant": "0.110.0", "hassos": None}, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/host/info", + json={ + "result": "ok", + "data": { + "result": "ok", + "data": { + "chassis": "vm", + "operating_system": "Debian GNU/Linux 10 (buster)", + "kernel": "4.19.0-6-amd64", + }, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/core/info", + json={"result": "ok", "data": {"version_latest": "1.0.0"}}, + ) + aioclient_mock.get( + "http://127.0.0.1/os/info", + json={"result": "ok", "data": {"version_latest": "1.0.0"}}, + ) + aioclient_mock.get( + "http://127.0.0.1/supervisor/info", + json={"result": "ok", "data": {"version_latest": "1.0.0"}}, + ) + aioclient_mock.get( + "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} + ) diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index eaeed74fbf7..2efb5b0744e 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -2,73 +2,16 @@ import os from unittest.mock import patch -import pytest - from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components import frontend from homeassistant.components.hassio import STORAGE_KEY -from homeassistant.components.hassio.const import ( - ATTR_DATA, - ATTR_ENDPOINT, - ATTR_METHOD, - EVENT_SUPERVISOR_EVENT, - WS_ID, - WS_TYPE, - WS_TYPE_API, - WS_TYPE_EVENT, -) -from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import async_capture_events +from . import mock_all # noqa MOCK_ENVIRON = {"HASSIO": "127.0.0.1", "HASSIO_TOKEN": "abcdefgh"} -@pytest.fixture(autouse=True) -def mock_all(aioclient_mock): - """Mock all setup requests.""" - aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) - aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) - aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) - aioclient_mock.get( - "http://127.0.0.1/info", - json={ - "result": "ok", - "data": {"supervisor": "222", "homeassistant": "0.110.0", "hassos": None}, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/host/info", - json={ - "result": "ok", - "data": { - "result": "ok", - "data": { - "chassis": "vm", - "operating_system": "Debian GNU/Linux 10 (buster)", - "kernel": "4.19.0-6-amd64", - }, - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/core/info", - json={"result": "ok", "data": {"version_latest": "1.0.0"}}, - ) - aioclient_mock.get( - "http://127.0.0.1/os/info", - json={"result": "ok", "data": {"version_latest": "1.0.0"}}, - ) - aioclient_mock.get( - "http://127.0.0.1/supervisor/info", - json={"result": "ok", "data": {"version_latest": "1.0.0"}}, - ) - aioclient_mock.get( - "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} - ) - - async def test_setup_api_ping(hass, aioclient_mock): """Test setup with API ping.""" with patch.dict(os.environ, MOCK_ENVIRON): @@ -359,58 +302,3 @@ async def test_service_calls_core(hassio_env, hass, aioclient_mock): assert mock_check_config.called assert aioclient_mock.call_count == 5 - - -async def test_websocket_supervisor_event( - hassio_env, hass: HomeAssistant, hass_ws_client -): - """Test Supervisor websocket event.""" - assert await async_setup_component(hass, "hassio", {}) - websocket_client = await hass_ws_client(hass) - - test_event = async_capture_events(hass, EVENT_SUPERVISOR_EVENT) - - await websocket_client.send_json( - {WS_ID: 1, WS_TYPE: WS_TYPE_EVENT, ATTR_DATA: {"event": "test"}} - ) - - assert await websocket_client.receive_json() - await hass.async_block_till_done() - - assert test_event[0].data == {"event": "test"} - - -async def test_websocket_supervisor_api( - hassio_env, hass: HomeAssistant, hass_ws_client, aioclient_mock -): - """Test Supervisor websocket api.""" - assert await async_setup_component(hass, "hassio", {}) - websocket_client = await hass_ws_client(hass) - aioclient_mock.post( - "http://127.0.0.1/snapshots/new/partial", - json={"result": "ok", "data": {"slug": "sn_slug"}}, - ) - - await websocket_client.send_json( - { - WS_ID: 1, - WS_TYPE: WS_TYPE_API, - ATTR_ENDPOINT: "/snapshots/new/partial", - ATTR_METHOD: "post", - } - ) - - msg = await websocket_client.receive_json() - assert msg["result"]["slug"] == "sn_slug" - - await websocket_client.send_json( - { - WS_ID: 2, - WS_TYPE: WS_TYPE_API, - ATTR_ENDPOINT: "/supervisor/info", - ATTR_METHOD: "get", - } - ) - - msg = await websocket_client.receive_json() - assert msg["result"]["version_latest"] == "1.0.0" diff --git a/tests/components/hassio/test_websocket_api.py b/tests/components/hassio/test_websocket_api.py new file mode 100644 index 00000000000..18da5df13ea --- /dev/null +++ b/tests/components/hassio/test_websocket_api.py @@ -0,0 +1,90 @@ +"""Test websocket API.""" +from homeassistant.components.hassio.const import ( + ATTR_DATA, + ATTR_ENDPOINT, + ATTR_METHOD, + ATTR_WS_EVENT, + EVENT_SUPERVISOR_EVENT, + WS_ID, + WS_TYPE, + WS_TYPE_API, + WS_TYPE_SUBSCRIBE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.setup import async_setup_component + +from . import mock_all # noqa + +from tests.common import async_mock_signal + + +async def test_ws_subscription(hassio_env, hass: HomeAssistant, hass_ws_client): + """Test websocket subscription.""" + assert await async_setup_component(hass, "hassio", {}) + client = await hass_ws_client(hass) + await client.send_json({WS_ID: 5, WS_TYPE: WS_TYPE_SUBSCRIBE}) + response = await client.receive_json() + assert response["success"] + + calls = async_mock_signal(hass, EVENT_SUPERVISOR_EVENT) + async_dispatcher_send(hass, EVENT_SUPERVISOR_EVENT, {"lorem": "ipsum"}) + + response = await client.receive_json() + assert response["event"]["lorem"] == "ipsum" + assert len(calls) == 1 + + await client.send_json( + { + WS_ID: 6, + WS_TYPE: "supervisor/event", + ATTR_DATA: {ATTR_WS_EVENT: "test", "lorem": "ipsum"}, + } + ) + response = await client.receive_json() + assert response["success"] + assert len(calls) == 2 + + response = await client.receive_json() + assert response["event"]["lorem"] == "ipsum" + + # Unsubscribe + await client.send_json({WS_ID: 7, WS_TYPE: "unsubscribe_events", "subscription": 5}) + response = await client.receive_json() + assert response["success"] + + +async def test_websocket_supervisor_api( + hassio_env, hass: HomeAssistant, hass_ws_client, aioclient_mock +): + """Test Supervisor websocket api.""" + assert await async_setup_component(hass, "hassio", {}) + websocket_client = await hass_ws_client(hass) + aioclient_mock.post( + "http://127.0.0.1/snapshots/new/partial", + json={"result": "ok", "data": {"slug": "sn_slug"}}, + ) + + await websocket_client.send_json( + { + WS_ID: 1, + WS_TYPE: WS_TYPE_API, + ATTR_ENDPOINT: "/snapshots/new/partial", + ATTR_METHOD: "post", + } + ) + + msg = await websocket_client.receive_json() + assert msg["result"]["slug"] == "sn_slug" + + await websocket_client.send_json( + { + WS_ID: 2, + WS_TYPE: WS_TYPE_API, + ATTR_ENDPOINT: "/supervisor/info", + ATTR_METHOD: "get", + } + ) + + msg = await websocket_client.receive_json() + assert msg["result"]["version_latest"] == "1.0.0" From a3cde9b19ea6fedfeb1107b3d36e20739050b400 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Thu, 25 Feb 2021 17:39:57 +0100 Subject: [PATCH 720/796] Bump python-garminconnect to 0.1.19 to fix broken api (#47020) --- homeassistant/components/garmin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/garmin_connect/manifest.json b/homeassistant/components/garmin_connect/manifest.json index c7880f9b416..59597750ce8 100644 --- a/homeassistant/components/garmin_connect/manifest.json +++ b/homeassistant/components/garmin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "garmin_connect", "name": "Garmin Connect", "documentation": "https://www.home-assistant.io/integrations/garmin_connect", - "requirements": ["garminconnect==0.1.16"], + "requirements": ["garminconnect==0.1.19"], "codeowners": ["@cyberjunky"], "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index 677ce0d0c2f..b90aa3ce936 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -626,7 +626,7 @@ fritzconnection==1.4.0 gTTS==2.2.2 # homeassistant.components.garmin_connect -garminconnect==0.1.16 +garminconnect==0.1.19 # homeassistant.components.geizhals geizhals==0.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 28478c4349a..9773824d221 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -326,7 +326,7 @@ fritzconnection==1.4.0 gTTS==2.2.2 # homeassistant.components.garmin_connect -garminconnect==0.1.16 +garminconnect==0.1.19 # homeassistant.components.geo_json_events # homeassistant.components.usgs_earthquakes_feed From cb2dd6d908a84c1488fcb5527323296b0dbb63a6 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 25 Feb 2021 09:51:18 +0100 Subject: [PATCH 721/796] Fix missing Shelly external input (#47028) * Add support for external input (Shelly 1/1pm add-on) * Make external sensor naming consistent * Fix case consistency --- homeassistant/components/shelly/binary_sensor.py | 7 ++++++- homeassistant/components/shelly/sensor.py | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 8f99e6a7a6e..18220fc9e3a 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -73,6 +73,11 @@ SENSORS = { default_enabled=False, removal_condition=is_momentary_input, ), + ("sensor", "extInput"): BlockAttributeDescription( + name="External Input", + device_class=DEVICE_CLASS_POWER, + default_enabled=False, + ), ("sensor", "motion"): BlockAttributeDescription( name="Motion", device_class=DEVICE_CLASS_MOTION ), @@ -86,7 +91,7 @@ REST_SENSORS = { default_enabled=False, ), "fwupdate": RestAttributeDescription( - name="Firmware update", + name="Firmware Update", icon="mdi:update", value=lambda status, _: status["update"]["has_update"], default_enabled=False, diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 32fb33877d3..472f3be4dae 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -133,7 +133,7 @@ SENSORS = { available=lambda block: block.sensorOp == "normal", ), ("sensor", "extTemp"): BlockAttributeDescription( - name="Temperature", + name="External Temperature", unit=temperature_unit, value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_TEMPERATURE, @@ -155,7 +155,7 @@ SENSORS = { icon="mdi:angle-acute", ), ("relay", "totalWorkTime"): BlockAttributeDescription( - name="Lamp life", + name="Lamp Life", unit=PERCENTAGE, icon="mdi:progress-wrench", value=lambda value: round(100 - (value / 3600 / SHAIR_MAX_WORK_HOURS), 1), From c797e0c8de88dd2f4e024df8110181b5c316e5d2 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 25 Feb 2021 02:14:32 -0500 Subject: [PATCH 722/796] Fix zwave_js unique ID migration logic (#47031) --- homeassistant/components/zwave_js/__init__.py | 74 +++++++++++++----- .../components/zwave_js/discovery.py | 5 -- homeassistant/components/zwave_js/entity.py | 2 +- tests/components/zwave_js/test_init.py | 77 ++++++++++++++++++- 4 files changed, 130 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index cc58e31066a..75bc95b7fe4 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -85,6 +85,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: dev_reg = await device_registry.async_get_registry(hass) ent_reg = entity_registry.async_get(hass) + @callback + def migrate_entity(platform: str, old_unique_id: str, new_unique_id: str) -> None: + """Check if entity with old unique ID exists, and if so migrate it to new ID.""" + if entity_id := ent_reg.async_get_entity_id(platform, DOMAIN, old_unique_id): + LOGGER.debug( + "Migrating entity %s from old unique ID '%s' to new unique ID '%s'", + entity_id, + old_unique_id, + new_unique_id, + ) + ent_reg.async_update_entity( + entity_id, + new_unique_id=new_unique_id, + ) + @callback def async_on_node_ready(node: ZwaveNode) -> None: """Handle node ready event.""" @@ -97,26 +112,49 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for disc_info in async_discover_values(node): LOGGER.debug("Discovered entity: %s", disc_info) - # This migration logic was added in 2021.3 to handle breaking change to - # value_id format. Some time in the future, this code block - # (and get_old_value_id helper) can be removed. - old_value_id = get_old_value_id(disc_info.primary_value) - old_unique_id = get_unique_id( - client.driver.controller.home_id, old_value_id + # This migration logic was added in 2021.3 to handle a breaking change to + # the value_id format. Some time in the future, this code block + # (as well as get_old_value_id helper and migrate_entity closure) can be + # removed. + value_ids = [ + # 2021.2.* format + get_old_value_id(disc_info.primary_value), + # 2021.3.0b0 format + disc_info.primary_value.value_id, + ] + + new_unique_id = get_unique_id( + client.driver.controller.home_id, + disc_info.primary_value.value_id, ) - if entity_id := ent_reg.async_get_entity_id( - disc_info.platform, DOMAIN, old_unique_id - ): - LOGGER.debug( - "Entity %s is using old unique ID, migrating to new one", entity_id - ) - ent_reg.async_update_entity( - entity_id, - new_unique_id=get_unique_id( - client.driver.controller.home_id, - disc_info.primary_value.value_id, - ), + + for value_id in value_ids: + old_unique_id = get_unique_id( + client.driver.controller.home_id, + f"{disc_info.primary_value.node.node_id}.{value_id}", ) + # Most entities have the same ID format, but notification binary sensors + # have a state key in their ID so we need to handle them differently + if ( + disc_info.platform == "binary_sensor" + and disc_info.platform_hint == "notification" + ): + for state_key in disc_info.primary_value.metadata.states: + # ignore idle key (0) + if state_key == "0": + continue + + migrate_entity( + disc_info.platform, + f"{old_unique_id}.{state_key}", + f"{new_unique_id}.{state_key}", + ) + + # Once we've iterated through all state keys, we can move on to the + # next item + continue + + migrate_entity(disc_info.platform, old_unique_id, new_unique_id) async_dispatcher_send( hass, f"{DOMAIN}_{entry.entry_id}_add_{disc_info.platform}", disc_info diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 248a34547b5..a40eb10de8b 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -24,11 +24,6 @@ class ZwaveDiscoveryInfo: # hint for the platform about this discovered entity platform_hint: Optional[str] = "" - @property - def value_id(self) -> str: - """Return the unique value_id belonging to primary value.""" - return f"{self.node.node_id}.{self.primary_value.value_id}" - @dataclass class ZWaveValueDiscoverySchema: diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 685fe50c9b6..d0ed9eb5291 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -32,7 +32,7 @@ class ZWaveBaseEntity(Entity): self.info = info self._name = self.generate_name() self._unique_id = get_unique_id( - self.client.driver.controller.home_id, self.info.value_id + self.client.driver.controller.home_id, self.info.primary_value.value_id ) # entities requiring additional values, can add extra ids to this list self.watched_value_ids = {self.info.primary_value.value_id} diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 3634454544f..f2815bec7f6 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -18,7 +18,7 @@ from homeassistant.config_entries import ( from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import device_registry, entity_registry -from .common import AIR_TEMPERATURE_SENSOR +from .common import AIR_TEMPERATURE_SENSOR, NOTIFICATION_MOTION_BINARY_SENSOR from tests.common import MockConfigEntry @@ -124,13 +124,15 @@ async def test_on_node_added_ready( ) -async def test_unique_id_migration(hass, multisensor_6_state, client, integration): - """Test unique ID is migrated from old format to new.""" +async def test_unique_id_migration_v1(hass, multisensor_6_state, client, integration): + """Test unique ID is migrated from old format to new (version 1).""" ent_reg = entity_registry.async_get(hass) + + # Migrate version 1 entity_name = AIR_TEMPERATURE_SENSOR.split(".")[1] # Create entity RegistryEntry using old unique ID format - old_unique_id = f"{client.driver.controller.home_id}.52-49-00-Air temperature-00" + old_unique_id = f"{client.driver.controller.home_id}.52.52-49-00-Air temperature-00" entity_entry = ent_reg.async_get_or_create( "sensor", DOMAIN, @@ -155,6 +157,73 @@ async def test_unique_id_migration(hass, multisensor_6_state, client, integratio assert entity_entry.unique_id == new_unique_id +async def test_unique_id_migration_v2(hass, multisensor_6_state, client, integration): + """Test unique ID is migrated from old format to new (version 2).""" + ent_reg = entity_registry.async_get(hass) + # Migrate version 2 + ILLUMINANCE_SENSOR = "sensor.multisensor_6_illuminance" + entity_name = ILLUMINANCE_SENSOR.split(".")[1] + + # Create entity RegistryEntry using old unique ID format + old_unique_id = f"{client.driver.controller.home_id}.52.52-49-0-Illuminance-00-00" + entity_entry = ent_reg.async_get_or_create( + "sensor", + DOMAIN, + old_unique_id, + suggested_object_id=entity_name, + config_entry=integration, + original_name=entity_name, + ) + assert entity_entry.entity_id == ILLUMINANCE_SENSOR + assert entity_entry.unique_id == old_unique_id + + # Add a ready node, unique ID should be migrated + node = Node(client, multisensor_6_state) + event = {"node": node} + + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + # Check that new RegistryEntry is using new unique ID format + entity_entry = ent_reg.async_get(ILLUMINANCE_SENSOR) + new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Illuminance-00-00" + assert entity_entry.unique_id == new_unique_id + + +async def test_unique_id_migration_notification_binary_sensor( + hass, multisensor_6_state, client, integration +): + """Test unique ID is migrated from old format to new for a notification binary sensor.""" + ent_reg = entity_registry.async_get(hass) + + entity_name = NOTIFICATION_MOTION_BINARY_SENSOR.split(".")[1] + + # Create entity RegistryEntry using old unique ID format + old_unique_id = f"{client.driver.controller.home_id}.52.52-113-00-Home Security-Motion sensor status.8" + entity_entry = ent_reg.async_get_or_create( + "binary_sensor", + DOMAIN, + old_unique_id, + suggested_object_id=entity_name, + config_entry=integration, + original_name=entity_name, + ) + assert entity_entry.entity_id == NOTIFICATION_MOTION_BINARY_SENSOR + assert entity_entry.unique_id == old_unique_id + + # Add a ready node, unique ID should be migrated + node = Node(client, multisensor_6_state) + event = {"node": node} + + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + # Check that new RegistryEntry is using new unique ID format + entity_entry = ent_reg.async_get(NOTIFICATION_MOTION_BINARY_SENSOR) + new_unique_id = f"{client.driver.controller.home_id}.52-113-0-Home Security-Motion sensor status-Motion sensor status.8" + assert entity_entry.unique_id == new_unique_id + + async def test_on_node_added_not_ready( hass, multisensor_6_state, client, integration, device_registry ): From 399c299cf2d40b745f61a7342a0667b9ac1971c4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 25 Feb 2021 00:48:19 -0800 Subject: [PATCH 723/796] Remove deprecated credstash + keyring (#47033) --- homeassistant/scripts/check_config.py | 7 +-- homeassistant/scripts/credstash.py | 74 --------------------------- homeassistant/scripts/keyring.py | 62 ---------------------- homeassistant/util/yaml/__init__.py | 3 +- homeassistant/util/yaml/const.py | 2 - homeassistant/util/yaml/loader.py | 53 +------------------ requirements_all.txt | 9 ---- requirements_test_all.txt | 9 ---- script/gen_requirements_all.py | 3 +- tests/util/yaml/test_init.py | 44 ---------------- 10 files changed, 4 insertions(+), 262 deletions(-) delete mode 100644 homeassistant/scripts/credstash.py delete mode 100644 homeassistant/scripts/keyring.py diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 992fce2ac87..f75594a546e 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -141,12 +141,7 @@ def run(script_args: List) -> int: if sval is None: print(" -", skey + ":", color("red", "not found")) continue - print( - " -", - skey + ":", - sval, - color("cyan", "[from:", flatsecret.get(skey, "keyring") + "]"), - ) + print(" -", skey + ":", sval) return len(res["except"]) diff --git a/homeassistant/scripts/credstash.py b/homeassistant/scripts/credstash.py deleted file mode 100644 index 99227d81b66..00000000000 --- a/homeassistant/scripts/credstash.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Script to get, put and delete secrets stored in credstash.""" -import argparse -import getpass - -from homeassistant.util.yaml import _SECRET_NAMESPACE - -# mypy: allow-untyped-defs - -REQUIREMENTS = ["credstash==1.15.0"] - - -def run(args): - """Handle credstash script.""" - parser = argparse.ArgumentParser( - description=( - "Modify Home Assistant secrets in credstash." - "Use the secrets in configuration files with: " - "!secret " - ) - ) - parser.add_argument("--script", choices=["credstash"]) - parser.add_argument( - "action", - choices=["get", "put", "del", "list"], - help="Get, put or delete a secret, or list all available secrets", - ) - parser.add_argument("name", help="Name of the secret", nargs="?", default=None) - parser.add_argument( - "value", help="The value to save when putting a secret", nargs="?", default=None - ) - - # pylint: disable=import-error, no-member, import-outside-toplevel - import credstash - - args = parser.parse_args(args) - table = _SECRET_NAMESPACE - - try: - credstash.listSecrets(table=table) - except Exception: # pylint: disable=broad-except - credstash.createDdbTable(table=table) - - if args.action == "list": - secrets = [i["name"] for i in credstash.listSecrets(table=table)] - deduped_secrets = sorted(set(secrets)) - - print("Saved secrets:") - for secret in deduped_secrets: - print(secret) - return 0 - - if args.name is None: - parser.print_help() - return 1 - - if args.action == "put": - if args.value: - the_secret = args.value - else: - the_secret = getpass.getpass(f"Please enter the secret for {args.name}: ") - current_version = credstash.getHighestVersion(args.name, table=table) - credstash.putSecret( - args.name, the_secret, version=int(current_version) + 1, table=table - ) - print(f"Secret {args.name} put successfully") - elif args.action == "get": - the_secret = credstash.getSecret(args.name, table=table) - if the_secret is None: - print(f"Secret {args.name} not found") - else: - print(f"Secret {args.name}={the_secret}") - elif args.action == "del": - credstash.deleteSecrets(args.name, table=table) - print(f"Deleted secret {args.name}") diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py deleted file mode 100644 index 0166d41ce0c..00000000000 --- a/homeassistant/scripts/keyring.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Script to get, set and delete secrets stored in the keyring.""" -import argparse -import getpass -import os - -from homeassistant.util.yaml import _SECRET_NAMESPACE - -# mypy: allow-untyped-defs -REQUIREMENTS = ["keyring==21.2.0", "keyrings.alt==3.4.0"] - - -def run(args): - """Handle keyring script.""" - parser = argparse.ArgumentParser( - description=( - "Modify Home Assistant secrets in the default keyring. " - "Use the secrets in configuration files with: " - "!secret " - ) - ) - parser.add_argument("--script", choices=["keyring"]) - parser.add_argument( - "action", - choices=["get", "set", "del", "info"], - help="Get, set or delete a secret", - ) - parser.add_argument("name", help="Name of the secret", nargs="?", default=None) - - import keyring # pylint: disable=import-outside-toplevel - - # pylint: disable=import-outside-toplevel - from keyring.util import platform_ as platform - - args = parser.parse_args(args) - - if args.action == "info": - keyr = keyring.get_keyring() - print("Keyring version {}\n".format(REQUIREMENTS[0].split("==")[1])) - print(f"Active keyring : {keyr.__module__}") - config_name = os.path.join(platform.config_root(), "keyringrc.cfg") - print(f"Config location : {config_name}") - print(f"Data location : {platform.data_root()}\n") - elif args.name is None: - parser.print_help() - return 1 - - if args.action == "set": - entered_secret = getpass.getpass(f"Please enter the secret for {args.name}: ") - keyring.set_password(_SECRET_NAMESPACE, args.name, entered_secret) - print(f"Secret {args.name} set successfully") - elif args.action == "get": - the_secret = keyring.get_password(_SECRET_NAMESPACE, args.name) - if the_secret is None: - print(f"Secret {args.name} not found") - else: - print(f"Secret {args.name}={the_secret}") - elif args.action == "del": - try: - keyring.delete_password(_SECRET_NAMESPACE, args.name) - print(f"Deleted secret {args.name}") - except keyring.errors.PasswordDeleteError: - print(f"Secret {args.name} not found") diff --git a/homeassistant/util/yaml/__init__.py b/homeassistant/util/yaml/__init__.py index ac4ac2f9a16..a152086ea82 100644 --- a/homeassistant/util/yaml/__init__.py +++ b/homeassistant/util/yaml/__init__.py @@ -1,5 +1,5 @@ """YAML utility functions.""" -from .const import _SECRET_NAMESPACE, SECRET_YAML +from .const import SECRET_YAML from .dumper import dump, save_yaml from .input import UndefinedSubstitution, extract_inputs, substitute from .loader import clear_secret_cache, load_yaml, parse_yaml, secret_yaml @@ -7,7 +7,6 @@ from .objects import Input __all__ = [ "SECRET_YAML", - "_SECRET_NAMESPACE", "Input", "dump", "save_yaml", diff --git a/homeassistant/util/yaml/const.py b/homeassistant/util/yaml/const.py index bf1615edb93..9d930b50fd6 100644 --- a/homeassistant/util/yaml/const.py +++ b/homeassistant/util/yaml/const.py @@ -1,4 +1,2 @@ """Constants.""" SECRET_YAML = "secrets.yaml" - -_SECRET_NAMESPACE = "homeassistant" diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 294cd0ac570..7d713c9f0c0 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -10,20 +10,9 @@ import yaml from homeassistant.exceptions import HomeAssistantError -from .const import _SECRET_NAMESPACE, SECRET_YAML +from .const import SECRET_YAML from .objects import Input, NodeListClass, NodeStrClass -try: - import keyring -except ImportError: - keyring = None - -try: - import credstash -except ImportError: - credstash = None - - # mypy: allow-untyped-calls, no-warn-return-any JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name @@ -32,9 +21,6 @@ DICT_T = TypeVar("DICT_T", bound=Dict) # pylint: disable=invalid-name _LOGGER = logging.getLogger(__name__) __SECRET_CACHE: Dict[str, JSON_TYPE] = {} -CREDSTASH_WARN = False -KEYRING_WARN = False - def clear_secret_cache() -> None: """Clear the secret cache. @@ -299,43 +285,6 @@ def secret_yaml(loader: SafeLineLoader, node: yaml.nodes.Node) -> JSON_TYPE: if not os.path.exists(secret_path) or len(secret_path) < 5: break # Somehow we got past the .homeassistant config folder - if keyring: - # do some keyring stuff - pwd = keyring.get_password(_SECRET_NAMESPACE, node.value) - if pwd: - global KEYRING_WARN # pylint: disable=global-statement - - if not KEYRING_WARN: - KEYRING_WARN = True - _LOGGER.warning( - "Keyring is deprecated and will be removed in March 2021." - ) - - _LOGGER.debug("Secret %s retrieved from keyring", node.value) - return pwd - - global credstash # pylint: disable=invalid-name, global-statement - - if credstash: - # pylint: disable=no-member - try: - pwd = credstash.getSecret(node.value, table=_SECRET_NAMESPACE) - if pwd: - global CREDSTASH_WARN # pylint: disable=global-statement - - if not CREDSTASH_WARN: - CREDSTASH_WARN = True - _LOGGER.warning( - "Credstash is deprecated and will be removed in March 2021." - ) - _LOGGER.debug("Secret %s retrieved from credstash", node.value) - return pwd - except credstash.ItemNotFound: - pass - except Exception: # pylint: disable=broad-except - # Catch if package installed and no config - credstash = None - raise HomeAssistantError(f"Secret {node.value} not defined") diff --git a/requirements_all.txt b/requirements_all.txt index b90aa3ce936..f88d343bd5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -448,9 +448,6 @@ construct==2.10.56 # homeassistant.components.coronavirus coronavirus==1.1.1 -# homeassistant.scripts.credstash -# credstash==1.15.0 - # homeassistant.components.datadog datadog==0.15.0 @@ -844,12 +841,6 @@ kaiterra-async-client==0.0.2 # homeassistant.components.keba keba-kecontact==1.1.0 -# homeassistant.scripts.keyring -keyring==21.2.0 - -# homeassistant.scripts.keyring -keyrings.alt==3.4.0 - # homeassistant.components.kiwi kiwiki-client==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9773824d221..a23f7da4353 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -239,9 +239,6 @@ construct==2.10.56 # homeassistant.components.coronavirus coronavirus==1.1.1 -# homeassistant.scripts.credstash -# credstash==1.15.0 - # homeassistant.components.datadog datadog==0.15.0 @@ -455,12 +452,6 @@ influxdb==5.2.3 # homeassistant.components.verisure jsonpath==0.82 -# homeassistant.scripts.keyring -keyring==21.2.0 - -# homeassistant.scripts.keyring -keyrings.alt==3.4.0 - # homeassistant.components.konnected konnected==1.2.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 7dd4924dac8..94365be9a50 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -21,7 +21,6 @@ COMMENT_REQUIREMENTS = ( "blinkt", "bluepy", "bme680", - "credstash", "decora", "decora_wifi", "envirophat", @@ -47,7 +46,7 @@ COMMENT_REQUIREMENTS = ( "VL53L1X2", ) -IGNORE_PIN = ("colorlog>2.1,<3", "keyring>=9.3,<10.0", "urllib3") +IGNORE_PIN = ("colorlog>2.1,<3", "urllib3") URL_PIN = ( "https://developers.home-assistant.io/docs/" diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index e28a12acf71..b3a8ca4e486 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -1,6 +1,5 @@ """Test Home Assistant yaml loader.""" import io -import logging import os import unittest from unittest.mock import patch @@ -15,14 +14,6 @@ from homeassistant.util.yaml import loader as yaml_loader from tests.common import get_test_config_dir, patch_yaml_files -@pytest.fixture(autouse=True) -def mock_credstash(): - """Mock credstash so it doesn't connect to the internet.""" - with patch.object(yaml_loader, "credstash") as mock_credstash: - mock_credstash.getSecret.return_value = None - yield mock_credstash - - def test_simple_list(): """Test simple list.""" conf = "config:\n - simple\n - list" @@ -294,20 +285,6 @@ def load_yaml(fname, string): return load_yaml_config_file(fname) -class FakeKeyring: - """Fake a keyring class.""" - - def __init__(self, secrets_dict): - """Store keyring dictionary.""" - self._secrets = secrets_dict - - # pylint: disable=protected-access - def get_password(self, domain, name): - """Retrieve password.""" - assert domain == yaml._SECRET_NAMESPACE - return self._secrets.get(name) - - class TestSecrets(unittest.TestCase): """Test the secrets parameter in the yaml utility.""" @@ -395,27 +372,6 @@ class TestSecrets(unittest.TestCase): "http:\n api_password: !secret test", ) - def test_secrets_keyring(self): - """Test keyring fallback & get_password.""" - yaml_loader.keyring = None # Ensure its not there - yaml_str = "http:\n api_password: !secret http_pw_keyring" - with pytest.raises(HomeAssistantError): - load_yaml(self._yaml_path, yaml_str) - - yaml_loader.keyring = FakeKeyring({"http_pw_keyring": "yeah"}) - _yaml = load_yaml(self._yaml_path, yaml_str) - assert {"http": {"api_password": "yeah"}} == _yaml - - @patch.object(yaml_loader, "credstash") - def test_secrets_credstash(self, mock_credstash): - """Test credstash fallback & get_password.""" - mock_credstash.getSecret.return_value = "yeah" - yaml_str = "http:\n api_password: !secret http_pw_credstash" - _yaml = load_yaml(self._yaml_path, yaml_str) - log = logging.getLogger() - log.error(_yaml["http"]) - assert {"api_password": "yeah"} == _yaml["http"] - def test_secrets_logger_removed(self): """Ensure logger: debug was removed.""" with pytest.raises(HomeAssistantError): From 33a6fb1baf9dcbad72df542d7947f7a0f10cd37e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 25 Feb 2021 19:42:48 +0000 Subject: [PATCH 724/796] Bumped version to 2021.3.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f9a8e3e99b3..19f9da062c9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 3 -PATCH_VERSION = "0b0" +PATCH_VERSION = "0b1" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From 35bce434ccc11b2757bb34efedf9071c5bc9da51 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 25 Feb 2021 21:34:04 +0100 Subject: [PATCH 725/796] Updated frontend to 20210225.0 (#47059) --- 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 623aaf42ca5..e8e9c44ae78 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210224.0" + "home-assistant-frontend==20210225.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e4f84f0c8ef..8f84546371e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.41.0 -home-assistant-frontend==20210224.0 +home-assistant-frontend==20210225.0 httpx==0.16.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index f88d343bd5c..579f7b3c568 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -763,7 +763,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210224.0 +home-assistant-frontend==20210225.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a23f7da4353..f92d7060bd1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -412,7 +412,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210224.0 +home-assistant-frontend==20210225.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 5228bbd43c9c5a10bbac250f26ce603c72415c4a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 26 Feb 2021 00:28:22 +0100 Subject: [PATCH 726/796] Revert CORS changes for my home assistant (#47064) * Revert CORS changes for my home assistant * Update test_init.py * Update test_init.py --- homeassistant/components/api/__init__.py | 1 - homeassistant/components/http/__init__.py | 2 +- tests/components/http/test_init.py | 5 +---- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index a82309094e3..e40a9332c38 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -178,7 +178,6 @@ class APIDiscoveryView(HomeAssistantView): requires_auth = False url = URL_API_DISCOVERY_INFO name = "api:discovery" - cors_allowed = True async def get(self, request): """Get discovery information.""" diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index d09cfe754a9..993d466ae18 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -59,7 +59,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_DEVELOPMENT = "0" # Cast to be able to load custom cards. # My to be able to check url and version info. -DEFAULT_CORS = ["https://cast.home-assistant.io", "https://my.home-assistant.io"] +DEFAULT_CORS = ["https://cast.home-assistant.io"] NO_LOGIN_ATTEMPT_THRESHOLD = -1 MAX_CLIENT_SIZE: int = 1024 ** 2 * 16 diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 9621b269081..993f0dba1fd 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -175,10 +175,7 @@ async def test_cors_defaults(hass): assert await async_setup_component(hass, "http", {}) assert len(mock_setup.mock_calls) == 1 - assert mock_setup.mock_calls[0][1][1] == [ - "https://cast.home-assistant.io", - "https://my.home-assistant.io", - ] + assert mock_setup.mock_calls[0][1][1] == ["https://cast.home-assistant.io"] async def test_storing_config(hass, aiohttp_client, aiohttp_unused_port): From a7a66e8ddb6419580ab1c4f51abf36b41f9d5302 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Feb 2021 23:58:35 -0600 Subject: [PATCH 727/796] Ensure hue options show the defaults when the config options have not yet been saved (#47067) --- homeassistant/components/hue/config_flow.py | 6 ++++-- homeassistant/components/hue/const.py | 2 +- tests/components/hue/test_config_flow.py | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index ecb3fd8c489..580b69251c2 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -18,6 +18,8 @@ from .bridge import authenticate_bridge from .const import ( # pylint: disable=unused-import CONF_ALLOW_HUE_GROUPS, CONF_ALLOW_UNREACHABLE, + DEFAULT_ALLOW_HUE_GROUPS, + DEFAULT_ALLOW_UNREACHABLE, DOMAIN, LOGGER, ) @@ -246,13 +248,13 @@ class HueOptionsFlowHandler(config_entries.OptionsFlow): vol.Optional( CONF_ALLOW_HUE_GROUPS, default=self.config_entry.options.get( - CONF_ALLOW_HUE_GROUPS, False + CONF_ALLOW_HUE_GROUPS, DEFAULT_ALLOW_HUE_GROUPS ), ): bool, vol.Optional( CONF_ALLOW_UNREACHABLE, default=self.config_entry.options.get( - CONF_ALLOW_UNREACHABLE, False + CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_UNREACHABLE ), ): bool, } diff --git a/homeassistant/components/hue/const.py b/homeassistant/components/hue/const.py index 4fa11f2ad58..593f74331ec 100644 --- a/homeassistant/components/hue/const.py +++ b/homeassistant/components/hue/const.py @@ -12,6 +12,6 @@ CONF_ALLOW_UNREACHABLE = "allow_unreachable" DEFAULT_ALLOW_UNREACHABLE = False CONF_ALLOW_HUE_GROUPS = "allow_hue_groups" -DEFAULT_ALLOW_HUE_GROUPS = True +DEFAULT_ALLOW_HUE_GROUPS = False DEFAULT_SCENE_TRANSITION = 4 diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index c7dc83183ae..57f4bd7fbca 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -640,6 +640,15 @@ async def test_options_flow(hass): assert result["type"] == "form" assert result["step_id"] == "init" + schema = result["data_schema"].schema + assert ( + _get_schema_default(schema, const.CONF_ALLOW_HUE_GROUPS) + == const.DEFAULT_ALLOW_HUE_GROUPS + ) + assert ( + _get_schema_default(schema, const.CONF_ALLOW_UNREACHABLE) + == const.DEFAULT_ALLOW_UNREACHABLE + ) result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -654,3 +663,11 @@ async def test_options_flow(hass): const.CONF_ALLOW_HUE_GROUPS: True, const.CONF_ALLOW_UNREACHABLE: True, } + + +def _get_schema_default(schema, key_name): + """Iterate schema to find a key.""" + for schema_key in schema: + if schema_key == key_name: + return schema_key.default() + raise KeyError(f"{key_name} not found in schema") From ae0d301fd91e0322000699709c3a3de70f157e5c Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 25 Feb 2021 20:41:54 -0500 Subject: [PATCH 728/796] catch ValueError when unique ID update fails because its taken and remove the duplicate entity (#47072) --- homeassistant/components/zwave_js/__init__.py | 18 +++++-- tests/components/zwave_js/test_init.py | 53 +++++++++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 75bc95b7fe4..93d511875af 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -95,10 +95,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: old_unique_id, new_unique_id, ) - ent_reg.async_update_entity( - entity_id, - new_unique_id=new_unique_id, - ) + try: + ent_reg.async_update_entity( + entity_id, + new_unique_id=new_unique_id, + ) + except ValueError: + LOGGER.debug( + ( + "Entity %s can't be migrated because the unique ID is taken. " + "Cleaning it up since it is likely no longer valid." + ), + entity_id, + ) + ent_reg.async_remove(entity_id) @callback def async_on_node_ready(node: ZwaveNode) -> None: diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index f2815bec7f6..bff2ecd198c 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -124,6 +124,59 @@ async def test_on_node_added_ready( ) +async def test_unique_id_migration_dupes( + hass, multisensor_6_state, client, integration +): + """Test we remove an entity when .""" + ent_reg = entity_registry.async_get(hass) + + entity_name = AIR_TEMPERATURE_SENSOR.split(".")[1] + + # Create entity RegistryEntry using old unique ID format + old_unique_id_1 = ( + f"{client.driver.controller.home_id}.52.52-49-00-Air temperature-00" + ) + entity_entry = ent_reg.async_get_or_create( + "sensor", + DOMAIN, + old_unique_id_1, + suggested_object_id=entity_name, + config_entry=integration, + original_name=entity_name, + ) + assert entity_entry.entity_id == AIR_TEMPERATURE_SENSOR + assert entity_entry.unique_id == old_unique_id_1 + + # Create entity RegistryEntry using b0 unique ID format + old_unique_id_2 = ( + f"{client.driver.controller.home_id}.52.52-49-0-Air temperature-00-00" + ) + entity_entry = ent_reg.async_get_or_create( + "sensor", + DOMAIN, + old_unique_id_2, + suggested_object_id=f"{entity_name}_1", + config_entry=integration, + original_name=entity_name, + ) + assert entity_entry.entity_id == f"{AIR_TEMPERATURE_SENSOR}_1" + assert entity_entry.unique_id == old_unique_id_2 + + # Add a ready node, unique ID should be migrated + node = Node(client, multisensor_6_state) + event = {"node": node} + + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + # Check that new RegistryEntry is using new unique ID format + entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR) + new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature-00-00" + assert entity_entry.unique_id == new_unique_id + + assert ent_reg.async_get(f"{AIR_TEMPERATURE_SENSOR}_1") is None + + async def test_unique_id_migration_v1(hass, multisensor_6_state, client, integration): """Test unique ID is migrated from old format to new (version 1).""" ent_reg = entity_registry.async_get(hass) From 2c30579a118ec78895d2a387687ff4cc45fc750c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 25 Feb 2021 22:01:08 -0800 Subject: [PATCH 729/796] Bump Z-Wave JS Server Python to 0.20.0 (#47076) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/test_config_flow.py | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index f5d9461e9e0..9e57a3b72e2 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.19.0"], + "requirements": ["zwave-js-server-python==0.20.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"] } diff --git a/requirements_all.txt b/requirements_all.txt index 579f7b3c568..bfd55ed68bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2397,4 +2397,4 @@ zigpy==0.32.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.19.0 +zwave-js-server-python==0.20.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f92d7060bd1..9dd674925d6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1234,4 +1234,4 @@ zigpy-znp==0.4.0 zigpy==0.32.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.19.0 +zwave-js-server-python==0.20.0 diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 73057f3fe21..08b0ffe3080 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -144,6 +144,8 @@ def mock_get_server_version(server_version_side_effect, server_version_timeout): driver_version="mock-driver-version", server_version="mock-server-version", home_id=1234, + min_schema_version=0, + max_schema_version=1, ) with patch( "homeassistant.components.zwave_js.config_flow.get_server_version", From 6cdd6c3f44d96599b457eb3e6b7c57a7c8f3acee Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 26 Feb 2021 06:01:42 +0000 Subject: [PATCH 730/796] Bumped version to 2021.3.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 19f9da062c9..aca184b4606 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 3 -PATCH_VERSION = "0b1" +PATCH_VERSION = "0b2" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From 101897c260d6d36399eac13ec6824b487c56bc6b Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 26 Feb 2021 18:34:40 +0100 Subject: [PATCH 731/796] Add support for v6 features to philips js integration (#46422) --- .../components/philips_js/__init__.py | 54 ++- .../components/philips_js/config_flow.py | 135 ++++-- homeassistant/components/philips_js/const.py | 3 + .../components/philips_js/manifest.json | 2 +- .../components/philips_js/media_player.py | 409 ++++++++++++++---- .../components/philips_js/strings.json | 6 +- .../philips_js/translations/en.json | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/philips_js/__init__.py | 60 ++- tests/components/philips_js/conftest.py | 13 +- .../components/philips_js/test_config_flow.py | 144 +++++- .../philips_js/test_device_trigger.py | 6 +- 13 files changed, 702 insertions(+), 138 deletions(-) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 11e84b6cd82..f3c2eb59789 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -8,8 +8,13 @@ from haphilipsjs import ConnectionFailure, PhilipsTV from homeassistant.components.automation import AutomationActionType from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_VERSION, CONF_HOST -from homeassistant.core import Context, HassJob, HomeAssistant, callback +from homeassistant.const import ( + CONF_API_VERSION, + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, +) +from homeassistant.core import CALLBACK_TYPE, Context, HassJob, HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -30,7 +35,12 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Philips TV from a config entry.""" - tvapi = PhilipsTV(entry.data[CONF_HOST], entry.data[CONF_API_VERSION]) + tvapi = PhilipsTV( + entry.data[CONF_HOST], + entry.data[CONF_API_VERSION], + username=entry.data.get(CONF_USERNAME), + password=entry.data.get(CONF_PASSWORD), + ) coordinator = PhilipsTVDataUpdateCoordinator(hass, tvapi) @@ -103,7 +113,9 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): def __init__(self, hass, api: PhilipsTV) -> None: """Set up the coordinator.""" self.api = api + self._notify_future: Optional[asyncio.Task] = None + @callback def _update_listeners(): for update_callback in self._listeners: update_callback() @@ -120,9 +132,43 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): ), ) + async def _notify_task(self): + while self.api.on and self.api.notify_change_supported: + if await self.api.notifyChange(130): + self.async_set_updated_data(None) + + @callback + def _async_notify_stop(self): + if self._notify_future: + self._notify_future.cancel() + self._notify_future = None + + @callback + def _async_notify_schedule(self): + if ( + (self._notify_future is None or self._notify_future.done()) + and self.api.on + and self.api.notify_change_supported + ): + self._notify_future = self.hass.loop.create_task(self._notify_task()) + + @callback + def async_remove_listener(self, update_callback: CALLBACK_TYPE) -> None: + """Remove data update.""" + super().async_remove_listener(update_callback) + if not self._listeners: + self._async_notify_stop() + + @callback + def _async_stop_refresh(self, event: asyncio.Event) -> None: + super()._async_stop_refresh(event) + self._async_notify_stop() + + @callback async def _async_update_data(self): """Fetch the latest data from the source.""" try: - await self.hass.async_add_executor_job(self.api.update) + await self.api.update() + self._async_notify_schedule() except ConnectionFailure: pass diff --git a/homeassistant/components/philips_js/config_flow.py b/homeassistant/components/philips_js/config_flow.py index 523918daa7c..778bcba282b 100644 --- a/homeassistant/components/philips_js/config_flow.py +++ b/homeassistant/components/philips_js/config_flow.py @@ -1,35 +1,47 @@ """Config flow for Philips TV integration.""" -import logging -from typing import Any, Dict, Optional, TypedDict +import platform +from typing import Any, Dict, Optional, Tuple, TypedDict -from haphilipsjs import ConnectionFailure, PhilipsTV +from haphilipsjs import ConnectionFailure, PairingFailure, PhilipsTV import voluptuous as vol from homeassistant import config_entries, core -from homeassistant.const import CONF_API_VERSION, CONF_HOST +from homeassistant.const import ( + CONF_API_VERSION, + CONF_HOST, + CONF_PASSWORD, + CONF_PIN, + CONF_USERNAME, +) -from .const import DOMAIN # pylint:disable=unused-import - -_LOGGER = logging.getLogger(__name__) +from . import LOGGER +from .const import ( # pylint:disable=unused-import + CONF_SYSTEM, + CONST_APP_ID, + CONST_APP_NAME, + DOMAIN, +) class FlowUserDict(TypedDict): """Data for user step.""" host: str - api_version: int -async def validate_input(hass: core.HomeAssistant, data: FlowUserDict): +async def validate_input( + hass: core.HomeAssistant, host: str, api_version: int +) -> Tuple[Dict, PhilipsTV]: """Validate the user input allows us to connect.""" - hub = PhilipsTV(data[CONF_HOST], data[CONF_API_VERSION]) + hub = PhilipsTV(host, api_version) - await hass.async_add_executor_job(hub.getSystem) + await hub.getSystem() + await hub.setTransport(hub.secured_transport) - if hub.system is None: - raise ConnectionFailure + if not hub.system: + raise ConnectionFailure("System data is empty") - return hub.system + return hub class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -38,7 +50,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL - _default = {} + _current = {} + _hub: PhilipsTV + _pair_state: Any async def async_step_import(self, conf: Dict[str, Any]): """Import a configuration from config.yaml.""" @@ -53,34 +67,99 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } ) + async def _async_create_current(self): + + system = self._current[CONF_SYSTEM] + return self.async_create_entry( + title=f"{system['name']} ({system['serialnumber']})", + data=self._current, + ) + + async def async_step_pair(self, user_input: Optional[Dict] = None): + """Attempt to pair with device.""" + assert self._hub + + errors = {} + schema = vol.Schema( + { + vol.Required(CONF_PIN): str, + } + ) + + if not user_input: + try: + self._pair_state = await self._hub.pairRequest( + CONST_APP_ID, + CONST_APP_NAME, + platform.node(), + platform.system(), + "native", + ) + except PairingFailure as exc: + LOGGER.debug(str(exc)) + return self.async_abort( + reason="pairing_failure", + description_placeholders={"error_id": exc.data.get("error_id")}, + ) + return self.async_show_form( + step_id="pair", data_schema=schema, errors=errors + ) + + try: + username, password = await self._hub.pairGrant( + self._pair_state, user_input[CONF_PIN] + ) + except PairingFailure as exc: + LOGGER.debug(str(exc)) + if exc.data.get("error_id") == "INVALID_PIN": + errors[CONF_PIN] = "invalid_pin" + return self.async_show_form( + step_id="pair", data_schema=schema, errors=errors + ) + + return self.async_abort( + reason="pairing_failure", + description_placeholders={"error_id": exc.data.get("error_id")}, + ) + + self._current[CONF_USERNAME] = username + self._current[CONF_PASSWORD] = password + return await self._async_create_current() + async def async_step_user(self, user_input: Optional[FlowUserDict] = None): """Handle the initial step.""" errors = {} if user_input: - self._default = user_input + self._current = user_input try: - system = await validate_input(self.hass, user_input) - except ConnectionFailure: + hub = await validate_input( + self.hass, user_input[CONF_HOST], user_input[CONF_API_VERSION] + ) + except ConnectionFailure as exc: + LOGGER.error(str(exc)) errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") + LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - await self.async_set_unique_id(system["serialnumber"]) - self._abort_if_unique_id_configured(updates=user_input) - data = {**user_input, "system": system} + await self.async_set_unique_id(hub.system["serialnumber"]) + self._abort_if_unique_id_configured() - return self.async_create_entry( - title=f"{system['name']} ({system['serialnumber']})", data=data - ) + self._current[CONF_SYSTEM] = hub.system + self._current[CONF_API_VERSION] = hub.api_version + self._hub = hub + + if hub.pairing_type == "digest_auth_pairing": + return await self.async_step_pair() + return await self._async_create_current() schema = vol.Schema( { - vol.Required(CONF_HOST, default=self._default.get(CONF_HOST)): str, + vol.Required(CONF_HOST, default=self._current.get(CONF_HOST)): str, vol.Required( - CONF_API_VERSION, default=self._default.get(CONF_API_VERSION) - ): vol.In([1, 6]), + CONF_API_VERSION, default=self._current.get(CONF_API_VERSION, 1) + ): vol.In([1, 5, 6]), } ) return self.async_show_form(step_id="user", data_schema=schema, errors=errors) diff --git a/homeassistant/components/philips_js/const.py b/homeassistant/components/philips_js/const.py index 893766b0083..5769a8979ce 100644 --- a/homeassistant/components/philips_js/const.py +++ b/homeassistant/components/philips_js/const.py @@ -2,3 +2,6 @@ DOMAIN = "philips_js" CONF_SYSTEM = "system" + +CONST_APP_ID = "homeassistant.io" +CONST_APP_NAME = "Home Assistant" diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index e41aa348732..e1e1fa69b6b 100644 --- a/homeassistant/components/philips_js/manifest.json +++ b/homeassistant/components/philips_js/manifest.json @@ -3,7 +3,7 @@ "name": "Philips TV", "documentation": "https://www.home-assistant.io/integrations/philips_js", "requirements": [ - "ha-philipsjs==0.1.0" + "ha-philipsjs==2.3.0" ], "codeowners": [ "@elupus" diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 20ef6ed9c0f..2b2714b20ce 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -1,6 +1,7 @@ """Media Player component to integrate TVs exposing the Joint Space API.""" -from typing import Any, Dict +from typing import Any, Dict, Optional +from haphilipsjs import ConnectionFailure import voluptuous as vol from homeassistant import config_entries @@ -11,15 +12,21 @@ from homeassistant.components.media_player import ( MediaPlayerEntity, ) from homeassistant.components.media_player.const import ( + MEDIA_CLASS_APP, MEDIA_CLASS_CHANNEL, MEDIA_CLASS_DIRECTORY, + MEDIA_TYPE_APP, + MEDIA_TYPE_APPS, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_CHANNELS, SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, + SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, @@ -27,7 +34,6 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.components.media_player.errors import BrowseError -from homeassistant.components.philips_js import PhilipsTVDataUpdateCoordinator from homeassistant.const import ( CONF_API_VERSION, CONF_HOST, @@ -40,7 +46,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import LOGGER as _LOGGER +from . import LOGGER as _LOGGER, PhilipsTVDataUpdateCoordinator from .const import CONF_SYSTEM, DOMAIN SUPPORT_PHILIPS_JS = ( @@ -53,16 +59,15 @@ SUPPORT_PHILIPS_JS = ( | SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY_MEDIA | SUPPORT_BROWSE_MEDIA + | SUPPORT_PLAY + | SUPPORT_PAUSE + | SUPPORT_STOP ) CONF_ON_ACTION = "turn_on_action" DEFAULT_API_VERSION = 1 -PREFIX_SEPARATOR = ": " -PREFIX_SOURCE = "Input" -PREFIX_CHANNEL = "Channel" - PLATFORM_SCHEMA = vol.All( cv.deprecated(CONF_HOST), cv.deprecated(CONF_NAME), @@ -131,12 +136,19 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): self._supports = SUPPORT_PHILIPS_JS self._system = system self._unique_id = unique_id + self._state = STATE_OFF + self._media_content_type: Optional[str] = None + self._media_content_id: Optional[str] = None + self._media_title: Optional[str] = None + self._media_channel: Optional[str] = None + super().__init__(coordinator) self._update_from_coordinator() - def _update_soon(self): + async def _async_update_soon(self): """Reschedule update task.""" - self.hass.add_job(self.coordinator.async_request_refresh) + self.async_write_ha_state() + await self.coordinator.async_request_refresh() @property def name(self): @@ -147,7 +159,9 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): def supported_features(self): """Flag media player features that are supported.""" supports = self._supports - if self._coordinator.turn_on: + if self._coordinator.turn_on or ( + self._tv.on and self._tv.powerstate is not None + ): supports |= SUPPORT_TURN_ON return supports @@ -155,7 +169,8 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): def state(self): """Get the device state. An exception means OFF state.""" if self._tv.on: - return STATE_ON + if self._tv.powerstate == "On" or self._tv.powerstate is None: + return STATE_ON return STATE_OFF @property @@ -168,22 +183,12 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): """List of available input sources.""" return list(self._sources.values()) - def select_source(self, source): + async def async_select_source(self, source): """Set the input source.""" - data = source.split(PREFIX_SEPARATOR, 1) - if data[0] == PREFIX_SOURCE: # Legacy way to set source - source_id = _inverted(self._sources).get(data[1]) - if source_id: - self._tv.setSource(source_id) - elif data[0] == PREFIX_CHANNEL: # Legacy way to set channel - channel_id = _inverted(self._channels).get(data[1]) - if channel_id: - self._tv.setChannel(channel_id) - else: - source_id = _inverted(self._sources).get(source) - if source_id: - self._tv.setSource(source_id) - self._update_soon() + source_id = _inverted(self._sources).get(source) + if source_id: + await self._tv.setSource(source_id) + await self._async_update_soon() @property def volume_level(self): @@ -197,78 +202,118 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): async def async_turn_on(self): """Turn on the device.""" - await self._coordinator.turn_on.async_run(self.hass, self._context) + if self._tv.on and self._tv.powerstate: + await self._tv.setPowerState("On") + self._state = STATE_ON + else: + await self._coordinator.turn_on.async_run(self.hass, self._context) + await self._async_update_soon() - def turn_off(self): + async def async_turn_off(self): """Turn off the device.""" - self._tv.sendKey("Standby") - self._tv.on = False - self._update_soon() + await self._tv.sendKey("Standby") + self._state = STATE_OFF + await self._async_update_soon() - def volume_up(self): + async def async_volume_up(self): """Send volume up command.""" - self._tv.sendKey("VolumeUp") - self._update_soon() + await self._tv.sendKey("VolumeUp") + await self._async_update_soon() - def volume_down(self): + async def async_volume_down(self): """Send volume down command.""" - self._tv.sendKey("VolumeDown") - self._update_soon() + await self._tv.sendKey("VolumeDown") + await self._async_update_soon() - def mute_volume(self, mute): + async def async_mute_volume(self, mute): """Send mute command.""" - self._tv.setVolume(None, mute) - self._update_soon() + if self._tv.muted != mute: + await self._tv.sendKey("Mute") + await self._async_update_soon() + else: + _LOGGER.debug("Ignoring request when already in expected state") - def set_volume_level(self, volume): + async def async_set_volume_level(self, volume): """Set volume level, range 0..1.""" - self._tv.setVolume(volume, self._tv.muted) - self._update_soon() + await self._tv.setVolume(volume, self._tv.muted) + await self._async_update_soon() - def media_previous_track(self): + async def async_media_previous_track(self): """Send rewind command.""" - self._tv.sendKey("Previous") - self._update_soon() + await self._tv.sendKey("Previous") + await self._async_update_soon() - def media_next_track(self): + async def async_media_next_track(self): """Send fast forward command.""" - self._tv.sendKey("Next") - self._update_soon() + await self._tv.sendKey("Next") + await self._async_update_soon() + + async def async_media_play_pause(self): + """Send pause command to media player.""" + if self._tv.quirk_playpause_spacebar: + await self._tv.sendUnicode(" ") + else: + await self._tv.sendKey("PlayPause") + await self._async_update_soon() + + async def async_media_play(self): + """Send pause command to media player.""" + await self._tv.sendKey("Play") + await self._async_update_soon() + + async def async_media_pause(self): + """Send play command to media player.""" + await self._tv.sendKey("Pause") + await self._async_update_soon() + + async def async_media_stop(self): + """Send play command to media player.""" + await self._tv.sendKey("Stop") + await self._async_update_soon() @property def media_channel(self): """Get current channel if it's a channel.""" - if self.media_content_type == MEDIA_TYPE_CHANNEL: - return self._channels.get(self._tv.channel_id) - return None + return self._media_channel @property def media_title(self): """Title of current playing media.""" - if self.media_content_type == MEDIA_TYPE_CHANNEL: - return self._channels.get(self._tv.channel_id) - return self._sources.get(self._tv.source_id) + return self._media_title @property def media_content_type(self): """Return content type of playing media.""" - if self._tv.source_id == "tv" or self._tv.source_id == "11": - return MEDIA_TYPE_CHANNEL - if self._tv.source_id is None and self._tv.channels: - return MEDIA_TYPE_CHANNEL - return None + return self._media_content_type @property def media_content_id(self): """Content type of current playing media.""" - if self.media_content_type == MEDIA_TYPE_CHANNEL: - return self._channels.get(self._tv.channel_id) + return self._media_content_id + + @property + def media_image_url(self): + """Image url of current playing media.""" + if self._media_content_id and self._media_content_type in ( + MEDIA_CLASS_APP, + MEDIA_CLASS_CHANNEL, + ): + return self.get_browse_image_url( + self._media_content_type, self._media_content_id, media_image_id=None + ) return None @property - def device_state_attributes(self): - """Return the state attributes.""" - return {"channel_list": list(self._channels.values())} + def app_id(self): + """ID of the current running app.""" + return self._tv.application_id + + @property + def app_name(self): + """Name of the current running app.""" + app = self._tv.applications.get(self._tv.application_id) + if app: + return app.get("label") @property def device_class(self): @@ -293,57 +338,243 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): "sw_version": self._system.get("softwareversion"), } - def play_media(self, media_type, media_id, **kwargs): + async def async_play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" _LOGGER.debug("Call play media type <%s>, Id <%s>", media_type, media_id) if media_type == MEDIA_TYPE_CHANNEL: - channel_id = _inverted(self._channels).get(media_id) + list_id, _, channel_id = media_id.partition("/") if channel_id: - self._tv.setChannel(channel_id) - self._update_soon() + await self._tv.setChannel(channel_id, list_id) + await self._async_update_soon() else: _LOGGER.error("Unable to find channel <%s>", media_id) + elif media_type == MEDIA_TYPE_APP: + app = self._tv.applications.get(media_id) + if app: + await self._tv.setApplication(app["intent"]) + await self._async_update_soon() + else: + _LOGGER.error("Unable to find application <%s>", media_id) else: _LOGGER.error("Unsupported media type <%s>", media_type) - async def async_browse_media(self, media_content_type=None, media_content_id=None): - """Implement the websocket media browsing helper.""" - if media_content_id not in (None, ""): - raise BrowseError( - f"Media not found: {media_content_type} / {media_content_id}" - ) + async def async_browse_media_channels(self, expanded): + """Return channel media objects.""" + if expanded: + children = [ + BrowseMedia( + title=channel.get("name", f"Channel: {channel_id}"), + media_class=MEDIA_CLASS_CHANNEL, + media_content_id=f"alltv/{channel_id}", + media_content_type=MEDIA_TYPE_CHANNEL, + can_play=True, + can_expand=False, + thumbnail=self.get_browse_image_url( + MEDIA_TYPE_APP, channel_id, media_image_id=None + ), + ) + for channel_id, channel in self._tv.channels.items() + ] + else: + children = None return BrowseMedia( title="Channels", media_class=MEDIA_CLASS_DIRECTORY, - media_content_id="", + media_content_id="channels", media_content_type=MEDIA_TYPE_CHANNELS, + children_media_class=MEDIA_TYPE_CHANNEL, + can_play=False, + can_expand=True, + children=children, + ) + + async def async_browse_media_favorites(self, list_id, expanded): + """Return channel media objects.""" + if expanded: + favorites = await self._tv.getFavoriteList(list_id) + if favorites: + + def get_name(channel): + channel_data = self._tv.channels.get(str(channel["ccid"])) + if channel_data: + return channel_data["name"] + return f"Channel: {channel['ccid']}" + + children = [ + BrowseMedia( + title=get_name(channel), + media_class=MEDIA_CLASS_CHANNEL, + media_content_id=f"{list_id}/{channel['ccid']}", + media_content_type=MEDIA_TYPE_CHANNEL, + can_play=True, + can_expand=False, + thumbnail=self.get_browse_image_url( + MEDIA_TYPE_APP, channel, media_image_id=None + ), + ) + for channel in favorites + ] + else: + children = None + else: + children = None + + favorite = self._tv.favorite_lists[list_id] + return BrowseMedia( + title=favorite.get("name", f"Favorites {list_id}"), + media_class=MEDIA_CLASS_DIRECTORY, + media_content_id=f"favorites/{list_id}", + media_content_type=MEDIA_TYPE_CHANNELS, + children_media_class=MEDIA_TYPE_CHANNEL, + can_play=False, + can_expand=True, + children=children, + ) + + async def async_browse_media_applications(self, expanded): + """Return application media objects.""" + if expanded: + children = [ + BrowseMedia( + title=application["label"], + media_class=MEDIA_CLASS_APP, + media_content_id=application_id, + media_content_type=MEDIA_TYPE_APP, + can_play=True, + can_expand=False, + thumbnail=self.get_browse_image_url( + MEDIA_TYPE_APP, application_id, media_image_id=None + ), + ) + for application_id, application in self._tv.applications.items() + ] + else: + children = None + + return BrowseMedia( + title="Applications", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_id="applications", + media_content_type=MEDIA_TYPE_APPS, + children_media_class=MEDIA_TYPE_APP, + can_play=False, + can_expand=True, + children=children, + ) + + async def async_browse_media_favorite_lists(self, expanded): + """Return favorite media objects.""" + if self._tv.favorite_lists and expanded: + children = [ + await self.async_browse_media_favorites(list_id, False) + for list_id in self._tv.favorite_lists + ] + else: + children = None + + return BrowseMedia( + title="Favorites", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_id="favorite_lists", + media_content_type=MEDIA_TYPE_CHANNELS, + children_media_class=MEDIA_TYPE_CHANNEL, + can_play=False, + can_expand=True, + children=children, + ) + + async def async_browse_media_root(self): + """Return root media objects.""" + + return BrowseMedia( + title="Library", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_id="", + media_content_type="", can_play=False, can_expand=True, children=[ - BrowseMedia( - title=channel, - media_class=MEDIA_CLASS_CHANNEL, - media_content_id=channel, - media_content_type=MEDIA_TYPE_CHANNEL, - can_play=True, - can_expand=False, - ) - for channel in self._channels.values() + await self.async_browse_media_channels(False), + await self.async_browse_media_applications(False), + await self.async_browse_media_favorite_lists(False), ], ) + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + if not self._tv.on: + raise BrowseError("Can't browse when tv is turned off") + + if media_content_id in (None, ""): + return await self.async_browse_media_root() + path = media_content_id.partition("/") + if path[0] == "channels": + return await self.async_browse_media_channels(True) + if path[0] == "applications": + return await self.async_browse_media_applications(True) + if path[0] == "favorite_lists": + return await self.async_browse_media_favorite_lists(True) + if path[0] == "favorites": + return await self.async_browse_media_favorites(path[2], True) + + raise BrowseError(f"Media not found: {media_content_type} / {media_content_id}") + + async def async_get_browse_image( + self, media_content_type, media_content_id, media_image_id=None + ): + """Serve album art. Returns (content, content_type).""" + try: + if media_content_type == MEDIA_TYPE_APP and media_content_id: + return await self._tv.getApplicationIcon(media_content_id) + if media_content_type == MEDIA_TYPE_CHANNEL and media_content_id: + return await self._tv.getChannelLogo(media_content_id) + except ConnectionFailure: + _LOGGER.warning("Failed to fetch image") + return None, None + + async def async_get_media_image(self): + """Serve album art. Returns (content, content_type).""" + return await self.async_get_browse_image( + self.media_content_type, self.media_content_id, None + ) + + @callback def _update_from_coordinator(self): + + if self._tv.on: + if self._tv.powerstate in ("Standby", "StandbyKeep"): + self._state = STATE_OFF + else: + self._state = STATE_ON + else: + self._state = STATE_OFF + self._sources = { srcid: source.get("name") or f"Source {srcid}" for srcid, source in (self._tv.sources or {}).items() } - self._channels = { - chid: channel.get("name") or f"Channel {chid}" - for chid, channel in (self._tv.channels or {}).items() - } + if self._tv.channel_active: + self._media_content_type = MEDIA_TYPE_CHANNEL + self._media_content_id = f"all/{self._tv.channel_id}" + self._media_title = self._tv.channels.get(self._tv.channel_id, {}).get( + "name" + ) + self._media_channel = self._media_title + elif self._tv.application_id: + self._media_content_type = MEDIA_TYPE_APP + self._media_content_id = self._tv.application_id + self._media_title = self._tv.applications.get( + self._tv.application_id, {} + ).get("label") + self._media_channel = None + else: + self._media_content_type = None + self._media_content_id = None + self._media_title = self._sources.get(self._tv.source_id) + self._media_channel = None @callback def _handle_coordinator_update(self) -> None: diff --git a/homeassistant/components/philips_js/strings.json b/homeassistant/components/philips_js/strings.json index 2267315501f..df65d453f2b 100644 --- a/homeassistant/components/philips_js/strings.json +++ b/homeassistant/components/philips_js/strings.json @@ -10,8 +10,10 @@ }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, + "unknown": "[%key:common::config_flow::error::unknown%]", + "pairing_failure": "Unable to pair: {error_id}", + "invalid_pin": "Invalid PIN" +}, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } diff --git a/homeassistant/components/philips_js/translations/en.json b/homeassistant/components/philips_js/translations/en.json index 249fe5a892d..b2022a01824 100644 --- a/homeassistant/components/philips_js/translations/en.json +++ b/homeassistant/components/philips_js/translations/en.json @@ -5,7 +5,9 @@ }, "error": { "cannot_connect": "Failed to connect", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "pairing_failure": "Unable to pair: {error_id}", + "invalid_pin": "Invalid PIN" }, "step": { "user": { diff --git a/requirements_all.txt b/requirements_all.txt index bfd55ed68bc..2cfcc67a25d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -721,7 +721,7 @@ guppy3==3.1.0 ha-ffmpeg==3.0.2 # homeassistant.components.philips_js -ha-philipsjs==0.1.0 +ha-philipsjs==2.3.0 # homeassistant.components.habitica habitipy==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9dd674925d6..de10ccaf6a9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -382,7 +382,7 @@ guppy3==3.1.0 ha-ffmpeg==3.0.2 # homeassistant.components.philips_js -ha-philipsjs==0.1.0 +ha-philipsjs==2.3.0 # homeassistant.components.habitica habitipy==0.2.0 diff --git a/tests/components/philips_js/__init__.py b/tests/components/philips_js/__init__.py index 1c96a6d4e55..9dea390a600 100644 --- a/tests/components/philips_js/__init__.py +++ b/tests/components/philips_js/__init__.py @@ -3,6 +3,9 @@ MOCK_SERIAL_NO = "1234567890" MOCK_NAME = "Philips TV" +MOCK_USERNAME = "mock_user" +MOCK_PASSWORD = "mock_password" + MOCK_SYSTEM = { "menulanguage": "English", "name": MOCK_NAME, @@ -12,14 +15,63 @@ MOCK_SYSTEM = { "model": "modelname", } -MOCK_USERINPUT = { - "host": "1.1.1.1", - "api_version": 1, +MOCK_SYSTEM_UNPAIRED = { + "menulanguage": "Dutch", + "name": "55PUS7181/12", + "country": "Netherlands", + "serialnumber": "ABCDEFGHIJKLF", + "softwareversion": "TPM191E_R.101.001.208.001", + "model": "65OLED855/12", + "deviceid": "1234567890", + "nettvversion": "6.0.2", + "epgsource": "one", + "api_version": {"Major": 6, "Minor": 2, "Patch": 0}, + "featuring": { + "jsonfeatures": { + "editfavorites": ["TVChannels", "SatChannels"], + "recordings": ["List", "Schedule", "Manage"], + "ambilight": ["LoungeLight", "Hue", "Ambilight"], + "menuitems": ["Setup_Menu"], + "textentry": [ + "context_based", + "initial_string_available", + "editor_info_available", + ], + "applications": ["TV_Apps", "TV_Games", "TV_Settings"], + "pointer": ["not_available"], + "inputkey": ["key"], + "activities": ["intent"], + "channels": ["preset_string"], + "mappings": ["server_mapping"], + }, + "systemfeatures": { + "tvtype": "consumer", + "content": ["dmr", "dms_tad"], + "tvsearch": "intent", + "pairing_type": "digest_auth_pairing", + "secured_transport": "True", + }, + }, } +MOCK_USERINPUT = { + "host": "1.1.1.1", +} + +MOCK_IMPORT = {"host": "1.1.1.1", "api_version": 6} + MOCK_CONFIG = { - **MOCK_USERINPUT, + "host": "1.1.1.1", + "api_version": 1, "system": MOCK_SYSTEM, } +MOCK_CONFIG_PAIRED = { + "host": "1.1.1.1", + "api_version": 6, + "username": MOCK_USERNAME, + "password": MOCK_PASSWORD, + "system": MOCK_SYSTEM_UNPAIRED, +} + MOCK_ENTITY_ID = "media_player.philips_tv" diff --git a/tests/components/philips_js/conftest.py b/tests/components/philips_js/conftest.py index 549ad77fb06..4b6150f9f81 100644 --- a/tests/components/philips_js/conftest.py +++ b/tests/components/philips_js/conftest.py @@ -1,6 +1,7 @@ """Standard setup for tests.""" -from unittest.mock import Mock, patch +from unittest.mock import create_autospec, patch +from haphilipsjs import PhilipsTV from pytest import fixture from homeassistant import setup @@ -20,10 +21,18 @@ async def setup_notification(hass): @fixture(autouse=True) def mock_tv(): """Disable component actual use.""" - tv = Mock(autospec="philips_js.PhilipsTV") + tv = create_autospec(PhilipsTV) tv.sources = {} tv.channels = {} + tv.application = None + tv.applications = {} tv.system = MOCK_SYSTEM + tv.api_version = 1 + tv.api_version_detected = None + tv.on = True + tv.notify_change_supported = False + tv.pairing_type = None + tv.powerstate = None with patch( "homeassistant.components.philips_js.config_flow.PhilipsTV", return_value=tv diff --git a/tests/components/philips_js/test_config_flow.py b/tests/components/philips_js/test_config_flow.py index 75caff78891..45e896319f1 100644 --- a/tests/components/philips_js/test_config_flow.py +++ b/tests/components/philips_js/test_config_flow.py @@ -1,12 +1,21 @@ """Test the Philips TV config flow.""" -from unittest.mock import patch +from unittest.mock import ANY, patch +from haphilipsjs import PairingFailure from pytest import fixture from homeassistant import config_entries from homeassistant.components.philips_js.const import DOMAIN -from . import MOCK_CONFIG, MOCK_USERINPUT +from . import ( + MOCK_CONFIG, + MOCK_CONFIG_PAIRED, + MOCK_IMPORT, + MOCK_PASSWORD, + MOCK_SYSTEM_UNPAIRED, + MOCK_USERINPUT, + MOCK_USERNAME, +) @fixture(autouse=True) @@ -27,12 +36,26 @@ def mock_setup_entry(): yield mock_setup_entry +@fixture +async def mock_tv_pairable(mock_tv): + """Return a mock tv that is pariable.""" + mock_tv.system = MOCK_SYSTEM_UNPAIRED + mock_tv.pairing_type = "digest_auth_pairing" + mock_tv.api_version = 6 + mock_tv.api_version_detected = 6 + mock_tv.secured_transport = True + + mock_tv.pairRequest.return_value = {} + mock_tv.pairGrant.return_value = MOCK_USERNAME, MOCK_PASSWORD + return mock_tv + + async def test_import(hass, mock_setup, mock_setup_entry): """Test we get an item on import.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_USERINPUT, + data=MOCK_IMPORT, ) assert result["type"] == "create_entry" @@ -47,7 +70,7 @@ async def test_import_exist(hass, mock_config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_USERINPUT, + data=MOCK_IMPORT, ) assert result["type"] == "abort" @@ -103,3 +126,116 @@ async def test_form_unexpected_error(hass, mock_tv): assert result["type"] == "form" assert result["errors"] == {"base": "unknown"} + + +async def test_pairing(hass, mock_tv_pairable, mock_setup, mock_setup_entry): + """Test we get the form.""" + mock_tv = mock_tv_pairable + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USERINPUT, + ) + + assert result["type"] == "form" + assert result["errors"] == {} + + mock_tv.setTransport.assert_called_with(True) + mock_tv.pairRequest.assert_called() + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"pin": "1234"} + ) + + assert result == { + "flow_id": ANY, + "type": "create_entry", + "description": None, + "description_placeholders": None, + "handler": "philips_js", + "result": ANY, + "title": "55PUS7181/12 (ABCDEFGHIJKLF)", + "data": MOCK_CONFIG_PAIRED, + "version": 1, + } + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_pair_request_failed( + hass, mock_tv_pairable, mock_setup, mock_setup_entry +): + """Test we get the form.""" + mock_tv = mock_tv_pairable + mock_tv.pairRequest.side_effect = PairingFailure({}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USERINPUT, + ) + + assert result == { + "flow_id": ANY, + "description_placeholders": {"error_id": None}, + "handler": "philips_js", + "reason": "pairing_failure", + "type": "abort", + } + + +async def test_pair_grant_failed(hass, mock_tv_pairable, mock_setup, mock_setup_entry): + """Test we get the form.""" + mock_tv = mock_tv_pairable + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USERINPUT, + ) + assert result["type"] == "form" + assert result["errors"] == {} + + mock_tv.setTransport.assert_called_with(True) + mock_tv.pairRequest.assert_called() + + # Test with invalid pin + mock_tv.pairGrant.side_effect = PairingFailure({"error_id": "INVALID_PIN"}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"pin": "1234"} + ) + + assert result["type"] == "form" + assert result["errors"] == {"pin": "invalid_pin"} + + # Test with unexpected failure + mock_tv.pairGrant.side_effect = PairingFailure({}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"pin": "1234"} + ) + + assert result == { + "flow_id": ANY, + "description_placeholders": {"error_id": None}, + "handler": "philips_js", + "reason": "pairing_failure", + "type": "abort", + } diff --git a/tests/components/philips_js/test_device_trigger.py b/tests/components/philips_js/test_device_trigger.py index 43c7c424cf9..ebda40f13e5 100644 --- a/tests/components/philips_js/test_device_trigger.py +++ b/tests/components/philips_js/test_device_trigger.py @@ -33,9 +33,13 @@ async def test_get_triggers(hass, mock_device): assert_lists_same(triggers, expected_triggers) -async def test_if_fires_on_turn_on_request(hass, calls, mock_entity, mock_device): +async def test_if_fires_on_turn_on_request( + hass, calls, mock_tv, mock_entity, mock_device +): """Test for turn_on and turn_off triggers firing.""" + mock_tv.on = False + assert await async_setup_component( hass, automation.DOMAIN, From 6a850a14816005146d140416be580cc8f6d80150 Mon Sep 17 00:00:00 2001 From: CurrentThread <62957822+CurrentThread@users.noreply.github.com> Date: Fri, 26 Feb 2021 11:52:47 +0100 Subject: [PATCH 732/796] Add support for Shelly SHBTN-2 device triggers (#46644) --- homeassistant/components/shelly/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index b4148801b35..0058374cfe7 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -111,7 +111,7 @@ def get_device_channel_name( def is_momentary_input(settings: dict, block: aioshelly.Block) -> bool: """Return true if input button settings is set to a momentary type.""" # Shelly Button type is fixed to momentary and no btn_type - if settings["device"]["type"] == "SHBTN-1": + if settings["device"]["type"] in ("SHBTN-1", "SHBTN-2"): return True button = settings.get("relays") or settings.get("lights") or settings.get("inputs") @@ -158,7 +158,7 @@ def get_input_triggers( else: subtype = f"button{int(block.channel)+1}" - if device.settings["device"]["type"] == "SHBTN-1": + if device.settings["device"]["type"] in ("SHBTN-1", "SHBTN-2"): trigger_types = SHBTN_1_INPUTS_EVENTS_TYPES elif device.settings["device"]["type"] == "SHIX3-1": trigger_types = SHIX3_1_INPUTS_EVENTS_TYPES From c12769213d8e1a0a3bb1c3fec5ba9b6dbaf4dd4d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Feb 2021 10:35:09 -0600 Subject: [PATCH 733/796] Add suggested area to hue (#47056) --- homeassistant/components/hue/const.py | 5 + homeassistant/components/hue/light.py | 151 ++++++++++++++++++++------ tests/components/hue/test_light.py | 99 ++++++++++++----- 3 files changed, 191 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/hue/const.py b/homeassistant/components/hue/const.py index 593f74331ec..b782ce70193 100644 --- a/homeassistant/components/hue/const.py +++ b/homeassistant/components/hue/const.py @@ -15,3 +15,8 @@ CONF_ALLOW_HUE_GROUPS = "allow_hue_groups" DEFAULT_ALLOW_HUE_GROUPS = False DEFAULT_SCENE_TRANSITION = 4 + +GROUP_TYPE_LIGHT_GROUP = "LightGroup" +GROUP_TYPE_ROOM = "Room" +GROUP_TYPE_LUMINAIRE = "Luminaire" +GROUP_TYPE_LIGHT_SOURCE = "LightSource" diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 821d482ec25..6384e47b45e 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -36,7 +36,14 @@ from homeassistant.helpers.update_coordinator import ( ) from homeassistant.util import color -from .const import DOMAIN as HUE_DOMAIN, REQUEST_REFRESH_DELAY +from .const import ( + DOMAIN as HUE_DOMAIN, + GROUP_TYPE_LIGHT_GROUP, + GROUP_TYPE_LIGHT_SOURCE, + GROUP_TYPE_LUMINAIRE, + GROUP_TYPE_ROOM, + REQUEST_REFRESH_DELAY, +) from .helpers import remove_devices SCAN_INTERVAL = timedelta(seconds=5) @@ -74,24 +81,35 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """ -def create_light(item_class, coordinator, bridge, is_group, api, item_id): +def create_light(item_class, coordinator, bridge, is_group, rooms, api, item_id): """Create the light.""" + api_item = api[item_id] + if is_group: supported_features = 0 - for light_id in api[item_id].lights: + for light_id in api_item.lights: if light_id not in bridge.api.lights: continue light = bridge.api.lights[light_id] supported_features |= SUPPORT_HUE.get(light.type, SUPPORT_HUE_EXTENDED) supported_features = supported_features or SUPPORT_HUE_EXTENDED else: - supported_features = SUPPORT_HUE.get(api[item_id].type, SUPPORT_HUE_EXTENDED) - return item_class(coordinator, bridge, is_group, api[item_id], supported_features) + supported_features = SUPPORT_HUE.get(api_item.type, SUPPORT_HUE_EXTENDED) + return item_class( + coordinator, bridge, is_group, api_item, supported_features, rooms + ) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Hue lights from a config entry.""" bridge = hass.data[HUE_DOMAIN][config_entry.entry_id] + api_version = tuple(int(v) for v in bridge.api.config.apiversion.split(".")) + rooms = {} + + allow_groups = bridge.allow_groups + supports_groups = api_version >= GROUP_MIN_API_VERSION + if allow_groups and not supports_groups: + _LOGGER.warning("Please update your Hue bridge to support groups") light_coordinator = DataUpdateCoordinator( hass, @@ -111,27 +129,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if not light_coordinator.last_update_success: raise PlatformNotReady - update_lights = partial( - async_update_items, - bridge, - bridge.api.lights, - {}, - async_add_entities, - partial(create_light, HueLight, light_coordinator, bridge, False), - ) - - # We add a listener after fetching the data, so manually trigger listener - bridge.reset_jobs.append(light_coordinator.async_add_listener(update_lights)) - update_lights() - - api_version = tuple(int(v) for v in bridge.api.config.apiversion.split(".")) - - allow_groups = bridge.allow_groups - if allow_groups and api_version < GROUP_MIN_API_VERSION: - _LOGGER.warning("Please update your Hue bridge to support groups") - allow_groups = False - - if not allow_groups: + if not supports_groups: + update_lights_without_group_support = partial( + async_update_items, + bridge, + bridge.api.lights, + {}, + async_add_entities, + partial(create_light, HueLight, light_coordinator, bridge, False, rooms), + None, + ) + # We add a listener after fetching the data, so manually trigger listener + bridge.reset_jobs.append( + light_coordinator.async_add_listener(update_lights_without_group_support) + ) return group_coordinator = DataUpdateCoordinator( @@ -145,17 +156,69 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ), ) - update_groups = partial( + if allow_groups: + update_groups = partial( + async_update_items, + bridge, + bridge.api.groups, + {}, + async_add_entities, + partial(create_light, HueLight, group_coordinator, bridge, True, None), + None, + ) + + bridge.reset_jobs.append(group_coordinator.async_add_listener(update_groups)) + + cancel_update_rooms_listener = None + + @callback + def _async_update_rooms(): + """Update rooms.""" + nonlocal cancel_update_rooms_listener + rooms.clear() + for item_id in bridge.api.groups: + group = bridge.api.groups[item_id] + if group.type != GROUP_TYPE_ROOM: + continue + for light_id in group.lights: + rooms[light_id] = group.name + + # Once we do a rooms update, we cancel the listener + # until the next time lights are added + bridge.reset_jobs.remove(cancel_update_rooms_listener) + cancel_update_rooms_listener() # pylint: disable=not-callable + cancel_update_rooms_listener = None + + @callback + def _setup_rooms_listener(): + nonlocal cancel_update_rooms_listener + if cancel_update_rooms_listener is not None: + # If there are new lights added before _async_update_rooms + # is called we should not add another listener + return + + cancel_update_rooms_listener = group_coordinator.async_add_listener( + _async_update_rooms + ) + bridge.reset_jobs.append(cancel_update_rooms_listener) + + _setup_rooms_listener() + await group_coordinator.async_refresh() + + update_lights_with_group_support = partial( async_update_items, bridge, - bridge.api.groups, + bridge.api.lights, {}, async_add_entities, - partial(create_light, HueLight, group_coordinator, bridge, True), + partial(create_light, HueLight, light_coordinator, bridge, False, rooms), + _setup_rooms_listener, ) - - bridge.reset_jobs.append(group_coordinator.async_add_listener(update_groups)) - await group_coordinator.async_refresh() + # We add a listener after fetching the data, so manually trigger listener + bridge.reset_jobs.append( + light_coordinator.async_add_listener(update_lights_with_group_support) + ) + update_lights_with_group_support() async def async_safe_fetch(bridge, fetch_method): @@ -171,7 +234,9 @@ async def async_safe_fetch(bridge, fetch_method): @callback -def async_update_items(bridge, api, current, async_add_entities, create_item): +def async_update_items( + bridge, api, current, async_add_entities, create_item, new_items_callback +): """Update items.""" new_items = [] @@ -185,6 +250,9 @@ def async_update_items(bridge, api, current, async_add_entities, create_item): bridge.hass.async_create_task(remove_devices(bridge, api, current)) if new_items: + # This is currently used to setup the listener to update rooms + if new_items_callback: + new_items_callback() async_add_entities(new_items) @@ -201,13 +269,14 @@ def hass_to_hue_brightness(value): class HueLight(CoordinatorEntity, LightEntity): """Representation of a Hue light.""" - def __init__(self, coordinator, bridge, is_group, light, supported_features): + def __init__(self, coordinator, bridge, is_group, light, supported_features, rooms): """Initialize the light.""" super().__init__(coordinator) self.light = light self.bridge = bridge self.is_group = is_group self._supported_features = supported_features + self._rooms = rooms if is_group: self.is_osram = False @@ -355,10 +424,15 @@ class HueLight(CoordinatorEntity, LightEntity): @property def device_info(self): """Return the device info.""" - if self.light.type in ("LightGroup", "Room", "Luminaire", "LightSource"): + if self.light.type in ( + GROUP_TYPE_LIGHT_GROUP, + GROUP_TYPE_ROOM, + GROUP_TYPE_LUMINAIRE, + GROUP_TYPE_LIGHT_SOURCE, + ): return None - return { + info = { "identifiers": {(HUE_DOMAIN, self.device_id)}, "name": self.name, "manufacturer": self.light.manufacturername, @@ -370,6 +444,11 @@ class HueLight(CoordinatorEntity, LightEntity): "via_device": (HUE_DOMAIN, self.bridge.api.config.bridgeid), } + if self.light.id in self._rooms: + info["suggested_area"] = self._rooms[self.light.id] + + return info + async def async_turn_on(self, **kwargs): """Turn the specified or all lights on.""" command = {"on": True} diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index 629a9a4c98b..39b9a5a23fc 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -7,6 +7,12 @@ import aiohue from homeassistant import config_entries from homeassistant.components import hue from homeassistant.components.hue import light as hue_light +from homeassistant.helpers.device_registry import ( + async_get_registry as async_get_device_registry, +) +from homeassistant.helpers.entity_registry import ( + async_get_registry as async_get_entity_registry, +) from homeassistant.util import color HUE_LIGHT_NS = "homeassistant.components.light.hue." @@ -211,8 +217,10 @@ async def test_no_lights_or_groups(hass, mock_bridge): async def test_lights(hass, mock_bridge): """Test the update_lights function with some lights.""" mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 1 + assert len(mock_bridge.mock_requests) == 2 # 2 lights assert len(hass.states.async_all()) == 2 @@ -230,6 +238,8 @@ async def test_lights(hass, mock_bridge): async def test_lights_color_mode(hass, mock_bridge): """Test that lights only report appropriate color mode.""" mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + await setup_bridge(hass, mock_bridge) lamp_1 = hass.states.get("light.hue_lamp_1") @@ -249,8 +259,8 @@ async def test_lights_color_mode(hass, mock_bridge): await hass.services.async_call( "light", "turn_on", {"entity_id": "light.hue_lamp_2"}, blocking=True ) - # 2x light update, 1 turn on request - assert len(mock_bridge.mock_requests) == 3 + # 2x light update, 1 group update, 1 turn on request + assert len(mock_bridge.mock_requests) == 4 lamp_1 = hass.states.get("light.hue_lamp_1") assert lamp_1 is not None @@ -332,9 +342,10 @@ async def test_new_group_discovered(hass, mock_bridge): async def test_new_light_discovered(hass, mock_bridge): """Test if 2nd update has a new light.""" mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + mock_bridge.mock_group_responses.append(GROUP_RESPONSE) await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 1 + assert len(mock_bridge.mock_requests) == 2 assert len(hass.states.async_all()) == 2 new_light_response = dict(LIGHT_RESPONSE) @@ -366,8 +377,8 @@ async def test_new_light_discovered(hass, mock_bridge): await hass.services.async_call( "light", "turn_on", {"entity_id": "light.hue_lamp_1"}, blocking=True ) - # 2x light update, 1 turn on request - assert len(mock_bridge.mock_requests) == 3 + # 2x light update, 1 group update, 1 turn on request + assert len(mock_bridge.mock_requests) == 4 assert len(hass.states.async_all()) == 3 light = hass.states.get("light.hue_lamp_3") @@ -407,9 +418,10 @@ async def test_group_removed(hass, mock_bridge): async def test_light_removed(hass, mock_bridge): """Test if 2nd update has removed light.""" mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + mock_bridge.mock_group_responses.append(GROUP_RESPONSE) await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 1 + assert len(mock_bridge.mock_requests) == 2 assert len(hass.states.async_all()) == 2 mock_bridge.mock_light_responses.clear() @@ -420,8 +432,8 @@ async def test_light_removed(hass, mock_bridge): "light", "turn_on", {"entity_id": "light.hue_lamp_1"}, blocking=True ) - # 2x light update, 1 turn on request - assert len(mock_bridge.mock_requests) == 3 + # 2x light update, 1 group update, 1 turn on request + assert len(mock_bridge.mock_requests) == 4 assert len(hass.states.async_all()) == 1 light = hass.states.get("light.hue_lamp_1") @@ -487,9 +499,10 @@ async def test_other_group_update(hass, mock_bridge): async def test_other_light_update(hass, mock_bridge): """Test changing one light that will impact state of other light.""" mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + mock_bridge.mock_group_responses.append(GROUP_RESPONSE) await setup_bridge(hass, mock_bridge) - assert len(mock_bridge.mock_requests) == 1 + assert len(mock_bridge.mock_requests) == 2 assert len(hass.states.async_all()) == 2 lamp_2 = hass.states.get("light.hue_lamp_2") @@ -526,8 +539,8 @@ async def test_other_light_update(hass, mock_bridge): await hass.services.async_call( "light", "turn_on", {"entity_id": "light.hue_lamp_1"}, blocking=True ) - # 2x light update, 1 turn on request - assert len(mock_bridge.mock_requests) == 3 + # 2x light update, 1 group update, 1 turn on request + assert len(mock_bridge.mock_requests) == 4 assert len(hass.states.async_all()) == 2 lamp_2 = hass.states.get("light.hue_lamp_2") @@ -549,7 +562,6 @@ async def test_update_timeout(hass, mock_bridge): async def test_update_unauthorized(hass, mock_bridge): """Test bridge marked as not authorized if unauthorized during update.""" mock_bridge.api.lights.update = Mock(side_effect=aiohue.Unauthorized) - mock_bridge.api.groups.update = Mock(side_effect=aiohue.Unauthorized) await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 0 assert len(hass.states.async_all()) == 0 @@ -559,6 +571,8 @@ async def test_update_unauthorized(hass, mock_bridge): async def test_light_turn_on_service(hass, mock_bridge): """Test calling the turn on service on a light.""" mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + await setup_bridge(hass, mock_bridge) light = hass.states.get("light.hue_lamp_2") assert light is not None @@ -575,10 +589,10 @@ async def test_light_turn_on_service(hass, mock_bridge): {"entity_id": "light.hue_lamp_2", "brightness": 100, "color_temp": 300}, blocking=True, ) - # 2x light update, 1 turn on request - assert len(mock_bridge.mock_requests) == 3 + # 2x light update, 1 group update, 1 turn on request + assert len(mock_bridge.mock_requests) == 4 - assert mock_bridge.mock_requests[1]["json"] == { + assert mock_bridge.mock_requests[2]["json"] == { "bri": 100, "on": True, "ct": 300, @@ -599,9 +613,9 @@ async def test_light_turn_on_service(hass, mock_bridge): blocking=True, ) - assert len(mock_bridge.mock_requests) == 5 + assert len(mock_bridge.mock_requests) == 6 - assert mock_bridge.mock_requests[3]["json"] == { + assert mock_bridge.mock_requests[4]["json"] == { "on": True, "xy": (0.138, 0.08), "alert": "none", @@ -611,6 +625,8 @@ async def test_light_turn_on_service(hass, mock_bridge): async def test_light_turn_off_service(hass, mock_bridge): """Test calling the turn on service on a light.""" mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + await setup_bridge(hass, mock_bridge) light = hass.states.get("light.hue_lamp_1") assert light is not None @@ -624,10 +640,11 @@ async def test_light_turn_off_service(hass, mock_bridge): await hass.services.async_call( "light", "turn_off", {"entity_id": "light.hue_lamp_1"}, blocking=True ) - # 2x light update, 1 turn on request - assert len(mock_bridge.mock_requests) == 3 - assert mock_bridge.mock_requests[1]["json"] == {"on": False, "alert": "none"} + # 2x light update, 1 for group update, 1 turn on request + assert len(mock_bridge.mock_requests) == 4 + + assert mock_bridge.mock_requests[2]["json"] == {"on": False, "alert": "none"} assert len(hass.states.async_all()) == 2 @@ -649,6 +666,7 @@ def test_available(): bridge=Mock(allow_unreachable=False), is_group=False, supported_features=hue_light.SUPPORT_HUE_EXTENDED, + rooms={}, ) assert light.available is False @@ -664,6 +682,7 @@ def test_available(): bridge=Mock(allow_unreachable=True), is_group=False, supported_features=hue_light.SUPPORT_HUE_EXTENDED, + rooms={}, ) assert light.available is True @@ -679,6 +698,7 @@ def test_available(): bridge=Mock(allow_unreachable=False), is_group=True, supported_features=hue_light.SUPPORT_HUE_EXTENDED, + rooms={}, ) assert light.available is True @@ -697,6 +717,7 @@ def test_hs_color(): bridge=Mock(), is_group=False, supported_features=hue_light.SUPPORT_HUE_EXTENDED, + rooms={}, ) assert light.hs_color is None @@ -712,6 +733,7 @@ def test_hs_color(): bridge=Mock(), is_group=False, supported_features=hue_light.SUPPORT_HUE_EXTENDED, + rooms={}, ) assert light.hs_color is None @@ -727,6 +749,7 @@ def test_hs_color(): bridge=Mock(), is_group=False, supported_features=hue_light.SUPPORT_HUE_EXTENDED, + rooms={}, ) assert light.hs_color == color.color_xy_to_hs(0.4, 0.5, LIGHT_GAMUT) @@ -742,7 +765,7 @@ async def test_group_features(hass, mock_bridge): "1": { "name": "Group 1", "lights": ["1", "2"], - "type": "Room", + "type": "LightGroup", "action": { "on": True, "bri": 254, @@ -757,8 +780,8 @@ async def test_group_features(hass, mock_bridge): "state": {"any_on": True, "all_on": False}, }, "2": { - "name": "Group 2", - "lights": ["3", "4"], + "name": "Living Room", + "lights": ["2", "3"], "type": "Room", "action": { "on": True, @@ -774,8 +797,8 @@ async def test_group_features(hass, mock_bridge): "state": {"any_on": True, "all_on": False}, }, "3": { - "name": "Group 3", - "lights": ["1", "3"], + "name": "Dining Room", + "lights": ["4"], "type": "Room", "action": { "on": True, @@ -900,6 +923,7 @@ async def test_group_features(hass, mock_bridge): mock_bridge.mock_light_responses.append(light_response) mock_bridge.mock_group_responses.append(group_response) await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 2 color_temp_feature = hue_light.SUPPORT_HUE["Color temperature light"] extended_color_feature = hue_light.SUPPORT_HUE["Extended color light"] @@ -907,8 +931,27 @@ async def test_group_features(hass, mock_bridge): group_1 = hass.states.get("light.group_1") assert group_1.attributes["supported_features"] == color_temp_feature - group_2 = hass.states.get("light.group_2") + group_2 = hass.states.get("light.living_room") assert group_2.attributes["supported_features"] == extended_color_feature - group_3 = hass.states.get("light.group_3") + group_3 = hass.states.get("light.dining_room") assert group_3.attributes["supported_features"] == extended_color_feature + + entity_registry = await async_get_entity_registry(hass) + device_registry = await async_get_device_registry(hass) + + entry = entity_registry.async_get("light.hue_lamp_1") + device_entry = device_registry.async_get(entry.device_id) + assert device_entry.suggested_area is None + + entry = entity_registry.async_get("light.hue_lamp_2") + device_entry = device_registry.async_get(entry.device_id) + assert device_entry.suggested_area == "Living Room" + + entry = entity_registry.async_get("light.hue_lamp_3") + device_entry = device_registry.async_get(entry.device_id) + assert device_entry.suggested_area == "Living Room" + + entry = entity_registry.async_get("light.hue_lamp_4") + device_entry = device_registry.async_get(entry.device_id) + assert device_entry.suggested_area == "Dining Room" From 4cd40d0f9f05e8e062c69705ca5352d91df3e9ac Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Fri, 26 Feb 2021 13:57:47 +0100 Subject: [PATCH 734/796] Bump bimmer_connected to 0.7.15 and fix bugs (#47066) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/__init__.py | 2 +- .../components/bmw_connected_drive/device_tracker.py | 4 ++-- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 9d794ace5be..a8bebfbc617 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -122,7 +122,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): def _update_all() -> None: """Update all BMW accounts.""" - for entry in hass.data[DOMAIN][DATA_ENTRIES].values(): + for entry in hass.data[DOMAIN][DATA_ENTRIES].copy().values(): entry[CONF_ACCOUNT].update() # Add update listener for config entry changes (options) diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index 7f069e741b8..25adf6cb09f 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -42,12 +42,12 @@ class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity): @property def latitude(self): """Return latitude value of the device.""" - return self._location[0] + return self._location[0] if self._location else None @property def longitude(self): """Return longitude value of the device.""" - return self._location[1] + return self._location[1] if self._location else None @property def name(self): diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index c1d90f713f4..bbff139187e 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.7.14"], + "requirements": ["bimmer_connected==0.7.15"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index 2cfcc67a25d..1a6ffa47e8a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -343,7 +343,7 @@ beautifulsoup4==4.9.3 bellows==0.21.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.14 +bimmer_connected==0.7.15 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de10ccaf6a9..3235c215b3d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -196,7 +196,7 @@ base36==0.1.1 bellows==0.21.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.14 +bimmer_connected==0.7.15 # homeassistant.components.blebox blebox_uniapi==1.3.2 From 255b6faa7f0d1f450f815bbd3cae488796677f45 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 26 Feb 2021 00:16:11 -0800 Subject: [PATCH 735/796] Upgrade aiohttp to 3.7.4 (#47077) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8f84546371e..10cf300b76b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ PyJWT==1.7.1 PyNaCl==1.3.0 -aiohttp==3.7.3 +aiohttp==3.7.4 aiohttp_cors==0.7.0 astral==1.10.1 async-upnp-client==0.14.13 diff --git a/requirements.txt b/requirements.txt index 14ebf2708ad..0a5b754dbfc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -c homeassistant/package_constraints.txt # Home Assistant Core -aiohttp==3.7.3 +aiohttp==3.7.4 astral==1.10.1 async_timeout==3.0.1 attrs==19.3.0 diff --git a/setup.py b/setup.py index 6dbe35760a6..ce7d6b6883d 100755 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ PROJECT_URLS = { PACKAGES = find_packages(exclude=["tests", "tests.*"]) REQUIRES = [ - "aiohttp==3.7.3", + "aiohttp==3.7.4", "astral==1.10.1", "async_timeout==3.0.1", "attrs==19.3.0", From 96e118ccfef382d38f15b6ecb4e8d02987381ec6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 26 Feb 2021 10:47:22 +0100 Subject: [PATCH 736/796] Bump pychromecast to 8.1.2 (#47085) --- 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 5963e93cf8c..28ccb78d5b9 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==8.1.0"], + "requirements": ["pychromecast==8.1.2"], "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 1a6ffa47e8a..e8f09ad002a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1305,7 +1305,7 @@ pycfdns==1.2.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==8.1.0 +pychromecast==8.1.2 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3235c215b3d..a55805b5365 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -688,7 +688,7 @@ pybotvac==0.0.20 pycfdns==1.2.1 # homeassistant.components.cast -pychromecast==8.1.0 +pychromecast==8.1.2 # homeassistant.components.climacell pyclimacell==0.14.0 From d55f0df09afb2c1227d1e4e6cdf56c459c821f0a Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 26 Feb 2021 20:19:23 +0100 Subject: [PATCH 737/796] Fix Z-Wave JS discovery schema for thermostat devices (#47087) Co-authored-by: Martin Hjelmare --- .../components/zwave_js/discovery.py | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index a40eb10de8b..f5f3d9e5c5b 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -75,6 +75,8 @@ class ZWaveDiscoverySchema: device_class_specific: Optional[Set[Union[str, int]]] = None # [optional] additional values that ALL need to be present on the node for this scheme to pass required_values: Optional[List[ZWaveValueDiscoverySchema]] = None + # [optional] additional values that MAY NOT be present on the node for this scheme to pass + absent_values: Optional[List[ZWaveValueDiscoverySchema]] = None # [optional] bool to specify if this primary value may be discovered by multiple platforms allow_multi: bool = False @@ -186,36 +188,30 @@ DISCOVERY_SCHEMAS = [ ), ), # climate + # thermostats supporting mode (and optional setpoint) ZWaveDiscoverySchema( platform="climate", - device_class_generic={"Thermostat"}, - device_class_specific={ - "Setback Thermostat", - "Thermostat General", - "Thermostat General V2", - "General Thermostat", - "General Thermostat V2", - }, primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.THERMOSTAT_MODE}, property={"mode"}, type={"number"}, ), ), - # climate - # setpoint thermostats + # thermostats supporting setpoint only (and thus not mode) ZWaveDiscoverySchema( platform="climate", - device_class_generic={"Thermostat"}, - device_class_specific={ - "Setpoint Thermostat", - "Unused", - }, primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.THERMOSTAT_SETPOINT}, property={"setpoint"}, type={"number"}, ), + absent_values=[ # mode must not be present to prevent dupes + ZWaveValueDiscoverySchema( + command_class={CommandClass.THERMOSTAT_MODE}, + property={"mode"}, + type={"number"}, + ), + ], ), # binary sensors ZWaveDiscoverySchema( @@ -436,6 +432,13 @@ def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None for val_scheme in schema.required_values ): continue + # check for values that may not be present + if schema.absent_values is not None: + if any( + any(check_value(val, val_scheme) for val in node.values.values()) + for val_scheme in schema.absent_values + ): + continue # all checks passed, this value belongs to an entity yield ZwaveDiscoveryInfo( node=value.node, From 1d1be8ad1a58d8c71064272cbebc0e1e75cb794f Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 26 Feb 2021 20:07:53 +0100 Subject: [PATCH 738/796] Bump aioshelly to 0.6.1 (#47088) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index c38869b3e0d..a757947c5cf 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==0.6.0"], + "requirements": ["aioshelly==0.6.1"], "zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }], "codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"] } diff --git a/requirements_all.txt b/requirements_all.txt index e8f09ad002a..37eecdba573 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -221,7 +221,7 @@ aiopylgtv==0.3.3 aiorecollect==1.0.1 # homeassistant.components.shelly -aioshelly==0.6.0 +aioshelly==0.6.1 # homeassistant.components.switcher_kis aioswitcher==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a55805b5365..91232ad8eaa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aiopylgtv==0.3.3 aiorecollect==1.0.1 # homeassistant.components.shelly -aioshelly==0.6.0 +aioshelly==0.6.1 # homeassistant.components.switcher_kis aioswitcher==1.2.1 From 5e2bafca563ffbe582096e90bdef242bffb5626c Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Fri, 26 Feb 2021 15:49:33 +0100 Subject: [PATCH 739/796] Add new machine generic-x86-64 to build matrix (#47095) The Intel NUC machine runs on most UEFI capable x86-64 machines today. Lets start a new machine generic-x86-64 which will replace intel-nuc over time. --- azure-pipelines-release.yml | 4 +++- machine/generic-x86-64 | 34 ++++++++++++++++++++++++++++++++++ machine/intel-nuc | 3 +++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 machine/generic-x86-64 diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 418fdf5b26c..5fe91325582 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -114,10 +114,12 @@ stages: pool: vmImage: 'ubuntu-latest' strategy: - maxParallel: 15 + maxParallel: 17 matrix: qemux86-64: buildMachine: 'qemux86-64' + generic-x86-64: + buildMachine: 'generic-x86-64' intel-nuc: buildMachine: 'intel-nuc' qemux86: diff --git a/machine/generic-x86-64 b/machine/generic-x86-64 new file mode 100644 index 00000000000..e858c382221 --- /dev/null +++ b/machine/generic-x86-64 @@ -0,0 +1,34 @@ +ARG BUILD_VERSION +FROM agners/amd64-homeassistant:$BUILD_VERSION + +RUN apk --no-cache add \ + libva-intel-driver \ + usbutils + +## +# Build libcec for HDMI-CEC +ARG LIBCEC_VERSION=6.0.2 +RUN apk add --no-cache \ + eudev-libs \ + p8-platform \ + && apk add --no-cache --virtual .build-dependencies \ + build-base \ + cmake \ + eudev-dev \ + swig \ + p8-platform-dev \ + linux-headers \ + && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \ + && cd /usr/src/libcec \ + && mkdir -p /usr/src/libcec/build \ + && cd /usr/src/libcec/build \ + && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \ + -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \ + -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \ + -DHAVE_LINUX_API=1 \ + .. \ + && make -j$(nproc) \ + && make install \ + && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \ + && apk del .build-dependencies \ + && rm -rf /usr/src/libcec* diff --git a/machine/intel-nuc b/machine/intel-nuc index 4c83228387d..b5538b8ccad 100644 --- a/machine/intel-nuc +++ b/machine/intel-nuc @@ -1,6 +1,9 @@ ARG BUILD_VERSION FROM homeassistant/amd64-homeassistant:$BUILD_VERSION +# NOTE: intel-nuc will be replaced by generic-x86-64. Make sure to apply +# changes in generic-x86-64 as well. + RUN apk --no-cache add \ libva-intel-driver \ usbutils From 0969cc985bebed16de5e85c3a371a12fc2648bec Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 26 Feb 2021 11:20:32 -0800 Subject: [PATCH 740/796] Bump google-nest-sdm to v0.2.12 to improve API call error messages (#47108) --- 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 c68dbe6ee2f..734261d9b08 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.2.10"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.2.12"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [{"macaddress":"18B430*"}] diff --git a/requirements_all.txt b/requirements_all.txt index 37eecdba573..d0437c6f9bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -682,7 +682,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.2.10 +google-nest-sdm==0.2.12 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 91232ad8eaa..3eab63288a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -367,7 +367,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.2.10 +google-nest-sdm==0.2.12 # homeassistant.components.gree greeclimate==0.10.3 From cdf7372fd8c8e0d2caa208000ebd7bc3a2450891 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 26 Feb 2021 19:21:15 +0000 Subject: [PATCH 741/796] Bumped version to 2021.3.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index aca184b4606..57308e6eed0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 3 -PATCH_VERSION = "0b2" +PATCH_VERSION = "0b3" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From 807bf15ff35d19599145991b22fe4eb3bc428f4c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 26 Feb 2021 13:28:52 -0800 Subject: [PATCH 742/796] Use async_capture_events to avoid running in executor (#47111) --- tests/components/alexa/test_smart_home.py | 10 ++----- tests/components/automation/test_init.py | 12 ++++---- tests/components/demo/test_notify.py | 6 ++-- .../google_assistant/test_smart_home.py | 29 +++++++++---------- .../components/google_assistant/test_trait.py | 5 ++-- tests/components/homeassistant/test_scene.py | 7 ++--- tests/components/homekit/conftest.py | 9 ++---- tests/components/shelly/conftest.py | 12 ++++---- 8 files changed, 39 insertions(+), 51 deletions(-) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 657bc407fb0..c018e07c264 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -26,7 +26,7 @@ from homeassistant.components.media_player.const import ( import homeassistant.components.vacuum as vacuum from homeassistant.config import async_process_ha_core_config from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.core import Context, callback +from homeassistant.core import Context from homeassistant.helpers import entityfilter from homeassistant.setup import async_setup_component @@ -42,17 +42,13 @@ from . import ( reported_properties, ) -from tests.common import async_mock_service +from tests.common import async_capture_events, async_mock_service @pytest.fixture def events(hass): """Fixture that catches alexa events.""" - events = [] - hass.bus.async_listen( - smart_home.EVENT_ALEXA_SMART_HOME, callback(lambda e: events.append(e)) - ) - yield events + return async_capture_events(hass, smart_home.EVENT_ALEXA_SMART_HOME) @pytest.fixture diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 3e498b52a08..91531481a99 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -30,7 +30,12 @@ from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import assert_setup_component, async_mock_service, mock_restore_cache +from tests.common import ( + assert_setup_component, + async_capture_events, + async_mock_service, + mock_restore_cache, +) from tests.components.logbook.test_init import MockLazyEventPartialState @@ -496,10 +501,7 @@ async def test_reload_config_service(hass, calls, hass_admin_user, hass_read_onl assert len(calls) == 1 assert calls[0].data.get("event") == "test_event" - test_reload_event = [] - hass.bus.async_listen( - EVENT_AUTOMATION_RELOADED, lambda event: test_reload_event.append(event) - ) + test_reload_event = async_capture_events(hass, EVENT_AUTOMATION_RELOADED) with patch( "homeassistant.config.load_yaml_config_file", diff --git a/tests/components/demo/test_notify.py b/tests/components/demo/test_notify.py index 7c7f83312dd..153f065235c 100644 --- a/tests/components/demo/test_notify.py +++ b/tests/components/demo/test_notify.py @@ -12,7 +12,7 @@ from homeassistant.core import callback from homeassistant.helpers import discovery from homeassistant.setup import async_setup_component -from tests.common import assert_setup_component +from tests.common import assert_setup_component, async_capture_events CONFIG = {notify.DOMAIN: {"platform": "demo"}} @@ -20,9 +20,7 @@ CONFIG = {notify.DOMAIN: {"platform": "demo"}} @pytest.fixture def events(hass): """Fixture that catches notify events.""" - events = [] - hass.bus.async_listen(demo.EVENT_NOTIFY, callback(lambda e: events.append(e))) - yield events + return async_capture_events(hass, demo.EVENT_NOTIFY) @pytest.fixture diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 9c8f9a48338..9531602ef0c 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -30,7 +30,12 @@ from homeassistant.setup import async_setup_component from . import BASIC_CONFIG, MockConfig -from tests.common import mock_area_registry, mock_device_registry, mock_registry +from tests.common import ( + async_capture_events, + mock_area_registry, + mock_device_registry, + mock_registry, +) REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf" @@ -77,8 +82,7 @@ async def test_sync_message(hass): }, ) - events = [] - hass.bus.async_listen(EVENT_SYNC_RECEIVED, events.append) + events = async_capture_events(hass, EVENT_SYNC_RECEIVED) result = await sh.async_handle_message( hass, @@ -192,8 +196,7 @@ async def test_sync_in_area(area_on_device, hass, registries): config = MockConfig(should_expose=lambda _: True, entity_config={}) - events = [] - hass.bus.async_listen(EVENT_SYNC_RECEIVED, events.append) + events = async_capture_events(hass, EVENT_SYNC_RECEIVED) result = await sh.async_handle_message( hass, @@ -295,8 +298,7 @@ async def test_query_message(hass): light3.entity_id = "light.color_temp_light" await light3.async_update_ha_state() - events = [] - hass.bus.async_listen(EVENT_QUERY_RECEIVED, events.append) + events = async_capture_events(hass, EVENT_QUERY_RECEIVED) result = await sh.async_handle_message( hass, @@ -387,11 +389,8 @@ async def test_execute(hass): "light", "turn_off", {"entity_id": "light.ceiling_lights"}, blocking=True ) - events = [] - hass.bus.async_listen(EVENT_COMMAND_RECEIVED, events.append) - - service_events = [] - hass.bus.async_listen(EVENT_CALL_SERVICE, service_events.append) + events = async_capture_events(hass, EVENT_COMMAND_RECEIVED) + service_events = async_capture_events(hass, EVENT_CALL_SERVICE) result = await sh.async_handle_message( hass, @@ -570,8 +569,7 @@ async def test_raising_error_trait(hass): {ATTR_MIN_TEMP: 15, ATTR_MAX_TEMP: 30, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) - events = [] - hass.bus.async_listen(EVENT_COMMAND_RECEIVED, events.append) + events = async_capture_events(hass, EVENT_COMMAND_RECEIVED) await hass.async_block_till_done() result = await sh.async_handle_message( @@ -660,8 +658,7 @@ async def test_unavailable_state_does_sync(hass): light._available = False # pylint: disable=protected-access await light.async_update_ha_state() - events = [] - hass.bus.async_listen(EVENT_SYNC_RECEIVED, events.append) + events = async_capture_events(hass, EVENT_SYNC_RECEIVED) result = await sh.async_handle_message( hass, diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index a9b1e9a97fb..ba189020513 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -54,7 +54,7 @@ from homeassistant.util import color from . import BASIC_CONFIG, MockConfig -from tests.common import async_mock_service +from tests.common import async_capture_events, async_mock_service REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf" @@ -84,8 +84,7 @@ async def test_brightness_light(hass): assert trt.query_attributes() == {"brightness": 95} - events = [] - hass.bus.async_listen(EVENT_CALL_SERVICE, events.append) + events = async_capture_events(hass, EVENT_CALL_SERVICE) calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) await trt.execute( diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index 30985432718..610bc371b25 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -8,17 +8,14 @@ from homeassistant.components.homeassistant import scene as ha_scene from homeassistant.components.homeassistant.scene import EVENT_SCENE_RELOADED from homeassistant.setup import async_setup_component -from tests.common import async_mock_service +from tests.common import async_capture_events, async_mock_service async def test_reload_config_service(hass): """Test the reload config service.""" assert await async_setup_component(hass, "scene", {}) - test_reloaded_event = [] - hass.bus.async_listen( - EVENT_SCENE_RELOADED, lambda event: test_reloaded_event.append(event) - ) + test_reloaded_event = async_capture_events(hass, EVENT_SCENE_RELOADED) with patch( "homeassistant.config.load_yaml_config_file", diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index ac51c4e6368..228b5f07837 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -5,7 +5,8 @@ from pyhap.accessory_driver import AccessoryDriver import pytest from homeassistant.components.homekit.const import EVENT_HOMEKIT_CHANGED -from homeassistant.core import callback as ha_callback + +from tests.common import async_capture_events @pytest.fixture @@ -24,8 +25,4 @@ def hk_driver(loop): @pytest.fixture def events(hass): """Yield caught homekit_changed events.""" - events = [] - hass.bus.async_listen( - EVENT_HOMEKIT_CHANGED, ha_callback(lambda e: events.append(e)) - ) - yield events + return async_capture_events(hass, EVENT_HOMEKIT_CHANGED) diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 7e7bd068842..51659cf7736 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -10,10 +10,14 @@ from homeassistant.components.shelly.const import ( DOMAIN, EVENT_SHELLY_CLICK, ) -from homeassistant.core import callback as ha_callback from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, async_mock_service, mock_device_registry +from tests.common import ( + MockConfigEntry, + async_capture_events, + async_mock_service, + mock_device_registry, +) MOCK_SETTINGS = { "name": "Test name", @@ -81,9 +85,7 @@ def calls(hass): @pytest.fixture def events(hass): """Yield caught shelly_click events.""" - ha_events = [] - hass.bus.async_listen(EVENT_SHELLY_CLICK, ha_callback(ha_events.append)) - yield ha_events + return async_capture_events(hass, EVENT_SHELLY_CLICK) @pytest.fixture From 505ca07c4e2c63f15792918e9b2d9ac7a6f9768a Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 27 Feb 2021 00:28:16 +0200 Subject: [PATCH 743/796] Fix Shelly RGBW (#47116) --- homeassistant/components/shelly/const.py | 4 +- homeassistant/components/shelly/light.py | 47 ++++++++++-------------- 2 files changed, 22 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 9d1c333b201..4fda656e7b4 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -74,5 +74,5 @@ INPUTS_EVENTS_SUBTYPES = { # Kelvin value for colorTemp KELVIN_MAX_VALUE = 6500 -KELVIN_MIN_VALUE = 2700 -KELVIN_MIN_VALUE_SHBLB_1 = 3000 +KELVIN_MIN_VALUE_WHITE = 2700 +KELVIN_MIN_VALUE_COLOR = 3000 diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 848ef990340..0379bfec1cf 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -28,20 +28,13 @@ from .const import ( DATA_CONFIG_ENTRY, DOMAIN, KELVIN_MAX_VALUE, - KELVIN_MIN_VALUE, - KELVIN_MIN_VALUE_SHBLB_1, + KELVIN_MIN_VALUE_COLOR, + KELVIN_MIN_VALUE_WHITE, ) from .entity import ShellyBlockEntity from .utils import async_remove_shelly_entity -def min_kelvin(model: str): - """Kelvin (min) for colorTemp.""" - if model in ["SHBLB-1"]: - return KELVIN_MIN_VALUE_SHBLB_1 - return KELVIN_MIN_VALUE - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up lights for device.""" wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][COAP] @@ -76,6 +69,8 @@ class ShellyLight(ShellyBlockEntity, LightEntity): self.control_result = None self.mode_result = None self._supported_features = 0 + self._min_kelvin = KELVIN_MIN_VALUE_WHITE + self._max_kelvin = KELVIN_MAX_VALUE if hasattr(block, "brightness") or hasattr(block, "gain"): self._supported_features |= SUPPORT_BRIGHTNESS @@ -85,6 +80,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity): self._supported_features |= SUPPORT_WHITE_VALUE if hasattr(block, "red") and hasattr(block, "green") and hasattr(block, "blue"): self._supported_features |= SUPPORT_COLOR + self._min_kelvin = KELVIN_MIN_VALUE_COLOR @property def supported_features(self) -> int: @@ -168,22 +164,19 @@ class ShellyLight(ShellyBlockEntity, LightEntity): else: color_temp = self.block.colorTemp - # If you set DUO to max mireds in Shelly app, 2700K, - # It reports 0 temp - if color_temp == 0: - return min_kelvin(self.wrapper.model) + color_temp = min(self._max_kelvin, max(self._min_kelvin, color_temp)) return int(color_temperature_kelvin_to_mired(color_temp)) @property def min_mireds(self) -> int: """Return the coldest color_temp that this light supports.""" - return int(color_temperature_kelvin_to_mired(KELVIN_MAX_VALUE)) + return int(color_temperature_kelvin_to_mired(self._max_kelvin)) @property def max_mireds(self) -> int: """Return the warmest color_temp that this light supports.""" - return int(color_temperature_kelvin_to_mired(min_kelvin(self.wrapper.model))) + return int(color_temperature_kelvin_to_mired(self._min_kelvin)) async def async_turn_on(self, **kwargs) -> None: """Turn on light.""" @@ -192,6 +185,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity): self.async_write_ha_state() return + set_mode = None params = {"turn": "on"} if ATTR_BRIGHTNESS in kwargs: tmp_brightness = int(kwargs[ATTR_BRIGHTNESS] / 255 * 100) @@ -201,27 +195,26 @@ class ShellyLight(ShellyBlockEntity, LightEntity): params["brightness"] = tmp_brightness if ATTR_COLOR_TEMP in kwargs: color_temp = color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) - color_temp = min( - KELVIN_MAX_VALUE, max(min_kelvin(self.wrapper.model), color_temp) - ) + color_temp = min(self._max_kelvin, max(self._min_kelvin, color_temp)) # Color temperature change - used only in white mode, switch device mode to white - if self.mode == "color": - self.mode_result = await self.wrapper.device.switch_light_mode("white") - params["red"] = params["green"] = params["blue"] = 255 + set_mode = "white" + params["red"] = params["green"] = params["blue"] = 255 params["temp"] = int(color_temp) - elif ATTR_HS_COLOR in kwargs: + if ATTR_HS_COLOR in kwargs: red, green, blue = color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) # Color channels change - used only in color mode, switch device mode to color - if self.mode == "white": - self.mode_result = await self.wrapper.device.switch_light_mode("color") + set_mode = "color" params["red"] = red params["green"] = green params["blue"] = blue - elif ATTR_WHITE_VALUE in kwargs: + if ATTR_WHITE_VALUE in kwargs: # White channel change - used only in color mode, switch device mode device to color - if self.mode == "white": - self.mode_result = await self.wrapper.device.switch_light_mode("color") + set_mode = "color" params["white"] = int(kwargs[ATTR_WHITE_VALUE]) + + if set_mode and self.mode != set_mode: + self.mode_result = await self.wrapper.device.switch_light_mode(set_mode) + self.control_result = await self.block.set_state(**params) self.async_write_ha_state() From dd4f8bf4b4d569b6d4ac3f49a7519826cfb6e69d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Feb 2021 18:33:31 -0600 Subject: [PATCH 744/796] Handle lutron_caseta fan speed being none (#47120) --- homeassistant/components/lutron_caseta/fan.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py index 330ff81d1d2..57b87b18320 100644 --- a/homeassistant/components/lutron_caseta/fan.py +++ b/homeassistant/components/lutron_caseta/fan.py @@ -44,6 +44,8 @@ class LutronCasetaFan(LutronCasetaDevice, FanEntity): @property def percentage(self) -> str: """Return the current speed percentage.""" + if self._device["fan_speed"] is None: + return None return ordered_list_item_to_percentage( ORDERED_NAMED_FAN_SPEEDS, self._device["fan_speed"] ) From 2b0f6716b32a5f2d8d737f36a0fee2477b50f655 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Feb 2021 18:33:13 -0600 Subject: [PATCH 745/796] Provide a human readable exception for the percentage util (#47121) --- homeassistant/util/percentage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/util/percentage.py b/homeassistant/util/percentage.py index 10a72a85dff..949af7dbb32 100644 --- a/homeassistant/util/percentage.py +++ b/homeassistant/util/percentage.py @@ -19,7 +19,7 @@ def ordered_list_item_to_percentage(ordered_list: List[str], item: str) -> int: """ if item not in ordered_list: - raise ValueError + raise ValueError(f'The item "{item}"" is not in "{ordered_list}"') list_len = len(ordered_list) list_position = ordered_list.index(item) + 1 @@ -42,7 +42,7 @@ def percentage_to_ordered_list_item(ordered_list: List[str], percentage: int) -> """ list_len = len(ordered_list) if not list_len: - raise ValueError + raise ValueError("The ordered list is empty") for offset, speed in enumerate(ordered_list): list_position = offset + 1 From e65b2231ba4e2f6adf02d7118e56941b709bd917 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 27 Feb 2021 01:32:51 +0100 Subject: [PATCH 746/796] Update frontend to 20210226.0 (#47123) --- 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 e8e9c44ae78..01f1c72f8d6 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210225.0" + "home-assistant-frontend==20210226.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 10cf300b76b..9506171303b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.41.0 -home-assistant-frontend==20210225.0 +home-assistant-frontend==20210226.0 httpx==0.16.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index d0437c6f9bc..e6fb8b88e4e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -763,7 +763,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210225.0 +home-assistant-frontend==20210226.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3eab63288a3..e3fc888d43f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -412,7 +412,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210225.0 +home-assistant-frontend==20210226.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From d9d979d50e948c38f4a4f0289563e1e9fd75f70f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 28 Feb 2021 05:41:06 -0800 Subject: [PATCH 747/796] Fix the updater schema (#47128) --- homeassistant/components/updater/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 9d65bb4c5d4..81910db38d6 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -27,7 +27,7 @@ UPDATER_URL = "https://updater.home-assistant.io/" CONFIG_SCHEMA = vol.Schema( { - DOMAIN: { + vol.Optional(DOMAIN, default={}): { vol.Optional(CONF_REPORTING, default=True): cv.boolean, vol.Optional(CONF_COMPONENT_REPORTING, default=False): cv.boolean, } @@ -56,13 +56,13 @@ async def async_setup(hass, config): # This component only makes sense in release versions _LOGGER.info("Running on 'dev', only analytics will be submitted") - conf = config.get(DOMAIN, {}) - if conf.get(CONF_REPORTING): + conf = config[DOMAIN] + if conf[CONF_REPORTING]: huuid = await hass.helpers.instance_id.async_get() else: huuid = None - include_components = conf.get(CONF_COMPONENT_REPORTING) + include_components = conf[CONF_COMPONENT_REPORTING] async def check_new_version() -> Updater: """Check if a new version is available and report if one is.""" From 104d5c510fefe5e64c4c704eab7f1fead8c33047 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 28 Feb 2021 21:19:27 +0100 Subject: [PATCH 748/796] Fix MQTT trigger where wanted payload may be parsed as an integer (#47162) --- .../components/mqtt/device_trigger.py | 12 ++- homeassistant/components/mqtt/trigger.py | 6 +- tests/components/mqtt/test_device_trigger.py | 75 +++++++++++++++++++ tests/components/mqtt/test_trigger.py | 25 +++++++ 4 files changed, 114 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 8969072553c..d6e2ee0fc65 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE, + CONF_VALUE_TEMPLATE, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -66,10 +67,11 @@ TRIGGER_DISCOVERY_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_AUTOMATION_TYPE): str, vol.Required(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, - vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_PAYLOAD, default=None): vol.Any(None, cv.string), - vol.Required(CONF_TYPE): cv.string, vol.Required(CONF_SUBTYPE): cv.string, + vol.Required(CONF_TOPIC): cv.string, + vol.Required(CONF_TYPE): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE, default=None): vol.Any(None, cv.string), }, validate_device_has_at_least_one_identifier, ) @@ -96,6 +98,8 @@ class TriggerInstance: } if self.trigger.payload: mqtt_config[CONF_PAYLOAD] = self.trigger.payload + if self.trigger.value_template: + mqtt_config[CONF_VALUE_TEMPLATE] = self.trigger.value_template mqtt_config = mqtt_trigger.TRIGGER_SCHEMA(mqtt_config) if self.remove: @@ -121,6 +125,7 @@ class Trigger: subtype: str = attr.ib() topic: str = attr.ib() type: str = attr.ib() + value_template: str = attr.ib() trigger_instances: List[TriggerInstance] = attr.ib(factory=list) async def add_trigger(self, action, automation_info): @@ -153,6 +158,7 @@ class Trigger: self.qos = config[CONF_QOS] topic_changed = self.topic != config[CONF_TOPIC] self.topic = config[CONF_TOPIC] + self.value_template = config[CONF_VALUE_TEMPLATE] # Unsubscribe+subscribe if this trigger is in use and topic has changed # If topic is same unsubscribe+subscribe will execute in the wrong order @@ -245,6 +251,7 @@ async def async_setup_trigger(hass, config, config_entry, discovery_data): payload=config[CONF_PAYLOAD], qos=config[CONF_QOS], remove_signal=remove_signal, + value_template=config[CONF_VALUE_TEMPLATE], ) else: await hass.data[DEVICE_TRIGGERS][discovery_id].update_trigger( @@ -325,6 +332,7 @@ async def async_attach_trigger( topic=None, payload=None, qos=None, + value_template=None, ) return await hass.data[DEVICE_TRIGGERS][discovery_id].add_trigger( action, automation_info diff --git a/homeassistant/components/mqtt/trigger.py b/homeassistant/components/mqtt/trigger.py index 459adabd418..82f7885b85d 100644 --- a/homeassistant/components/mqtt/trigger.py +++ b/homeassistant/components/mqtt/trigger.py @@ -48,11 +48,13 @@ async def async_attach_trigger(hass, config, action, automation_info): template.attach(hass, wanted_payload) if wanted_payload: - wanted_payload = wanted_payload.async_render(variables, limited=True) + wanted_payload = wanted_payload.async_render( + variables, limited=True, parse_result=False + ) template.attach(hass, topic) if isinstance(topic, template.Template): - topic = topic.async_render(variables, limited=True) + topic = topic.async_render(variables, limited=True, parse_result=False) topic = mqtt.util.valid_subscribe_topic(topic) template.attach(hass, value_template) diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index f200de6a274..210dac19e0c 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -290,6 +290,81 @@ async def test_if_fires_on_mqtt_message(hass, device_reg, calls, mqtt_mock): assert calls[1].data["some"] == "long_press" +async def test_if_fires_on_mqtt_message_template(hass, device_reg, calls, mqtt_mock): + """Test triggers firing.""" + data1 = ( + '{ "automation_type":"trigger",' + ' "device":{"identifiers":["0AFFD2"]},' + " \"payload\": \"{{ 'foo_press'|regex_replace('foo', 'short') }}\"," + ' "topic": "foobar/triggers/button{{ sqrt(16)|round }}",' + ' "type": "button_short_press",' + ' "subtype": "button_1",' + ' "value_template": "{{ value_json.button }}"}' + ) + data2 = ( + '{ "automation_type":"trigger",' + ' "device":{"identifiers":["0AFFD2"]},' + " \"payload\": \"{{ 'foo_press'|regex_replace('foo', 'long') }}\"," + ' "topic": "foobar/triggers/button{{ sqrt(16)|round }}",' + ' "type": "button_long_press",' + ' "subtype": "button_2",' + ' "value_template": "{{ value_json.button }}"}' + ) + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla2/config", data2) + await hass.async_block_till_done() + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device_entry.id, + "discovery_id": "bla1", + "type": "button_short_press", + "subtype": "button_1", + }, + "action": { + "service": "test.automation", + "data_template": {"some": ("short_press")}, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device_entry.id, + "discovery_id": "bla2", + "type": "button_1", + "subtype": "button_long_press", + }, + "action": { + "service": "test.automation", + "data_template": {"some": ("long_press")}, + }, + }, + ] + }, + ) + + # Fake short press. + async_fire_mqtt_message(hass, "foobar/triggers/button4", '{"button":"short_press"}') + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "short_press" + + # Fake long press. + async_fire_mqtt_message(hass, "foobar/triggers/button4", '{"button":"long_press"}') + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "long_press" + + async def test_if_fires_on_mqtt_message_late_discover( hass, device_reg, calls, mqtt_mock ): diff --git a/tests/components/mqtt/test_trigger.py b/tests/components/mqtt/test_trigger.py index 23078b9ba23..d0a86e08655 100644 --- a/tests/components/mqtt/test_trigger.py +++ b/tests/components/mqtt/test_trigger.py @@ -81,6 +81,31 @@ async def test_if_fires_on_topic_and_payload_match(hass, calls): assert len(calls) == 1 +async def test_if_fires_on_topic_and_payload_match2(hass, calls): + """Test if message is fired on topic and payload match. + + Make sure a payload which would render as a non string can still be matched. + """ + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "mqtt", + "topic": "test-topic", + "payload": "0", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + async_fire_mqtt_message(hass, "test-topic", "0") + await hass.async_block_till_done() + assert len(calls) == 1 + + async def test_if_fires_on_templated_topic_and_payload_match(hass, calls): """Test if message is fired on templated topic and payload match.""" assert await async_setup_component( From 552da0327eaa32917d26f75940bad8cd36f80246 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Sun, 28 Feb 2021 14:33:48 +0100 Subject: [PATCH 749/796] Bump builder to get generic-x86-64 nightly builds (#47164) --- azure-pipelines-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 5fe91325582..74aa05e58f3 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -14,7 +14,7 @@ schedules: always: true variables: - name: versionBuilder - value: '2020.11.0' + value: '2021.02.0' - group: docker - group: github - group: twine From db098d90ddc87ec535c54953a55d9077ce0c598e Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 28 Feb 2021 10:55:14 -0500 Subject: [PATCH 750/796] Bump ZHA quirks to 0.0.54 (#47172) --- 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 ad2bf5f17c5..d7bb0dbe5bc 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.53", + "zha-quirks==0.0.54", "zigpy-cc==0.5.2", "zigpy-deconz==0.11.1", "zigpy==0.32.0", diff --git a/requirements_all.txt b/requirements_all.txt index e6fb8b88e4e..6dd457f792f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2367,7 +2367,7 @@ zengge==0.2 zeroconf==0.28.8 # homeassistant.components.zha -zha-quirks==0.0.53 +zha-quirks==0.0.54 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3fc888d43f..1ae58db2865 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1213,7 +1213,7 @@ zeep[async]==4.0.0 zeroconf==0.28.8 # homeassistant.components.zha -zha-quirks==0.0.53 +zha-quirks==0.0.54 # homeassistant.components.zha zigpy-cc==0.5.2 From e93868f85b016ecadb452a3c02cb22917d80c8c5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Feb 2021 12:27:36 -0600 Subject: [PATCH 751/796] Update HAP-python to 3.3.1 (#47180) Fixes disconnect when setting a single char fails https://github.com/ikalchev/HAP-python/compare/v3.3.0...v3.3.1 --- 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 acc61408a48..4d1598c728c 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.3.0", + "HAP-python==3.3.1", "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1", diff --git a/requirements_all.txt b/requirements_all.txt index 6dd457f792f..010cb3d49e1 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.3.0 +HAP-python==3.3.1 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ae58db2865..5e8df0c89fb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.1.8 # homeassistant.components.homekit -HAP-python==3.3.0 +HAP-python==3.3.1 # homeassistant.components.flick_electric PyFlick==0.0.2 From 6887474ddc4f7525d446b58cd04e79b1654f6102 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 28 Feb 2021 20:22:46 +0000 Subject: [PATCH 752/796] Bumped version to 2021.3.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 57308e6eed0..c6cccb49d43 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 3 -PATCH_VERSION = "0b3" +PATCH_VERSION = "0b4" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From 8513250628a61b387304c7682d664200fff757bf Mon Sep 17 00:00:00 2001 From: AJ Schmidt Date: Sun, 28 Feb 2021 18:21:04 -0500 Subject: [PATCH 753/796] Update AlarmDecoder dependency (#46841) --- homeassistant/components/alarmdecoder/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alarmdecoder/manifest.json b/homeassistant/components/alarmdecoder/manifest.json index 1697858718d..c3e72e407c2 100644 --- a/homeassistant/components/alarmdecoder/manifest.json +++ b/homeassistant/components/alarmdecoder/manifest.json @@ -2,7 +2,7 @@ "domain": "alarmdecoder", "name": "AlarmDecoder", "documentation": "https://www.home-assistant.io/integrations/alarmdecoder", - "requirements": ["adext==0.3"], + "requirements": ["adext==0.4.1"], "codeowners": ["@ajschmidt8"], "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index 010cb3d49e1..05bc4f01b79 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -108,7 +108,7 @@ adafruit-circuitpython-mcp230xx==2.2.2 adb-shell[async]==0.2.1 # homeassistant.components.alarmdecoder -adext==0.3 +adext==0.4.1 # homeassistant.components.adguard adguardhome==0.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e8df0c89fb..78cb4d8f8ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -48,7 +48,7 @@ accuweather==0.1.0 adb-shell[async]==0.2.1 # homeassistant.components.alarmdecoder -adext==0.3 +adext==0.4.1 # homeassistant.components.adguard adguardhome==0.4.2 From aa9b4458568e93be1929bc5aa9bfef20b2ca1ab0 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 28 Feb 2021 21:25:40 +0100 Subject: [PATCH 754/796] Fix Xiaomi Miio discovery (#47134) --- .../components/xiaomi_miio/config_flow.py | 17 +++++++---- .../components/xiaomi_miio/strings.json | 28 +++++++++---------- .../xiaomi_miio/test_config_flow.py | 14 ++++------ 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index d7e2198f72f..2e069b30da3 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -1,5 +1,6 @@ """Config flow to configure Xiaomi Miio.""" import logging +from re import search import voluptuous as vol @@ -24,7 +25,6 @@ from .device import ConnectXiaomiDevice _LOGGER = logging.getLogger(__name__) DEFAULT_GATEWAY_NAME = "Xiaomi Gateway" -DEFAULT_DEVICE_NAME = "Xiaomi Device" DEVICE_SETTINGS = { vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)), @@ -57,14 +57,21 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): name = discovery_info.get("name") self.host = discovery_info.get("host") self.mac = discovery_info.get("properties", {}).get("mac") + if self.mac is None: + poch = discovery_info.get("properties", {}).get("poch", "") + result = search(r"mac=\w+", poch) + if result is not None: + self.mac = result.group(0).split("=")[1] if not name or not self.host or not self.mac: return self.async_abort(reason="not_xiaomi_miio") + self.mac = format_mac(self.mac) + # Check which device is discovered. for gateway_model in MODELS_GATEWAY: if name.startswith(gateway_model.replace(".", "-")): - unique_id = format_mac(self.mac) + unique_id = self.mac await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured({CONF_HOST: self.host}) @@ -75,12 +82,12 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_device() for device_model in MODELS_ALL_DEVICES: if name.startswith(device_model.replace(".", "-")): - unique_id = format_mac(self.mac) + unique_id = self.mac await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured({CONF_HOST: self.host}) self.context.update( - {"title_placeholders": {"name": f"Miio Device {self.host}"}} + {"title_placeholders": {"name": f"{device_model} {self.host}"}} ) return await self.async_step_device() @@ -132,7 +139,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) # Setup all other Miio Devices - name = user_input.get(CONF_NAME, DEFAULT_DEVICE_NAME) + name = user_input.get(CONF_NAME, model) for device_model in MODELS_ALL_DEVICES: if model.startswith(device_model): diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json index 90710baebca..e3d9376bc31 100644 --- a/homeassistant/components/xiaomi_miio/strings.json +++ b/homeassistant/components/xiaomi_miio/strings.json @@ -1,24 +1,24 @@ { "config": { - "flow_title": "Xiaomi Miio: {name}", - "step": { - "device": { - "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway", - "description": "You will need the 32 character [%key:common::config_flow::data::api_token%], see https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instructions. Please note, that this [%key:common::config_flow::data::api_token%] is different from the key used by the Xiaomi Aqara integration.", - "data": { - "host": "[%key:common::config_flow::data::ip%]", - "token": "[%key:common::config_flow::data::api_token%]", - "model": "Device model (Optional)" - } - } + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown_device": "The device model is not known, not able to setup the device using config flow." }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" + "flow_title": "Xiaomi Miio: {name}", + "step": { + "device": { + "data": { + "host": "[%key:common::config_flow::data::ip%]", + "model": "Device model (Optional)", + "token": "[%key:common::config_flow::data::api_token%]" + }, + "description": "You will need the 32 character [%key:common::config_flow::data::api_token%], see https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instructions. Please note, that this [%key:common::config_flow::data::api_token%] is different from the key used by the Xiaomi Aqara integration.", + "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + } } } } diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index f4f7b5e2b46..f53fe6e40b4 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -6,10 +6,7 @@ from miio import DeviceException from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.xiaomi_miio import const -from homeassistant.components.xiaomi_miio.config_flow import ( - DEFAULT_DEVICE_NAME, - DEFAULT_GATEWAY_NAME, -) +from homeassistant.components.xiaomi_miio.config_flow import DEFAULT_GATEWAY_NAME from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN ZEROCONF_NAME = "name" @@ -21,6 +18,7 @@ TEST_TOKEN = "12345678901234567890123456789012" TEST_NAME = "Test_Gateway" TEST_MODEL = const.MODELS_GATEWAY[0] TEST_MAC = "ab:cd:ef:gh:ij:kl" +TEST_MAC_DEVICE = "abcdefghijkl" TEST_GATEWAY_ID = TEST_MAC TEST_HARDWARE_VERSION = "AB123" TEST_FIRMWARE_VERSION = "1.2.3_456" @@ -294,7 +292,7 @@ async def test_config_flow_step_device_manual_model_succes(hass): ) assert result["type"] == "create_entry" - assert result["title"] == DEFAULT_DEVICE_NAME + assert result["title"] == overwrite_model assert result["data"] == { const.CONF_FLOW_TYPE: const.CONF_DEVICE, CONF_HOST: TEST_HOST, @@ -328,7 +326,7 @@ async def config_flow_device_success(hass, model_to_test): ) assert result["type"] == "create_entry" - assert result["title"] == DEFAULT_DEVICE_NAME + assert result["title"] == model_to_test assert result["data"] == { const.CONF_FLOW_TYPE: const.CONF_DEVICE, CONF_HOST: TEST_HOST, @@ -346,7 +344,7 @@ async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test): data={ zeroconf.ATTR_HOST: TEST_HOST, ZEROCONF_NAME: zeroconf_name_to_test, - ZEROCONF_PROP: {ZEROCONF_MAC: TEST_MAC}, + ZEROCONF_PROP: {"poch": f"0:mac={TEST_MAC_DEVICE}\x00"}, }, ) @@ -368,7 +366,7 @@ async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test): ) assert result["type"] == "create_entry" - assert result["title"] == DEFAULT_DEVICE_NAME + assert result["title"] == model_to_test assert result["data"] == { const.CONF_FLOW_TYPE: const.CONF_DEVICE, CONF_HOST: TEST_HOST, From 0e951f288b2da43bc0398dd94d2eea8b9552cb26 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 28 Feb 2021 21:41:09 -0700 Subject: [PATCH 755/796] Bump simplisafe-python to 9.6.7 (#47206) --- 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 b18bafb0bbf..de5199ccd4c 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.4"], + "requirements": ["simplisafe-python==9.6.7"], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index 05bc4f01b79..1418ebb2d7b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2044,7 +2044,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.6.4 +simplisafe-python==9.6.7 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 78cb4d8f8ce..c5f401d2eca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1051,7 +1051,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.6.4 +simplisafe-python==9.6.7 # homeassistant.components.slack slackclient==2.5.0 From 62e224ecb098d8cd0ffe45eed5450e9c3b30c1fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Feb 2021 22:42:09 -0600 Subject: [PATCH 756/796] Increment the homekit config version when restarting (#47209) If an entity changes between restart the iOS/controller device may have cached the old chars for the accessory. To force the iOS/controller to reload the chars, we increment the config version when Home Assistant restarts --- homeassistant/components/homekit/__init__.py | 7 +++++-- tests/components/homekit/test_homekit.py | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 534ea3c6f95..c042872f4cd 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -491,8 +491,11 @@ class HomeKit: # as pyhap uses a random one until state is restored if os.path.exists(persist_file): self.driver.load() - else: - self.driver.persist() + self.driver.state.config_version += 1 + if self.driver.state.config_version > 65535: + self.driver.state.config_version = 1 + + self.driver.persist() def reset_accessories(self, entity_ids): """Reset the accessory to load the latest configuration.""" diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index ec324602684..9ce3e96f06f 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -540,6 +540,7 @@ async def test_homekit_start(hass, hk_driver, device_reg): assert (device_registry.CONNECTION_NETWORK_MAC, formatted_mac) in device.connections assert len(device_reg.devices) == 1 + assert homekit.driver.state.config_version == 2 async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, mock_zeroconf): From 4907c12964a8d6eb228c144456d5f0fa8f141c8d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Mar 2021 02:00:31 -0600 Subject: [PATCH 757/796] Bump HAP-python to 3.3.2 to fix unavailable condition on restart (#47213) Fixes https://github.com/ikalchev/HAP-python/compare/v3.3.1...v3.3.2 --- 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 4d1598c728c..ac3fb0251e2 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.3.1", + "HAP-python==3.3.2", "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1", diff --git a/requirements_all.txt b/requirements_all.txt index 1418ebb2d7b..48d2374c034 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.3.1 +HAP-python==3.3.2 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c5f401d2eca..09eacd9b78a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.1.8 # homeassistant.components.homekit -HAP-python==3.3.1 +HAP-python==3.3.2 # homeassistant.components.flick_electric PyFlick==0.0.2 From b9edd0d7added1bce1fbfb58650ba7a5000049b0 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Mon, 1 Mar 2021 09:17:41 +0100 Subject: [PATCH 758/796] Fix generic-x86-64 build (#47214) Replace the wrong Docker Hub repository slipped in during testing. --- machine/generic-x86-64 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/machine/generic-x86-64 b/machine/generic-x86-64 index e858c382221..4c83228387d 100644 --- a/machine/generic-x86-64 +++ b/machine/generic-x86-64 @@ -1,5 +1,5 @@ ARG BUILD_VERSION -FROM agners/amd64-homeassistant:$BUILD_VERSION +FROM homeassistant/amd64-homeassistant:$BUILD_VERSION RUN apk --no-cache add \ libva-intel-driver \ From f192b3c1e5e5de21d5e5928d0a61b46fbf79e8ea Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 1 Mar 2021 08:32:13 +0000 Subject: [PATCH 759/796] Bumped version to 2021.3.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c6cccb49d43..86abfa635f0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 3 -PATCH_VERSION = "0b4" +PATCH_VERSION = "0b5" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From 819738a15c5b197ad661c46db5d6d0bfbf8370ba Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 2 Mar 2021 02:12:49 +0100 Subject: [PATCH 760/796] Update color logic for zwave_js light platform (#47110) Co-authored-by: Raman Gupta <7243222+raman325@users.noreply.github.com> --- homeassistant/components/zwave_js/light.py | 159 +++++++++++++-------- tests/components/zwave_js/test_light.py | 45 +++--- 2 files changed, 122 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index 6ed0286e184..d9c31210bea 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -1,6 +1,6 @@ """Support for Z-Wave lights.""" import logging -from typing import Any, Callable, Optional, Tuple +from typing import Any, Callable, Dict, Optional, Tuple from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import ColorComponent, CommandClass @@ -30,6 +30,17 @@ from .entity import ZWaveBaseEntity LOGGER = logging.getLogger(__name__) +MULTI_COLOR_MAP = { + ColorComponent.WARM_WHITE: "warmWhite", + ColorComponent.COLD_WHITE: "coldWhite", + ColorComponent.RED: "red", + ColorComponent.GREEN: "green", + ColorComponent.BLUE: "blue", + ColorComponent.AMBER: "amber", + ColorComponent.CYAN: "cyan", + ColorComponent.PURPLE: "purple", +} + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable @@ -149,21 +160,21 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): # RGB/HS color hs_color = kwargs.get(ATTR_HS_COLOR) if hs_color is not None and self._supports_color: - # set white levels to 0 when setting rgb - await self._async_set_color("Warm White", 0) - await self._async_set_color("Cold White", 0) red, green, blue = color_util.color_hs_to_RGB(*hs_color) - await self._async_set_color("Red", red) - await self._async_set_color("Green", green) - await self._async_set_color("Blue", blue) + colors = { + ColorComponent.RED: red, + ColorComponent.GREEN: green, + ColorComponent.BLUE: blue, + } + if self._supports_color_temp: + # turn of white leds when setting rgb + colors[ColorComponent.WARM_WHITE] = 0 + colors[ColorComponent.COLD_WHITE] = 0 + await self._async_set_colors(colors) # Color temperature color_temp = kwargs.get(ATTR_COLOR_TEMP) if color_temp is not None and self._supports_color_temp: - # turn off rgb when setting white values - await self._async_set_color("Red", 0) - await self._async_set_color("Green", 0) - await self._async_set_color("Blue", 0) # Limit color temp to min/max values cold = max( 0, @@ -177,17 +188,28 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): ), ) warm = 255 - cold - await self._async_set_color("Warm White", warm) - await self._async_set_color("Cold White", cold) + await self._async_set_colors( + { + # turn off color leds when setting color temperature + ColorComponent.RED: 0, + ColorComponent.GREEN: 0, + ColorComponent.BLUE: 0, + ColorComponent.WARM_WHITE: warm, + ColorComponent.COLD_WHITE: cold, + } + ) # White value white_value = kwargs.get(ATTR_WHITE_VALUE) if white_value is not None and self._supports_white_value: - # turn off rgb when setting white values - await self._async_set_color("Red", 0) - await self._async_set_color("Green", 0) - await self._async_set_color("Blue", 0) - await self._async_set_color("Warm White", white_value) + # white led brightness is controlled by white level + # rgb leds (if any) can be on at the same time + await self._async_set_colors( + { + ColorComponent.WARM_WHITE: white_value, + ColorComponent.COLD_WHITE: white_value, + } + ) # set brightness await self._async_set_brightness( @@ -198,24 +220,33 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): """Turn the light off.""" await self._async_set_brightness(0, kwargs.get(ATTR_TRANSITION)) - async def _async_set_color(self, color_name: str, new_value: int) -> None: - """Set defined color to given value.""" - try: - property_key = ColorComponent[color_name.upper().replace(" ", "_")].value - except KeyError: - raise ValueError( - "Illegal color name specified, color must be one of " - f"{','.join([color.name for color in ColorComponent])}" - ) from None - cur_zwave_value = self.get_zwave_value( - "currentColor", + async def _async_set_colors(self, colors: Dict[ColorComponent, int]) -> None: + """Set (multiple) defined colors to given value(s).""" + # prefer the (new) combined color property + # https://github.com/zwave-js/node-zwave-js/pull/1782 + combined_color_val = self.get_zwave_value( + "targetColor", CommandClass.SWITCH_COLOR, - value_property_key=property_key.key, - value_property_key_name=property_key.name, + value_property_key=None, + value_property_key_name=None, ) - # guard for unsupported command - if cur_zwave_value is None: + if combined_color_val and isinstance(combined_color_val.value, dict): + colors_dict = {} + for color, value in colors.items(): + color_name = MULTI_COLOR_MAP[color] + colors_dict[color_name] = value + # set updated color object + await self.info.node.async_set_value(combined_color_val, colors_dict) return + + # fallback to setting the color(s) one by one if multicolor fails + # not sure this is needed at all, but just in case + for color, value in colors.items(): + await self._async_set_color(color, value) + + async def _async_set_color(self, color: ColorComponent, new_value: int) -> None: + """Set defined color to given value.""" + property_key = color.value # actually set the new color value target_zwave_value = self.get_zwave_value( "targetColor", @@ -224,6 +255,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): value_property_key_name=property_key.name, ) if target_zwave_value is None: + # guard for unsupported color return await self.info.node.async_set_value(target_zwave_value, new_value) @@ -231,9 +263,6 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): self, brightness: Optional[int], transition: Optional[int] = None ) -> None: """Set new brightness to light.""" - if brightness is None and self.info.primary_value.value: - # there is no point in setting default brightness when light is already on - return if brightness is None: # Level 255 means to set it to previous value. zwave_brightness = 255 @@ -282,8 +311,9 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): @callback def _calculate_color_values(self) -> None: """Calculate light colors.""" - - # RGB support + # NOTE: We lookup all values here (instead of relying on the multicolor one) + # to find out what colors are supported + # as this is a simple lookup by key, this not heavy red_val = self.get_zwave_value( "currentColor", CommandClass.SWITCH_COLOR, @@ -302,19 +332,6 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): value_property_key=ColorComponent.BLUE.value.key, value_property_key_name=ColorComponent.BLUE.value.name, ) - if red_val and green_val and blue_val: - self._supports_color = True - # convert to HS - if ( - red_val.value is not None - and green_val.value is not None - and blue_val.value is not None - ): - self._hs_color = color_util.color_RGB_to_hs( - red_val.value, green_val.value, blue_val.value - ) - - # White colors ww_val = self.get_zwave_value( "currentColor", CommandClass.SWITCH_COLOR, @@ -327,23 +344,47 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): value_property_key=ColorComponent.COLD_WHITE.value.key, value_property_key_name=ColorComponent.COLD_WHITE.value.name, ) + # prefer the (new) combined color property + # https://github.com/zwave-js/node-zwave-js/pull/1782 + combined_color_val = self.get_zwave_value( + "currentColor", + CommandClass.SWITCH_COLOR, + value_property_key=None, + value_property_key_name=None, + ) + if combined_color_val and isinstance(combined_color_val.value, dict): + multi_color = combined_color_val.value + else: + multi_color = {} + + # RGB support + if red_val and green_val and blue_val: + # prefer values from the multicolor property + red = multi_color.get("red", red_val.value) + green = multi_color.get("green", green_val.value) + blue = multi_color.get("blue", blue_val.value) + self._supports_color = True + # convert to HS + self._hs_color = color_util.color_RGB_to_hs(red, green, blue) + + # color temperature support if ww_val and cw_val: - # Color temperature (CW + WW) Support self._supports_color_temp = True + warm_white = multi_color.get("warmWhite", ww_val.value) + cold_white = multi_color.get("coldWhite", cw_val.value) # Calculate color temps based on whites - cold_level = cw_val.value or 0 - if cold_level or ww_val.value is not None: + if cold_white or warm_white: self._color_temp = round( self._max_mireds - - ((cold_level / 255) * (self._max_mireds - self._min_mireds)) + - ((cold_white / 255) * (self._max_mireds - self._min_mireds)) ) else: self._color_temp = None + # only one white channel (warm white) = white_level support elif ww_val: - # only one white channel (warm white) self._supports_white_value = True - self._white_value = ww_val.value + self._white_value = multi_color.get("warmWhite", ww_val.value) + # only one white channel (cool white) = white_level support elif cw_val: - # only one white channel (cool white) self._supports_white_value = True - self._white_value = cw_val.value + self._white_value = multi_color.get("coldWhite", cw_val.value) diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py index ca36ea35393..8991776fed6 100644 --- a/tests/components/zwave_js/test_light.py +++ b/tests/components/zwave_js/test_light.py @@ -137,62 +137,62 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command_no_wait.call_args_list) == 5 - warm_args = client.async_send_command_no_wait.call_args_list[0][0][ - 0 - ] # warm white 0 + assert len(client.async_send_command_no_wait.call_args_list) == 6 + warm_args = client.async_send_command_no_wait.call_args_list[0][0][0] # red 255 assert warm_args["command"] == "node.set_value" assert warm_args["nodeId"] == 39 assert warm_args["valueId"]["commandClassName"] == "Color Switch" assert warm_args["valueId"]["commandClass"] == 51 assert warm_args["valueId"]["endpoint"] == 0 - assert warm_args["valueId"]["metadata"]["label"] == "Target value (Warm White)" + assert warm_args["valueId"]["metadata"]["label"] == "Target value (Red)" assert warm_args["valueId"]["property"] == "targetColor" assert warm_args["valueId"]["propertyName"] == "targetColor" - assert warm_args["value"] == 0 + assert warm_args["value"] == 255 - cold_args = client.async_send_command_no_wait.call_args_list[1][0][ - 0 - ] # cold white 0 + cold_args = client.async_send_command_no_wait.call_args_list[1][0][0] # green 76 assert cold_args["command"] == "node.set_value" assert cold_args["nodeId"] == 39 assert cold_args["valueId"]["commandClassName"] == "Color Switch" assert cold_args["valueId"]["commandClass"] == 51 assert cold_args["valueId"]["endpoint"] == 0 - assert cold_args["valueId"]["metadata"]["label"] == "Target value (Cold White)" + assert cold_args["valueId"]["metadata"]["label"] == "Target value (Green)" assert cold_args["valueId"]["property"] == "targetColor" assert cold_args["valueId"]["propertyName"] == "targetColor" - assert cold_args["value"] == 0 - red_args = client.async_send_command_no_wait.call_args_list[2][0][0] # red 255 + assert cold_args["value"] == 76 + red_args = client.async_send_command_no_wait.call_args_list[2][0][0] # blue 255 assert red_args["command"] == "node.set_value" assert red_args["nodeId"] == 39 assert red_args["valueId"]["commandClassName"] == "Color Switch" assert red_args["valueId"]["commandClass"] == 51 assert red_args["valueId"]["endpoint"] == 0 - assert red_args["valueId"]["metadata"]["label"] == "Target value (Red)" + assert red_args["valueId"]["metadata"]["label"] == "Target value (Blue)" assert red_args["valueId"]["property"] == "targetColor" assert red_args["valueId"]["propertyName"] == "targetColor" assert red_args["value"] == 255 - green_args = client.async_send_command_no_wait.call_args_list[3][0][0] # green 76 + green_args = client.async_send_command_no_wait.call_args_list[3][0][ + 0 + ] # warm white 0 assert green_args["command"] == "node.set_value" assert green_args["nodeId"] == 39 assert green_args["valueId"]["commandClassName"] == "Color Switch" assert green_args["valueId"]["commandClass"] == 51 assert green_args["valueId"]["endpoint"] == 0 - assert green_args["valueId"]["metadata"]["label"] == "Target value (Green)" + assert green_args["valueId"]["metadata"]["label"] == "Target value (Warm White)" assert green_args["valueId"]["property"] == "targetColor" assert green_args["valueId"]["propertyName"] == "targetColor" - assert green_args["value"] == 76 - blue_args = client.async_send_command_no_wait.call_args_list[4][0][0] # blue 255 + assert green_args["value"] == 0 + blue_args = client.async_send_command_no_wait.call_args_list[4][0][ + 0 + ] # cold white 0 assert blue_args["command"] == "node.set_value" assert blue_args["nodeId"] == 39 assert blue_args["valueId"]["commandClassName"] == "Color Switch" assert blue_args["valueId"]["commandClass"] == 51 assert blue_args["valueId"]["endpoint"] == 0 - assert blue_args["valueId"]["metadata"]["label"] == "Target value (Blue)" + assert blue_args["valueId"]["metadata"]["label"] == "Target value (Cold White)" assert blue_args["valueId"]["property"] == "targetColor" assert blue_args["valueId"]["propertyName"] == "targetColor" - assert blue_args["value"] == 255 + assert blue_args["value"] == 0 # Test rgb color update from value updated event red_event = Event( @@ -232,7 +232,6 @@ async def test_light(hass, client, bulb_6_multi_color, integration): state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY) assert state.state == STATE_ON assert state.attributes[ATTR_BRIGHTNESS] == 255 - assert state.attributes[ATTR_COLOR_TEMP] == 370 assert state.attributes[ATTR_RGB_COLOR] == (255, 76, 255) client.async_send_command_no_wait.reset_mock() @@ -245,7 +244,7 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command_no_wait.call_args_list) == 5 + assert len(client.async_send_command_no_wait.call_args_list) == 6 client.async_send_command_no_wait.reset_mock() @@ -257,7 +256,7 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command_no_wait.call_args_list) == 5 + assert len(client.async_send_command_no_wait.call_args_list) == 6 red_args = client.async_send_command_no_wait.call_args_list[0][0][0] # red 0 assert red_args["command"] == "node.set_value" assert red_args["nodeId"] == 39 @@ -367,7 +366,7 @@ async def test_light(hass, client, bulb_6_multi_color, integration): blocking=True, ) - assert len(client.async_send_command_no_wait.call_args_list) == 5 + assert len(client.async_send_command_no_wait.call_args_list) == 6 client.async_send_command_no_wait.reset_mock() From ea65f612cc4efc1e41c2357dc7a4ad98f86047dc Mon Sep 17 00:00:00 2001 From: Max Chodorowski Date: Mon, 1 Mar 2021 09:38:07 +0000 Subject: [PATCH 761/796] Fix number of reported issues by github integration (#47203) --- homeassistant/components/github/sensor.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 312e726b91d..80d05ae1b9c 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -228,18 +228,25 @@ class GitHubData: self.stargazers = repo.stargazers_count self.forks = repo.forks_count - open_issues = repo.get_issues(state="open", sort="created") - if open_issues is not None: - self.open_issue_count = open_issues.totalCount - if open_issues.totalCount > 0: - self.latest_open_issue_url = open_issues[0].html_url - open_pull_requests = repo.get_pulls(state="open", sort="created") if open_pull_requests is not None: self.pull_request_count = open_pull_requests.totalCount if open_pull_requests.totalCount > 0: self.latest_open_pr_url = open_pull_requests[0].html_url + open_issues = repo.get_issues(state="open", sort="created") + if open_issues is not None: + if self.pull_request_count is None: + self.open_issue_count = open_issues.totalCount + else: + # pull requests are treated as issues too so we need to reduce the received count + self.open_issue_count = ( + open_issues.totalCount - self.pull_request_count + ) + + if open_issues.totalCount > 0: + self.latest_open_issue_url = open_issues[0].html_url + latest_commit = repo.get_commits()[0] self.latest_commit_sha = latest_commit.sha self.latest_commit_message = latest_commit.commit.message From b2a3c35e3a36d6372a6c4d3daf0d806339a28821 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 1 Mar 2021 12:38:49 +0100 Subject: [PATCH 762/796] Fix race when disabling config entries (#47210) * Fix race when disabling config entries * Remove unused constant --- homeassistant/config_entries.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index dbc0dd01454..b54300faaa7 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -69,8 +69,6 @@ ENTRY_STATE_SETUP_RETRY = "setup_retry" ENTRY_STATE_NOT_LOADED = "not_loaded" # An error occurred when trying to unload the entry ENTRY_STATE_FAILED_UNLOAD = "failed_unload" -# The config entry is disabled -ENTRY_STATE_DISABLED = "disabled" UNRECOVERABLE_STATES = (ENTRY_STATE_MIGRATION_ERROR, ENTRY_STATE_FAILED_UNLOAD) @@ -802,11 +800,14 @@ class ConfigEntries: entry.disabled_by = disabled_by self._async_schedule_save() + # Unload the config entry, then fire an event + reload_result = await self.async_reload(entry_id) + self.hass.bus.async_fire( EVENT_CONFIG_ENTRY_DISABLED_BY_UPDATED, {"config_entry_id": entry_id} ) - return await self.async_reload(entry_id) + return reload_result @callback def async_update_entry( From c28903103d97e07b9772d2c31d629d3f85770a9b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Mar 2021 18:56:42 -0600 Subject: [PATCH 763/796] Fix harmony failing to switch activities when a switch is in progress (#47212) Co-authored-by: Paulus Schoutsen --- homeassistant/components/harmony/data.py | 28 +++++++++++-------- .../components/harmony/subscriber.py | 15 ++++++++++ 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py index 8c1d137bc85..340596ff1ef 100644 --- a/homeassistant/components/harmony/data.py +++ b/homeassistant/components/harmony/data.py @@ -22,17 +22,8 @@ class HarmonyData(HarmonySubscriberMixin): self._name = name self._unique_id = unique_id self._available = False - - callbacks = { - "config_updated": self._config_updated, - "connect": self._connected, - "disconnect": self._disconnected, - "new_activity_starting": self._activity_starting, - "new_activity": self._activity_started, - } - self._client = HarmonyClient( - ip_address=address, callbacks=ClientCallbackType(**callbacks) - ) + self._client = None + self._address = address @property def activities(self): @@ -105,6 +96,18 @@ class HarmonyData(HarmonySubscriberMixin): async def connect(self) -> bool: """Connect to the Harmony Hub.""" _LOGGER.debug("%s: Connecting", self._name) + + callbacks = { + "config_updated": self._config_updated, + "connect": self._connected, + "disconnect": self._disconnected, + "new_activity_starting": self._activity_starting, + "new_activity": self._activity_started, + } + self._client = HarmonyClient( + ip_address=self._address, callbacks=ClientCallbackType(**callbacks) + ) + try: if not await self._client.connect(): _LOGGER.warning("%s: Unable to connect to HUB", self._name) @@ -113,6 +116,7 @@ class HarmonyData(HarmonySubscriberMixin): except aioexc.TimeOut: _LOGGER.warning("%s: Connection timed-out", self._name) return False + return True async def shutdown(self): @@ -159,10 +163,12 @@ class HarmonyData(HarmonySubscriberMixin): ) return + await self.async_lock_start_activity() try: await self._client.start_activity(activity_id) except aioexc.TimeOut: _LOGGER.error("%s: Starting activity %s timed-out", self.name, activity) + self.async_unlock_start_activity() async def async_power_off(self): """Start the PowerOff activity.""" diff --git a/homeassistant/components/harmony/subscriber.py b/homeassistant/components/harmony/subscriber.py index d3bed33d560..b2652cc43d1 100644 --- a/homeassistant/components/harmony/subscriber.py +++ b/homeassistant/components/harmony/subscriber.py @@ -1,5 +1,6 @@ """Mixin class for handling harmony callback subscriptions.""" +import asyncio import logging from typing import Any, Callable, NamedTuple, Optional @@ -29,6 +30,17 @@ class HarmonySubscriberMixin: super().__init__() self._hass = hass self._subscriptions = [] + self._activity_lock = asyncio.Lock() + + async def async_lock_start_activity(self): + """Acquire the lock.""" + await self._activity_lock.acquire() + + @callback + def async_unlock_start_activity(self): + """Release the lock.""" + if self._activity_lock.locked(): + self._activity_lock.release() @callback def async_subscribe(self, update_callbacks: HarmonyCallback) -> Callable: @@ -51,11 +63,13 @@ class HarmonySubscriberMixin: def _connected(self, _=None) -> None: _LOGGER.debug("connected") + self.async_unlock_start_activity() self._available = True self._call_callbacks("connected") def _disconnected(self, _=None) -> None: _LOGGER.debug("disconnected") + self.async_unlock_start_activity() self._available = False self._call_callbacks("disconnected") @@ -65,6 +79,7 @@ class HarmonySubscriberMixin: def _activity_started(self, activity_info: tuple) -> None: _LOGGER.debug("activity %s started", activity_info) + self.async_unlock_start_activity() self._call_callbacks("activity_started", activity_info) def _call_callbacks(self, callback_func_name: str, argument: tuple = None): From acdad8a28cf0e04326f852ebc6126c6c59b67d70 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Mar 2021 01:27:26 +0100 Subject: [PATCH 764/796] Fix duplicate template handling in Persistent Notifications (#47217) --- .../persistent_notification/__init__.py | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 5f08f79dc00..589cc97baea 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -11,6 +11,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.template import Template from homeassistant.loader import bind_hass from homeassistant.util import slugify import homeassistant.util.dt as dt_util @@ -35,8 +36,8 @@ SERVICE_MARK_READ = "mark_read" SCHEMA_SERVICE_CREATE = vol.Schema( { - vol.Required(ATTR_MESSAGE): cv.template, - vol.Optional(ATTR_TITLE): cv.template, + vol.Required(ATTR_MESSAGE): vol.Any(cv.dynamic_template, cv.string), + vol.Optional(ATTR_TITLE): vol.Any(cv.dynamic_template, cv.string), vol.Optional(ATTR_NOTIFICATION_ID): cv.string, } ) @@ -118,22 +119,24 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: attr = {} if title is not None: - try: - title.hass = hass - title = title.async_render(parse_result=False) - except TemplateError as ex: - _LOGGER.error("Error rendering title %s: %s", title, ex) - title = title.template + if isinstance(title, Template): + try: + title.hass = hass + title = title.async_render(parse_result=False) + except TemplateError as ex: + _LOGGER.error("Error rendering title %s: %s", title, ex) + title = title.template attr[ATTR_TITLE] = title attr[ATTR_FRIENDLY_NAME] = title - try: - message.hass = hass - message = message.async_render(parse_result=False) - except TemplateError as ex: - _LOGGER.error("Error rendering message %s: %s", message, ex) - message = message.template + if isinstance(message, Template): + try: + message.hass = hass + message = message.async_render(parse_result=False) + except TemplateError as ex: + _LOGGER.error("Error rendering message %s: %s", message, ex) + message = message.template attr[ATTR_MESSAGE] = message From 30ccd33e7f483ab6f8b878f129eacfde63d39bb3 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 1 Mar 2021 12:46:02 +0100 Subject: [PATCH 765/796] Fix Xiaomi Miio flow unique_id for non discovery flows (#47222) --- homeassistant/components/xiaomi_miio/config_flow.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 2e069b30da3..d6ee83e9842 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -125,7 +125,9 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): for gateway_model in MODELS_GATEWAY: if model.startswith(gateway_model): unique_id = self.mac - await self.async_set_unique_id(unique_id) + await self.async_set_unique_id( + unique_id, raise_on_progress=False + ) self._abort_if_unique_id_configured() return self.async_create_entry( title=DEFAULT_GATEWAY_NAME, @@ -144,7 +146,9 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): for device_model in MODELS_ALL_DEVICES: if model.startswith(device_model): unique_id = self.mac - await self.async_set_unique_id(unique_id) + await self.async_set_unique_id( + unique_id, raise_on_progress=False + ) self._abort_if_unique_id_configured() return self.async_create_entry( title=name, From c411f0dcdc45af31c9fe4c4e3d23ac7c11203b53 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 1 Mar 2021 18:27:43 +0200 Subject: [PATCH 766/796] Fix Shelly Polling (#47224) --- homeassistant/components/shelly/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index d4423dc3a88..ccb52127525 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -108,6 +108,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): "Setup for device %s will resume when device is online", entry.title ) device.subscribe_updates(_async_device_online) + await device.coap_request("s") else: # Restore sensors for sleeping device _LOGGER.debug("Setting up offline device %s", entry.title) From 118c996a9fca6c1dd744f21b87dee762e2adc4ec Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 1 Mar 2021 23:34:26 +0100 Subject: [PATCH 767/796] Pass variables to initial evaluation of template trigger (#47236) * Pass variables to initial evaluation of template trigger * Add test * Clarify test --- homeassistant/components/template/trigger.py | 4 ++- tests/components/template/test_trigger.py | 38 ++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index 9e6ee086c73..1f378c59335 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -41,7 +41,9 @@ async def async_attach_trigger( # Arm at setup if the template is already false. try: - if not result_as_boolean(value_template.async_render()): + if not result_as_boolean( + value_template.async_render(automation_info["variables"]) + ): armed = True except exceptions.TemplateError as ex: _LOGGER.warning( diff --git a/tests/components/template/test_trigger.py b/tests/components/template/test_trigger.py index 3ba79e85bf2..55311005201 100644 --- a/tests/components/template/test_trigger.py +++ b/tests/components/template/test_trigger.py @@ -135,6 +135,44 @@ async def test_if_not_fires_when_true_at_setup(hass, calls): assert len(calls) == 0 +async def test_if_not_fires_when_true_at_setup_variables(hass, calls): + """Test for not firing during startup + trigger_variables.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger_variables": {"entity": "test.entity"}, + "trigger": { + "platform": "template", + "value_template": '{{ is_state(entity|default("test.entity2"), "hello") }}', + }, + "action": {"service": "test.automation"}, + } + }, + ) + + assert len(calls) == 0 + + # Assert that the trigger doesn't fire immediately when it's setup + # If trigger_variable 'entity' is not passed to initial check at setup, the + # trigger will immediately fire + hass.states.async_set("test.entity", "hello", force_update=True) + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set("test.entity", "goodbye", force_update=True) + await hass.async_block_till_done() + assert len(calls) == 0 + + # Assert that the trigger fires after state change + # If trigger_variable 'entity' is not passed to the template trigger, the + # trigger will never fire because it falls back to 'test.entity2' + hass.states.async_set("test.entity", "hello", force_update=True) + await hass.async_block_till_done() + assert len(calls) == 1 + + async def test_if_not_fires_because_fail(hass, calls): """Test for not firing after TemplateError.""" hass.states.async_set("test.number", "1") From 8cf0fcc7f3ba25f810267a1a9f5e684ab0aedb7f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 1 Mar 2021 17:08:36 -0700 Subject: [PATCH 768/796] Bump simplisafe-python to 9.6.8 (#47241) --- 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 de5199ccd4c..6122428ea98 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.7"], + "requirements": ["simplisafe-python==9.6.8"], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index 48d2374c034..5ab088e5788 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2044,7 +2044,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.6.7 +simplisafe-python==9.6.8 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 09eacd9b78a..fbc39974f40 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1051,7 +1051,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.6.7 +simplisafe-python==9.6.8 # homeassistant.components.slack slackclient==2.5.0 From 3ebe31e172d4090dd83d9c4322b71b62a9f9c214 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Mar 2021 17:18:47 -0600 Subject: [PATCH 769/796] Fix lutron caseta fan handling of speed off (#47244) --- homeassistant/components/lutron_caseta/fan.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py index 57b87b18320..edda379aedc 100644 --- a/homeassistant/components/lutron_caseta/fan.py +++ b/homeassistant/components/lutron_caseta/fan.py @@ -46,6 +46,8 @@ class LutronCasetaFan(LutronCasetaDevice, FanEntity): """Return the current speed percentage.""" if self._device["fan_speed"] is None: return None + if self._device["fan_speed"] == FAN_OFF: + return 0 return ordered_list_item_to_percentage( ORDERED_NAMED_FAN_SPEEDS, self._device["fan_speed"] ) From 88d29bcf2016eb39b2bf2ab31981cf45c05f3840 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 1 Mar 2021 18:24:55 -0500 Subject: [PATCH 770/796] Add suggested area for zwave_js devices (#47250) --- homeassistant/components/zwave_js/__init__.py | 19 ++++++++++-------- tests/components/zwave_js/common.py | 3 +++ tests/components/zwave_js/conftest.py | 6 ++---- tests/components/zwave_js/test_init.py | 20 ++++++++++++++++++- tests/components/zwave_js/test_light.py | 8 +++++--- .../zwave_js/eaton_rf9640_dimmer_state.json | 2 +- 6 files changed, 41 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 93d511875af..798fd9fda2c 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -67,14 +67,17 @@ def register_node_in_dev_reg( node: ZwaveNode, ) -> None: """Register node in dev reg.""" - device = dev_reg.async_get_or_create( - config_entry_id=entry.entry_id, - identifiers={get_device_id(client, node)}, - sw_version=node.firmware_version, - name=node.name or node.device_config.description or f"Node {node.node_id}", - model=node.device_config.label, - manufacturer=node.device_config.manufacturer, - ) + params = { + "config_entry_id": entry.entry_id, + "identifiers": {get_device_id(client, node)}, + "sw_version": node.firmware_version, + "name": node.name or node.device_config.description or f"Node {node.node_id}", + "model": node.device_config.label, + "manufacturer": node.device_config.manufacturer, + } + if node.location: + params["suggested_area"] = node.location + device = dev_reg.async_get_or_create(**params) async_dispatcher_send(hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device) diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index ebba16136a0..a5ee628754e 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -16,3 +16,6 @@ PROPERTY_DOOR_STATUS_BINARY_SENSOR = ( CLIMATE_RADIO_THERMOSTAT_ENTITY = "climate.z_wave_thermostat" CLIMATE_DANFOSS_LC13_ENTITY = "climate.living_connect_z_thermostat" CLIMATE_FLOOR_THERMOSTAT_ENTITY = "climate.floor_thermostat" +BULB_6_MULTI_COLOR_LIGHT_ENTITY = "light.bulb_6_multi_color" +EATON_RF9640_ENTITY = "light.allloaddimmer" +AEON_SMART_SWITCH_LIGHT_ENTITY = "light.smart_switch_6" diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index e0bc588abf4..72835fb17c1 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -10,9 +10,7 @@ from zwave_js_server.model.driver import Driver from zwave_js_server.model.node import Node from zwave_js_server.version import VersionInfo -from homeassistant.helpers.device_registry import ( - async_get_registry as async_get_device_registry, -) +from homeassistant.helpers.device_registry import async_get as async_get_device_registry from tests.common import MockConfigEntry, load_fixture @@ -20,7 +18,7 @@ from tests.common import MockConfigEntry, load_fixture @pytest.fixture(name="device_registry") async def device_registry_fixture(hass): """Return the device registry.""" - return await async_get_device_registry(hass) + return async_get_device_registry(hass) @pytest.fixture(name="controller_state", scope="session") diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index bff2ecd198c..2a2f249c361 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -18,7 +18,11 @@ from homeassistant.config_entries import ( from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import device_registry, entity_registry -from .common import AIR_TEMPERATURE_SENSOR, NOTIFICATION_MOTION_BINARY_SENSOR +from .common import ( + AIR_TEMPERATURE_SENSOR, + EATON_RF9640_ENTITY, + NOTIFICATION_MOTION_BINARY_SENSOR, +) from tests.common import MockConfigEntry @@ -467,3 +471,17 @@ async def test_removed_device(hass, client, multiple_devices, integration): ) assert len(entity_entries) == 15 assert dev_reg.async_get_device({get_device_id(client, old_node)}) is None + + +async def test_suggested_area(hass, client, eaton_rf9640_dimmer): + """Test that suggested area works.""" + dev_reg = device_registry.async_get(hass) + ent_reg = entity_registry.async_get(hass) + + entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"}) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity = ent_reg.async_get(EATON_RF9640_ENTITY) + assert dev_reg.async_get(entity.device_id).area_id is not None diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py index 8991776fed6..c16e2474980 100644 --- a/tests/components/zwave_js/test_light.py +++ b/tests/components/zwave_js/test_light.py @@ -12,9 +12,11 @@ from homeassistant.components.light import ( ) from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON -BULB_6_MULTI_COLOR_LIGHT_ENTITY = "light.bulb_6_multi_color" -EATON_RF9640_ENTITY = "light.allloaddimmer" -AEON_SMART_SWITCH_LIGHT_ENTITY = "light.smart_switch_6" +from .common import ( + AEON_SMART_SWITCH_LIGHT_ENTITY, + BULB_6_MULTI_COLOR_LIGHT_ENTITY, + EATON_RF9640_ENTITY, +) async def test_light(hass, client, bulb_6_multi_color, integration): diff --git a/tests/fixtures/zwave_js/eaton_rf9640_dimmer_state.json b/tests/fixtures/zwave_js/eaton_rf9640_dimmer_state.json index db815506a6b..b11d2bfd180 100644 --- a/tests/fixtures/zwave_js/eaton_rf9640_dimmer_state.json +++ b/tests/fixtures/zwave_js/eaton_rf9640_dimmer_state.json @@ -27,7 +27,7 @@ "nodeType": 0, "roleType": 5, "name": "AllLoadDimmer", - "location": "", + "location": "LivingRoom", "deviceConfig": { "manufacturerId": 26, "manufacturer": "Eaton", From bd29d82728ed3c0e9e1c1b58bc7d47e422fc8351 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 2 Mar 2021 00:32:39 +0100 Subject: [PATCH 771/796] Update frontend to 20210301.0 (#47252) --- 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 01f1c72f8d6..e8f9ff2698d 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210226.0" + "home-assistant-frontend==20210301.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9506171303b..cb211fb1962 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.41.0 -home-assistant-frontend==20210226.0 +home-assistant-frontend==20210301.0 httpx==0.16.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 5ab088e5788..e27b43f94a8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -763,7 +763,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210226.0 +home-assistant-frontend==20210301.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fbc39974f40..400cc372b2b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -412,7 +412,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210226.0 +home-assistant-frontend==20210301.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 3117e47e1be6742c2844dad1aee9b7e0b5e3aa65 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 1 Mar 2021 16:12:48 -0800 Subject: [PATCH 772/796] Revert "Fix the updater schema (#47128)" (#47254) This reverts commit 98be703d90e44efe43b1a17c7e5243e5097b00b1. --- homeassistant/components/updater/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 81910db38d6..9d65bb4c5d4 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -27,7 +27,7 @@ UPDATER_URL = "https://updater.home-assistant.io/" CONFIG_SCHEMA = vol.Schema( { - vol.Optional(DOMAIN, default={}): { + DOMAIN: { vol.Optional(CONF_REPORTING, default=True): cv.boolean, vol.Optional(CONF_COMPONENT_REPORTING, default=False): cv.boolean, } @@ -56,13 +56,13 @@ async def async_setup(hass, config): # This component only makes sense in release versions _LOGGER.info("Running on 'dev', only analytics will be submitted") - conf = config[DOMAIN] - if conf[CONF_REPORTING]: + conf = config.get(DOMAIN, {}) + if conf.get(CONF_REPORTING): huuid = await hass.helpers.instance_id.async_get() else: huuid = None - include_components = conf[CONF_COMPONENT_REPORTING] + include_components = conf.get(CONF_COMPONENT_REPORTING) async def check_new_version() -> Updater: """Check if a new version is available and report if one is.""" From ec954746040e2b12e7b45bc346868401f25cf0f6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 2 Mar 2021 01:17:40 +0000 Subject: [PATCH 773/796] Bumped version to 2021.3.0b6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 86abfa635f0..2edbfa33a10 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 3 -PATCH_VERSION = "0b5" +PATCH_VERSION = "0b6" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From ab5173c4cf07c7f2b9ec64241137c060e4711154 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 25 Feb 2021 00:05:20 +0000 Subject: [PATCH 774/796] [ci skip] Translation update --- .../components/arcam_fmj/translations/nl.json | 1 + .../components/asuswrt/translations/nl.json | 1 + .../components/august/translations/nl.json | 3 +- .../components/blink/translations/nl.json | 1 + .../components/bond/translations/ca.json | 4 +-- .../components/bond/translations/et.json | 4 +-- .../components/bond/translations/fr.json | 4 +-- .../components/bond/translations/nl.json | 1 + .../components/bond/translations/no.json | 4 +-- .../components/bond/translations/pl.json | 4 +-- .../components/bond/translations/ru.json | 4 +-- .../components/bond/translations/zh-Hant.json | 4 +-- .../components/broadlink/translations/nl.json | 1 + .../components/climacell/translations/af.json | 10 ++++++ .../components/climacell/translations/ca.json | 34 +++++++++++++++++++ .../components/climacell/translations/en.json | 34 +++++++++++++++++++ .../components/climacell/translations/et.json | 34 +++++++++++++++++++ .../components/climacell/translations/fr.json | 34 +++++++++++++++++++ .../components/climacell/translations/nl.json | 32 +++++++++++++++++ .../components/climacell/translations/no.json | 34 +++++++++++++++++++ .../components/climacell/translations/ru.json | 34 +++++++++++++++++++ .../components/daikin/translations/nl.json | 4 ++- .../components/enocean/translations/nl.json | 7 ++++ .../faa_delays/translations/en.json | 12 ++++--- .../faa_delays/translations/fr.json | 21 ++++++++++++ .../faa_delays/translations/nl.json | 21 ++++++++++++ .../fireservicerota/translations/nl.json | 3 +- .../fireservicerota/translations/no.json | 2 +- .../components/firmata/translations/nl.json | 7 ++++ .../flunearyou/translations/nl.json | 3 ++ .../components/fritzbox/translations/nl.json | 1 + .../components/goalzero/translations/nl.json | 1 + .../home_connect/translations/nl.json | 3 +- .../components/homekit/translations/ca.json | 8 ++--- .../components/homekit/translations/en.json | 21 ++++++++++-- .../components/homekit/translations/et.json | 10 +++--- .../components/homekit/translations/fr.json | 8 ++--- .../components/homekit/translations/no.json | 8 ++--- .../components/homekit/translations/pl.json | 8 ++--- .../components/homekit/translations/ru.json | 8 ++--- .../homekit/translations/zh-Hant.json | 8 ++--- .../huawei_lte/translations/nl.json | 1 + .../components/icloud/translations/nl.json | 6 ++-- .../components/ifttt/translations/nl.json | 3 +- .../components/insteon/translations/nl.json | 3 ++ .../keenetic_ndms2/translations/nl.json | 3 +- .../components/kmtronic/translations/fr.json | 21 ++++++++++++ .../components/kmtronic/translations/pl.json | 21 ++++++++++++ .../components/litejet/translations/ca.json | 19 +++++++++++ .../components/litejet/translations/fr.json | 16 +++++++++ .../components/litejet/translations/no.json | 19 +++++++++++ .../components/litejet/translations/pl.json | 19 +++++++++++ .../components/litejet/translations/ru.json | 19 +++++++++++ .../components/litejet/translations/tr.json | 9 +++++ .../litejet/translations/zh-Hant.json | 19 +++++++++++ .../litterrobot/translations/fr.json | 20 +++++++++++ .../litterrobot/translations/no.json | 20 +++++++++++ .../litterrobot/translations/pl.json | 20 +++++++++++ .../components/locative/translations/nl.json | 3 +- .../components/mailgun/translations/nl.json | 3 +- .../components/mazda/translations/nl.json | 3 +- .../components/mullvad/translations/ca.json | 22 ++++++++++++ .../components/mullvad/translations/en.json | 6 ++++ .../components/mullvad/translations/et.json | 22 ++++++++++++ .../components/mullvad/translations/fr.json | 22 ++++++++++++ .../components/mullvad/translations/nl.json | 22 ++++++++++++ .../components/mullvad/translations/no.json | 22 ++++++++++++ .../components/mullvad/translations/ru.json | 22 ++++++++++++ .../components/mullvad/translations/tr.json | 12 +++++++ .../components/netatmo/translations/et.json | 22 ++++++++++++ .../components/netatmo/translations/fr.json | 22 ++++++++++++ .../components/netatmo/translations/nl.json | 25 +++++++++++++- .../components/netatmo/translations/ru.json | 22 ++++++++++++ .../components/netatmo/translations/tr.json | 16 +++++++++ .../nightscout/translations/nl.json | 1 + .../components/nzbget/translations/nl.json | 1 + .../plum_lightpad/translations/nl.json | 3 +- .../components/poolsense/translations/nl.json | 3 +- .../translations/fr.json | 21 ++++++++++++ .../components/rpi_power/translations/nl.json | 5 +++ .../ruckus_unleashed/translations/nl.json | 1 + .../components/sharkiq/translations/nl.json | 1 + .../components/smappee/translations/nl.json | 3 +- .../components/sms/translations/nl.json | 3 +- .../components/somfy/translations/nl.json | 1 + .../speedtestdotnet/translations/nl.json | 5 +++ .../components/spider/translations/nl.json | 3 ++ .../components/subaru/translations/fr.json | 31 +++++++++++++++++ .../components/syncthru/translations/nl.json | 3 +- .../components/tile/translations/nl.json | 3 +- .../components/toon/translations/nl.json | 2 ++ .../totalconnect/translations/fr.json | 11 +++++- .../totalconnect/translations/nl.json | 5 +++ .../totalconnect/translations/no.json | 17 ++++++++-- .../totalconnect/translations/pl.json | 17 ++++++++-- .../xiaomi_miio/translations/no.json | 1 + .../xiaomi_miio/translations/pl.json | 1 + .../components/zwave_js/translations/ca.json | 7 +++- .../components/zwave_js/translations/et.json | 7 +++- .../components/zwave_js/translations/fr.json | 7 +++- .../components/zwave_js/translations/no.json | 7 +++- .../components/zwave_js/translations/pl.json | 7 +++- .../components/zwave_js/translations/ru.json | 7 +++- .../zwave_js/translations/zh-Hant.json | 7 +++- 104 files changed, 1060 insertions(+), 81 deletions(-) create mode 100644 homeassistant/components/climacell/translations/af.json create mode 100644 homeassistant/components/climacell/translations/ca.json create mode 100644 homeassistant/components/climacell/translations/en.json create mode 100644 homeassistant/components/climacell/translations/et.json create mode 100644 homeassistant/components/climacell/translations/fr.json create mode 100644 homeassistant/components/climacell/translations/nl.json create mode 100644 homeassistant/components/climacell/translations/no.json create mode 100644 homeassistant/components/climacell/translations/ru.json create mode 100644 homeassistant/components/enocean/translations/nl.json create mode 100644 homeassistant/components/faa_delays/translations/fr.json create mode 100644 homeassistant/components/faa_delays/translations/nl.json create mode 100644 homeassistant/components/firmata/translations/nl.json create mode 100644 homeassistant/components/kmtronic/translations/fr.json create mode 100644 homeassistant/components/kmtronic/translations/pl.json create mode 100644 homeassistant/components/litejet/translations/ca.json create mode 100644 homeassistant/components/litejet/translations/fr.json create mode 100644 homeassistant/components/litejet/translations/no.json create mode 100644 homeassistant/components/litejet/translations/pl.json create mode 100644 homeassistant/components/litejet/translations/ru.json create mode 100644 homeassistant/components/litejet/translations/tr.json create mode 100644 homeassistant/components/litejet/translations/zh-Hant.json create mode 100644 homeassistant/components/litterrobot/translations/fr.json create mode 100644 homeassistant/components/litterrobot/translations/no.json create mode 100644 homeassistant/components/litterrobot/translations/pl.json create mode 100644 homeassistant/components/mullvad/translations/ca.json create mode 100644 homeassistant/components/mullvad/translations/et.json create mode 100644 homeassistant/components/mullvad/translations/fr.json create mode 100644 homeassistant/components/mullvad/translations/nl.json create mode 100644 homeassistant/components/mullvad/translations/no.json create mode 100644 homeassistant/components/mullvad/translations/ru.json create mode 100644 homeassistant/components/mullvad/translations/tr.json create mode 100644 homeassistant/components/rituals_perfume_genie/translations/fr.json create mode 100644 homeassistant/components/subaru/translations/fr.json diff --git a/homeassistant/components/arcam_fmj/translations/nl.json b/homeassistant/components/arcam_fmj/translations/nl.json index 5607b426cc9..03465d5c53d 100644 --- a/homeassistant/components/arcam_fmj/translations/nl.json +++ b/homeassistant/components/arcam_fmj/translations/nl.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", "cannot_connect": "Kan geen verbinding maken" }, "error": { diff --git a/homeassistant/components/asuswrt/translations/nl.json b/homeassistant/components/asuswrt/translations/nl.json index 1128a820cd5..9d1e76aaf2b 100644 --- a/homeassistant/components/asuswrt/translations/nl.json +++ b/homeassistant/components/asuswrt/translations/nl.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Kan geen verbinding maken", "invalid_host": "Ongeldige hostnaam of IP-adres", + "ssh_not_file": "SSH-sleutelbestand niet gevonden", "unknown": "Onverwachte fout" }, "step": { diff --git a/homeassistant/components/august/translations/nl.json b/homeassistant/components/august/translations/nl.json index 1697f634d9a..e48d27801cc 100644 --- a/homeassistant/components/august/translations/nl.json +++ b/homeassistant/components/august/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Account al geconfigureerd" + "already_configured": "Account al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "cannot_connect": "Verbinding mislukt, probeer het opnieuw", diff --git a/homeassistant/components/blink/translations/nl.json b/homeassistant/components/blink/translations/nl.json index 4067bf75f83..f1f1ce7888b 100644 --- a/homeassistant/components/blink/translations/nl.json +++ b/homeassistant/components/blink/translations/nl.json @@ -4,6 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { + "cannot_connect": "Kan geen verbinding maken", "invalid_access_token": "Ongeldig toegangstoken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" diff --git a/homeassistant/components/bond/translations/ca.json b/homeassistant/components/bond/translations/ca.json index 3903ea77c34..1d1df915630 100644 --- a/homeassistant/components/bond/translations/ca.json +++ b/homeassistant/components/bond/translations/ca.json @@ -9,13 +9,13 @@ "old_firmware": "Hi ha un programari antic i no compatible al dispositiu Bond - actualitza'l abans de continuar", "unknown": "Error inesperat" }, - "flow_title": "Bond: {bond_id} ({host})", + "flow_title": "Bond: {name} ({host})", "step": { "confirm": { "data": { "access_token": "Token d'acc\u00e9s" }, - "description": "Vols configurar {bond_id}?" + "description": "Vols configurar {name}?" }, "user": { "data": { diff --git a/homeassistant/components/bond/translations/et.json b/homeassistant/components/bond/translations/et.json index dc6a8414bce..5e9a8e4493f 100644 --- a/homeassistant/components/bond/translations/et.json +++ b/homeassistant/components/bond/translations/et.json @@ -9,13 +9,13 @@ "old_firmware": "Bondi seadme ei toeta vana p\u00fcsivara - uuenda enne j\u00e4tkamist", "unknown": "Tundmatu viga" }, - "flow_title": "Bond: {bond_id} ( {host} )", + "flow_title": "Bond: {name} ( {host} )", "step": { "confirm": { "data": { "access_token": "Juurdep\u00e4\u00e4sut\u00f5end" }, - "description": "Kas soovid seadistada teenuse {bond_id} ?" + "description": "Kas soovid seadistada teenust {name} ?" }, "user": { "data": { diff --git a/homeassistant/components/bond/translations/fr.json b/homeassistant/components/bond/translations/fr.json index 496a21339cb..d9eb14b1a62 100644 --- a/homeassistant/components/bond/translations/fr.json +++ b/homeassistant/components/bond/translations/fr.json @@ -9,13 +9,13 @@ "old_firmware": "Ancien micrologiciel non pris en charge sur l'appareil Bond - veuillez mettre \u00e0 niveau avant de continuer", "unknown": "Erreur inattendue" }, - "flow_title": "Bond : {bond_id} ({h\u00f4te})", + "flow_title": "Lien : {name} ({host})", "step": { "confirm": { "data": { "access_token": "Jeton d'acc\u00e8s" }, - "description": "Voulez-vous configurer {bond_id} ?" + "description": "Voulez-vous configurer {name}?" }, "user": { "data": { diff --git a/homeassistant/components/bond/translations/nl.json b/homeassistant/components/bond/translations/nl.json index 8010dfc2e78..b5d8c593ea9 100644 --- a/homeassistant/components/bond/translations/nl.json +++ b/homeassistant/components/bond/translations/nl.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "access_token": "Toegangstoken", "host": "Host" } } diff --git a/homeassistant/components/bond/translations/no.json b/homeassistant/components/bond/translations/no.json index 01ff745eed3..c09b7a17635 100644 --- a/homeassistant/components/bond/translations/no.json +++ b/homeassistant/components/bond/translations/no.json @@ -9,13 +9,13 @@ "old_firmware": "Gammel fastvare som ikke st\u00f8ttes p\u00e5 Bond-enheten \u2013 vennligst oppgrader f\u00f8r du fortsetter", "unknown": "Uventet feil" }, - "flow_title": "", + "flow_title": "Obligasjon: {name} ({host})", "step": { "confirm": { "data": { "access_token": "Tilgangstoken" }, - "description": "Vil du konfigurere {bond_id}?" + "description": "Vil du konfigurere {name}?" }, "user": { "data": { diff --git a/homeassistant/components/bond/translations/pl.json b/homeassistant/components/bond/translations/pl.json index c50c270b74c..6f5f2d276ff 100644 --- a/homeassistant/components/bond/translations/pl.json +++ b/homeassistant/components/bond/translations/pl.json @@ -9,13 +9,13 @@ "old_firmware": "Stare, nieobs\u0142ugiwane oprogramowanie na urz\u0105dzeniu Bond - zaktualizuj przed kontynuowaniem", "unknown": "Nieoczekiwany b\u0142\u0105d" }, - "flow_title": "Bond: {bond_id} ({host})", + "flow_title": "Bond: {name} ({host})", "step": { "confirm": { "data": { "access_token": "Token dost\u0119pu" }, - "description": "Czy chcesz skonfigurowa\u0107 {bond_id}?" + "description": "Czy chcesz skonfigurowa\u0107 {name}?" }, "user": { "data": { diff --git a/homeassistant/components/bond/translations/ru.json b/homeassistant/components/bond/translations/ru.json index e6c4067d8ac..cdc37fc27f7 100644 --- a/homeassistant/components/bond/translations/ru.json +++ b/homeassistant/components/bond/translations/ru.json @@ -9,13 +9,13 @@ "old_firmware": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u0443\u0441\u0442\u0430\u0440\u0435\u043b\u0430 \u0438 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "Bond {bond_id} ({host})", + "flow_title": "Bond: {name} ({host})", "step": { "confirm": { "data": { "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430" }, - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {bond_id}?" + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" }, "user": { "data": { diff --git a/homeassistant/components/bond/translations/zh-Hant.json b/homeassistant/components/bond/translations/zh-Hant.json index af652c54509..1c5327dc662 100644 --- a/homeassistant/components/bond/translations/zh-Hant.json +++ b/homeassistant/components/bond/translations/zh-Hant.json @@ -9,13 +9,13 @@ "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})", + "flow_title": "Bond\uff1a{name} ({host})", "step": { "confirm": { "data": { "access_token": "\u5b58\u53d6\u5bc6\u9470" }, - "description": "\u662f\u5426\u8981\u8a2d\u5b9a {bond_id}\uff1f" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" }, "user": { "data": { diff --git a/homeassistant/components/broadlink/translations/nl.json b/homeassistant/components/broadlink/translations/nl.json index 2f3a7313f75..7f85335d7bb 100644 --- a/homeassistant/components/broadlink/translations/nl.json +++ b/homeassistant/components/broadlink/translations/nl.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", "cannot_connect": "Kon niet verbinden", "invalid_host": "Ongeldige hostnaam of IP-adres", "not_supported": "Apparaat wordt niet ondersteund", diff --git a/homeassistant/components/climacell/translations/af.json b/homeassistant/components/climacell/translations/af.json new file mode 100644 index 00000000000..b62fc7023a4 --- /dev/null +++ b/homeassistant/components/climacell/translations/af.json @@ -0,0 +1,10 @@ +{ + "options": { + "step": { + "init": { + "title": "Update ClimaCell opties" + } + } + }, + "title": "ClimaCell" +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/ca.json b/homeassistant/components/climacell/translations/ca.json new file mode 100644 index 00000000000..23afb6a3d90 --- /dev/null +++ b/homeassistant/components/climacell/translations/ca.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_api_key": "Clau API inv\u00e0lida", + "rate_limited": "Freq\u00fc\u00e8ncia limitada temporalment, torna-ho a provar m\u00e9s tard.", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nom" + }, + "description": "Si no es proporcionen la Latitud i Longitud, s'utilitzaran els valors per defecte de la configuraci\u00f3 de Home Assistant. Es crear\u00e0 una entitat per a cada tipus de previsi\u00f3, per\u00f2 nom\u00e9s s'habilitaran les que seleccionis." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "forecast_types": "Tipus de previsi\u00f3", + "timestep": "Minuts entre previsions NowCast" + }, + "description": "Si decideixes activar l'entitat de predicci\u00f3 \"nowcast\", podr\u00e0s configurar l'interval en minuts entre cada previsi\u00f3. El nombre de previsions proporcionades dep\u00e8n d'aquest interval de minuts.", + "title": "Actualitzaci\u00f3 de les opcions de ClimaCell" + } + } + }, + "title": "ClimaCell" +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/en.json b/homeassistant/components/climacell/translations/en.json new file mode 100644 index 00000000000..ed3ead421e1 --- /dev/null +++ b/homeassistant/components/climacell/translations/en.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Failed to connect", + "invalid_api_key": "Invalid API key", + "rate_limited": "Currently rate limited, please try again later.", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name" + }, + "description": "If Latitude and Longitude are not provided, the default values in the Home Assistant configuration will be used. An entity will be created for each forecast type but only the ones you select will be enabled by default." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "forecast_types": "Forecast Type(s)", + "timestep": "Min. Between NowCast Forecasts" + }, + "description": "If you choose to enable the `nowcast` forecast entity, you can configure the number of minutes between each forecast. The number of forecasts provided depends on the number of minutes chosen between forecasts.", + "title": "Update ClimaCell Options" + } + } + }, + "title": "ClimaCell" +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/et.json b/homeassistant/components/climacell/translations/et.json new file mode 100644 index 00000000000..3722c258afa --- /dev/null +++ b/homeassistant/components/climacell/translations/et.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_api_key": "Vale API v\u00f5ti", + "rate_limited": "Hetkel on p\u00e4ringud piiratud, proovi hiljem uuesti.", + "unknown": "Tundmatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad", + "name": "Nimi" + }, + "description": "Kui [%key:component::climacell::config::step::user::d ata::latitude%] ja [%key:component::climacell::config::step::user::d ata::longitude%] andmed pole sisestatud kasutatakse Home Assistanti vaikev\u00e4\u00e4rtusi. Olem luuakse iga prognoosit\u00fc\u00fcbi jaoks kuid vaikimisi lubatakse ainult need, mille valid." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "forecast_types": "Prognoosi t\u00fc\u00fcp (t\u00fc\u00fcbid)", + "timestep": "Minuteid NowCasti prognooside vahel" + }, + "description": "Kui otsustad lubada \"nowcast\" prognoosi\u00fcksuse, saad seadistada minutite arvu iga prognoosi vahel. Esitatavate prognooside arv s\u00f5ltub prognooside vahel valitud minutite arvust.", + "title": "V\u00e4rskenda ClimaCell suvandeid" + } + } + }, + "title": "ClimaCell" +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/fr.json b/homeassistant/components/climacell/translations/fr.json new file mode 100644 index 00000000000..8fd3f7b7122 --- /dev/null +++ b/homeassistant/components/climacell/translations/fr.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_api_key": "Cl\u00e9 API invalide", + "rate_limited": "Currently rate limited, please try again later.", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nom" + }, + "description": "Si Latitude et Longitude ne sont pas fournis, les valeurs par d\u00e9faut de la configuration de Home Assistant seront utilis\u00e9es. Une entit\u00e9 sera cr\u00e9\u00e9e pour chaque type de pr\u00e9vision, mais seules celles que vous s\u00e9lectionnez seront activ\u00e9es par d\u00e9faut." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "forecast_types": "Type(s) de pr\u00e9vision", + "timestep": "Min. Entre les pr\u00e9visions NowCast" + }, + "description": "Si vous choisissez d'activer l'entit\u00e9 de pr\u00e9vision \u00abnowcast\u00bb, vous pouvez configurer le nombre de minutes entre chaque pr\u00e9vision. Le nombre de pr\u00e9visions fournies d\u00e9pend du nombre de minutes choisies entre les pr\u00e9visions.", + "title": "Mettre \u00e0 jour les options de ClimaCell" + } + } + }, + "title": "ClimaCell" +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/nl.json b/homeassistant/components/climacell/translations/nl.json new file mode 100644 index 00000000000..488a43ae24e --- /dev/null +++ b/homeassistant/components/climacell/translations/nl.json @@ -0,0 +1,32 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_api_key": "Ongeldige API-sleutel", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "name": "Naam" + }, + "description": "Indien Breedtegraad en Lengtegraad niet worden opgegeven, worden de standaardwaarden in de Home Assistant-configuratie gebruikt. Er wordt een entiteit gemaakt voor elk voorspellingstype maar alleen degene die u selecteert worden standaard ingeschakeld." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "forecast_types": "Voorspellingstype(n)" + }, + "description": "Als u ervoor kiest om de `nowcast` voorspellingsentiteit in te schakelen, kan u het aantal minuten tussen elke voorspelling configureren. Het aantal voorspellingen hangt af van het aantal gekozen minuten tussen de voorspellingen.", + "title": "Update ClimaCell Opties" + } + } + }, + "title": "ClimaCell" +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/no.json b/homeassistant/components/climacell/translations/no.json new file mode 100644 index 00000000000..64845ff7697 --- /dev/null +++ b/homeassistant/components/climacell/translations/no.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_api_key": "Ugyldig API-n\u00f8kkel", + "rate_limited": "Prisen er for \u00f8yeblikket begrenset. Pr\u00f8v igjen senere.", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "name": "Navn" + }, + "description": "Hvis Breddegrad and Lengdegrad er ikke gitt, vil standardverdiene i Home Assistant-konfigurasjonen bli brukt. Det blir opprettet en enhet for hver prognosetype, men bare de du velger blir aktivert som standard." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "forecast_types": "Prognosetype(r)", + "timestep": "Min. Mellom NowCast Prognoser" + }, + "description": "Hvis du velger \u00e5 aktivere \u00abnowcast\u00bb -varselenheten, kan du konfigurere antall minutter mellom hver prognose. Antall angitte prognoser avhenger av antall minutter som er valgt mellom prognosene.", + "title": "Oppdater ClimaCell Alternativer" + } + } + }, + "title": "ClimaCell" +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/ru.json b/homeassistant/components/climacell/translations/ru.json new file mode 100644 index 00000000000..2cce63d95ea --- /dev/null +++ b/homeassistant/components/climacell/translations/ru.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "rate_limited": "\u041f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u043e \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u043f\u044b\u0442\u043e\u043a, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\u0415\u0441\u043b\u0438 \u0428\u0438\u0440\u043e\u0442\u0430 \u0438 \u0414\u043e\u043b\u0433\u043e\u0442\u0430 \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u044b, \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0431\u0443\u0434\u0443\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 Home Assistant. \u041e\u0431\u044a\u0435\u043a\u0442\u044b \u0431\u0443\u0434\u0443\u0442 \u0441\u043e\u0437\u0434\u0430\u043d\u044b \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0442\u0438\u043f\u0430 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430, \u043d\u043e \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0431\u0443\u0434\u0443\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0435 \u0412\u0430\u043c\u0438." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "forecast_types": "\u0422\u0438\u043f(\u044b) \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430", + "timestep": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)" + }, + "description": "\u0415\u0441\u043b\u0438 \u0412\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0435\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 'nowcast', \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430.", + "title": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 ClimaCell" + } + } + }, + "title": "ClimaCell" +} \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/nl.json b/homeassistant/components/daikin/translations/nl.json index 69d52436beb..e4cf54eb365 100644 --- a/homeassistant/components/daikin/translations/nl.json +++ b/homeassistant/components/daikin/translations/nl.json @@ -5,7 +5,9 @@ "cannot_connect": "Kon niet verbinden" }, "error": { - "invalid_auth": "Ongeldige authenticatie" + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" }, "step": { "user": { diff --git a/homeassistant/components/enocean/translations/nl.json b/homeassistant/components/enocean/translations/nl.json new file mode 100644 index 00000000000..79aaec23123 --- /dev/null +++ b/homeassistant/components/enocean/translations/nl.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/faa_delays/translations/en.json b/homeassistant/components/faa_delays/translations/en.json index 48e9e1c8993..e78b15c68cb 100644 --- a/homeassistant/components/faa_delays/translations/en.json +++ b/homeassistant/components/faa_delays/translations/en.json @@ -4,16 +4,18 @@ "already_configured": "This airport is already configured." }, "error": { - "invalid_airport": "Airport code is not valid" + "cannot_connect": "Failed to connect", + "invalid_airport": "Airport code is not valid", + "unknown": "Unexpected error" }, "step": { "user": { - "title": "FAA Delays", - "description": "Enter a US Airport Code in IATA Format", "data": { "id": "Airport" - } + }, + "description": "Enter a US Airport Code in IATA Format", + "title": "FAA Delays" } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/faa_delays/translations/fr.json b/homeassistant/components/faa_delays/translations/fr.json new file mode 100644 index 00000000000..996a22c8422 --- /dev/null +++ b/homeassistant/components/faa_delays/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cet a\u00e9roport est d\u00e9j\u00e0 configur\u00e9." + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_airport": "Le code de l'a\u00e9roport n'est pas valide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "id": "A\u00e9roport" + }, + "description": "Entrez un code d'a\u00e9roport am\u00e9ricain au format IATA", + "title": "D\u00e9lais FAA" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/faa_delays/translations/nl.json b/homeassistant/components/faa_delays/translations/nl.json new file mode 100644 index 00000000000..3dbc55f5b1b --- /dev/null +++ b/homeassistant/components/faa_delays/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Deze luchthaven is al geconfigureerd." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_airport": "Luchthavencode is ongeldig", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "id": "Luchthaven" + }, + "description": "Voer een Amerikaanse luchthavencode in IATA-indeling in", + "title": "FAA-vertragingen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/nl.json b/homeassistant/components/fireservicerota/translations/nl.json index 7289d53e71f..3a6ba936dee 100644 --- a/homeassistant/components/fireservicerota/translations/nl.json +++ b/homeassistant/components/fireservicerota/translations/nl.json @@ -14,7 +14,8 @@ "reauth": { "data": { "password": "Wachtwoord" - } + }, + "description": "Authenticatietokens zijn ongeldig geworden, log in om ze opnieuw te maken." }, "user": { "data": { diff --git a/homeassistant/components/fireservicerota/translations/no.json b/homeassistant/components/fireservicerota/translations/no.json index af1ceba2c97..be485577e65 100644 --- a/homeassistant/components/fireservicerota/translations/no.json +++ b/homeassistant/components/fireservicerota/translations/no.json @@ -15,7 +15,7 @@ "data": { "password": "Passord" }, - "description": "Godkjenningstokener ble ugyldige, logg inn for \u00e5 gjenopprette dem" + "description": "Autentiseringstokener ble ugyldige, logg inn for \u00e5 gjenskape dem." }, "user": { "data": { diff --git a/homeassistant/components/firmata/translations/nl.json b/homeassistant/components/firmata/translations/nl.json new file mode 100644 index 00000000000..7cb0141826a --- /dev/null +++ b/homeassistant/components/firmata/translations/nl.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "Kan geen verbinding maken" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/nl.json b/homeassistant/components/flunearyou/translations/nl.json index c63a59e18e7..0ff044abc5e 100644 --- a/homeassistant/components/flunearyou/translations/nl.json +++ b/homeassistant/components/flunearyou/translations/nl.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Deze co\u00f6rdinaten zijn al geregistreerd." }, + "error": { + "unknown": "Onverwachte fout" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/fritzbox/translations/nl.json b/homeassistant/components/fritzbox/translations/nl.json index 71a80dbd577..9bfe2ef6be6 100644 --- a/homeassistant/components/fritzbox/translations/nl.json +++ b/homeassistant/components/fritzbox/translations/nl.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Deze AVM FRITZ!Box is al geconfigureerd.", "already_in_progress": "AVM FRITZ!Box configuratie is al bezig.", + "no_devices_found": "Geen apparaten gevonden op het netwerk", "not_supported": "Verbonden met AVM FRITZ! Box, maar het kan geen Smart Home-apparaten bedienen.", "reauth_successful": "Herauthenticatie was succesvol" }, diff --git a/homeassistant/components/goalzero/translations/nl.json b/homeassistant/components/goalzero/translations/nl.json index 86958670d70..4d9b5a397dd 100644 --- a/homeassistant/components/goalzero/translations/nl.json +++ b/homeassistant/components/goalzero/translations/nl.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "host": "Host", "name": "Naam" }, "description": "Eerst moet u de Goal Zero-app downloaden: https://www.goalzero.com/product-features/yeti-app/ \n\n Volg de instructies om je Yeti te verbinden met je wifi-netwerk. Haal dan de host-ip van uw router. DHCP moet zijn ingesteld in uw routerinstellingen voor het apparaat om ervoor te zorgen dat het host-ip niet verandert. Raadpleeg de gebruikershandleiding van uw router." diff --git a/homeassistant/components/home_connect/translations/nl.json b/homeassistant/components/home_connect/translations/nl.json index 41b27cc387f..25a81209607 100644 --- a/homeassistant/components/home_connect/translations/nl.json +++ b/homeassistant/components/home_connect/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "missing_configuration": "Het component is niet geconfigureerd. Volg de documentatie." + "missing_configuration": "Het component is niet geconfigureerd. Volg de documentatie.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})" }, "create_entry": { "default": "Succesvol geverifieerd" diff --git a/homeassistant/components/homekit/translations/ca.json b/homeassistant/components/homekit/translations/ca.json index 0870b05a6d1..dbd83622d8a 100644 --- a/homeassistant/components/homekit/translations/ca.json +++ b/homeassistant/components/homekit/translations/ca.json @@ -19,7 +19,7 @@ "title": "Selecciona els dominis a incloure" }, "pairing": { - "description": "Tan aviat com {name} estigui llest, la vinculaci\u00f3 estar\u00e0 disponible a \"Notificacions\" com a \"Configuraci\u00f3 de l'enlla\u00e7 HomeKit\".", + "description": "Per completar la vinculaci\u00f3, segueix les instruccions a \"Configuraci\u00f3 de l'enlla\u00e7 HomeKit\" sota \"Notificacions\".", "title": "Vinculaci\u00f3 HomeKit" }, "user": { @@ -28,8 +28,8 @@ "include_domains": "Dominis a incloure", "mode": "Mode" }, - "description": "La integraci\u00f3 HomeKit et permetr\u00e0 l'acc\u00e9s a les teves entitats de Home Assistant a HomeKit. En mode enlla\u00e7, els enlla\u00e7os HomeKit estan limitats a un m\u00e0xim de 150 accessoris per inst\u00e0ncia (incl\u00f2s el propi enlla\u00e7). Si volguessis enlla\u00e7ar m\u00e9s accessoris que el m\u00e0xim perm\u00e8s, \u00e9s recomanable que utilitzis diferents enlla\u00e7os HomeKit per a dominis diferents. La configuraci\u00f3 avan\u00e7ada d'entitat nom\u00e9s est\u00e0 disponible en YAML. Per obtenir el millor rendiment i evitar errors de disponibilitat inesperats , crea i vincula una inst\u00e0ncia HomeKit en mode accessori per a cada repoductor multim\u00e8dia/TV i c\u00e0mera.", - "title": "Activaci\u00f3 de HomeKit" + "description": "Selecciona els dominis a incloure. S'inclouran totes les entitats del domini compatibles. Es crear\u00e0 una inst\u00e0ncia HomeKit en mode accessori per a cada repoductor multim\u00e8dia/TV i c\u00e0mera.", + "title": "Selecciona els dominis a incloure" } } }, @@ -55,7 +55,7 @@ "entities": "Entitats", "mode": "Mode" }, - "description": "Tria les entitats que vulguis incloure. En mode accessori, nom\u00e9s s'inclou una sola entitat. En mode enlla\u00e7 inclusiu, s'exposaran totes les entitats del domini tret de que se'n seleccionin algunes en concret. En mode enlla\u00e7 excusiu, s'inclouran totes les entitats del domini excepte les entitats excloses. Per obtenir el millor rendiment i evitar errors de disponibilitat inesperats , crea i vincula una inst\u00e0ncia HomeKit en mode accessori per a cada repoductor multim\u00e8dia/TV i c\u00e0mera.", + "description": "Tria les entitats que vulguis incloure. En mode accessori, nom\u00e9s s'inclou una sola entitat. En mode enlla\u00e7 inclusiu, s'exposaran totes les entitats del domini tret de que se'n seleccionin algunes en concret. En mode enlla\u00e7 excusiu, s'inclouran totes les entitats del domini excepte les entitats excloses. Per obtenir el millor rendiment, es crea una inst\u00e0ncia HomeKit per a cada repoductor multim\u00e8dia/TV i c\u00e0mera.", "title": "Selecciona les entitats a incloure" }, "init": { diff --git a/homeassistant/components/homekit/translations/en.json b/homeassistant/components/homekit/translations/en.json index e9aaeb60df8..3b0129567c4 100644 --- a/homeassistant/components/homekit/translations/en.json +++ b/homeassistant/components/homekit/translations/en.json @@ -4,13 +4,29 @@ "port_name_in_use": "An accessory or bridge with the same name or port is already configured." }, "step": { + "accessory_mode": { + "data": { + "entity_id": "Entity" + }, + "description": "Choose the entity to be included. In accessory mode, only a single entity is included.", + "title": "Select entity to be included" + }, + "bridge_mode": { + "data": { + "include_domains": "Domains to include" + }, + "description": "Choose the domains to be included. All supported entities in the domain will be included.", + "title": "Select domains to be included" + }, "pairing": { "description": "To complete pairing following the instructions in \u201cNotifications\u201d under \u201cHomeKit Pairing\u201d.", "title": "Pair HomeKit" }, "user": { "data": { - "include_domains": "Domains to include" + "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", + "include_domains": "Domains to include", + "mode": "Mode" }, "description": "Choose the domains to be included. All supported entities in the domain will be included. A separate HomeKit instance in accessory mode will be created for each tv media player and camera.", "title": "Select domains to be included" @@ -21,7 +37,8 @@ "step": { "advanced": { "data": { - "auto_start": "Autostart (disable if you are calling the homekit.start service manually)" + "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.", "title": "Advanced Configuration" diff --git a/homeassistant/components/homekit/translations/et.json b/homeassistant/components/homekit/translations/et.json index 37bff5f9b70..8c24d2d2251 100644 --- a/homeassistant/components/homekit/translations/et.json +++ b/homeassistant/components/homekit/translations/et.json @@ -19,7 +19,7 @@ "title": "Vali kaasatavad domeenid" }, "pairing": { - "description": "Niipea kui {name} on valmis, on sidumine saadaval jaotises \"Notifications\" kui \"HomeKit Bridge Setup\".", + "description": "Sidumise l\u00f5puleviimiseks j\u00e4rgi jaotises \"HomeKiti sidumine\" toodud juhiseid alajaotises \"Teatised\".", "title": "HomeKiti sidumine" }, "user": { @@ -28,8 +28,8 @@ "include_domains": "Kaasatavad domeenid", "mode": "Re\u017eiim" }, - "description": "HomeKiti integreerimine v\u00f5imaldab teil p\u00e4\u00e4seda juurde HomeKiti \u00fcksustele Home Assistant. Sildire\u017eiimis on HomeKit Bridges piiratud 150 lisaseadmega, sealhulgas sild ise. Kui soovid \u00fchendada rohkem lisatarvikuid, on soovitatav kasutada erinevate domeenide jaoks mitut HomeKiti silda. \u00dcksuse \u00fcksikasjalik konfiguratsioon on esmase silla jaoks saadaval ainult YAML-i kaudu. Parema tulemuse saavutamiseks ja ootamatute seadmete kadumise v\u00e4ltimiseks loo ja seo eraldi HomeKiti seade tarviku re\u017eiimis kga meediaesitaja ja kaamera jaoks.", - "title": "Aktiveeri HomeKit" + "description": "Vali kaasatavad domeenid. Kaasatakse k\u00f5ik domeenis toetatud olemid. Iga telemeedia pleieri ja kaamera jaoks luuakse eraldi HomeKiti eksemplar tarvikure\u017eiimis.", + "title": "Vali kaasatavad domeenid" } } }, @@ -55,12 +55,12 @@ "entities": "Olemid", "mode": "Re\u017eiim" }, - "description": "Vali kaasatavad olemid. Tarvikute re\u017eiimis on kaasatav ainult \u00fcks olem. Silla re\u017eiimis, kuvatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud juhul, kui valitud on kindlad olemid. Silla v\u00e4listamisre\u017eiimis kaasatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud v\u00e4listatud olemid.", + "description": "Vali kaasatavad olemid. Tarvikute re\u017eiimis on kaasatav ainult \u00fcks olem. Silla re\u017eiimis, kuvatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud juhul, kui valitud on kindlad olemid. Silla v\u00e4listamisre\u017eiimis kaasatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud v\u00e4listatud olemid. Parima kasutuskogemuse jaoks on eraldi HomeKit seadmed iga meediumim\u00e4ngija ja kaamera jaoks.", "title": "Vali kaasatavd olemid" }, "init": { "data": { - "include_domains": "Kaasatavad domeenid", + "include_domains": "Kaasatud domeenid", "mode": "Re\u017eiim" }, "description": "HomeKiti saab seadistada silla v\u00f5i \u00fche lisaseadme avaldamiseks. Lisare\u017eiimis saab kasutada ainult \u00fchte \u00fcksust. Teleriseadmete klassiga meediumipleierite n\u00f5uetekohaseks toimimiseks on vaja lisare\u017eiimi. \u201eKaasatavate domeenide\u201d \u00fcksused puutuvad kokku HomeKitiga. J\u00e4rgmisel ekraanil saad valida, millised \u00fcksused sellesse loendisse lisada v\u00f5i sellest v\u00e4lja j\u00e4tta.", diff --git a/homeassistant/components/homekit/translations/fr.json b/homeassistant/components/homekit/translations/fr.json index a0f10c9684a..4721514e615 100644 --- a/homeassistant/components/homekit/translations/fr.json +++ b/homeassistant/components/homekit/translations/fr.json @@ -19,7 +19,7 @@ "title": "S\u00e9lectionnez les domaines \u00e0 inclure" }, "pairing": { - "description": "D\u00e8s que le pont {name} est pr\u00eat, l'appairage sera disponible dans \"Notifications\" sous \"Configuration de la Passerelle HomeKit\".", + "description": "Pour compl\u00e9ter l'appariement, suivez les instructions dans les \"Notifications\" sous \"Appariement HomeKit\".", "title": "Appairage de la Passerelle Homekit" }, "user": { @@ -28,8 +28,8 @@ "include_domains": "Domaines \u00e0 inclure", "mode": "Mode" }, - "description": "La passerelle HomeKit vous permettra d'acc\u00e9der \u00e0 vos entit\u00e9s Home Assistant dans HomeKit. Les passerelles HomeKit sont limit\u00e9es \u00e0 150 accessoires par instance, y compris la passerelle elle-m\u00eame. Si vous souhaitez connecter plus que le nombre maximum d'accessoires, il est recommand\u00e9 d'utiliser plusieurs passerelles HomeKit pour diff\u00e9rents domaines. La configuration d\u00e9taill\u00e9e des entit\u00e9s est uniquement disponible via YAML pour la passerelle principale.", - "title": "Activer la Passerelle HomeKit" + "description": "Choisissez les domaines \u00e0 inclure. Toutes les entit\u00e9s prises en charge dans le domaine seront incluses. Une instance HomeKit distincte en mode accessoire sera cr\u00e9\u00e9e pour chaque lecteur multim\u00e9dia TV et cam\u00e9ra.", + "title": "S\u00e9lectionnez les domaines \u00e0 inclure" } } }, @@ -60,7 +60,7 @@ }, "init": { "data": { - "include_domains": "Domaine \u00e0 inclure", + "include_domains": "Domaines \u00e0 inclure", "mode": "Mode" }, "description": "Les entit\u00e9s des \u00abdomaines \u00e0 inclure\u00bb seront pont\u00e9es vers HomeKit. Vous pourrez s\u00e9lectionner les entit\u00e9s \u00e0 exclure de cette liste sur l'\u00e9cran suivant.", diff --git a/homeassistant/components/homekit/translations/no.json b/homeassistant/components/homekit/translations/no.json index 9a64def4156..4748fe63af2 100644 --- a/homeassistant/components/homekit/translations/no.json +++ b/homeassistant/components/homekit/translations/no.json @@ -19,7 +19,7 @@ "title": "Velg domener som skal inkluderes" }, "pairing": { - "description": "S\u00e5 snart {name} er klart, vil sammenkobling v\u00e6re tilgjengelig i \"Notifications\" som \"HomeKit Bridge Setup\".", + "description": "For \u00e5 fullf\u00f8re sammenkoblingen ved \u00e5 f\u00f8lge instruksjonene i \"Varsler\" under \"Sammenkobling av HomeKit\".", "title": "Koble sammen HomeKit" }, "user": { @@ -28,8 +28,8 @@ "include_domains": "Domener \u00e5 inkludere", "mode": "Modus" }, - "description": "HomeKit-integrasjonen gir deg tilgang til Home Assistant-enhetene dine i HomeKit. I bromodus er HomeKit Bridges begrenset til 150 tilbeh\u00f8r per forekomst inkludert selve broen. Hvis du \u00f8nsker \u00e5 bygge bro over maksimalt antall tilbeh\u00f8r, anbefales det at du bruker flere HomeKit-broer for forskjellige domener. Detaljert enhetskonfigurasjon er bare tilgjengelig via YAML. For best ytelse og for \u00e5 forhindre uventet utilgjengelighet, opprett og par sammen en egen HomeKit-forekomst i tilbeh\u00f8rsmodus for hver tv-mediaspiller og kamera.", - "title": "Aktiver HomeKit" + "description": "Velg domenene som skal inkluderes. Alle st\u00f8ttede enheter i domenet vil bli inkludert. Det opprettes en egen HomeKit-forekomst i tilbeh\u00f8rsmodus for hver tv-mediaspiller og kamera.", + "title": "Velg domener som skal inkluderes" } } }, @@ -55,7 +55,7 @@ "entities": "Entiteter", "mode": "Modus" }, - "description": "Velg enhetene som skal inkluderes. I tilbeh\u00f8rsmodus er bare \u00e9n enkelt enhet inkludert. I bridge include-modus inkluderes alle enheter i domenet med mindre bestemte enheter er valgt. I brounnlatingsmodus inkluderes alle enheter i domenet, med unntak av de utelatte enhetene. For best mulig ytelse, og for \u00e5 forhindre uventet utilgjengelighet, opprett og par en separat HomeKit-forekomst i tilbeh\u00f8rsmodus for hver tv-mediespiller og kamera.", + "description": "Velg enhetene som skal inkluderes. I tilbeh\u00f8rsmodus er bare en enkelt enhet inkludert. I bridge-inkluderingsmodus vil alle enheter i domenet bli inkludert, med mindre spesifikke enheter er valgt. I bridge-ekskluderingsmodus vil alle enheter i domenet bli inkludert, bortsett fra de ekskluderte enhetene. For best ytelse vil et eget HomeKit-tilbeh\u00f8r v\u00e6re TV-mediaspiller og kamera.", "title": "Velg enheter som skal inkluderes" }, "init": { diff --git a/homeassistant/components/homekit/translations/pl.json b/homeassistant/components/homekit/translations/pl.json index 2679a4de20a..ef35ff667c4 100644 --- a/homeassistant/components/homekit/translations/pl.json +++ b/homeassistant/components/homekit/translations/pl.json @@ -19,7 +19,7 @@ "title": "Wybierz uwzgl\u0119dniane domeny" }, "pairing": { - "description": "Gdy tylko {name} b\u0119dzie gotowy, opcja parowania b\u0119dzie dost\u0119pna w \u201ePowiadomieniach\u201d jako \u201eKonfiguracja mostka HomeKit\u201d.", + "description": "Aby doko\u0144czy\u0107 parowanie, post\u0119puj wg instrukcji \u201eParowanie HomeKit\u201d w \u201ePowiadomieniach\u201d.", "title": "Parowanie z HomeKit" }, "user": { @@ -28,8 +28,8 @@ "include_domains": "Domeny do uwzgl\u0119dnienia", "mode": "Tryb" }, - "description": "Integracja HomeKit pozwala na dost\u0119p do Twoich encji Home Assistant w HomeKit. W trybie \"Mostka\", mostki HomeKit s\u0105 ograniczone do 150 urz\u0105dze\u0144, w\u0142\u0105czaj\u0105c w to sam mostek. Je\u015bli chcesz wi\u0119cej ni\u017c dozwolona maksymalna liczba urz\u0105dze\u0144, zaleca si\u0119 u\u017cywanie wielu most\u00f3w HomeKit dla r\u00f3\u017cnych domen. Szczeg\u00f3\u0142owa konfiguracja encji jest dost\u0119pna tylko w trybie YAML dla g\u0142\u00f3wnego mostka. Dla najlepszej wydajno\u015bci oraz by zapobiec nieprzewidzianej niedost\u0119pno\u015bci urz\u0105dzenia, utw\u00f3rz i sparuj oddzieln\u0105 instancj\u0119 HomeKit w trybie akcesorium dla ka\u017cdego media playera oraz kamery.", - "title": "Aktywacja HomeKit" + "description": "Wybierz domeny do uwzgl\u0119dnienia. Wszystkie wspierane encje w danej domenie b\u0119d\u0105 uwzgl\u0119dnione. W trybie akcesorium, oddzielna instancja HomeKit zostanie utworzona dla ka\u017cdego tv media playera oraz kamery.", + "title": "Wybierz uwzgl\u0119dniane domeny" } } }, @@ -55,7 +55,7 @@ "entities": "Encje", "mode": "Tryb" }, - "description": "Wybierz encje, kt\u00f3re maj\u0105 by\u0107 uwzgl\u0119dnione. W trybie \"Akcesorium\" tylko jedna encja jest uwzgl\u0119dniona. W trybie \"Uwzgl\u0119dnij mostek\", wszystkie encje w danej domenie b\u0119d\u0105 uwzgl\u0119dnione, chyba \u017ce wybrane s\u0105 tylko konkretne encje. W trybie \"Wyklucz mostek\", wszystkie encje b\u0119d\u0105 uwzgl\u0119dnione, z wyj\u0105tkiem tych wybranych. Dla najlepszej wydajno\u015bci oraz by zapobiec nieprzewidzianej niedost\u0119pno\u015bci urz\u0105dzenia, utw\u00f3rz i sparuj oddzieln\u0105 instancj\u0119 HomeKit w trybie akcesorium dla ka\u017cdego media playera oraz kamery.", + "description": "Wybierz encje, kt\u00f3re maj\u0105 by\u0107 uwzgl\u0119dnione. W trybie \"Akcesorium\" tylko jedna encja jest uwzgl\u0119dniona. W trybie \"Uwzgl\u0119dnij mostek\", wszystkie encje w danej domenie b\u0119d\u0105 uwzgl\u0119dnione, chyba \u017ce wybrane s\u0105 tylko konkretne encje. W trybie \"Wyklucz mostek\", wszystkie encje b\u0119d\u0105 uwzgl\u0119dnione, z wyj\u0105tkiem tych wybranych. Dla najlepszej wydajno\u015bci, zostanie utworzone oddzielne akcesorium HomeKit dla ka\u017cdego tv media playera oraz kamery.", "title": "Wybierz encje, kt\u00f3re maj\u0105 by\u0107 uwzgl\u0119dnione" }, "init": { diff --git a/homeassistant/components/homekit/translations/ru.json b/homeassistant/components/homekit/translations/ru.json index 84346aed2ef..d00744e4cb4 100644 --- a/homeassistant/components/homekit/translations/ru.json +++ b/homeassistant/components/homekit/translations/ru.json @@ -19,7 +19,7 @@ "title": "\u0412\u044b\u0431\u043e\u0440 \u0434\u043e\u043c\u0435\u043d\u043e\u0432 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit" }, "pairing": { - "description": "\u041a\u0430\u043a \u0442\u043e\u043b\u044c\u043a\u043e {name} \u0431\u0443\u0434\u0435\u0442 \u0433\u043e\u0442\u043e\u0432\u043e, \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u0432 \"\u0423\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f\u0445\" \u043a\u0430\u043a \"HomeKit Bridge Setup\".", + "description": "\u0414\u043b\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f \u0441\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c, \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u043c \u0432 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0438 \"HomeKit Pairing\".", "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 HomeKit" }, "user": { @@ -28,8 +28,8 @@ "include_domains": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u044b", "mode": "\u0420\u0435\u0436\u0438\u043c" }, - "description": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u043e\u0431\u044a\u0435\u043a\u0442\u0430\u043c Home Assistant \u0447\u0435\u0440\u0435\u0437 HomeKit. HomeKit Bridge \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d 150 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430\u043c\u0438 \u043d\u0430 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440, \u0432\u043a\u043b\u044e\u0447\u0430\u044f \u0441\u0430\u043c \u0431\u0440\u0438\u0434\u0436. \u0415\u0441\u043b\u0438 \u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0431\u043e\u043b\u044c\u0448\u0435, \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e HomeKit Bridge \u0434\u043b\u044f \u0440\u0430\u0437\u043d\u044b\u0445 \u0434\u043e\u043c\u0435\u043d\u043e\u0432. \u0414\u0435\u0442\u0430\u043b\u044c\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 YAML. \u0414\u043b\u044f \u043b\u0443\u0447\u0448\u0435\u0439 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0438 \u043f\u0440\u0435\u0434\u043e\u0442\u0432\u0440\u0430\u0449\u0435\u043d\u0438\u044f \u043d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u043e\u0441\u0442\u0435\u0439 \u0441\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0443\u044e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440\u0430 \u0438\u043b\u0438 \u043a\u0430\u043c\u0435\u0440\u044b.", - "title": "HomeKit" + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u043e\u043c\u0435\u043d\u044b. \u0411\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u0437 \u0434\u043e\u043c\u0435\u043d\u0430. \u0414\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440\u0430 \u0438\u043b\u0438 \u043a\u0430\u043c\u0435\u0440\u044b \u0431\u0443\u0434\u0435\u0442 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0430\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430.", + "title": "\u0412\u044b\u0431\u043e\u0440 \u0434\u043e\u043c\u0435\u043d\u043e\u0432 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit" } } }, @@ -55,7 +55,7 @@ "entities": "\u041e\u0431\u044a\u0435\u043a\u0442\u044b", "mode": "\u0420\u0435\u0436\u0438\u043c" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u043e\u0431\u044a\u0435\u043a\u0442. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u043c\u043e\u0441\u0442\u0430 \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u0435\u0441\u043b\u0438 \u043d\u0435 \u0432\u044b\u0431\u0440\u0430\u043d\u044b \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u043a\u0440\u043e\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044b\u0445. \u0414\u043b\u044f \u043b\u0443\u0447\u0448\u0435\u0439 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0438 \u043f\u0440\u0435\u0434\u043e\u0442\u0432\u0440\u0430\u0449\u0435\u043d\u0438\u044f \u043d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u043e\u0441\u0442\u0435\u0439 \u0441\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0443\u044e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440\u0430 \u0438\u043b\u0438 \u043a\u0430\u043c\u0435\u0440\u044b.", + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u043e\u0431\u044a\u0435\u043a\u0442. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u043c\u043e\u0441\u0442\u0430 \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u0435\u0441\u043b\u0438 \u043d\u0435 \u0432\u044b\u0431\u0440\u0430\u043d\u044b \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u043a\u0440\u043e\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044b\u0445. \u0414\u043b\u044f \u0443\u043b\u0443\u0447\u0448\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0442\u044c \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0443\u044e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440\u0430 \u0438\u043b\u0438 \u043a\u0430\u043c\u0435\u0440\u044b.", "title": "\u0412\u044b\u0431\u043e\u0440 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit" }, "init": { diff --git a/homeassistant/components/homekit/translations/zh-Hant.json b/homeassistant/components/homekit/translations/zh-Hant.json index 605263c4489..95a0782cf12 100644 --- a/homeassistant/components/homekit/translations/zh-Hant.json +++ b/homeassistant/components/homekit/translations/zh-Hant.json @@ -19,7 +19,7 @@ "title": "\u9078\u64c7\u8981\u5305\u542b\u7684\u7db2\u57df" }, "pairing": { - "description": "\u65bc {name} \u5c31\u7dd2\u5f8c\u3001\u5c07\u6703\u65bc\u300c\u901a\u77e5\u300d\u4e2d\u986f\u793a\u300cHomeKit Bridge \u8a2d\u5b9a\u300d\u7684\u914d\u5c0d\u8cc7\u8a0a\u3002", + "description": "\u6b32\u5b8c\u6210\u914d\u5c0d\u3001\u8acb\u8ddf\u96a8\u300c\u901a\u77e5\u300d\u5167\u7684\u300cHomekit \u914d\u5c0d\u300d\u6307\u5f15\u3002", "title": "\u914d\u5c0d HomeKit" }, "user": { @@ -28,8 +28,8 @@ "include_domains": "\u5305\u542b\u7db2\u57df", "mode": "\u6a21\u5f0f" }, - "description": "HomeKit \u6574\u5408\u5c07\u53ef\u5141\u8a31\u65bc Homekit \u4e2d\u4f7f\u7528 Home Assistant \u5be6\u9ad4\u3002\u65bc\u6a4b\u63a5\u6a21\u5f0f\u4e0b\u3001HomeKit Bridges \u6700\u9ad8\u9650\u5236\u70ba 150 \u500b\u914d\u4ef6\u3001\u5305\u542b Bridge \u672c\u8eab\u3002\u5047\u5982\u60f3\u8981\u4f7f\u7528\u8d85\u904e\u9650\u5236\u4ee5\u4e0a\u7684\u914d\u4ef6\uff0c\u5efa\u8b70\u53ef\u4ee5\u4e0d\u540c\u7db2\u57df\u4f7f\u7528\u591a\u500b HomeKit bridges \u9054\u5230\u6b64\u9700\u6c42\u3002\u50c5\u80fd\u65bc\u4e3b Bridge \u4ee5 YAML \u8a2d\u5b9a\u8a73\u7d30\u5be6\u9ad4\u3002\u70ba\u53d6\u5f97\u6700\u4f73\u6548\u80fd\u3001\u4e26\u907f\u514d\u672a\u9810\u671f\u7121\u6cd5\u4f7f\u7528\u72c0\u614b\uff0c\u96fb\u8996\u5a92\u9ad4\u64ad\u653e\u5668\u8207\u651d\u5f71\u6a5f\uff0c\u8acb\u65bc Homekit \u914d\u4ef6\u6a21\u5f0f\u4e2d\u5206\u5225\u9032\u884c\u914d\u5c0d\u3002", - "title": "\u555f\u7528 HomeKit" + "description": "\u9078\u64c7\u6240\u8981\u5305\u542b\u7684\u7db2\u57df\uff0c\u6240\u6709\u8a72\u7db2\u57df\u5167\u652f\u63f4\u7684\u5be6\u9ad4\u90fd\u5c07\u6703\u88ab\u5305\u542b\u3002 \u5176\u4ed6 Homekit \u5a92\u9ad4\u64ad\u653e\u5668\u8207\u651d\u5f71\u6a5f\u5be6\u4f8b\uff0c\u5c07\u6703\u4ee5\u914d\u4ef6\u6a21\u5f0f\u65b0\u589e\u3002", + "title": "\u9078\u64c7\u8981\u5305\u542b\u7684\u7db2\u57df" } } }, @@ -55,7 +55,7 @@ "entities": "\u5be6\u9ad4", "mode": "\u6a21\u5f0f" }, - "description": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4\u3002\u65bc\u914d\u4ef6\u6a21\u5f0f\u4e0b\u3001\u50c5\u6709\u55ae\u4e00\u5be6\u9ad4\u5c07\u6703\u5305\u542b\u3002\u65bc\u6a4b\u63a5\u5305\u542b\u6a21\u5f0f\u4e0b\u3001\u6240\u6709\u7db2\u57df\u7684\u5be6\u9ad4\u90fd\u5c07\u5305\u542b\uff0c\u9664\u975e\u9078\u64c7\u7279\u5b9a\u7684\u5be6\u9ad4\u3002\u65bc\u6a4b\u63a5\u6392\u9664\u6a21\u5f0f\u4e2d\u3001\u6240\u6709\u7db2\u57df\u4e2d\u7684\u5be6\u9ad4\u90fd\u5c07\u5305\u542b\uff0c\u9664\u4e86\u6392\u9664\u7684\u5be6\u9ad4\u3002\u70ba\u53d6\u5f97\u6700\u4f73\u6548\u80fd\u3001\u4e26\u907f\u514d\u672a\u9810\u671f\u7121\u6cd5\u4f7f\u7528\u72c0\u614b\uff0c\u96fb\u8996\u5a92\u9ad4\u64ad\u653e\u5668\u8207\u651d\u5f71\u6a5f\uff0c\u8acb\u65bc Homekit \u914d\u4ef6\u6a21\u5f0f\u4e2d\u5206\u5225\u9032\u884c\u914d\u5c0d\u3002", + "description": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4\u3002\u65bc\u914d\u4ef6\u6a21\u5f0f\u4e0b\u3001\u50c5\u6709\u55ae\u4e00\u5be6\u9ad4\u5c07\u6703\u5305\u542b\u3002\u65bc\u6a4b\u63a5\u5305\u542b\u6a21\u5f0f\u4e0b\u3001\u6240\u6709\u7db2\u57df\u7684\u5be6\u9ad4\u90fd\u5c07\u5305\u542b\uff0c\u9664\u975e\u9078\u64c7\u7279\u5b9a\u7684\u5be6\u9ad4\u3002\u65bc\u6a4b\u63a5\u6392\u9664\u6a21\u5f0f\u4e2d\u3001\u6240\u6709\u7db2\u57df\u4e2d\u7684\u5be6\u9ad4\u90fd\u5c07\u5305\u542b\uff0c\u9664\u4e86\u6392\u9664\u7684\u5be6\u9ad4\u3002\u70ba\u53d6\u5f97\u6700\u4f73\u6548\u80fd\u3001\u96fb\u8996\u5a92\u9ad4\u64ad\u653e\u5668\u8207\u651d\u5f71\u6a5f\uff0c\u5c07\u65bc Homekit \u914d\u4ef6\u6a21\u5f0f\u9032\u884c\u3002", "title": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4" }, "init": { diff --git a/homeassistant/components/huawei_lte/translations/nl.json b/homeassistant/components/huawei_lte/translations/nl.json index dd51fdc1bc5..d420093996c 100644 --- a/homeassistant/components/huawei_lte/translations/nl.json +++ b/homeassistant/components/huawei_lte/translations/nl.json @@ -19,6 +19,7 @@ "user": { "data": { "password": "Wachtwoord", + "url": "URL", "username": "Gebruikersnaam" }, "description": "Voer de toegangsgegevens van het apparaat in. Opgeven van gebruikersnaam en wachtwoord is optioneel, maar biedt ondersteuning voor meer integratiefuncties. Aan de andere kant kan het gebruik van een geautoriseerde verbinding problemen veroorzaken bij het openen van het webinterface van het apparaat buiten de Home Assitant, terwijl de integratie actief is en andersom.", diff --git a/homeassistant/components/icloud/translations/nl.json b/homeassistant/components/icloud/translations/nl.json index 97673069054..b150c8d5b16 100644 --- a/homeassistant/components/icloud/translations/nl.json +++ b/homeassistant/components/icloud/translations/nl.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Account reeds geconfigureerd", - "no_device": "Op geen van uw apparaten is \"Find my iPhone\" geactiveerd" + "no_device": "Op geen van uw apparaten is \"Find my iPhone\" geactiveerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "invalid_auth": "Ongeldige authenticatie", @@ -14,7 +15,8 @@ "data": { "password": "Wachtwoord" }, - "description": "Uw eerder ingevoerde wachtwoord voor {username} werkt niet meer. Update uw wachtwoord om deze integratie te blijven gebruiken." + "description": "Uw eerder ingevoerde wachtwoord voor {username} werkt niet meer. Update uw wachtwoord om deze integratie te blijven gebruiken.", + "title": "Verifieer de integratie opnieuw" }, "trusted_device": { "data": { diff --git a/homeassistant/components/ifttt/translations/nl.json b/homeassistant/components/ifttt/translations/nl.json index e7da47dd658..82006860db3 100644 --- a/homeassistant/components/ifttt/translations/nl.json +++ b/homeassistant/components/ifttt/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.", + "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." }, "create_entry": { "default": "Om evenementen naar de Home Assistant te verzenden, moet u de actie \"Een webverzoek doen\" gebruiken vanuit de [IFTTT Webhook-applet]({applet_url}). \n\n Vul de volgende info in: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\nZie [the documentation]({docs_url}) voor informatie over het configureren van automatiseringen om inkomende gegevens te verwerken." diff --git a/homeassistant/components/insteon/translations/nl.json b/homeassistant/components/insteon/translations/nl.json index d2f73fca37b..e4f7d4a8102 100644 --- a/homeassistant/components/insteon/translations/nl.json +++ b/homeassistant/components/insteon/translations/nl.json @@ -28,6 +28,9 @@ "title": "Insteon Hub versie 2" }, "plm": { + "data": { + "device": "USB-apparaatpad" + }, "description": "Configureer de Insteon PowerLink Modem (PLM).", "title": "Insteon PLM" }, diff --git a/homeassistant/components/keenetic_ndms2/translations/nl.json b/homeassistant/components/keenetic_ndms2/translations/nl.json index f422e2641f6..c3c08575052 100644 --- a/homeassistant/components/keenetic_ndms2/translations/nl.json +++ b/homeassistant/components/keenetic_ndms2/translations/nl.json @@ -21,7 +21,8 @@ "step": { "user": { "data": { - "interfaces": "Kies interfaces om te scannen" + "interfaces": "Kies interfaces om te scannen", + "scan_interval": "Scaninterval" } } } diff --git a/homeassistant/components/kmtronic/translations/fr.json b/homeassistant/components/kmtronic/translations/fr.json new file mode 100644 index 00000000000..45620fe7795 --- /dev/null +++ b/homeassistant/components/kmtronic/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/pl.json b/homeassistant/components/kmtronic/translations/pl.json new file mode 100644 index 00000000000..25dab56796c --- /dev/null +++ b/homeassistant/components/kmtronic/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/ca.json b/homeassistant/components/litejet/translations/ca.json new file mode 100644 index 00000000000..39e2a56dc4d --- /dev/null +++ b/homeassistant/components/litejet/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "error": { + "open_failed": "No s'ha pogut obrir el port s\u00e8rie especificat." + }, + "step": { + "user": { + "data": { + "port": "Port" + }, + "description": "Connecta el port RS232-2 LiteJet a l'ordinador i introdueix la ruta al port s\u00e8rie del dispositiu.\n\nEl LiteJet MCP ha d'estar configurat amb una velocitat de 19.2 k baudis, 8 bits de dades, 1 bit de parada, sense paritat i ha de transmetre un 'CR' despr\u00e9s de cada resposta.", + "title": "Connexi\u00f3 amb LiteJet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/fr.json b/homeassistant/components/litejet/translations/fr.json new file mode 100644 index 00000000000..455ba7fdc0c --- /dev/null +++ b/homeassistant/components/litejet/translations/fr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "step": { + "user": { + "data": { + "port": "Port" + }, + "description": "Connectez le port RS232-2 du LiteJet \u00e0 votre ordinateur et entrez le chemin d'acc\u00e8s au p\u00e9riph\u00e9rique de port s\u00e9rie. \n\n Le LiteJet MCP doit \u00eatre configur\u00e9 pour 19,2 K bauds, 8 bits de donn\u00e9es, 1 bit d'arr\u00eat, sans parit\u00e9 et pour transmettre un \u00abCR\u00bb apr\u00e8s chaque r\u00e9ponse.", + "title": "Connectez-vous \u00e0 LiteJet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/no.json b/homeassistant/components/litejet/translations/no.json new file mode 100644 index 00000000000..26ccd333546 --- /dev/null +++ b/homeassistant/components/litejet/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "error": { + "open_failed": "Kan ikke \u00e5pne den angitte serielle porten." + }, + "step": { + "user": { + "data": { + "port": "Port" + }, + "description": "Koble LiteJets RS232-2-port til datamaskinen og skriv stien til den serielle portenheten. \n\n LiteJet MCP m\u00e5 konfigureres for 19,2 K baud, 8 databiter, 1 stoppbit, ingen paritet, og for \u00e5 overf\u00f8re en 'CR' etter hvert svar.", + "title": "Koble til LiteJet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/pl.json b/homeassistant/components/litejet/translations/pl.json new file mode 100644 index 00000000000..20e5d68288d --- /dev/null +++ b/homeassistant/components/litejet/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "error": { + "open_failed": "Nie mo\u017cna otworzy\u0107 okre\u015blonego portu szeregowego." + }, + "step": { + "user": { + "data": { + "port": "Port" + }, + "description": "Pod\u0142\u0105cz port RS232-2 LiteJet do komputera i wprowad\u017a \u015bcie\u017ck\u0119 do urz\u0105dzenia portu szeregowego. \n\nLiteJet MCP musi by\u0107 skonfigurowany dla szybko\u015bci 19,2K, 8 bit\u00f3w danych, 1 bit stopu, brak parzysto\u015bci i przesy\u0142anie \u201eCR\u201d po ka\u017cdej odpowiedzi.", + "title": "Po\u0142\u0105czenie z LiteJet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/ru.json b/homeassistant/components/litejet/translations/ru.json new file mode 100644 index 00000000000..c90e6956301 --- /dev/null +++ b/homeassistant/components/litejet/translations/ru.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "error": { + "open_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442." + }, + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u043f\u043e\u0440\u0442 RS232-2 LiteJet \u043a \u043a\u043e\u043c\u043f\u044c\u044e\u0442\u0435\u0440\u0443, \u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0443\u0442\u044c \u043a \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u043c\u0443 \u043f\u043e\u0440\u0442\u0443. \n\nLiteJet MCP \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u043d\u0430 \u0441\u043a\u043e\u0440\u043e\u0441\u0442\u044c 19,2 \u041a\u0431\u043e\u0434, 8 \u0431\u0438\u0442 \u0434\u0430\u043d\u043d\u044b\u0445, 1 \u0441\u0442\u043e\u043f\u043e\u0432\u044b\u0439 \u0431\u0438\u0442, \u0431\u0435\u0437 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f \u0447\u0435\u0442\u043d\u043e\u0441\u0442\u0438 \u0438 \u043d\u0430 \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0443 'CR' \u043f\u043e\u0441\u043b\u0435 \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043e\u0442\u0432\u0435\u0442\u0430.", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a LiteJet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/tr.json b/homeassistant/components/litejet/translations/tr.json new file mode 100644 index 00000000000..de4ea12cb6f --- /dev/null +++ b/homeassistant/components/litejet/translations/tr.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "LiteJet'e Ba\u011flan\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/zh-Hant.json b/homeassistant/components/litejet/translations/zh-Hant.json new file mode 100644 index 00000000000..8a268f3db49 --- /dev/null +++ b/homeassistant/components/litejet/translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "error": { + "open_failed": "\u7121\u6cd5\u958b\u555f\u6307\u5b9a\u7684\u5e8f\u5217\u57e0" + }, + "step": { + "user": { + "data": { + "port": "\u901a\u8a0a\u57e0" + }, + "description": "\u9023\u7dda\u81f3\u96fb\u8166\u4e0a\u7684 LiteJet RS232-2 \u57e0\uff0c\u4e26\u8f38\u5165\u5e8f\u5217\u57e0\u88dd\u7f6e\u7684\u8def\u5f91\u3002\n\nLiteJet MCP \u5fc5\u9808\u8a2d\u5b9a\u70ba\u901a\u8a0a\u901f\u7387 19.2 K baud\u30018 \u6578\u64da\u4f4d\u5143\u30011 \u505c\u6b62\u4f4d\u5143\u3001\u7121\u540c\u4f4d\u4f4d\u5143\u4e26\u65bc\u6bcf\u500b\u56de\u5fa9\u5f8c\u50b3\u9001 'CR'\u3002", + "title": "\u9023\u7dda\u81f3 LiteJet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/fr.json b/homeassistant/components/litterrobot/translations/fr.json new file mode 100644 index 00000000000..aa84ec33d8c --- /dev/null +++ b/homeassistant/components/litterrobot/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/no.json b/homeassistant/components/litterrobot/translations/no.json new file mode 100644 index 00000000000..4ea7b2401c3 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/pl.json b/homeassistant/components/litterrobot/translations/pl.json new file mode 100644 index 00000000000..8a08a06c699 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/translations/nl.json b/homeassistant/components/locative/translations/nl.json index e02378432ab..16cbbc77277 100644 --- a/homeassistant/components/locative/translations/nl.json +++ b/homeassistant/components/locative/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.", + "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." }, "create_entry": { "default": "Om locaties naar Home Assistant te sturen, moet u de Webhook-functie instellen in de Locative app. \n\n Vul de volgende info in: \n\n - URL: `{webhook_url}` \n - Methode: POST \n\n Zie [de documentatie]({docs_url}) voor meer informatie." diff --git a/homeassistant/components/mailgun/translations/nl.json b/homeassistant/components/mailgun/translations/nl.json index 772a67c118e..dea33946af5 100644 --- a/homeassistant/components/mailgun/translations/nl.json +++ b/homeassistant/components/mailgun/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.", + "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." }, "create_entry": { "default": "Om evenementen naar Home Assistant te verzenden, moet u [Webhooks with Mailgun]({mailgun_url}) instellen. \n\n Vul de volgende info in: \n\n - URL: `{webhook_url}` \n - Methode: POST \n - Inhoudstype: application/json \n\n Zie [de documentatie]({docs_url}) voor informatie over het configureren van automatiseringen om binnenkomende gegevens te verwerken." diff --git a/homeassistant/components/mazda/translations/nl.json b/homeassistant/components/mazda/translations/nl.json index 86f1e656e51..3198bfb4192 100644 --- a/homeassistant/components/mazda/translations/nl.json +++ b/homeassistant/components/mazda/translations/nl.json @@ -25,5 +25,6 @@ } } } - } + }, + "title": "Mazda Connected Services" } \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/ca.json b/homeassistant/components/mullvad/translations/ca.json new file mode 100644 index 00000000000..f81781cbc0f --- /dev/null +++ b/homeassistant/components/mullvad/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Vols configurar la integraci\u00f3 Mullvad VPN?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/en.json b/homeassistant/components/mullvad/translations/en.json index 45664554aed..fcfa89ef082 100644 --- a/homeassistant/components/mullvad/translations/en.json +++ b/homeassistant/components/mullvad/translations/en.json @@ -5,10 +5,16 @@ }, "error": { "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, "step": { "user": { + "data": { + "host": "Host", + "password": "Password", + "username": "Username" + }, "description": "Set up the Mullvad VPN integration?" } } diff --git a/homeassistant/components/mullvad/translations/et.json b/homeassistant/components/mullvad/translations/et.json new file mode 100644 index 00000000000..671d18a2cd3 --- /dev/null +++ b/homeassistant/components/mullvad/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendumine nurjus", + "invalid_auth": "Tuvastamise viga", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "Kas seadistada Mullvad VPN sidumine?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/fr.json b/homeassistant/components/mullvad/translations/fr.json new file mode 100644 index 00000000000..1a8b10de809 --- /dev/null +++ b/homeassistant/components/mullvad/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "Configurez l'int\u00e9gration VPN Mullvad?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/nl.json b/homeassistant/components/mullvad/translations/nl.json new file mode 100644 index 00000000000..aa4d80ac71d --- /dev/null +++ b/homeassistant/components/mullvad/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "De Mullvad VPN-integratie instellen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/no.json b/homeassistant/components/mullvad/translations/no.json new file mode 100644 index 00000000000..d33f2640445 --- /dev/null +++ b/homeassistant/components/mullvad/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Sette opp Mullvad VPN-integrasjon?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/ru.json b/homeassistant/components/mullvad/translations/ru.json new file mode 100644 index 00000000000..ff34530e4a9 --- /dev/null +++ b/homeassistant/components/mullvad/translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Mullvad VPN." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/tr.json b/homeassistant/components/mullvad/translations/tr.json new file mode 100644 index 00000000000..0f3ddabfc4f --- /dev/null +++ b/homeassistant/components/mullvad/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/et.json b/homeassistant/components/netatmo/translations/et.json index 99e062b3842..8725eb48016 100644 --- a/homeassistant/components/netatmo/translations/et.json +++ b/homeassistant/components/netatmo/translations/et.json @@ -15,6 +15,28 @@ } } }, + "device_automation": { + "trigger_subtype": { + "away": "eemal", + "hg": "k\u00fclmumiskaitse", + "schedule": "ajastus" + }, + "trigger_type": { + "alarm_started": "{entity_name} tuvastas h\u00e4ire", + "animal": "{entity_name} tuvastas looma", + "cancel_set_point": "{entity_name} on oma ajakava j\u00e4tkanud", + "human": "{entity_name} tuvastas inimese", + "movement": "{entity_name} tuvastas liikumise", + "outdoor": "{entity_name} tuvastas v\u00e4lise s\u00fcndmuse", + "person": "{entity_name} tuvastas isiku", + "person_away": "{entity_name} tuvastas inimese eemaldumise", + "set_point": "Sihttemperatuur {entity_name} on k\u00e4sitsi m\u00e4\u00e4ratud", + "therm_mode": "{entity_name} l\u00fclitus olekusse {subtype}.", + "turned_off": "{entity_name} l\u00fclitus v\u00e4lja", + "turned_on": "{entity_name} l\u00fclitus sisse", + "vehicle": "{entity_name} tuvastas s\u00f5iduki" + } + }, "options": { "step": { "public_weather": { diff --git a/homeassistant/components/netatmo/translations/fr.json b/homeassistant/components/netatmo/translations/fr.json index fe8fc74d273..6c294d467ab 100644 --- a/homeassistant/components/netatmo/translations/fr.json +++ b/homeassistant/components/netatmo/translations/fr.json @@ -15,6 +15,28 @@ } } }, + "device_automation": { + "trigger_subtype": { + "away": "absent", + "hg": "garde-gel", + "schedule": "horaire" + }, + "trigger_type": { + "alarm_started": "{entity_name} a d\u00e9tect\u00e9 une alarme", + "animal": "{entity_name} a d\u00e9tect\u00e9 un animal", + "cancel_set_point": "{entity_name} a repris son programme", + "human": "{entity_name} a d\u00e9tect\u00e9 une personne", + "movement": "{entity_name} a d\u00e9tect\u00e9 un mouvement", + "outdoor": "{entity_name} a d\u00e9tect\u00e9 un \u00e9v\u00e9nement ext\u00e9rieur", + "person": "{entity_name} a d\u00e9tect\u00e9 une personne", + "person_away": "{entity_name} a d\u00e9tect\u00e9 qu\u2019une personne est partie", + "set_point": "Temp\u00e9rature cible {entity_name} d\u00e9finie manuellement", + "therm_mode": "{entity_name} est pass\u00e9 \u00e0 \"{subtype}\"", + "turned_off": "{entity_name} d\u00e9sactiv\u00e9", + "turned_on": "{entity_name} activ\u00e9", + "vehicle": "{entity_name} a d\u00e9tect\u00e9 un v\u00e9hicule" + } + }, "options": { "step": { "public_weather": { diff --git a/homeassistant/components/netatmo/translations/nl.json b/homeassistant/components/netatmo/translations/nl.json index eab1d9741ad..431f105df3d 100644 --- a/homeassistant/components/netatmo/translations/nl.json +++ b/homeassistant/components/netatmo/translations/nl.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Time-out genereren autorisatie-URL.", - "missing_configuration": "Het component is niet geconfigureerd. Volg de documentatie." + "missing_configuration": "Het component is niet geconfigureerd. Volg de documentatie.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})" }, "create_entry": { "default": "Succesvol geauthenticeerd met Netatmo." @@ -13,6 +14,28 @@ } } }, + "device_automation": { + "trigger_subtype": { + "away": "afwezig", + "hg": "vorstbescherming", + "schedule": "schema" + }, + "trigger_type": { + "alarm_started": "{entity_name} heeft een alarm gedetecteerd", + "animal": "{entity_name} heeft een dier gedetecteerd", + "cancel_set_point": "{entity_name} heeft zijn schema hervat", + "human": "{entity_name} heeft een mens gedetecteerd", + "movement": "{entity_name} heeft beweging gedetecteerd", + "outdoor": "{entity_name} heeft een buitengebeurtenis gedetecteerd", + "person": "{entity_name} heeft een persoon gedetecteerd", + "person_away": "{entity_name} heeft gedetecteerd dat een persoon is vertrokken", + "set_point": "Doeltemperatuur {entity_name} handmatig ingesteld", + "therm_mode": "{entity_name} is overgeschakeld naar \"{subtype}\"", + "turned_off": "{entity_name} uitgeschakeld", + "turned_on": "{entity_name} ingeschakeld", + "vehicle": "{entity_name} heeft een voertuig gedetecteerd" + } + }, "options": { "step": { "public_weather": { diff --git a/homeassistant/components/netatmo/translations/ru.json b/homeassistant/components/netatmo/translations/ru.json index c9be7e60825..b25e0843967 100644 --- a/homeassistant/components/netatmo/translations/ru.json +++ b/homeassistant/components/netatmo/translations/ru.json @@ -15,6 +15,28 @@ } } }, + "device_automation": { + "trigger_subtype": { + "away": "\u043d\u0435 \u0434\u043e\u043c\u0430", + "hg": "\u0437\u0430\u0449\u0438\u0442\u0430 \u043e\u0442 \u0437\u0430\u043c\u0435\u0440\u0437\u0430\u043d\u0438\u044f", + "schedule": "\u0440\u0430\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0435" + }, + "trigger_type": { + "alarm_started": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u0440\u0435\u0432\u043e\u0433\u0443", + "animal": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0436\u0438\u0432\u043e\u0442\u043d\u043e\u0435", + "cancel_set_point": "{entity_name} \u0432\u043e\u0437\u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442 \u0441\u0432\u043e\u0435 \u0440\u0430\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0435", + "human": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0447\u0435\u043b\u043e\u0432\u0435\u043a\u0430", + "movement": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "outdoor": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u043d\u0430 \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u043c \u0432\u043e\u0437\u0434\u0443\u0445\u0435", + "person": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0435\u0440\u0441\u043e\u043d\u0443", + "person_away": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442, \u0447\u0442\u043e \u043f\u0435\u0440\u0441\u043e\u043d\u0430 \u0443\u0448\u043b\u0430", + "set_point": "\u0426\u0435\u043b\u0435\u0432\u0430\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 {entity_name} \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0430 \u0432\u0440\u0443\u0447\u043d\u0443\u044e", + "therm_mode": "{entity_name} \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u043d\u0430 \"{subtype}\"", + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "vehicle": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u043e\u0435 \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043e" + } + }, "options": { "step": { "public_weather": { diff --git a/homeassistant/components/netatmo/translations/tr.json b/homeassistant/components/netatmo/translations/tr.json index 94dd5b3fb0f..69646be2292 100644 --- a/homeassistant/components/netatmo/translations/tr.json +++ b/homeassistant/components/netatmo/translations/tr.json @@ -4,6 +4,22 @@ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." } }, + "device_automation": { + "trigger_subtype": { + "away": "uzakta", + "hg": "donma korumas\u0131", + "schedule": "Zamanlama" + }, + "trigger_type": { + "alarm_started": "{entity_name} bir alarm alg\u0131lad\u0131", + "animal": "{entity_name} bir hayvan tespit etti", + "cancel_set_point": "{entity_name} zamanlamas\u0131na devam etti", + "human": "{entity_name} bir insan alg\u0131lad\u0131", + "movement": "{entity_name} hareket alg\u0131lad\u0131", + "turned_off": "{entity_name} kapat\u0131ld\u0131", + "turned_on": "{entity_name} a\u00e7\u0131ld\u0131" + } + }, "options": { "step": { "public_weather": { diff --git a/homeassistant/components/nightscout/translations/nl.json b/homeassistant/components/nightscout/translations/nl.json index 208299fd442..0146996dce5 100644 --- a/homeassistant/components/nightscout/translations/nl.json +++ b/homeassistant/components/nightscout/translations/nl.json @@ -4,6 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, diff --git a/homeassistant/components/nzbget/translations/nl.json b/homeassistant/components/nzbget/translations/nl.json index f5f1bfd39ed..89d58d14292 100644 --- a/homeassistant/components/nzbget/translations/nl.json +++ b/homeassistant/components/nzbget/translations/nl.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "host": "Host", "name": "Naam", "password": "Wachtwoord", "port": "Poort", diff --git a/homeassistant/components/plum_lightpad/translations/nl.json b/homeassistant/components/plum_lightpad/translations/nl.json index 7f0f85b7326..8410cabbbb9 100644 --- a/homeassistant/components/plum_lightpad/translations/nl.json +++ b/homeassistant/components/plum_lightpad/translations/nl.json @@ -9,7 +9,8 @@ "step": { "user": { "data": { - "password": "Wachtwoord" + "password": "Wachtwoord", + "username": "E-mail" } } } diff --git a/homeassistant/components/poolsense/translations/nl.json b/homeassistant/components/poolsense/translations/nl.json index 46fc915d7bd..38ef34d5afc 100644 --- a/homeassistant/components/poolsense/translations/nl.json +++ b/homeassistant/components/poolsense/translations/nl.json @@ -11,7 +11,8 @@ "data": { "email": "E-mail", "password": "Wachtwoord" - } + }, + "description": "Wil je beginnen met instellen?" } } } diff --git a/homeassistant/components/rituals_perfume_genie/translations/fr.json b/homeassistant/components/rituals_perfume_genie/translations/fr.json new file mode 100644 index 00000000000..2a1fb9c8bb8 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Mot de passe" + }, + "title": "Connectez-vous \u00e0 votre compte Rituals" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/nl.json b/homeassistant/components/rpi_power/translations/nl.json index 72f9ff82ba4..8c15279dca8 100644 --- a/homeassistant/components/rpi_power/translations/nl.json +++ b/homeassistant/components/rpi_power/translations/nl.json @@ -2,6 +2,11 @@ "config": { "abort": { "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." + }, + "step": { + "confirm": { + "description": "Wil je beginnen met instellen?" + } } }, "title": "Raspberry Pi Voeding Checker" diff --git a/homeassistant/components/ruckus_unleashed/translations/nl.json b/homeassistant/components/ruckus_unleashed/translations/nl.json index 0569c39321a..8ad15260b0d 100644 --- a/homeassistant/components/ruckus_unleashed/translations/nl.json +++ b/homeassistant/components/ruckus_unleashed/translations/nl.json @@ -4,6 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, diff --git a/homeassistant/components/sharkiq/translations/nl.json b/homeassistant/components/sharkiq/translations/nl.json index 96c10f3e2f0..3acfdbdf074 100644 --- a/homeassistant/components/sharkiq/translations/nl.json +++ b/homeassistant/components/sharkiq/translations/nl.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Account is al geconfigureerd", "cannot_connect": "Kan geen verbinding maken", + "reauth_successful": "Herauthenticatie was succesvol", "unknown": "Onverwachte fout" }, "error": { diff --git a/homeassistant/components/smappee/translations/nl.json b/homeassistant/components/smappee/translations/nl.json index ebcc16dafac..10a4fe2efab 100644 --- a/homeassistant/components/smappee/translations/nl.json +++ b/homeassistant/components/smappee/translations/nl.json @@ -6,7 +6,8 @@ "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", "cannot_connect": "Kan geen verbinding maken", "invalid_mdns": "Niet-ondersteund apparaat voor de Smappee-integratie.", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen." + "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})" }, "step": { "local": { diff --git a/homeassistant/components/sms/translations/nl.json b/homeassistant/components/sms/translations/nl.json index 75dd593982a..ddcc54d239f 100644 --- a/homeassistant/components/sms/translations/nl.json +++ b/homeassistant/components/sms/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, "error": { "cannot_connect": "Kon niet verbinden", diff --git a/homeassistant/components/somfy/translations/nl.json b/homeassistant/components/somfy/translations/nl.json index 423dbb6a2bb..b7f077f2c73 100644 --- a/homeassistant/components/somfy/translations/nl.json +++ b/homeassistant/components/somfy/translations/nl.json @@ -3,6 +3,7 @@ "abort": { "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", "missing_configuration": "Het Somfy-component is niet geconfigureerd. Gelieve de documentatie te volgen.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "create_entry": { diff --git a/homeassistant/components/speedtestdotnet/translations/nl.json b/homeassistant/components/speedtestdotnet/translations/nl.json index 0c0c184b5fe..1fe99195f7a 100644 --- a/homeassistant/components/speedtestdotnet/translations/nl.json +++ b/homeassistant/components/speedtestdotnet/translations/nl.json @@ -3,6 +3,11 @@ "abort": { "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", "wrong_server_id": "Server-ID is niet geldig" + }, + "step": { + "user": { + "description": "Wil je beginnen met instellen?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/spider/translations/nl.json b/homeassistant/components/spider/translations/nl.json index f0b4ddf59a9..bc7683ac0a4 100644 --- a/homeassistant/components/spider/translations/nl.json +++ b/homeassistant/components/spider/translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, "error": { "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" diff --git a/homeassistant/components/subaru/translations/fr.json b/homeassistant/components/subaru/translations/fr.json new file mode 100644 index 00000000000..a6bf6902aab --- /dev/null +++ b/homeassistant/components/subaru/translations/fr.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion" + }, + "error": { + "bad_pin_format": "Le code PIN doit \u00eatre compos\u00e9 de 4 chiffres", + "cannot_connect": "\u00c9chec de connexion", + "incorrect_pin": "PIN incorrect", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "pin": { + "data": { + "pin": "PIN" + }, + "description": "Veuillez entrer votre NIP MySubaru\nREMARQUE : Tous les v\u00e9hicules en compte doivent avoir le m\u00eame NIP", + "title": "Configuration de Subaru Starlink" + }, + "user": { + "data": { + "country": "Choisissez le pays", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthru/translations/nl.json b/homeassistant/components/syncthru/translations/nl.json index 349b4b2818e..b1beb4058bc 100644 --- a/homeassistant/components/syncthru/translations/nl.json +++ b/homeassistant/components/syncthru/translations/nl.json @@ -10,7 +10,8 @@ "step": { "user": { "data": { - "name": "Naam" + "name": "Naam", + "url": "Webinterface URL" } } } diff --git a/homeassistant/components/tile/translations/nl.json b/homeassistant/components/tile/translations/nl.json index c160ac631ee..236d250122a 100644 --- a/homeassistant/components/tile/translations/nl.json +++ b/homeassistant/components/tile/translations/nl.json @@ -9,7 +9,8 @@ "step": { "user": { "data": { - "password": "Wachtwoord" + "password": "Wachtwoord", + "username": "E-mail" }, "title": "Tegel configureren" } diff --git a/homeassistant/components/toon/translations/nl.json b/homeassistant/components/toon/translations/nl.json index cf77b94d025..a0cda915172 100644 --- a/homeassistant/components/toon/translations/nl.json +++ b/homeassistant/components/toon/translations/nl.json @@ -3,6 +3,8 @@ "abort": { "already_configured": "De geselecteerde overeenkomst is al geconfigureerd.", "authorize_url_fail": "Onbekende fout bij het genereren van een autorisatie-URL.", + "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", "no_agreements": "Dit account heeft geen Toon schermen.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout [check the help section] ( {docs_url} )", "unknown_authorize_url_generation": "Onbekende fout bij het genereren van een autorisatie-URL." diff --git a/homeassistant/components/totalconnect/translations/fr.json b/homeassistant/components/totalconnect/translations/fr.json index 6526d0a9800..40ca767b4ac 100644 --- a/homeassistant/components/totalconnect/translations/fr.json +++ b/homeassistant/components/totalconnect/translations/fr.json @@ -1,12 +1,21 @@ { "config": { "abort": { - "already_configured": "Compte d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Compte d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "invalid_auth": "Authentification invalide" }, "step": { + "locations": { + "data": { + "location": "Emplacement" + } + }, + "reauth_confirm": { + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, "user": { "data": { "password": "Mot de passe", diff --git a/homeassistant/components/totalconnect/translations/nl.json b/homeassistant/components/totalconnect/translations/nl.json index 1f4fb5490d1..94d8e3ac01e 100644 --- a/homeassistant/components/totalconnect/translations/nl.json +++ b/homeassistant/components/totalconnect/translations/nl.json @@ -8,6 +8,11 @@ "invalid_auth": "Ongeldige authenticatie" }, "step": { + "locations": { + "data": { + "location": "Locatie" + } + }, "reauth_confirm": { "title": "Verifieer de integratie opnieuw" }, diff --git a/homeassistant/components/totalconnect/translations/no.json b/homeassistant/components/totalconnect/translations/no.json index e80f86696fc..9c98d6ad1e7 100644 --- a/homeassistant/components/totalconnect/translations/no.json +++ b/homeassistant/components/totalconnect/translations/no.json @@ -1,12 +1,25 @@ { "config": { "abort": { - "already_configured": "Kontoen er allerede konfigurert" + "already_configured": "Kontoen er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { - "invalid_auth": "Ugyldig godkjenning" + "invalid_auth": "Ugyldig godkjenning", + "usercode": "Brukerkode er ikke gyldig for denne brukeren p\u00e5 dette stedet" }, "step": { + "locations": { + "data": { + "location": "Plassering" + }, + "description": "Angi brukerkoden for denne brukeren p\u00e5 denne plasseringen", + "title": "Brukerkoder for plassering" + }, + "reauth_confirm": { + "description": "Total Connect m\u00e5 godkjenne kontoen din p\u00e5 nytt", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "password": "Passord", diff --git a/homeassistant/components/totalconnect/translations/pl.json b/homeassistant/components/totalconnect/translations/pl.json index 530d632040c..ff2ca2351e6 100644 --- a/homeassistant/components/totalconnect/translations/pl.json +++ b/homeassistant/components/totalconnect/translations/pl.json @@ -1,12 +1,25 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane" + "already_configured": "Konto jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { - "invalid_auth": "Niepoprawne uwierzytelnienie" + "invalid_auth": "Niepoprawne uwierzytelnienie", + "usercode": "Nieprawid\u0142owy kod u\u017cytkownika dla u\u017cytkownika w tej lokalizacji" }, "step": { + "locations": { + "data": { + "location": "Lokalizacja" + }, + "description": "Wprowad\u017a kod u\u017cytkownika dla u\u017cytkownika w tej lokalizacji", + "title": "Kody lokalizacji u\u017cytkownika" + }, + "reauth_confirm": { + "description": "Integracja Total Connect wymaga ponownego uwierzytelnienia Twojego konta", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, "user": { "data": { "password": "Has\u0142o", diff --git a/homeassistant/components/xiaomi_miio/translations/no.json b/homeassistant/components/xiaomi_miio/translations/no.json index 3375cac31d5..0a6cf433d87 100644 --- a/homeassistant/components/xiaomi_miio/translations/no.json +++ b/homeassistant/components/xiaomi_miio/translations/no.json @@ -14,6 +14,7 @@ "device": { "data": { "host": "IP adresse", + "model": "Enhetsmodell (valgfritt)", "name": "Navnet p\u00e5 enheten", "token": "API-token" }, diff --git a/homeassistant/components/xiaomi_miio/translations/pl.json b/homeassistant/components/xiaomi_miio/translations/pl.json index 50eb4d16887..80528b71370 100644 --- a/homeassistant/components/xiaomi_miio/translations/pl.json +++ b/homeassistant/components/xiaomi_miio/translations/pl.json @@ -14,6 +14,7 @@ "device": { "data": { "host": "Adres IP", + "model": "Model urz\u0105dzenia (opcjonalnie)", "name": "Nazwa urz\u0105dzenia", "token": "Token API" }, diff --git a/homeassistant/components/zwave_js/translations/ca.json b/homeassistant/components/zwave_js/translations/ca.json index 6806b5072c1..731c0bbcea8 100644 --- a/homeassistant/components/zwave_js/translations/ca.json +++ b/homeassistant/components/zwave_js/translations/ca.json @@ -6,6 +6,7 @@ "addon_install_failed": "No s'ha pogut instal\u00b7lar el complement Z-Wave JS.", "addon_missing_discovery_info": "Falta la informaci\u00f3 de descobriment del complement Z-Wave JS.", "addon_set_config_failed": "No s'ha pogut establir la configuraci\u00f3 de Z-Wave JS.", + "addon_start_failed": "No s'ha pogut iniciar el complement Z-Wave JS.", "already_configured": "El dispositiu ja est\u00e0 configurat", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "cannot_connect": "Ha fallat la connexi\u00f3" @@ -17,7 +18,8 @@ "unknown": "Error inesperat" }, "progress": { - "install_addon": "Espera mentre finalitza la instal\u00b7laci\u00f3 del complement Z-Wave JS. Pot tardar uns quants minuts." + "install_addon": "Espera mentre finalitza la instal\u00b7laci\u00f3 del complement Z-Wave JS. Pot tardar uns quants minuts.", + "start_addon": "Espera mentre es completa la inicialitzaci\u00f3 del complement Z-Wave JS. Pot tardar uns segons." }, "step": { "configure_addon": { @@ -45,6 +47,9 @@ "description": "Vols utilitzar el complement Supervisor de Z-Wave JS?", "title": "Selecciona el m\u00e8tode de connexi\u00f3" }, + "start_addon": { + "title": "El complement Z-Wave JS s'est\u00e0 iniciant." + }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/zwave_js/translations/et.json b/homeassistant/components/zwave_js/translations/et.json index d51507b616f..4c68e63530f 100644 --- a/homeassistant/components/zwave_js/translations/et.json +++ b/homeassistant/components/zwave_js/translations/et.json @@ -6,6 +6,7 @@ "addon_install_failed": "Z-Wave JS lisandmooduli paigaldamine nurjus.", "addon_missing_discovery_info": "Z-Wave JS lisandmooduli tuvastusteave puudub.", "addon_set_config_failed": "Z-Wave JS konfiguratsiooni m\u00e4\u00e4ramine nurjus.", + "addon_start_failed": "Z-Wave JS-i lisandmooduli k\u00e4ivitamine nurjus.", "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "already_in_progress": "Seadistamine on juba k\u00e4imas", "cannot_connect": "\u00dchendamine nurjus" @@ -17,7 +18,8 @@ "unknown": "Ootamatu t\u00f5rge" }, "progress": { - "install_addon": "Palun oota kuni Z-Wave JS lisandmoodul on paigaldatud. See v\u00f5ib v\u00f5tta mitu minutit." + "install_addon": "Palun oota kuni Z-Wave JS lisandmoodul on paigaldatud. See v\u00f5ib v\u00f5tta mitu minutit.", + "start_addon": "Palun oota kuni Z-Wave JS lisandmooduli ak\u00e4ivitumine l\u00f5ppeb. See v\u00f5ib v\u00f5tta m\u00f5ned sekundid." }, "step": { "configure_addon": { @@ -45,6 +47,9 @@ "description": "Kas soovid kasutada Z-Wave JSi halduri lisandmoodulit?", "title": "Vali \u00fchendusviis" }, + "start_addon": { + "title": "Z-Wave JS lisandmoodul k\u00e4ivitub." + }, "user": { "data": { "url": "" diff --git a/homeassistant/components/zwave_js/translations/fr.json b/homeassistant/components/zwave_js/translations/fr.json index 040e3997ac1..9cc8bf822b8 100644 --- a/homeassistant/components/zwave_js/translations/fr.json +++ b/homeassistant/components/zwave_js/translations/fr.json @@ -6,6 +6,7 @@ "addon_install_failed": "\u00c9chec de l'installation du module compl\u00e9mentaire Z-Wave JS.", "addon_missing_discovery_info": "Informations manquantes sur la d\u00e9couverte du module compl\u00e9mentaire Z-Wave JS.", "addon_set_config_failed": "\u00c9chec de la d\u00e9finition de la configuration Z-Wave JS.", + "addon_start_failed": "\u00c9chec du d\u00e9marrage du module compl\u00e9mentaire Z-Wave JS.", "already_configured": "Le p\u00e9riph\u00e9rique est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "cannot_connect": "\u00c9chec de la connexion " @@ -17,7 +18,8 @@ "unknown": "Erreur inattendue" }, "progress": { - "install_addon": "Veuillez patienter pendant l'installation du module compl\u00e9mentaire Z-Wave JS. Cela peut prendre plusieurs minutes." + "install_addon": "Veuillez patienter pendant l'installation du module compl\u00e9mentaire Z-Wave JS. Cela peut prendre plusieurs minutes.", + "start_addon": "Veuillez patienter pendant le d\u00e9marrage du module compl\u00e9mentaire Z-Wave JS. Cela peut prendre quelques secondes." }, "step": { "configure_addon": { @@ -45,6 +47,9 @@ "description": "Voulez-vous utiliser le module compl\u00e9mentaire Z-Wave JS Supervisor?", "title": "S\u00e9lectionner la m\u00e9thode de connexion" }, + "start_addon": { + "title": "Le module compl\u00e9mentaire Z-Wave JS est d\u00e9marr\u00e9." + }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/zwave_js/translations/no.json b/homeassistant/components/zwave_js/translations/no.json index b724fb34e48..acd049fc561 100644 --- a/homeassistant/components/zwave_js/translations/no.json +++ b/homeassistant/components/zwave_js/translations/no.json @@ -6,6 +6,7 @@ "addon_install_failed": "Kunne ikke installere Z-Wave JS-tillegg", "addon_missing_discovery_info": "Manglende oppdagelsesinformasjon for Z-Wave JS-tillegg", "addon_set_config_failed": "Kunne ikke angi Z-Wave JS-konfigurasjon", + "addon_start_failed": "Kunne ikke starte Z-Wave JS-tillegget.", "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "cannot_connect": "Tilkobling mislyktes" @@ -17,7 +18,8 @@ "unknown": "Uventet feil" }, "progress": { - "install_addon": "Vent mens installasjonen av Z-Wave JS-tillegg er ferdig. Dette kan ta flere minutter." + "install_addon": "Vent mens installasjonen av Z-Wave JS-tillegg er ferdig. Dette kan ta flere minutter.", + "start_addon": "Vent mens Z-Wave JS-tilleggsstarten er fullf\u00f8rt. Dette kan ta noen sekunder." }, "step": { "configure_addon": { @@ -45,6 +47,9 @@ "description": "Vil du bruke Z-Wave JS Supervisor-tillegg?", "title": "Velg tilkoblingsmetode" }, + "start_addon": { + "title": "Z-Wave JS-tillegget starter." + }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/zwave_js/translations/pl.json b/homeassistant/components/zwave_js/translations/pl.json index b139b0dacc6..2bfd994132b 100644 --- a/homeassistant/components/zwave_js/translations/pl.json +++ b/homeassistant/components/zwave_js/translations/pl.json @@ -6,6 +6,7 @@ "addon_install_failed": "Nie uda\u0142o si\u0119 zainstalowa\u0107 dodatku Z-Wave JS", "addon_missing_discovery_info": "Brak informacji wykrywania dodatku Z-Wave JS", "addon_set_config_failed": "Nie uda\u0142o si\u0119 skonfigurowa\u0107 Z-Wave JS", + "addon_start_failed": "Nie uda\u0142o si\u0119 uruchomi\u0107 dodatku Z-Wave JS.", "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja jest ju\u017c w toku", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" @@ -17,7 +18,8 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "progress": { - "install_addon": "Poczekaj, a\u017c zako\u0144czy si\u0119 instalacja dodatku Z-Wave JS. Mo\u017ce to zaj\u0105\u0107 kilka minut." + "install_addon": "Poczekaj, a\u017c zako\u0144czy si\u0119 instalacja dodatku Z-Wave JS. Mo\u017ce to zaj\u0105\u0107 kilka minut.", + "start_addon": "Poczekaj, a\u017c zako\u0144czy si\u0119 uruchamianie dodatku Z-Wave JS. Mo\u017ce to zaj\u0105\u0107 chwil\u0119." }, "step": { "configure_addon": { @@ -45,6 +47,9 @@ "description": "Czy chcesz skorzysta\u0107 z dodatku Z-Wave JS Supervisor?", "title": "Wybierz metod\u0119 po\u0142\u0105czenia" }, + "start_addon": { + "title": "Dodatek Z-Wave JS uruchamia si\u0119." + }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/zwave_js/translations/ru.json b/homeassistant/components/zwave_js/translations/ru.json index 5b7e5f47017..1a65ce3ea71 100644 --- a/homeassistant/components/zwave_js/translations/ru.json +++ b/homeassistant/components/zwave_js/translations/ru.json @@ -6,6 +6,7 @@ "addon_install_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS.", "addon_missing_discovery_info": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0438 Z-Wave JS.", "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 Z-Wave JS.", + "addon_start_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS.", "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.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." @@ -17,7 +18,8 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "progress": { - "install_addon": "\u041f\u043e\u0434\u043e\u0436\u0434\u0438\u0442\u0435, \u043f\u043e\u043a\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS. \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." + "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 Z-Wave JS. \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.", + "start_addon": "\u041f\u043e\u0434\u043e\u0436\u0434\u0438\u0442\u0435, \u043f\u043e\u043a\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044f \u0437\u0430\u043f\u0443\u0441\u043a \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0437\u0430\u043d\u044f\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0435\u043a\u0443\u043d\u0434." }, "step": { "configure_addon": { @@ -45,6 +47,9 @@ "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Supervisor Z-Wave JS?", "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" }, + "start_addon": { + "title": "\u0414\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f" + }, "user": { "data": { "url": "URL-\u0430\u0434\u0440\u0435\u0441" diff --git a/homeassistant/components/zwave_js/translations/zh-Hant.json b/homeassistant/components/zwave_js/translations/zh-Hant.json index b9ff1b41920..f1495b1aeda 100644 --- a/homeassistant/components/zwave_js/translations/zh-Hant.json +++ b/homeassistant/components/zwave_js/translations/zh-Hant.json @@ -6,6 +6,7 @@ "addon_install_failed": "Z-Wave JS add-on \u5b89\u88dd\u5931\u6557\u3002", "addon_missing_discovery_info": "\u7f3a\u5c11 Z-Wave JS add-on \u63a2\u7d22\u8cc7\u8a0a\u3002", "addon_set_config_failed": "Z-Wave JS add-on \u8a2d\u5b9a\u5931\u6557\u3002", + "addon_start_failed": "Z-Wave JS add-on \u555f\u59cb\u5931\u6557\u3002", "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" @@ -17,7 +18,8 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "progress": { - "install_addon": "\u8acb\u7a0d\u7b49 Z-Wave JS add-on \u5b89\u88dd\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002" + "install_addon": "\u8acb\u7a0d\u7b49 Z-Wave JS add-on \u5b89\u88dd\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002", + "start_addon": "\u8acb\u7a0d\u7b49 Z-Wave JS add-on \u555f\u59cb\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002" }, "step": { "configure_addon": { @@ -45,6 +47,9 @@ "description": "\u662f\u5426\u8981\u4f7f\u7528 Z-Wave JS Supervisor add-on\uff1f", "title": "\u9078\u64c7\u9023\u7dda\u985e\u578b" }, + "start_addon": { + "title": "Z-Wave JS add-on \u555f\u59cb\u4e2d\u3002" + }, "user": { "data": { "url": "\u7db2\u5740" From be4de15a109e663eca547b147574365f25fa9767 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 26 Feb 2021 00:06:13 +0000 Subject: [PATCH 775/796] [ci skip] Translation update --- .../components/airvisual/translations/lb.json | 9 ++++- .../components/bond/translations/cs.json | 2 +- .../components/climacell/translations/cs.json | 20 ++++++++++ .../components/climacell/translations/es.json | 30 +++++++++++++++ .../climacell/translations/zh-Hant.json | 34 +++++++++++++++++ .../faa_delays/translations/cs.json | 8 ++++ .../faa_delays/translations/es.json | 19 ++++++++++ .../faa_delays/translations/et.json | 21 +++++++++++ .../faa_delays/translations/ru.json | 21 +++++++++++ .../faa_delays/translations/zh-Hant.json | 21 +++++++++++ .../components/homekit/translations/cs.json | 2 +- .../components/kmtronic/translations/cs.json | 21 +++++++++++ .../components/litejet/translations/es.json | 16 ++++++++ .../components/mazda/translations/es.json | 1 + .../components/mullvad/translations/cs.json | 21 +++++++++++ .../components/mullvad/translations/es.json | 9 +++++ .../mullvad/translations/zh-Hant.json | 22 +++++++++++ .../components/netatmo/translations/es.json | 22 +++++++++++ .../netatmo/translations/zh-Hant.json | 22 +++++++++++ .../translations/es.json | 9 +++++ .../components/shelly/translations/nl.json | 4 +- .../components/soma/translations/cs.json | 2 +- .../components/spotify/translations/cs.json | 2 +- .../components/subaru/translations/cs.json | 28 ++++++++++++++ .../components/subaru/translations/es.json | 37 +++++++++++++++++++ .../totalconnect/translations/cs.json | 8 +++- .../totalconnect/translations/es.json | 10 ++++- .../wolflink/translations/sensor.nl.json | 16 ++++++++ .../xiaomi_miio/translations/es.json | 1 + .../components/zwave_js/translations/es.json | 7 +++- 30 files changed, 435 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/climacell/translations/cs.json create mode 100644 homeassistant/components/climacell/translations/es.json create mode 100644 homeassistant/components/climacell/translations/zh-Hant.json create mode 100644 homeassistant/components/faa_delays/translations/cs.json create mode 100644 homeassistant/components/faa_delays/translations/es.json create mode 100644 homeassistant/components/faa_delays/translations/et.json create mode 100644 homeassistant/components/faa_delays/translations/ru.json create mode 100644 homeassistant/components/faa_delays/translations/zh-Hant.json create mode 100644 homeassistant/components/kmtronic/translations/cs.json create mode 100644 homeassistant/components/litejet/translations/es.json create mode 100644 homeassistant/components/mullvad/translations/cs.json create mode 100644 homeassistant/components/mullvad/translations/es.json create mode 100644 homeassistant/components/mullvad/translations/zh-Hant.json create mode 100644 homeassistant/components/rituals_perfume_genie/translations/es.json create mode 100644 homeassistant/components/subaru/translations/cs.json create mode 100644 homeassistant/components/subaru/translations/es.json create mode 100644 homeassistant/components/wolflink/translations/sensor.nl.json diff --git a/homeassistant/components/airvisual/translations/lb.json b/homeassistant/components/airvisual/translations/lb.json index 5e45098c11d..d6799ba6e37 100644 --- a/homeassistant/components/airvisual/translations/lb.json +++ b/homeassistant/components/airvisual/translations/lb.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "Feeler beim verbannen", "general_error": "Onerwaarte Feeler", - "invalid_api_key": "Ong\u00ebltegen API Schl\u00ebssel" + "invalid_api_key": "Ong\u00ebltegen API Schl\u00ebssel", + "location_not_found": "Standuert net fonnt." }, "step": { "geography": { @@ -19,6 +20,12 @@ "description": "Benotz Airvisual cloud API fir eng geografescher Lag z'iwwerwaachen.", "title": "Geografie ariichten" }, + "geography_by_name": { + "data": { + "city": "Stad", + "country": "Land" + } + }, "node_pro": { "data": { "ip_address": "Host", diff --git a/homeassistant/components/bond/translations/cs.json b/homeassistant/components/bond/translations/cs.json index 677c7e80236..13135dbf53e 100644 --- a/homeassistant/components/bond/translations/cs.json +++ b/homeassistant/components/bond/translations/cs.json @@ -15,7 +15,7 @@ "data": { "access_token": "P\u0159\u00edstupov\u00fd token" }, - "description": "Chcete nastavit {bond_id} ?" + "description": "Chcete nastavit {name}?" }, "user": { "data": { diff --git a/homeassistant/components/climacell/translations/cs.json b/homeassistant/components/climacell/translations/cs.json new file mode 100644 index 00000000000..1ae29deb08c --- /dev/null +++ b/homeassistant/components/climacell/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", + "name": "Jm\u00e9no" + } + } + } + }, + "title": "ClimaCell" +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/es.json b/homeassistant/components/climacell/translations/es.json new file mode 100644 index 00000000000..4c4d8fcc9bb --- /dev/null +++ b/homeassistant/components/climacell/translations/es.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "rate_limited": "Actualmente la tarifa est\u00e1 limitada, por favor int\u00e9ntelo m\u00e1s tarde." + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre" + }, + "description": "Si no se proporcionan Latitud y Longitud , se utilizar\u00e1n los valores predeterminados en la configuraci\u00f3n de Home Assistant. Se crear\u00e1 una entidad para cada tipo de pron\u00f3stico, pero solo las que seleccione estar\u00e1n habilitadas de forma predeterminada." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "forecast_types": "Tipo(s) de pron\u00f3stico", + "timestep": "Min. Entre pron\u00f3sticos de NowCast" + }, + "description": "Si elige habilitar la entidad de pron\u00f3stico \"nowcast\", puede configurar el n\u00famero de minutos entre cada pron\u00f3stico. El n\u00famero de pron\u00f3sticos proporcionados depende del n\u00famero de minutos elegidos entre los pron\u00f3sticos.", + "title": "Actualizar las opciones ClimaCell" + } + } + }, + "title": "ClimaCell" +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/zh-Hant.json b/homeassistant/components/climacell/translations/zh-Hant.json new file mode 100644 index 00000000000..76eaf50b932 --- /dev/null +++ b/homeassistant/components/climacell/translations/zh-Hant.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_api_key": "API \u5bc6\u9470\u7121\u6548", + "rate_limited": "\u9054\u5230\u9650\u5236\u983b\u7387\u3001\u8acb\u7a0d\u5019\u518d\u8a66\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "name": "\u540d\u7a31" + }, + "description": "\u5047\u5982\u672a\u63d0\u4f9b\u7def\u5ea6\u8207\u7d93\u5ea6\uff0c\u5c07\u6703\u4f7f\u7528 Home Assistant \u8a2d\u5b9a\u4f5c\u70ba\u9810\u8a2d\u503c\u3002\u6bcf\u4e00\u500b\u9810\u5831\u985e\u578b\u90fd\u6703\u7522\u751f\u4e00\u7d44\u5be6\u9ad4\uff0c\u6216\u8005\u9810\u8a2d\u70ba\u6240\u9078\u64c7\u555f\u7528\u7684\u9810\u5831\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "forecast_types": "\u9810\u5831\u985e\u578b", + "timestep": "NowCast \u9810\u5831\u9593\u9694\u5206\u9418" + }, + "description": "\u5047\u5982\u9078\u64c7\u958b\u555f `nowcast` \u9810\u5831\u5be6\u9ad4\u3001\u5c07\u53ef\u4ee5\u8a2d\u5b9a\u9810\u5831\u983b\u7387\u9593\u9694\u5206\u9418\u6578\u3002\u6839\u64da\u6240\u8f38\u5165\u7684\u9593\u9694\u6642\u9593\u5c07\u6c7a\u5b9a\u9810\u5831\u7684\u6578\u76ee\u3002", + "title": "\u66f4\u65b0 ClimaCell \u9078\u9805" + } + } + }, + "title": "ClimaCell" +} \ No newline at end of file diff --git a/homeassistant/components/faa_delays/translations/cs.json b/homeassistant/components/faa_delays/translations/cs.json new file mode 100644 index 00000000000..60e4aed57a2 --- /dev/null +++ b/homeassistant/components/faa_delays/translations/cs.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/faa_delays/translations/es.json b/homeassistant/components/faa_delays/translations/es.json new file mode 100644 index 00000000000..94eca99dda3 --- /dev/null +++ b/homeassistant/components/faa_delays/translations/es.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Este aeropuerto ya est\u00e1 configurado." + }, + "error": { + "invalid_airport": "El c\u00f3digo del aeropuerto no es v\u00e1lido" + }, + "step": { + "user": { + "data": { + "id": "Aeropuerto" + }, + "description": "Introduzca un c\u00f3digo de aeropuerto estadounidense en formato IATA", + "title": "Retrasos de la FAA" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/faa_delays/translations/et.json b/homeassistant/components/faa_delays/translations/et.json new file mode 100644 index 00000000000..75b52558374 --- /dev/null +++ b/homeassistant/components/faa_delays/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "See lennujaam on juba seadistatud." + }, + "error": { + "cannot_connect": "\u00dchendumine nurjus", + "invalid_airport": "Lennujaama kood ei sobi", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "id": "Lennujaam" + }, + "description": "Sisesta USA lennujaama kood IATA vormingus", + "title": "" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/faa_delays/translations/ru.json b/homeassistant/components/faa_delays/translations/ru.json new file mode 100644 index 00000000000..d68810fc957 --- /dev/null +++ b/homeassistant/components/faa_delays/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0430\u044d\u0440\u043e\u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_airport": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434 \u0430\u044d\u0440\u043e\u043f\u043e\u0440\u0442\u0430.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "id": "\u0410\u044d\u0440\u043e\u043f\u043e\u0440\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0430\u044d\u0440\u043e\u043f\u043e\u0440\u0442\u0430 \u0421\u0428\u0410 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 IATA.", + "title": "FAA Delays" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/faa_delays/translations/zh-Hant.json b/homeassistant/components/faa_delays/translations/zh-Hant.json new file mode 100644 index 00000000000..f2585bb790f --- /dev/null +++ b/homeassistant/components/faa_delays/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u6b64\u6a5f\u5834\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_airport": "\u6a5f\u5834\u4ee3\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "id": "\u6a5f\u5834" + }, + "description": "\u8f38\u5165\u7f8e\u570b\u6a5f\u5834 IATA \u4ee3\u78bc", + "title": "FAA \u822a\u73ed\u5ef6\u8aa4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/cs.json b/homeassistant/components/homekit/translations/cs.json index faf1b1d74fc..cdfaed9183c 100644 --- a/homeassistant/components/homekit/translations/cs.json +++ b/homeassistant/components/homekit/translations/cs.json @@ -17,7 +17,7 @@ "include_domains": "Dom\u00e9ny, kter\u00e9 maj\u00ed b\u00fdt zahrnuty", "mode": "Re\u017eim" }, - "title": "Aktivace HomeKit" + "title": "Vyberte dom\u00e9ny, kter\u00e9 chcete zahrnout" } } }, diff --git a/homeassistant/components/kmtronic/translations/cs.json b/homeassistant/components/kmtronic/translations/cs.json new file mode 100644 index 00000000000..0f02cd974c2 --- /dev/null +++ b/homeassistant/components/kmtronic/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/es.json b/homeassistant/components/litejet/translations/es.json new file mode 100644 index 00000000000..b0641022bf0 --- /dev/null +++ b/homeassistant/components/litejet/translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "open_failed": "No se puede abrir el puerto serie especificado." + }, + "step": { + "user": { + "data": { + "port": "Puerto" + }, + "description": "Conecte el puerto RS232-2 del LiteJet a su computadora e ingrese la ruta al dispositivo del puerto serial. \n\nEl LiteJet MCP debe configurarse para 19,2 K baudios, 8 bits de datos, 1 bit de parada, sin paridad y para transmitir un 'CR' despu\u00e9s de cada respuesta.", + "title": "Conectarse a LiteJet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/es.json b/homeassistant/components/mazda/translations/es.json index 72fc9ce7389..868ae0d770e 100644 --- a/homeassistant/components/mazda/translations/es.json +++ b/homeassistant/components/mazda/translations/es.json @@ -6,6 +6,7 @@ "step": { "reauth": { "data": { + "password": "Contrase\u00f1a", "region": "Regi\u00f3n" }, "description": "Ha fallado la autenticaci\u00f3n para los Servicios Conectados de Mazda. Por favor, introduce tus credenciales actuales.", diff --git a/homeassistant/components/mullvad/translations/cs.json b/homeassistant/components/mullvad/translations/cs.json new file mode 100644 index 00000000000..0f02cd974c2 --- /dev/null +++ b/homeassistant/components/mullvad/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/es.json b/homeassistant/components/mullvad/translations/es.json new file mode 100644 index 00000000000..d6a17561c3d --- /dev/null +++ b/homeassistant/components/mullvad/translations/es.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "\u00bfConfigurar la integraci\u00f3n VPN de Mullvad?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/zh-Hant.json b/homeassistant/components/mullvad/translations/zh-Hant.json new file mode 100644 index 00000000000..d78c36b72d7 --- /dev/null +++ b/homeassistant/components/mullvad/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8a2d\u5b9a Mullvad VPN \u6574\u5408\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/es.json b/homeassistant/components/netatmo/translations/es.json index 556fe2626d4..b1159c1dd9d 100644 --- a/homeassistant/components/netatmo/translations/es.json +++ b/homeassistant/components/netatmo/translations/es.json @@ -15,6 +15,28 @@ } } }, + "device_automation": { + "trigger_subtype": { + "away": "fuera", + "hg": "protector contra las heladas", + "schedule": "Horario" + }, + "trigger_type": { + "alarm_started": "{entity_name} ha detectado una alarma", + "animal": "{entity_name} ha detectado un animal", + "cancel_set_point": "{entity_name} ha reanudado su programaci\u00f3n", + "human": "{entity_name} ha detectado una persona", + "movement": "{entity_name} ha detectado movimiento", + "outdoor": "{entity_name} ha detectado un evento en el exterior", + "person": "{entity_name} ha detectado una persona", + "person_away": "{entity_name} ha detectado que una persona se ha ido", + "set_point": "Temperatura objetivo {entity_name} fijada manualmente", + "therm_mode": "{entity_name} cambi\u00f3 a \" {subtype} \"", + "turned_off": "{entity_name} desactivado", + "turned_on": "{entity_name} activado", + "vehicle": "{entity_name} ha detectado un veh\u00edculo" + } + }, "options": { "step": { "public_weather": { diff --git a/homeassistant/components/netatmo/translations/zh-Hant.json b/homeassistant/components/netatmo/translations/zh-Hant.json index e396deabb68..e62836f9a7e 100644 --- a/homeassistant/components/netatmo/translations/zh-Hant.json +++ b/homeassistant/components/netatmo/translations/zh-Hant.json @@ -15,6 +15,28 @@ } } }, + "device_automation": { + "trigger_subtype": { + "away": "\u96e2\u5bb6", + "hg": "\u9632\u51cd\u6a21\u5f0f", + "schedule": "\u6392\u7a0b" + }, + "trigger_type": { + "alarm_started": "{entity_name}\u5075\u6e2c\u5230\u8b66\u5831", + "animal": "{entity_name}\u5075\u6e2c\u5230\u52d5\u7269", + "cancel_set_point": "{entity_name}\u5df2\u6062\u5fa9\u5176\u6392\u7a0b", + "human": "{entity_name}\u5075\u6e2c\u5230\u4eba\u985e", + "movement": "{entity_name}\u5075\u6e2c\u5230\u52d5\u4f5c", + "outdoor": "{entity_name}\u5075\u6e2c\u5230\u6236\u5916\u52d5\u4f5c", + "person": "{entity_name}\u5075\u6e2c\u5230\u4eba\u54e1", + "person_away": "{entity_name}\u5075\u6e2c\u5230\u4eba\u54e1\u5df2\u96e2\u958b", + "set_point": "\u624b\u52d5\u8a2d\u5b9a{entity_name}\u76ee\u6a19\u6eab\u5ea6", + "therm_mode": "{entity_name}\u5207\u63db\u81f3 \"{subtype}\"", + "turned_off": "{entity_name}\u5df2\u95dc\u9589", + "turned_on": "{entity_name}\u5df2\u958b\u555f", + "vehicle": "{entity_name}\u5075\u6e2c\u5230\u8eca\u8f1b" + } + }, "options": { "step": { "public_weather": { diff --git a/homeassistant/components/rituals_perfume_genie/translations/es.json b/homeassistant/components/rituals_perfume_genie/translations/es.json new file mode 100644 index 00000000000..bc74ecfd7ea --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/es.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Con\u00e9ctese a su cuenta de Rituals" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/nl.json b/homeassistant/components/shelly/translations/nl.json index 7084a972e29..c486b9c6bfe 100644 --- a/homeassistant/components/shelly/translations/nl.json +++ b/homeassistant/components/shelly/translations/nl.json @@ -8,7 +8,7 @@ "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, - "flow_title": "Shelly: {name}", + "flow_title": "{name}", "step": { "confirm_discovery": { "description": "Wilt u het {model} bij {host} instellen? Voordat apparaten op batterijen kunnen worden ingesteld, moet het worden gewekt door op de knop op het apparaat te drukken." @@ -16,7 +16,7 @@ "credentials": { "data": { "password": "Wachtwoord", - "username": "Benutzername" + "username": "Gebruikersnaam" } }, "user": { diff --git a/homeassistant/components/soma/translations/cs.json b/homeassistant/components/soma/translations/cs.json index 5a27562df71..ba1261c1100 100644 --- a/homeassistant/components/soma/translations/cs.json +++ b/homeassistant/components/soma/translations/cs.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "M\u016f\u017eete nastavit pouze jeden \u00fa\u010det Soma.", - "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el.", "connection_error": "P\u0159ipojen\u00ed k za\u0159\u00edzen\u00ed SOMA Connect se nezda\u0159ilo.", "missing_configuration": "Integrace Soma nen\u00ed nastavena. Postupujte podle dokumentace.", "result_error": "SOMA Connect odpov\u011bd\u011blo chybov\u00fdm stavem." diff --git a/homeassistant/components/spotify/translations/cs.json b/homeassistant/components/spotify/translations/cs.json index f8f122e63e2..69cd1b1623a 100644 --- a/homeassistant/components/spotify/translations/cs.json +++ b/homeassistant/components/spotify/translations/cs.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el.", "missing_configuration": "Integrace Spotify nen\u00ed nastavena. Postupujte podle dokumentace.", "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})" }, diff --git a/homeassistant/components/subaru/translations/cs.json b/homeassistant/components/subaru/translations/cs.json new file mode 100644 index 00000000000..ee3bf7347ca --- /dev/null +++ b/homeassistant/components/subaru/translations/cs.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "error": { + "bad_pin_format": "PIN by m\u011bl m\u00edt 4 \u010d\u00edslice", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "incorrect_pin": "Nespr\u00e1vn\u00fd PIN", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "pin": { + "data": { + "pin": "PIN k\u00f3d" + } + }, + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/es.json b/homeassistant/components/subaru/translations/es.json new file mode 100644 index 00000000000..deccc23c75d --- /dev/null +++ b/homeassistant/components/subaru/translations/es.json @@ -0,0 +1,37 @@ +{ + "config": { + "error": { + "bad_pin_format": "El PIN debe tener 4 d\u00edgitos", + "incorrect_pin": "PIN incorrecto" + }, + "step": { + "pin": { + "data": { + "pin": "PIN" + }, + "description": "Por favor, introduzca su PIN de MySubaru\nNOTA: Todos los veh\u00edculos de la cuenta deben tener el mismo PIN", + "title": "Configuraci\u00f3n de Subaru Starlink" + }, + "user": { + "data": { + "country": "Seleccionar pa\u00eds", + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "description": "Por favor, introduzca sus credenciales de MySubaru\nNOTA: La configuraci\u00f3n inicial puede tardar hasta 30 segundos", + "title": "Configuraci\u00f3n de Subaru Starlink" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "update_enabled": "Habilitar el sondeo de veh\u00edculos" + }, + "description": "Cuando est\u00e1 habilitado, el sondeo de veh\u00edculos enviar\u00e1 un comando remoto a su veh\u00edculo cada 2 horas para obtener nuevos datos del sensor. Sin sondeo del veh\u00edculo, los nuevos datos del sensor solo se reciben cuando el veh\u00edculo env\u00eda datos autom\u00e1ticamente (normalmente despu\u00e9s de apagar el motor).", + "title": "Opciones de Subaru Starlink" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/cs.json b/homeassistant/components/totalconnect/translations/cs.json index 60e2196b387..74dece0c54e 100644 --- a/homeassistant/components/totalconnect/translations/cs.json +++ b/homeassistant/components/totalconnect/translations/cs.json @@ -1,12 +1,18 @@ { "config": { "abort": { - "already_configured": "\u00da\u010det je ji\u017e nastaven" + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, "step": { + "locations": { + "data": { + "location": "Um\u00edst\u011bn\u00ed" + } + }, "user": { "data": { "password": "Heslo", diff --git a/homeassistant/components/totalconnect/translations/es.json b/homeassistant/components/totalconnect/translations/es.json index 48af1bed0f4..090d9271dee 100644 --- a/homeassistant/components/totalconnect/translations/es.json +++ b/homeassistant/components/totalconnect/translations/es.json @@ -4,9 +4,17 @@ "already_configured": "La cuenta ya ha sido configurada" }, "error": { - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "usercode": "El c\u00f3digo de usuario no es v\u00e1lido para este usuario en esta ubicaci\u00f3n" }, "step": { + "locations": { + "description": "Ingrese el c\u00f3digo de usuario para este usuario en esta ubicaci\u00f3n", + "title": "C\u00f3digos de usuario de ubicaci\u00f3n" + }, + "reauth_confirm": { + "description": "Total Connect necesita volver a autentificar tu cuenta" + }, "user": { "data": { "password": "Contrase\u00f1a", diff --git a/homeassistant/components/wolflink/translations/sensor.nl.json b/homeassistant/components/wolflink/translations/sensor.nl.json new file mode 100644 index 00000000000..da03cc43b4b --- /dev/null +++ b/homeassistant/components/wolflink/translations/sensor.nl.json @@ -0,0 +1,16 @@ +{ + "state": { + "wolflink__state": { + "frost_warmwasser": "DHW vorst", + "frostschutz": "Vorstbescherming", + "gasdruck": "Gasdruk", + "glt_betrieb": "BMS-modus", + "heizbetrieb": "Verwarmingsmodus", + "heizgerat_mit_speicher": "Boiler met cilinder", + "heizung": "Verwarmen", + "initialisierung": "Initialisatie", + "kalibration": "Kalibratie", + "kalibration_heizbetrieb": "Kalibratie verwarmingsmodus" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/es.json b/homeassistant/components/xiaomi_miio/translations/es.json index fd4b8c36a8b..60a989ade0d 100644 --- a/homeassistant/components/xiaomi_miio/translations/es.json +++ b/homeassistant/components/xiaomi_miio/translations/es.json @@ -13,6 +13,7 @@ "step": { "device": { "data": { + "model": "Modelo de dispositivo (opcional)", "name": "Nombre del dispositivo" }, "description": "Necesitar\u00e1 la clave de 32 caracteres Token API, consulte https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token para obtener instrucciones. Tenga en cuenta que esta Token API es diferente de la clave utilizada por la integraci\u00f3n de Xiaomi Aqara.", diff --git a/homeassistant/components/zwave_js/translations/es.json b/homeassistant/components/zwave_js/translations/es.json index 32d7a6d2e6d..26fd155a0ad 100644 --- a/homeassistant/components/zwave_js/translations/es.json +++ b/homeassistant/components/zwave_js/translations/es.json @@ -6,6 +6,7 @@ "addon_install_failed": "No se ha podido instalar el complemento Z-Wave JS.", "addon_missing_discovery_info": "Falta informaci\u00f3n de descubrimiento del complemento Z-Wave JS.", "addon_set_config_failed": "Fallo en la configuraci\u00f3n de Z-Wave JS.", + "addon_start_failed": "No se ha podido iniciar el complemento Z-Wave JS.", "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", "cannot_connect": "No se pudo conectar" @@ -17,7 +18,8 @@ "unknown": "Error inesperado" }, "progress": { - "install_addon": "Espera mientras termina la instalaci\u00f3n del complemento Z-Wave JS. Puede tardar varios minutos." + "install_addon": "Espera mientras termina la instalaci\u00f3n del complemento Z-Wave JS. Puede tardar varios minutos.", + "start_addon": "Espere mientras se completa el inicio del complemento Z-Wave JS. Esto puede tardar unos segundos." }, "step": { "configure_addon": { @@ -45,6 +47,9 @@ "description": "\u00bfQuieres utilizar el complemento Z-Wave JS Supervisor?", "title": "Selecciona el m\u00e9todo de conexi\u00f3n" }, + "start_addon": { + "title": "Se est\u00e1 iniciando el complemento Z-Wave JS." + }, "user": { "data": { "url": "URL" From 3b459cd59af1873317a1bd90d507c9a87a2131c2 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 27 Feb 2021 00:05:45 +0000 Subject: [PATCH 776/796] [ci skip] Translation update --- .../accuweather/translations/nl.json | 4 ++ .../alarmdecoder/translations/nl.json | 2 +- .../azure_devops/translations/nl.json | 14 +++++- .../components/bond/translations/it.json | 4 +- .../components/bond/translations/nl.json | 5 +++ .../components/broadlink/translations/nl.json | 9 ++++ .../components/climacell/translations/it.json | 34 ++++++++++++++ .../components/climacell/translations/no.json | 2 +- .../components/cover/translations/nl.json | 3 +- .../faa_delays/translations/it.json | 21 +++++++++ .../faa_delays/translations/no.json | 21 +++++++++ .../components/gogogate2/translations/nl.json | 2 +- .../components/hlk_sw16/translations/nl.json | 1 + .../components/homekit/translations/it.json | 8 ++-- .../components/homekit/translations/nl.json | 6 +-- .../components/insteon/translations/nl.json | 3 ++ .../components/kmtronic/translations/it.json | 21 +++++++++ .../components/kodi/translations/nl.json | 7 ++- .../components/litejet/translations/it.json | 19 ++++++++ .../litterrobot/translations/it.json | 20 +++++++++ .../media_player/translations/nl.json | 2 +- .../components/mqtt/translations/nl.json | 1 + .../components/mullvad/translations/it.json | 22 ++++++++++ .../components/netatmo/translations/it.json | 22 ++++++++++ .../components/netatmo/translations/nl.json | 7 ++- .../components/netatmo/translations/no.json | 22 ++++++++++ .../philips_js/translations/en.json | 4 +- .../philips_js/translations/et.json | 2 + .../rainmachine/translations/nl.json | 2 +- .../components/risco/translations/nl.json | 4 +- .../components/sentry/translations/nl.json | 3 ++ .../simplisafe/translations/nl.json | 6 ++- .../somfy_mylink/translations/nl.json | 2 +- .../components/spotify/translations/nl.json | 2 +- .../components/subaru/translations/it.json | 44 +++++++++++++++++++ .../components/syncthru/translations/nl.json | 5 +++ .../totalconnect/translations/it.json | 17 ++++++- .../components/volumio/translations/nl.json | 1 + .../wolflink/translations/sensor.nl.json | 11 ++++- .../xiaomi_miio/translations/it.json | 1 + .../xiaomi_miio/translations/nl.json | 1 + .../zoneminder/translations/nl.json | 2 +- .../components/zwave_js/translations/it.json | 7 ++- .../components/zwave_js/translations/nl.json | 2 +- 44 files changed, 365 insertions(+), 33 deletions(-) create mode 100644 homeassistant/components/climacell/translations/it.json create mode 100644 homeassistant/components/faa_delays/translations/it.json create mode 100644 homeassistant/components/faa_delays/translations/no.json create mode 100644 homeassistant/components/kmtronic/translations/it.json create mode 100644 homeassistant/components/litejet/translations/it.json create mode 100644 homeassistant/components/litterrobot/translations/it.json create mode 100644 homeassistant/components/mullvad/translations/it.json create mode 100644 homeassistant/components/subaru/translations/it.json diff --git a/homeassistant/components/accuweather/translations/nl.json b/homeassistant/components/accuweather/translations/nl.json index ff0d81f94d3..4bf5f9fce45 100644 --- a/homeassistant/components/accuweather/translations/nl.json +++ b/homeassistant/components/accuweather/translations/nl.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, "error": { + "cannot_connect": "Kan geen verbinding maken", "invalid_api_key": "API-sleutel", "requests_exceeded": "Het toegestane aantal verzoeken aan de Accuweather API is overschreden. U moet wachten of de API-sleutel wijzigen." }, diff --git a/homeassistant/components/alarmdecoder/translations/nl.json b/homeassistant/components/alarmdecoder/translations/nl.json index 1af1e8d803c..1ea9cb98b56 100644 --- a/homeassistant/components/alarmdecoder/translations/nl.json +++ b/homeassistant/components/alarmdecoder/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "AlarmDecoder-apparaat is al geconfigureerd." + "already_configured": "Apparaat is al geconfigureerd" }, "create_entry": { "default": "Succesvol verbonden met AlarmDecoder." diff --git a/homeassistant/components/azure_devops/translations/nl.json b/homeassistant/components/azure_devops/translations/nl.json index aef6a717895..07dc59e1a56 100644 --- a/homeassistant/components/azure_devops/translations/nl.json +++ b/homeassistant/components/azure_devops/translations/nl.json @@ -1,11 +1,21 @@ { "config": { "abort": { - "already_configured": "Account is al geconfigureerd" + "already_configured": "Account is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "cannot_connect": "Kan geen verbinding maken", - "invalid_auth": "Ongeldige authenticatie" + "invalid_auth": "Ongeldige authenticatie", + "project_error": "Kon geen projectinformatie ophalen." + }, + "step": { + "user": { + "data": { + "organization": "Organisatie", + "project": "Project" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/bond/translations/it.json b/homeassistant/components/bond/translations/it.json index d3ac1ab6b49..e22ad82e1fd 100644 --- a/homeassistant/components/bond/translations/it.json +++ b/homeassistant/components/bond/translations/it.json @@ -9,13 +9,13 @@ "old_firmware": "Firmware precedente non supportato sul dispositivo Bond - si prega di aggiornare prima di continuare", "unknown": "Errore imprevisto" }, - "flow_title": "Bond: {bond_id} ({host})", + "flow_title": "Bond: {name} ({host})", "step": { "confirm": { "data": { "access_token": "Token di accesso" }, - "description": "Vuoi configurare {bond_id}?" + "description": "Vuoi configurare {name}?" }, "user": { "data": { diff --git a/homeassistant/components/bond/translations/nl.json b/homeassistant/components/bond/translations/nl.json index b5d8c593ea9..a76c7a69d7f 100644 --- a/homeassistant/components/bond/translations/nl.json +++ b/homeassistant/components/bond/translations/nl.json @@ -10,6 +10,11 @@ }, "flow_title": "Bond: {bond_id} ({host})", "step": { + "confirm": { + "data": { + "access_token": "Toegangstoken" + } + }, "user": { "data": { "access_token": "Toegangstoken", diff --git a/homeassistant/components/broadlink/translations/nl.json b/homeassistant/components/broadlink/translations/nl.json index 7f85335d7bb..d2db5476555 100644 --- a/homeassistant/components/broadlink/translations/nl.json +++ b/homeassistant/components/broadlink/translations/nl.json @@ -18,6 +18,15 @@ "finish": { "data": { "name": "Naam" + }, + "title": "Kies een naam voor het apparaat" + }, + "reset": { + "title": "Ontgrendel het apparaat" + }, + "unlock": { + "data": { + "unlock": "Ja, doe het." } }, "user": { diff --git a/homeassistant/components/climacell/translations/it.json b/homeassistant/components/climacell/translations/it.json new file mode 100644 index 00000000000..cc7df4f8ab3 --- /dev/null +++ b/homeassistant/components/climacell/translations/it.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_api_key": "Chiave API non valida", + "rate_limited": "Al momento la tariffa \u00e8 limitata, riprova pi\u00f9 tardi.", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "latitude": "Latitudine", + "longitude": "Logitudine", + "name": "Nome" + }, + "description": "Se Latitudine e Logitudine non vengono forniti, verranno utilizzati i valori predefiniti nella configurazione di Home Assistant. Verr\u00e0 creata un'entit\u00e0 per ogni tipo di previsione, ma solo quelli selezionati saranno abilitati per impostazione predefinita." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "forecast_types": "Tipo(i) di previsione", + "timestep": "Minuti tra le previsioni di NowCast" + }, + "description": "Se scegli di abilitare l'entit\u00e0 di previsione `nowcast`, puoi configurare il numero di minuti tra ogni previsione. Il numero di previsioni fornite dipende dal numero di minuti scelti tra le previsioni.", + "title": "Aggiorna le opzioni di ClimaCell" + } + } + }, + "title": "ClimaCell" +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/no.json b/homeassistant/components/climacell/translations/no.json index 64845ff7697..af07ce716d0 100644 --- a/homeassistant/components/climacell/translations/no.json +++ b/homeassistant/components/climacell/translations/no.json @@ -14,7 +14,7 @@ "longitude": "Lengdegrad", "name": "Navn" }, - "description": "Hvis Breddegrad and Lengdegrad er ikke gitt, vil standardverdiene i Home Assistant-konfigurasjonen bli brukt. Det blir opprettet en enhet for hver prognosetype, men bare de du velger blir aktivert som standard." + "description": "Hvis Breddegrad og Lengdegrad ikke er oppgitt, vil standardverdiene i Home Assistant-konfigurasjonen bli brukt. Det blir opprettet en enhet for hver prognosetype, men bare de du velger blir aktivert som standard." } } }, diff --git a/homeassistant/components/cover/translations/nl.json b/homeassistant/components/cover/translations/nl.json index 679d9360a82..8b1ca3c3500 100644 --- a/homeassistant/components/cover/translations/nl.json +++ b/homeassistant/components/cover/translations/nl.json @@ -6,7 +6,8 @@ "open": "Open {entity_name}", "open_tilt": "Open de kanteling {entity_name}", "set_position": "Stel de positie van {entity_name} in", - "set_tilt_position": "Stel de {entity_name} kantelpositie in" + "set_tilt_position": "Stel de {entity_name} kantelpositie in", + "stop": "Stop {entity_name}" }, "condition_type": { "is_closed": "{entity_name} is gesloten", diff --git a/homeassistant/components/faa_delays/translations/it.json b/homeassistant/components/faa_delays/translations/it.json new file mode 100644 index 00000000000..e1bf6ad0646 --- /dev/null +++ b/homeassistant/components/faa_delays/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Questo aeroporto \u00e8 gi\u00e0 configurato." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_airport": "Il codice dell'aeroporto non \u00e8 valido", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "id": "Aeroporto" + }, + "description": "Immettere un codice aeroporto statunitense in formato IATA", + "title": "Ritardi FAA" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/faa_delays/translations/no.json b/homeassistant/components/faa_delays/translations/no.json new file mode 100644 index 00000000000..c481f90bf75 --- /dev/null +++ b/homeassistant/components/faa_delays/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Denne flyplassen er allerede konfigurert." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_airport": "Flyplasskoden er ikke gyldig", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "id": "Flyplass" + }, + "description": "Skriv inn en amerikansk flyplasskode i IATA-format", + "title": "FAA forsinkelser" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/nl.json b/homeassistant/components/gogogate2/translations/nl.json index ad8e894d093..5418735ec07 100644 --- a/homeassistant/components/gogogate2/translations/nl.json +++ b/homeassistant/components/gogogate2/translations/nl.json @@ -15,7 +15,7 @@ "username": "Gebruikersnaam" }, "description": "Geef hieronder de vereiste informatie op.", - "title": "Stel GogoGate2 in" + "title": "Stel GogoGate2 of iSmartGate in" } } } diff --git a/homeassistant/components/hlk_sw16/translations/nl.json b/homeassistant/components/hlk_sw16/translations/nl.json index 0569c39321a..8ad15260b0d 100644 --- a/homeassistant/components/hlk_sw16/translations/nl.json +++ b/homeassistant/components/hlk_sw16/translations/nl.json @@ -4,6 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json index 9a85d1e6e9f..fee64457652 100644 --- a/homeassistant/components/homekit/translations/it.json +++ b/homeassistant/components/homekit/translations/it.json @@ -19,7 +19,7 @@ "title": "Seleziona i domini da includere" }, "pairing": { - "description": "Non appena il {name} \u00e8 pronto, l'associazione sar\u00e0 disponibile in \"Notifiche\" come \"Configurazione HomeKit Bridge\".", + "description": "Per completare l'associazione, seguire le istruzioni in \"Notifiche\" sotto \"Associazione HomeKit\".", "title": "Associa HomeKit" }, "user": { @@ -28,8 +28,8 @@ "include_domains": "Domini da includere", "mode": "Modalit\u00e0" }, - "description": "L'integrazione di HomeKit ti consentir\u00e0 di accedere alle entit\u00e0 di Home Assistant in HomeKit. In modalit\u00e0 bridge, i bridge HomeKit sono limitati a 150 accessori per istanza, incluso il bridge stesso. Se desideri eseguire il bridge di un numero di accessori superiore a quello massimo, si consiglia di utilizzare pi\u00f9 bridge HomeKit per domini diversi. La configurazione dettagliata dell'entit\u00e0 \u00e8 disponibile solo tramite YAML per il bridge principale.", - "title": "Attiva HomeKit" + "description": "Scegli i domini da includere. Verranno incluse tutte le entit\u00e0 supportate nel dominio. Verr\u00e0 creata un'istanza HomeKit separata in modalit\u00e0 accessorio per ogni lettore multimediale TV e telecamera.", + "title": "Seleziona i domini da includere" } } }, @@ -55,7 +55,7 @@ "entities": "Entit\u00e0", "mode": "Modalit\u00e0" }, - "description": "Scegliere le entit\u00e0 da includere. In modalit\u00e0 accessorio, \u00e8 inclusa una sola entit\u00e0. In modalit\u00e0 di inclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, a meno che non siano selezionate entit\u00e0 specifiche. In modalit\u00e0 di esclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, ad eccezione delle entit\u00e0 escluse. Per prestazioni ottimali e per evitare una indisponibilit\u00e0 imprevista, creare e associare un'istanza HomeKit separata in modalit\u00e0 accessorio per ogni lettore multimediale, TV e videocamera.", + "description": "Scegliere le entit\u00e0 da includere. In modalit\u00e0 accessorio, \u00e8 inclusa una sola entit\u00e0. In modalit\u00e0 di inclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, a meno che non siano selezionate entit\u00e0 specifiche. In modalit\u00e0 di esclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, ad eccezione delle entit\u00e0 escluse. Per prestazioni ottimali, ci sar\u00e0 una HomeKit separata in modalit\u00e0 accessorio per ogni lettore multimediale, TV e videocamera.", "title": "Seleziona le entit\u00e0 da includere" }, "init": { diff --git a/homeassistant/components/homekit/translations/nl.json b/homeassistant/components/homekit/translations/nl.json index bcf61fe9868..9013723ac6c 100644 --- a/homeassistant/components/homekit/translations/nl.json +++ b/homeassistant/components/homekit/translations/nl.json @@ -11,7 +11,7 @@ }, "pairing": { "description": "Zodra de {name} klaar is, is het koppelen beschikbaar in \"Meldingen\" als \"HomeKit Bridge Setup\".", - "title": "Koppel HomeKit Bridge" + "title": "Koppel HomeKit" }, "user": { "data": { @@ -20,7 +20,7 @@ "mode": "Mode" }, "description": "De HomeKit-integratie geeft u toegang tot uw Home Assistant-entiteiten in HomeKit. In bridge-modus zijn HomeKit-bruggen beperkt tot 150 accessoires per exemplaar, inclusief de brug zelf. Als u meer dan het maximale aantal accessoires wilt overbruggen, is het aan te raden om meerdere HomeKit-bridges voor verschillende domeinen te gebruiken. Gedetailleerde entiteitsconfiguratie is alleen beschikbaar via YAML voor de primaire bridge.", - "title": "Activeer HomeKit Bridge" + "title": "Selecteer domeinen die u wilt opnemen" } } }, @@ -57,7 +57,7 @@ }, "yaml": { "description": "Deze invoer wordt beheerd via YAML", - "title": "Pas de HomeKit Bridge-opties aan" + "title": "Pas de HomeKit-opties aan" } } } diff --git a/homeassistant/components/insteon/translations/nl.json b/homeassistant/components/insteon/translations/nl.json index e4f7d4a8102..98a27fb1139 100644 --- a/homeassistant/components/insteon/translations/nl.json +++ b/homeassistant/components/insteon/translations/nl.json @@ -87,6 +87,9 @@ "remove_override": "Verwijder een apparaatoverschrijving.", "remove_x10": "Verwijder een X10-apparaat." } + }, + "remove_x10": { + "title": "Insteon" } } } diff --git a/homeassistant/components/kmtronic/translations/it.json b/homeassistant/components/kmtronic/translations/it.json new file mode 100644 index 00000000000..e9356485e08 --- /dev/null +++ b/homeassistant/components/kmtronic/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/nl.json b/homeassistant/components/kodi/translations/nl.json index 8eb4a39cfb6..57476791b8f 100644 --- a/homeassistant/components/kodi/translations/nl.json +++ b/homeassistant/components/kodi/translations/nl.json @@ -11,6 +11,7 @@ "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, + "flow_title": "Kodi: {name}", "step": { "credentials": { "data": { @@ -19,11 +20,15 @@ }, "description": "Voer uw Kodi gebruikersnaam en wachtwoord in. Deze zijn te vinden in Systeem / Instellingen / Netwerk / Services." }, + "discovery_confirm": { + "description": "Wil je Kodi (`{name}`) toevoegen aan Home Assistant?", + "title": "Kodi ontdekt" + }, "user": { "data": { "host": "Host", "port": "Poort", - "ssl": "Maak verbinding via SSL" + "ssl": "Gebruik een SSL-certificaat" }, "description": "Kodi-verbindingsinformatie. Zorg ervoor dat u \"Controle van Kodi via HTTP toestaan\" in Systeem / Instellingen / Netwerk / Services inschakelt." }, diff --git a/homeassistant/components/litejet/translations/it.json b/homeassistant/components/litejet/translations/it.json new file mode 100644 index 00000000000..5b3dc46753d --- /dev/null +++ b/homeassistant/components/litejet/translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "error": { + "open_failed": "Impossibile aprire la porta seriale specificata." + }, + "step": { + "user": { + "data": { + "port": "Porta" + }, + "description": "Collega la porta RS232-2 del LiteJet al tuo computer e inserisci il percorso del dispositivo della porta seriale. \n\nL'MCP LiteJet deve essere configurato per 19,2 K baud, 8 bit di dati, 1 bit di stop, nessuna parit\u00e0 e per trasmettere un \"CR\" dopo ogni risposta.", + "title": "Connetti a LiteJet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/it.json b/homeassistant/components/litterrobot/translations/it.json new file mode 100644 index 00000000000..843262aa318 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/translations/nl.json b/homeassistant/components/media_player/translations/nl.json index 37c1d6b4d9e..6ad22742533 100644 --- a/homeassistant/components/media_player/translations/nl.json +++ b/homeassistant/components/media_player/translations/nl.json @@ -22,7 +22,7 @@ "on": "Aan", "paused": "Gepauzeerd", "playing": "Afspelen", - "standby": "Standby" + "standby": "Stand-by" } }, "title": "Mediaspeler" diff --git a/homeassistant/components/mqtt/translations/nl.json b/homeassistant/components/mqtt/translations/nl.json index a0ab0e497da..3b3ebf9fe3b 100644 --- a/homeassistant/components/mqtt/translations/nl.json +++ b/homeassistant/components/mqtt/translations/nl.json @@ -63,6 +63,7 @@ }, "options": { "data": { + "birth_enable": "Geboortebericht inschakelen", "birth_payload": "Birth message payload", "birth_topic": "Birth message onderwerp" } diff --git a/homeassistant/components/mullvad/translations/it.json b/homeassistant/components/mullvad/translations/it.json new file mode 100644 index 00000000000..47cd8290f21 --- /dev/null +++ b/homeassistant/components/mullvad/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "username": "Nome utente" + }, + "description": "Configurare l'integrazione VPN Mullvad?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/it.json b/homeassistant/components/netatmo/translations/it.json index 46c2d7d2721..152f7d47597 100644 --- a/homeassistant/components/netatmo/translations/it.json +++ b/homeassistant/components/netatmo/translations/it.json @@ -15,6 +15,28 @@ } } }, + "device_automation": { + "trigger_subtype": { + "away": "Fuori casa", + "hg": "protezione antigelo", + "schedule": "programma" + }, + "trigger_type": { + "alarm_started": "{entity_name} ha rilevato un allarme", + "animal": "{entity_name} ha rilevato un animale", + "cancel_set_point": "{entity_name} ha ripreso il suo programma", + "human": "{entity_name} ha rilevato un essere umano", + "movement": "{entity_name} ha rilevato un movimento", + "outdoor": "{entity_name} ha rilevato un evento all'esterno", + "person": "{entity_name} ha rilevato una persona", + "person_away": "{entity_name} ha rilevato che una persona \u00e8 uscita", + "set_point": "{entity_name} temperatura desiderata impostata manualmente", + "therm_mode": "{entity_name} \u00e8 passato a \"{subtype}\"", + "turned_off": "{entity_name} disattivato", + "turned_on": "{entity_name} attivato", + "vehicle": "{entity_name} ha rilevato un veicolo" + } + }, "options": { "step": { "public_weather": { diff --git a/homeassistant/components/netatmo/translations/nl.json b/homeassistant/components/netatmo/translations/nl.json index 431f105df3d..0bdc3170a5a 100644 --- a/homeassistant/components/netatmo/translations/nl.json +++ b/homeassistant/components/netatmo/translations/nl.json @@ -3,7 +3,8 @@ "abort": { "authorize_url_timeout": "Time-out genereren autorisatie-URL.", "missing_configuration": "Het component is niet geconfigureerd. Volg de documentatie.", - "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})" + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, "create_entry": { "default": "Succesvol geauthenticeerd met Netatmo." @@ -41,6 +42,10 @@ "public_weather": { "data": { "area_name": "Naam van het gebied", + "lat_ne": "Breedtegraad Noordoostelijke hoek", + "lat_sw": "Breedtegraad Zuidwestelijke hoek", + "lon_ne": "Lengtegraad Noordoostelijke hoek", + "lon_sw": "Lengtegraad Zuidwestelijke hoek", "mode": "Berekening", "show_on_map": "Toon op kaart" } diff --git a/homeassistant/components/netatmo/translations/no.json b/homeassistant/components/netatmo/translations/no.json index 387dbe7b26c..9e3e24d5771 100644 --- a/homeassistant/components/netatmo/translations/no.json +++ b/homeassistant/components/netatmo/translations/no.json @@ -15,6 +15,28 @@ } } }, + "device_automation": { + "trigger_subtype": { + "away": "borte", + "hg": "frostvakt", + "schedule": "Tidsplan" + }, + "trigger_type": { + "alarm_started": "{entity_name} oppdaget en alarm", + "animal": "{entity_name} oppdaget et dyr", + "cancel_set_point": "{entity_name} har gjenopptatt tidsplanen", + "human": "{entity_name} oppdaget et menneske", + "movement": "{entity_name} oppdaget bevegelse", + "outdoor": "{entity_name} oppdaget en utend\u00f8rs hendelse", + "person": "{entity_name} oppdaget en person", + "person_away": "{entity_name} oppdaget at en person har forlatt", + "set_point": "M\u00e5ltemperatur {entity_name} angis manuelt", + "therm_mode": "{entity_name} byttet til \"{subtype}\"", + "turned_off": "{entity_name} sl\u00e5tt av", + "turned_on": "{entity_name} sl\u00e5tt p\u00e5", + "vehicle": "{entity_name} oppdaget et kj\u00f8ret\u00f8y" + } + }, "options": { "step": { "public_weather": { diff --git a/homeassistant/components/philips_js/translations/en.json b/homeassistant/components/philips_js/translations/en.json index b2022a01824..65d4f417b9f 100644 --- a/homeassistant/components/philips_js/translations/en.json +++ b/homeassistant/components/philips_js/translations/en.json @@ -5,9 +5,9 @@ }, "error": { "cannot_connect": "Failed to connect", - "unknown": "Unexpected error", + "invalid_pin": "Invalid PIN", "pairing_failure": "Unable to pair: {error_id}", - "invalid_pin": "Invalid PIN" + "unknown": "Unexpected error" }, "step": { "user": { diff --git a/homeassistant/components/philips_js/translations/et.json b/homeassistant/components/philips_js/translations/et.json index c77ef726411..9953df9c272 100644 --- a/homeassistant/components/philips_js/translations/et.json +++ b/homeassistant/components/philips_js/translations/et.json @@ -5,6 +5,8 @@ }, "error": { "cannot_connect": "\u00dchendamine nurjus", + "invalid_pin": "Vale PIN kood", + "pairing_failure": "Sidumine nurjus: {error_id}", "unknown": "Ootamatu t\u00f5rge" }, "step": { diff --git a/homeassistant/components/rainmachine/translations/nl.json b/homeassistant/components/rainmachine/translations/nl.json index 02411ea999f..119e4c641af 100644 --- a/homeassistant/components/rainmachine/translations/nl.json +++ b/homeassistant/components/rainmachine/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Deze RainMachine controller is al geconfigureerd." + "already_configured": "Apparaat is al geconfigureerd" }, "error": { "invalid_auth": "Ongeldige authenticatie" diff --git a/homeassistant/components/risco/translations/nl.json b/homeassistant/components/risco/translations/nl.json index 34bcb4ab98a..97d0d454a4f 100644 --- a/homeassistant/components/risco/translations/nl.json +++ b/homeassistant/components/risco/translations/nl.json @@ -30,8 +30,8 @@ }, "init": { "data": { - "code_arm_required": "Pincode vereist om in te schakelen", - "code_disarm_required": "Pincode vereist om uit te schakelen" + "code_arm_required": "PIN-code vereist om in te schakelen", + "code_disarm_required": "PIN-code vereist om uit te schakelen" }, "title": "Configureer opties" }, diff --git a/homeassistant/components/sentry/translations/nl.json b/homeassistant/components/sentry/translations/nl.json index 37437dfe836..64b7f1b73f7 100644 --- a/homeassistant/components/sentry/translations/nl.json +++ b/homeassistant/components/sentry/translations/nl.json @@ -9,6 +9,9 @@ }, "step": { "user": { + "data": { + "dsn": "DSN" + }, "description": "Voer uw Sentry DSN in", "title": "Sentry" } diff --git a/homeassistant/components/simplisafe/translations/nl.json b/homeassistant/components/simplisafe/translations/nl.json index b285b288525..d3196c591cb 100644 --- a/homeassistant/components/simplisafe/translations/nl.json +++ b/homeassistant/components/simplisafe/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Dit SimpliSafe-account is al in gebruik." + "already_configured": "Dit SimpliSafe-account is al in gebruik.", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "identifier_exists": "Account bestaat al", @@ -13,7 +14,8 @@ "data": { "password": "Wachtwoord" }, - "description": "Uw toegangstoken is verlopen of ingetrokken. Voer uw wachtwoord in om uw account opnieuw te koppelen." + "description": "Uw toegangstoken is verlopen of ingetrokken. Voer uw wachtwoord in om uw account opnieuw te koppelen.", + "title": "Verifieer de integratie opnieuw" }, "user": { "data": { diff --git a/homeassistant/components/somfy_mylink/translations/nl.json b/homeassistant/components/somfy_mylink/translations/nl.json index a63320919c6..b0ae5c9d3ad 100644 --- a/homeassistant/components/somfy_mylink/translations/nl.json +++ b/homeassistant/components/somfy_mylink/translations/nl.json @@ -8,7 +8,7 @@ "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, - "flow_title": "Somfy MyLink {mac} ( {ip} )", + "flow_title": "Somfy MyLink {mac} ({ip})", "step": { "user": { "data": { diff --git a/homeassistant/components/spotify/translations/nl.json b/homeassistant/components/spotify/translations/nl.json index bdc86919f74..46b18857fe8 100644 --- a/homeassistant/components/spotify/translations/nl.json +++ b/homeassistant/components/spotify/translations/nl.json @@ -15,7 +15,7 @@ }, "reauth_confirm": { "description": "De Spotify integratie moet opnieuw worden geverifieerd met Spotify voor account: {account}", - "title": "Verifieer opnieuw met Spotify" + "title": "Verifieer de integratie opnieuw" } } } diff --git a/homeassistant/components/subaru/translations/it.json b/homeassistant/components/subaru/translations/it.json new file mode 100644 index 00000000000..6dbb0702f46 --- /dev/null +++ b/homeassistant/components/subaru/translations/it.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi" + }, + "error": { + "bad_pin_format": "Il PIN deve essere di 4 cifre", + "cannot_connect": "Impossibile connettersi", + "incorrect_pin": "PIN errato", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "pin": { + "data": { + "pin": "PIN" + }, + "description": "Inserisci il tuo PIN MySubaru\nNOTA: tutti i veicoli nell'account devono avere lo stesso PIN", + "title": "Configurazione Subaru Starlink" + }, + "user": { + "data": { + "country": "Seleziona il paese", + "password": "Password", + "username": "Nome utente" + }, + "description": "Inserisci le tue credenziali MySubaru\nNOTA: la configurazione iniziale pu\u00f2 richiedere fino a 30 secondi", + "title": "Configurazione Subaru Starlink" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "update_enabled": "Abilita l'interrogazione del veicolo" + }, + "description": "Quando abilitata, l'interrogazione del veicolo invier\u00e0 un comando remoto al tuo veicolo ogni 2 ore per ottenere nuovi dati del sensore. Senza l'interrogazione del veicolo, i nuovi dati del sensore verranno ricevuti solo quando il veicolo invier\u00e0 automaticamente i dati (normalmente dopo lo spegnimento del motore).", + "title": "Opzioni Subaru Starlink" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthru/translations/nl.json b/homeassistant/components/syncthru/translations/nl.json index b1beb4058bc..799e19ea371 100644 --- a/homeassistant/components/syncthru/translations/nl.json +++ b/homeassistant/components/syncthru/translations/nl.json @@ -8,6 +8,11 @@ }, "flow_title": "Samsung SyncThru Printer: {name}", "step": { + "confirm": { + "data": { + "name": "Naam" + } + }, "user": { "data": { "name": "Naam", diff --git a/homeassistant/components/totalconnect/translations/it.json b/homeassistant/components/totalconnect/translations/it.json index 2a12d00f57d..18ecf648310 100644 --- a/homeassistant/components/totalconnect/translations/it.json +++ b/homeassistant/components/totalconnect/translations/it.json @@ -1,12 +1,25 @@ { "config": { "abort": { - "already_configured": "L'account \u00e8 gi\u00e0 configurato" + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { - "invalid_auth": "Autenticazione non valida" + "invalid_auth": "Autenticazione non valida", + "usercode": "Codice utente non valido per questo utente in questa posizione" }, "step": { + "locations": { + "data": { + "location": "Posizione" + }, + "description": "Immettere il codice utente per questo utente in questa posizione", + "title": "Codici utente posizione" + }, + "reauth_confirm": { + "description": "Total Connect deve autenticare nuovamente il tuo account", + "title": "Autenticare nuovamente l'integrazione" + }, "user": { "data": { "password": "Password", diff --git a/homeassistant/components/volumio/translations/nl.json b/homeassistant/components/volumio/translations/nl.json index 9179418def9..9e11dbad82b 100644 --- a/homeassistant/components/volumio/translations/nl.json +++ b/homeassistant/components/volumio/translations/nl.json @@ -4,6 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { + "cannot_connect": "Kan geen verbinding maken", "unknown": "Onverwachte fout" }, "step": { diff --git a/homeassistant/components/wolflink/translations/sensor.nl.json b/homeassistant/components/wolflink/translations/sensor.nl.json index da03cc43b4b..ae205d79aef 100644 --- a/homeassistant/components/wolflink/translations/sensor.nl.json +++ b/homeassistant/components/wolflink/translations/sensor.nl.json @@ -10,7 +10,16 @@ "heizung": "Verwarmen", "initialisierung": "Initialisatie", "kalibration": "Kalibratie", - "kalibration_heizbetrieb": "Kalibratie verwarmingsmodus" + "kalibration_heizbetrieb": "Kalibratie verwarmingsmodus", + "permanent": "Permanent", + "standby": "Stand-by", + "start": "Start", + "storung": "Fout", + "test": "Test", + "tpw": "TPW", + "urlaubsmodus": "Vakantiemodus", + "ventilprufung": "Kleptest", + "warmwasser": "DHW" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/it.json b/homeassistant/components/xiaomi_miio/translations/it.json index 68202e1631e..aa48ba7cfa8 100644 --- a/homeassistant/components/xiaomi_miio/translations/it.json +++ b/homeassistant/components/xiaomi_miio/translations/it.json @@ -14,6 +14,7 @@ "device": { "data": { "host": "Indirizzo IP", + "model": "Modello del dispositivo (opzionale)", "name": "Nome del dispositivo", "token": "Token API" }, diff --git a/homeassistant/components/xiaomi_miio/translations/nl.json b/homeassistant/components/xiaomi_miio/translations/nl.json index 3ea12e3a465..66209e61ee6 100644 --- a/homeassistant/components/xiaomi_miio/translations/nl.json +++ b/homeassistant/components/xiaomi_miio/translations/nl.json @@ -8,6 +8,7 @@ "cannot_connect": "Kan geen verbinding maken", "no_device_selected": "Geen apparaat geselecteerd, selecteer 1 apparaat alstublieft" }, + "flow_title": "Xiaomi Miio: {name}", "step": { "device": { "data": { diff --git a/homeassistant/components/zoneminder/translations/nl.json b/homeassistant/components/zoneminder/translations/nl.json index f4f071d9097..8aed5085391 100644 --- a/homeassistant/components/zoneminder/translations/nl.json +++ b/homeassistant/components/zoneminder/translations/nl.json @@ -23,7 +23,7 @@ "password": "Wachtwoord", "path": "ZM-pad", "path_zms": "ZMS-pad", - "ssl": "Gebruik SSL voor verbindingen met ZoneMinder", + "ssl": "Gebruik een SSL-certificaat", "username": "Gebruikersnaam", "verify_ssl": "Verifieer SSL-certificaat" }, diff --git a/homeassistant/components/zwave_js/translations/it.json b/homeassistant/components/zwave_js/translations/it.json index 5f0868a3f74..abe0ab066fb 100644 --- a/homeassistant/components/zwave_js/translations/it.json +++ b/homeassistant/components/zwave_js/translations/it.json @@ -6,6 +6,7 @@ "addon_install_failed": "Impossibile installare il componente aggiuntivo Z-Wave JS.", "addon_missing_discovery_info": "Informazioni sul rilevamento del componente aggiuntivo Z-Wave JS mancanti.", "addon_set_config_failed": "Impossibile impostare la configurazione di Z-Wave JS.", + "addon_start_failed": "Impossibile avviare il componente aggiuntivo Z-Wave JS.", "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "cannot_connect": "Impossibile connettersi" @@ -17,7 +18,8 @@ "unknown": "Errore imprevisto" }, "progress": { - "install_addon": "Attendi il termine dell'installazione del componente aggiuntivo Z-Wave JS. Questa operazione pu\u00f2 richiedere diversi minuti." + "install_addon": "Attendi il termine dell'installazione del componente aggiuntivo Z-Wave JS. Questa operazione pu\u00f2 richiedere diversi minuti.", + "start_addon": "Attendi il completamento dell'avvio del componente aggiuntivo Z-Wave JS. L'operazione potrebbe richiedere alcuni secondi." }, "step": { "configure_addon": { @@ -45,6 +47,9 @@ "description": "Desideri utilizzare il componente aggiuntivo Z-Wave JS Supervisor?", "title": "Seleziona il metodo di connessione" }, + "start_addon": { + "title": "Il componente aggiuntivo Z-Wave JS si sta avviando." + }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/zwave_js/translations/nl.json b/homeassistant/components/zwave_js/translations/nl.json index 7f46c02ece5..c15cfd26f31 100644 --- a/homeassistant/components/zwave_js/translations/nl.json +++ b/homeassistant/components/zwave_js/translations/nl.json @@ -5,7 +5,7 @@ "addon_info_failed": "Ophalen van Z-Wave JS add-on-info is mislukt.", "addon_install_failed": "Kan de Z-Wave JS add-on niet installeren.", "addon_missing_discovery_info": "De Z-Wave JS addon mist ontdekkings informatie", - "addon_set_config_failed": "Instellen van de Z-Wave JS-configuratie is mislukt.", + "addon_set_config_failed": "Instellen van de Z-Wave JS configuratie is mislukt.", "already_configured": "Apparaat is al geconfigureerd", "already_in_progress": "De configuratiestroom is al aan de gang", "cannot_connect": "Kan geen verbinding maken" From d81155327ad33bfbbc3465f7631b0a5f3b497f4e Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 28 Feb 2021 00:07:08 +0000 Subject: [PATCH 777/796] [ci skip] Translation update --- .../components/climacell/translations/no.json | 2 +- .../faa_delays/translations/ca.json | 21 ++++++++++++++++++ .../faa_delays/translations/no.json | 2 +- .../components/litejet/translations/fr.json | 3 +++ .../components/litejet/translations/no.json | 2 +- .../components/netatmo/translations/ca.json | 22 +++++++++++++++++++ .../philips_js/translations/ca.json | 2 ++ .../philips_js/translations/fr.json | 2 ++ .../philips_js/translations/no.json | 2 ++ .../philips_js/translations/ru.json | 2 ++ .../components/subaru/translations/fr.json | 15 ++++++++++++- .../components/subaru/translations/no.json | 2 +- .../totalconnect/translations/fr.json | 8 +++++-- .../xiaomi_miio/translations/fr.json | 1 + .../components/zwave_js/translations/no.json | 4 ++-- 15 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/faa_delays/translations/ca.json diff --git a/homeassistant/components/climacell/translations/no.json b/homeassistant/components/climacell/translations/no.json index af07ce716d0..d59f5590518 100644 --- a/homeassistant/components/climacell/translations/no.json +++ b/homeassistant/components/climacell/translations/no.json @@ -23,7 +23,7 @@ "init": { "data": { "forecast_types": "Prognosetype(r)", - "timestep": "Min. Mellom NowCast Prognoser" + "timestep": "Min. mellom NowCast prognoser" }, "description": "Hvis du velger \u00e5 aktivere \u00abnowcast\u00bb -varselenheten, kan du konfigurere antall minutter mellom hver prognose. Antall angitte prognoser avhenger av antall minutter som er valgt mellom prognosene.", "title": "Oppdater ClimaCell Alternativer" diff --git a/homeassistant/components/faa_delays/translations/ca.json b/homeassistant/components/faa_delays/translations/ca.json new file mode 100644 index 00000000000..e7e600f7f07 --- /dev/null +++ b/homeassistant/components/faa_delays/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Aeroport ja est\u00e0 configurat." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_airport": "Codi d'aeroport inv\u00e0lid", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "id": "Aeroport" + }, + "description": "Introdueix codi d'un aeroport dels EUA en format IATA", + "title": "FAA Delays" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/faa_delays/translations/no.json b/homeassistant/components/faa_delays/translations/no.json index c481f90bf75..5a5aac723ad 100644 --- a/homeassistant/components/faa_delays/translations/no.json +++ b/homeassistant/components/faa_delays/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Denne flyplassen er allerede konfigurert." + "already_configured": "Denne flyplassen er allerede konfigurert" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/litejet/translations/fr.json b/homeassistant/components/litejet/translations/fr.json index 455ba7fdc0c..89459d1829f 100644 --- a/homeassistant/components/litejet/translations/fr.json +++ b/homeassistant/components/litejet/translations/fr.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, + "error": { + "open_failed": "Impossible d'ouvrir le port s\u00e9rie sp\u00e9cifi\u00e9." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/litejet/translations/no.json b/homeassistant/components/litejet/translations/no.json index 26ccd333546..d3206ca2897 100644 --- a/homeassistant/components/litejet/translations/no.json +++ b/homeassistant/components/litejet/translations/no.json @@ -4,7 +4,7 @@ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { - "open_failed": "Kan ikke \u00e5pne den angitte serielle porten." + "open_failed": "Kan ikke \u00e5pne den angitte serielle porten" }, "step": { "user": { diff --git a/homeassistant/components/netatmo/translations/ca.json b/homeassistant/components/netatmo/translations/ca.json index a6b8b5c2b82..809223a04ae 100644 --- a/homeassistant/components/netatmo/translations/ca.json +++ b/homeassistant/components/netatmo/translations/ca.json @@ -15,6 +15,28 @@ } } }, + "device_automation": { + "trigger_subtype": { + "away": "a fora", + "hg": "protecci\u00f3 contra gelades", + "schedule": "programaci\u00f3" + }, + "trigger_type": { + "alarm_started": "{entity_name} ha detectat una alarma", + "animal": "{entity_name} ha detectat un animal", + "cancel_set_point": "{entity_name} ha repr\u00e8s la programaci\u00f3", + "human": "{entity_name} ha detectat un hum\u00e0", + "movement": "{entity_name} ha detectat moviment", + "outdoor": "{entity_name} ha detectat un esdeveniment a fora", + "person": "{entity_name} ha detectat una persona", + "person_away": "{entity_name} ha detectat una marxant", + "set_point": "Temperatura objectiu {entity_name} configurada manualment", + "therm_mode": "{entity_name} ha canviar a \"{subtype}\"", + "turned_off": "{entity_name} s'ha apagat", + "turned_on": "{entity_name} s'ha engegat", + "vehicle": "{entity_name} ha detectat un vehicle" + } + }, "options": { "step": { "public_weather": { diff --git a/homeassistant/components/philips_js/translations/ca.json b/homeassistant/components/philips_js/translations/ca.json index 505a6472ea8..980bb6800e1 100644 --- a/homeassistant/components/philips_js/translations/ca.json +++ b/homeassistant/components/philips_js/translations/ca.json @@ -5,6 +5,8 @@ }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_pin": "PIN inv\u00e0lid", + "pairing_failure": "No s'ha pogut vincular: {error_id}", "unknown": "Error inesperat" }, "step": { diff --git a/homeassistant/components/philips_js/translations/fr.json b/homeassistant/components/philips_js/translations/fr.json index 9ae65c18fa4..25c28edcf1d 100644 --- a/homeassistant/components/philips_js/translations/fr.json +++ b/homeassistant/components/philips_js/translations/fr.json @@ -5,6 +5,8 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", + "invalid_pin": "NIP invalide", + "pairing_failure": "Association impossible: {error_id}", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/philips_js/translations/no.json b/homeassistant/components/philips_js/translations/no.json index dadf15fb67a..a9c647a644b 100644 --- a/homeassistant/components/philips_js/translations/no.json +++ b/homeassistant/components/philips_js/translations/no.json @@ -5,6 +5,8 @@ }, "error": { "cannot_connect": "Tilkobling mislyktes", + "invalid_pin": "Ugyldig PIN", + "pairing_failure": "Kan ikke parre: {error_id}", "unknown": "Uventet feil" }, "step": { diff --git a/homeassistant/components/philips_js/translations/ru.json b/homeassistant/components/philips_js/translations/ru.json index 9306ecf7a29..83511ff246a 100644 --- a/homeassistant/components/philips_js/translations/ru.json +++ b/homeassistant/components/philips_js/translations/ru.json @@ -5,6 +5,8 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_pin": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434.", + "pairing_failure": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435: {error_id}.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/subaru/translations/fr.json b/homeassistant/components/subaru/translations/fr.json index a6bf6902aab..25544534297 100644 --- a/homeassistant/components/subaru/translations/fr.json +++ b/homeassistant/components/subaru/translations/fr.json @@ -24,7 +24,20 @@ "country": "Choisissez le pays", "password": "Mot de passe", "username": "Nom d'utilisateur" - } + }, + "description": "Veuillez saisir vos identifiants MySubaru\n REMARQUE: la configuration initiale peut prendre jusqu'\u00e0 30 secondes", + "title": "Configuration de Subaru Starlink" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "update_enabled": "Activer l'interrogation des v\u00e9hicules" + }, + "description": "Lorsqu'elle est activ\u00e9e, l'interrogation du v\u00e9hicule enverra une commande \u00e0 distance \u00e0 votre v\u00e9hicule toutes les 2 heures pour obtenir de nouvelles donn\u00e9es de capteur. Sans interrogation du v\u00e9hicule, les nouvelles donn\u00e9es de capteur ne sont re\u00e7ues que lorsque le v\u00e9hicule pousse automatiquement les donn\u00e9es (normalement apr\u00e8s l'arr\u00eat du moteur).", + "title": "Options de Subaru Starlink" } } } diff --git a/homeassistant/components/subaru/translations/no.json b/homeassistant/components/subaru/translations/no.json index f1a263d5cb4..25b0f7bec29 100644 --- a/homeassistant/components/subaru/translations/no.json +++ b/homeassistant/components/subaru/translations/no.json @@ -37,7 +37,7 @@ "update_enabled": "Aktiver polling av kj\u00f8ret\u00f8y" }, "description": "N\u00e5r dette er aktivert, sender polling av kj\u00f8ret\u00f8y en fjernkommando til kj\u00f8ret\u00f8yet annenhver time for \u00e5 skaffe nye sensordata. Uten kj\u00f8ret\u00f8yoppm\u00e5ling mottas nye sensordata bare n\u00e5r kj\u00f8ret\u00f8yet automatisk skyver data (normalt etter motorstans).", - "title": "Subaru Starlink Alternativer" + "title": "Subaru Starlink alternativer" } } } diff --git a/homeassistant/components/totalconnect/translations/fr.json b/homeassistant/components/totalconnect/translations/fr.json index 40ca767b4ac..b46bf127963 100644 --- a/homeassistant/components/totalconnect/translations/fr.json +++ b/homeassistant/components/totalconnect/translations/fr.json @@ -5,15 +5,19 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification invalide", + "usercode": "Code d'utilisateur non valide pour cet utilisateur \u00e0 cet emplacement" }, "step": { "locations": { "data": { "location": "Emplacement" - } + }, + "description": "Saisissez le code d'utilisateur de cet utilisateur \u00e0 cet emplacement", + "title": "Codes d'utilisateur de l'emplacement" }, "reauth_confirm": { + "description": "Total Connect doit r\u00e9-authentifier votre compte", "title": "R\u00e9-authentifier l'int\u00e9gration" }, "user": { diff --git a/homeassistant/components/xiaomi_miio/translations/fr.json b/homeassistant/components/xiaomi_miio/translations/fr.json index 10ce9972818..30def127e7a 100644 --- a/homeassistant/components/xiaomi_miio/translations/fr.json +++ b/homeassistant/components/xiaomi_miio/translations/fr.json @@ -14,6 +14,7 @@ "device": { "data": { "host": "Adresse IP", + "model": "Mod\u00e8le d'appareil (facultatif)", "name": "Nom de l'appareil", "token": "Jeton d'API" }, diff --git a/homeassistant/components/zwave_js/translations/no.json b/homeassistant/components/zwave_js/translations/no.json index acd049fc561..f893d2d7684 100644 --- a/homeassistant/components/zwave_js/translations/no.json +++ b/homeassistant/components/zwave_js/translations/no.json @@ -19,7 +19,7 @@ }, "progress": { "install_addon": "Vent mens installasjonen av Z-Wave JS-tillegg er ferdig. Dette kan ta flere minutter.", - "start_addon": "Vent mens Z-Wave JS-tilleggsstarten er fullf\u00f8rt. Dette kan ta noen sekunder." + "start_addon": "Vent mens Z-Wave JS-tillegget er ferdig startet. Dette kan ta noen sekunder." }, "step": { "configure_addon": { @@ -48,7 +48,7 @@ "title": "Velg tilkoblingsmetode" }, "start_addon": { - "title": "Z-Wave JS-tillegget starter." + "title": "Z-Wave JS-tillegget starter" }, "user": { "data": { From 13516aa90c79b9a557af620efe1c40c28adf8455 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 1 Mar 2021 00:09:01 +0000 Subject: [PATCH 778/796] [ci skip] Translation update --- .../components/aemet/translations/de.json | 19 ++++++++++++ .../components/airly/translations/ca.json | 4 ++- .../components/airly/translations/en.json | 4 ++- .../components/airly/translations/et.json | 4 ++- .../components/airly/translations/ru.json | 4 ++- .../airly/translations/zh-Hant.json | 4 ++- .../components/asuswrt/translations/de.json | 24 +++++++++++++++ .../awair/translations/zh-Hant.json | 10 +++---- .../blink/translations/zh-Hant.json | 2 +- .../components/bond/translations/zh-Hant.json | 4 +-- .../components/climacell/translations/de.json | 19 ++++++++++++ .../components/climacell/translations/he.json | 17 +++++++++++ .../cloudflare/translations/zh-Hant.json | 4 +-- .../components/econet/translations/de.json | 21 ++++++++++++++ .../faa_delays/translations/de.json | 8 +++++ .../faa_delays/translations/he.json | 18 ++++++++++++ .../fireservicerota/translations/zh-Hant.json | 2 +- .../components/fritzbox/translations/de.json | 2 +- .../components/habitica/translations/de.json | 17 +++++++++++ .../components/homekit/translations/he.json | 10 +++++++ .../huisbaasje/translations/de.json | 21 ++++++++++++++ .../hyperion/translations/zh-Hant.json | 16 +++++----- .../juicenet/translations/zh-Hant.json | 4 +-- .../keenetic_ndms2/translations/de.json | 21 ++++++++++++++ .../components/kmtronic/translations/de.json | 21 ++++++++++++++ .../components/litejet/translations/de.json | 14 +++++++++ .../components/litejet/translations/he.json | 11 +++++++ .../litterrobot/translations/de.json | 20 +++++++++++++ .../lutron_caseta/translations/de.json | 7 +++++ .../components/lyric/translations/de.json | 16 ++++++++++ .../components/mazda/translations/de.json | 29 +++++++++++++++++++ .../media_player/translations/de.json | 7 +++++ .../melcloud/translations/zh-Hant.json | 2 +- .../components/mullvad/translations/de.json | 21 ++++++++++++++ .../components/mullvad/translations/he.json | 21 ++++++++++++++ .../components/mysensors/translations/de.json | 16 ++++++++++ .../components/netatmo/translations/he.json | 11 +++++++ .../nightscout/translations/et.json | 2 +- .../components/nuki/translations/de.json | 18 ++++++++++++ .../components/nuki/translations/zh-Hant.json | 2 +- .../philips_js/translations/de.json | 20 +++++++++++++ .../philips_js/translations/he.json | 7 +++++ .../philips_js/translations/zh-Hant.json | 2 ++ .../components/plaato/translations/de.json | 1 + .../plaato/translations/zh-Hant.json | 8 ++--- .../components/plex/translations/zh-Hant.json | 8 ++--- .../point/translations/zh-Hant.json | 2 +- .../components/powerwall/translations/de.json | 7 +++-- .../components/powerwall/translations/et.json | 2 +- .../translations/de.json | 20 +++++++++++++ .../components/roku/translations/de.json | 1 + .../simplisafe/translations/zh-Hant.json | 2 +- .../smartthings/translations/et.json | 2 +- .../smartthings/translations/zh-Hant.json | 12 ++++---- .../components/smarttub/translations/de.json | 20 +++++++++++++ .../components/subaru/translations/de.json | 28 ++++++++++++++++++ .../components/tesla/translations/de.json | 4 +++ .../tibber/translations/zh-Hant.json | 6 ++-- .../totalconnect/translations/de.json | 11 ++++++- .../components/tuya/translations/et.json | 4 +-- .../components/unifi/translations/de.json | 4 ++- .../vilfo/translations/zh-Hant.json | 4 +-- .../vizio/translations/zh-Hant.json | 6 ++-- .../xiaomi_miio/translations/de.json | 7 +++++ .../xiaomi_miio/translations/en.json | 2 +- .../xiaomi_miio/translations/zh-Hant.json | 8 ++--- .../components/zwave_js/translations/de.json | 14 ++++++++- 67 files changed, 621 insertions(+), 68 deletions(-) create mode 100644 homeassistant/components/aemet/translations/de.json create mode 100644 homeassistant/components/asuswrt/translations/de.json create mode 100644 homeassistant/components/climacell/translations/de.json create mode 100644 homeassistant/components/climacell/translations/he.json create mode 100644 homeassistant/components/econet/translations/de.json create mode 100644 homeassistant/components/faa_delays/translations/de.json create mode 100644 homeassistant/components/faa_delays/translations/he.json create mode 100644 homeassistant/components/habitica/translations/de.json create mode 100644 homeassistant/components/huisbaasje/translations/de.json create mode 100644 homeassistant/components/keenetic_ndms2/translations/de.json create mode 100644 homeassistant/components/kmtronic/translations/de.json create mode 100644 homeassistant/components/litejet/translations/de.json create mode 100644 homeassistant/components/litejet/translations/he.json create mode 100644 homeassistant/components/litterrobot/translations/de.json create mode 100644 homeassistant/components/lyric/translations/de.json create mode 100644 homeassistant/components/mazda/translations/de.json create mode 100644 homeassistant/components/mullvad/translations/de.json create mode 100644 homeassistant/components/mullvad/translations/he.json create mode 100644 homeassistant/components/mysensors/translations/de.json create mode 100644 homeassistant/components/netatmo/translations/he.json create mode 100644 homeassistant/components/nuki/translations/de.json create mode 100644 homeassistant/components/philips_js/translations/de.json create mode 100644 homeassistant/components/philips_js/translations/he.json create mode 100644 homeassistant/components/rituals_perfume_genie/translations/de.json create mode 100644 homeassistant/components/smarttub/translations/de.json create mode 100644 homeassistant/components/subaru/translations/de.json diff --git a/homeassistant/components/aemet/translations/de.json b/homeassistant/components/aemet/translations/de.json new file mode 100644 index 00000000000..d7254aea92f --- /dev/null +++ b/homeassistant/components/aemet/translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Standort ist bereits konfiguriert" + }, + "error": { + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/translations/ca.json b/homeassistant/components/airly/translations/ca.json index 95400de23b4..e76cec94f4c 100644 --- a/homeassistant/components/airly/translations/ca.json +++ b/homeassistant/components/airly/translations/ca.json @@ -22,7 +22,9 @@ }, "system_health": { "info": { - "can_reach_server": "Servidor d'Airly accessible" + "can_reach_server": "Servidor d'Airly accessible", + "requests_per_day": "Sol\u00b7licituds per dia permeses", + "requests_remaining": "Sol\u00b7licituds permeses restants" } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/en.json b/homeassistant/components/airly/translations/en.json index 720f68f8349..0a5426c87d8 100644 --- a/homeassistant/components/airly/translations/en.json +++ b/homeassistant/components/airly/translations/en.json @@ -22,7 +22,9 @@ }, "system_health": { "info": { - "can_reach_server": "Reach Airly server" + "can_reach_server": "Reach Airly server", + "requests_per_day": "Allowed requests per day", + "requests_remaining": "Remaining allowed requests" } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/et.json b/homeassistant/components/airly/translations/et.json index 8cbfd138257..c5c9359c67f 100644 --- a/homeassistant/components/airly/translations/et.json +++ b/homeassistant/components/airly/translations/et.json @@ -22,7 +22,9 @@ }, "system_health": { "info": { - "can_reach_server": "\u00dchendus Airly serveriga" + "can_reach_server": "\u00dchendus Airly serveriga", + "requests_per_day": "Lubatud taotlusi p\u00e4evas", + "requests_remaining": "J\u00e4\u00e4nud lubatud taotlusi" } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/ru.json b/homeassistant/components/airly/translations/ru.json index b1469af787e..41ca90a8c02 100644 --- a/homeassistant/components/airly/translations/ru.json +++ b/homeassistant/components/airly/translations/ru.json @@ -22,7 +22,9 @@ }, "system_health": { "info": { - "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 Airly" + "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 Airly", + "requests_per_day": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0432 \u0434\u0435\u043d\u044c", + "requests_remaining": "\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/zh-Hant.json b/homeassistant/components/airly/translations/zh-Hant.json index 4d60b158c4c..19ef2ae7532 100644 --- a/homeassistant/components/airly/translations/zh-Hant.json +++ b/homeassistant/components/airly/translations/zh-Hant.json @@ -22,7 +22,9 @@ }, "system_health": { "info": { - "can_reach_server": "\u9023\u7dda Airly \u4f3a\u670d\u5668" + "can_reach_server": "\u9023\u7dda Airly \u4f3a\u670d\u5668", + "requests_per_day": "\u6bcf\u65e5\u5141\u8a31\u7684\u8acb\u6c42", + "requests_remaining": "\u5176\u9918\u5141\u8a31\u7684\u8acb\u6c42" } } } \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/de.json b/homeassistant/components/asuswrt/translations/de.json new file mode 100644 index 00000000000..433bf17b814 --- /dev/null +++ b/homeassistant/components/asuswrt/translations/de.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "mode": "Modus", + "name": "Name", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/zh-Hant.json b/homeassistant/components/awair/translations/zh-Hant.json index 11fe9ff88b3..0bd7749c65f 100644 --- a/homeassistant/components/awair/translations/zh-Hant.json +++ b/homeassistant/components/awair/translations/zh-Hant.json @@ -6,23 +6,23 @@ "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { - "invalid_access_token": "\u5b58\u53d6\u5bc6\u9470\u7121\u6548", + "invalid_access_token": "\u5b58\u53d6\u6b0a\u6756\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { "reauth": { "data": { - "access_token": "\u5b58\u53d6\u5bc6\u9470", + "access_token": "\u5b58\u53d6\u6b0a\u6756", "email": "\u96fb\u5b50\u90f5\u4ef6" }, - "description": "\u8acb\u91cd\u65b0\u8f38\u5165 Awair \u958b\u767c\u8005\u5b58\u53d6\u5bc6\u9470\u3002" + "description": "\u8acb\u91cd\u65b0\u8f38\u5165 Awair \u958b\u767c\u8005\u5b58\u53d6\u6b0a\u6756\u3002" }, "user": { "data": { - "access_token": "\u5b58\u53d6\u5bc6\u9470", + "access_token": "\u5b58\u53d6\u6b0a\u6756", "email": "\u96fb\u5b50\u90f5\u4ef6" }, - "description": "\u5fc5\u9808\u5148\u8a3b\u518a Awair \u958b\u767c\u8005\u5b58\u53d6\u5bc6\u9470\uff1ahttps://developer.getawair.com/onboard/login" + "description": "\u5fc5\u9808\u5148\u8a3b\u518a Awair \u958b\u767c\u8005\u5b58\u53d6\u6b0a\u6756\uff1ahttps://developer.getawair.com/onboard/login" } } } diff --git a/homeassistant/components/blink/translations/zh-Hant.json b/homeassistant/components/blink/translations/zh-Hant.json index 3d05dc82abc..d2c42bf5531 100644 --- a/homeassistant/components/blink/translations/zh-Hant.json +++ b/homeassistant/components/blink/translations/zh-Hant.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_access_token": "\u5b58\u53d6\u5bc6\u9470\u7121\u6548", + "invalid_access_token": "\u5b58\u53d6\u6b0a\u6756\u7121\u6548", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, diff --git a/homeassistant/components/bond/translations/zh-Hant.json b/homeassistant/components/bond/translations/zh-Hant.json index 1c5327dc662..8bb8e178869 100644 --- a/homeassistant/components/bond/translations/zh-Hant.json +++ b/homeassistant/components/bond/translations/zh-Hant.json @@ -13,13 +13,13 @@ "step": { "confirm": { "data": { - "access_token": "\u5b58\u53d6\u5bc6\u9470" + "access_token": "\u5b58\u53d6\u6b0a\u6756" }, "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" }, "user": { "data": { - "access_token": "\u5b58\u53d6\u5bc6\u9470", + "access_token": "\u5b58\u53d6\u6b0a\u6756", "host": "\u4e3b\u6a5f\u7aef" } } diff --git a/homeassistant/components/climacell/translations/de.json b/homeassistant/components/climacell/translations/de.json new file mode 100644 index 00000000000..f18197e1cca --- /dev/null +++ b/homeassistant/components/climacell/translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "name": "Name" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/he.json b/homeassistant/components/climacell/translations/he.json new file mode 100644 index 00000000000..81a4b5c1fce --- /dev/null +++ b/homeassistant/components/climacell/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", + "name": "\u05e9\u05dd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/zh-Hant.json b/homeassistant/components/cloudflare/translations/zh-Hant.json index 1be70def034..d9a05269748 100644 --- a/homeassistant/components/cloudflare/translations/zh-Hant.json +++ b/homeassistant/components/cloudflare/translations/zh-Hant.json @@ -19,9 +19,9 @@ }, "user": { "data": { - "api_token": "API \u5bc6\u9470" + "api_token": "API \u6b0a\u6756" }, - "description": "\u6b64\u6574\u5408\u9700\u8981\u5e33\u865f\u4e2d\u6240\u6709\u5340\u57df Zone:Zone:Read \u8207 Zone:DNS:Edit \u6b0a\u9650 API \u5bc6\u9470\u3002", + "description": "\u6b64\u6574\u5408\u9700\u8981\u5e33\u865f\u4e2d\u6240\u6709\u5340\u57df Zone:Zone:Read \u8207 Zone:DNS:Edit \u6b0a\u9650 API \u6b0a\u6756\u3002", "title": "\u9023\u7dda\u81f3 Cloudflare" }, "zone": { diff --git a/homeassistant/components/econet/translations/de.json b/homeassistant/components/econet/translations/de.json new file mode 100644 index 00000000000..854d61f1790 --- /dev/null +++ b/homeassistant/components/econet/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "step": { + "user": { + "data": { + "email": "E-Mail", + "password": "Passwort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/faa_delays/translations/de.json b/homeassistant/components/faa_delays/translations/de.json new file mode 100644 index 00000000000..72b837c862c --- /dev/null +++ b/homeassistant/components/faa_delays/translations/de.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/faa_delays/translations/he.json b/homeassistant/components/faa_delays/translations/he.json new file mode 100644 index 00000000000..af8d410eb18 --- /dev/null +++ b/homeassistant/components/faa_delays/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e0\u05de\u05dc \u05ea\u05e2\u05d5\u05e4\u05d4 \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, + "error": { + "cannot_connect": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "id": "\u05e0\u05de\u05dc \u05ea\u05e2\u05d5\u05e4\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/zh-Hant.json b/homeassistant/components/fireservicerota/translations/zh-Hant.json index af3cba40dc6..8e5f4d9f20d 100644 --- a/homeassistant/components/fireservicerota/translations/zh-Hant.json +++ b/homeassistant/components/fireservicerota/translations/zh-Hant.json @@ -15,7 +15,7 @@ "data": { "password": "\u5bc6\u78bc" }, - "description": "\u8a8d\u8b49\u5bc6\u9470\u5df2\u7d93\u5931\u6548\uff0c\u8acb\u767b\u5165\u91cd\u65b0\u65b0\u589e\u3002" + "description": "\u8a8d\u8b49\u6b0a\u6756\u5df2\u7d93\u5931\u6548\uff0c\u8acb\u767b\u5165\u91cd\u65b0\u65b0\u589e\u3002" }, "user": { "data": { diff --git a/homeassistant/components/fritzbox/translations/de.json b/homeassistant/components/fritzbox/translations/de.json index 8e79076bda6..16263722482 100644 --- a/homeassistant/components/fritzbox/translations/de.json +++ b/homeassistant/components/fritzbox/translations/de.json @@ -24,7 +24,7 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "Aktualisiere deine Anmeldeinformationen f\u00fcr {name} ." + "description": "Aktualisiere deine Anmeldeinformationen f\u00fcr {name}." }, "user": { "data": { diff --git a/homeassistant/components/habitica/translations/de.json b/homeassistant/components/habitica/translations/de.json new file mode 100644 index 00000000000..04f985946fb --- /dev/null +++ b/homeassistant/components/habitica/translations/de.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_credentials": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "url": "URL" + } + } + } + }, + "title": "Habitica" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/he.json b/homeassistant/components/homekit/translations/he.json index 87ad743dca5..6acebca0ca4 100644 --- a/homeassistant/components/homekit/translations/he.json +++ b/homeassistant/components/homekit/translations/he.json @@ -1,6 +1,16 @@ { "options": { "step": { + "include_exclude": { + "data": { + "mode": "\u05de\u05e6\u05d1" + } + }, + "init": { + "data": { + "mode": "\u05de\u05e6\u05d1" + } + }, "yaml": { "description": "\u05d9\u05e9\u05d5\u05ea \u05d6\u05d5 \u05e0\u05e9\u05dc\u05d8\u05ea \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea YAML" } diff --git a/homeassistant/components/huisbaasje/translations/de.json b/homeassistant/components/huisbaasje/translations/de.json new file mode 100644 index 00000000000..ca3f90536d4 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "connection_exception": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unauthenticated_exception": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/zh-Hant.json b/homeassistant/components/hyperion/translations/zh-Hant.json index ed003131bf2..bb8eacd5376 100644 --- a/homeassistant/components/hyperion/translations/zh-Hant.json +++ b/homeassistant/components/hyperion/translations/zh-Hant.json @@ -3,8 +3,8 @@ "abort": { "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "auth_new_token_not_granted_error": "\u65b0\u5275\u5bc6\u9470\u672a\u7372\u5f97 Hyperion UI \u6838\u51c6", - "auth_new_token_not_work_error": "\u4f7f\u7528\u65b0\u5275\u5bc6\u9470\u8a8d\u8b49\u5931\u6557", + "auth_new_token_not_granted_error": "\u65b0\u5275\u6b0a\u6756\u672a\u7372\u5f97 Hyperion UI \u6838\u51c6", + "auth_new_token_not_work_error": "\u4f7f\u7528\u65b0\u5275\u6b0a\u6756\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", @@ -12,13 +12,13 @@ }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_access_token": "\u5b58\u53d6\u5bc6\u9470\u7121\u6548" + "invalid_access_token": "\u5b58\u53d6\u6b0a\u6756\u7121\u6548" }, "step": { "auth": { "data": { - "create_token": "\u81ea\u52d5\u65b0\u5275\u5bc6\u9470", - "token": "\u6216\u63d0\u4f9b\u73fe\u6709\u5bc6\u9470" + "create_token": "\u81ea\u52d5\u65b0\u5275\u6b0a\u6756", + "token": "\u6216\u63d0\u4f9b\u73fe\u6709\u6b0a\u6756" }, "description": "\u8a2d\u5b9a Hyperion Ambilight \u4f3a\u670d\u5668\u8a8d\u8b49" }, @@ -27,11 +27,11 @@ "title": "\u78ba\u8a8d\u9644\u52a0 Hyperion Ambilight \u670d\u52d9" }, "create_token": { - "description": "\u9ede\u9078\u4e0b\u65b9 **\u50b3\u9001** \u4ee5\u8acb\u6c42\u65b0\u8a8d\u8b49\u5bc6\u9470\u3002\u5c07\u6703\u91cd\u65b0\u5c0e\u5411\u81f3 Hyperion UI \u4ee5\u6838\u51c6\u8981\u6c42\u3002\u8acb\u78ba\u8a8d\u986f\u793a ID \u70ba \"{auth_id}\"", - "title": "\u81ea\u52d5\u65b0\u5275\u8a8d\u8b49\u5bc6\u9470" + "description": "\u9ede\u9078\u4e0b\u65b9 **\u50b3\u9001** \u4ee5\u8acb\u6c42\u65b0\u8a8d\u8b49\u6b0a\u6756\u3002\u5c07\u6703\u91cd\u65b0\u5c0e\u5411\u81f3 Hyperion UI \u4ee5\u6838\u51c6\u8981\u6c42\u3002\u8acb\u78ba\u8a8d\u986f\u793a ID \u70ba \"{auth_id}\"", + "title": "\u81ea\u52d5\u65b0\u5275\u8a8d\u8b49\u6b0a\u6756" }, "create_token_external": { - "title": "\u63a5\u53d7 Hyperion UI \u4e2d\u7684\u65b0\u5bc6\u9470" + "title": "\u63a5\u53d7 Hyperion UI \u4e2d\u7684\u65b0\u6b0a\u6756" }, "user": { "data": { diff --git a/homeassistant/components/juicenet/translations/zh-Hant.json b/homeassistant/components/juicenet/translations/zh-Hant.json index 815edb1fb27..f310babfd80 100644 --- a/homeassistant/components/juicenet/translations/zh-Hant.json +++ b/homeassistant/components/juicenet/translations/zh-Hant.json @@ -11,9 +11,9 @@ "step": { "user": { "data": { - "api_token": "API \u5bc6\u9470" + "api_token": "API \u6b0a\u6756" }, - "description": "\u5c07\u9700\u8981\u7531 https://home.juice.net/Manage \u53d6\u5f97 API \u5bc6\u9470\u3002", + "description": "\u5c07\u9700\u8981\u7531 https://home.juice.net/Manage \u53d6\u5f97 API \u6b0a\u6756\u3002", "title": "\u9023\u7dda\u81f3 JuiceNet" } } diff --git a/homeassistant/components/keenetic_ndms2/translations/de.json b/homeassistant/components/keenetic_ndms2/translations/de.json new file mode 100644 index 00000000000..71ce0154639 --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/de.json b/homeassistant/components/kmtronic/translations/de.json new file mode 100644 index 00000000000..625c7372347 --- /dev/null +++ b/homeassistant/components/kmtronic/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/de.json b/homeassistant/components/litejet/translations/de.json new file mode 100644 index 00000000000..492314e5cc6 --- /dev/null +++ b/homeassistant/components/litejet/translations/de.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/he.json b/homeassistant/components/litejet/translations/he.json new file mode 100644 index 00000000000..a06c89f1d2a --- /dev/null +++ b/homeassistant/components/litejet/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u05e4\u05d5\u05e8\u05d8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/de.json b/homeassistant/components/litterrobot/translations/de.json new file mode 100644 index 00000000000..0eee2778d05 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ 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 13f8c6bd800..b6aacf2d0ef 100644 --- a/homeassistant/components/lutron_caseta/translations/de.json +++ b/homeassistant/components/lutron_caseta/translations/de.json @@ -6,6 +6,13 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/de.json b/homeassistant/components/lyric/translations/de.json new file mode 100644 index 00000000000..5bab6ed132b --- /dev/null +++ b/homeassistant/components/lyric/translations/de.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen." + }, + "create_entry": { + "default": "Erfolgreich authentifiziert" + }, + "step": { + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/de.json b/homeassistant/components/mazda/translations/de.json new file mode 100644 index 00000000000..4e23becb8af --- /dev/null +++ b/homeassistant/components/mazda/translations/de.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "reauth": { + "data": { + "email": "E-Mail", + "password": "Passwort", + "region": "Region" + } + }, + "user": { + "data": { + "email": "E-Mail", + "password": "Passwort", + "region": "Region" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/translations/de.json b/homeassistant/components/media_player/translations/de.json index a7f25fa9d7c..4909c85d053 100644 --- a/homeassistant/components/media_player/translations/de.json +++ b/homeassistant/components/media_player/translations/de.json @@ -6,6 +6,13 @@ "is_on": "{entity_name} ist eingeschaltet", "is_paused": "{entity_name} ist pausiert", "is_playing": "{entity_name} spielt" + }, + "trigger_type": { + "idle": "{entity_name} wird inaktiv", + "paused": "{entity_name} ist angehalten", + "playing": "{entity_name} beginnt zu spielen", + "turned_off": "{entity_name} ausgeschaltet", + "turned_on": "{entity_name} eingeschaltet" } }, "state": { diff --git a/homeassistant/components/melcloud/translations/zh-Hant.json b/homeassistant/components/melcloud/translations/zh-Hant.json index 9947b5ac990..27f4d0e5d7f 100644 --- a/homeassistant/components/melcloud/translations/zh-Hant.json +++ b/homeassistant/components/melcloud/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u5df2\u4f7f\u7528\u6b64\u90f5\u4ef6\u8a2d\u5b9a MELCloud \u6574\u5408\u3002\u5b58\u53d6\u5bc6\u9470\u5df2\u66f4\u65b0\u3002" + "already_configured": "\u5df2\u4f7f\u7528\u6b64\u90f5\u4ef6\u8a2d\u5b9a MELCloud \u6574\u5408\u3002\u5b58\u53d6\u6b0a\u6756\u5df2\u66f4\u65b0\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/mullvad/translations/de.json b/homeassistant/components/mullvad/translations/de.json new file mode 100644 index 00000000000..625c7372347 --- /dev/null +++ b/homeassistant/components/mullvad/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/he.json b/homeassistant/components/mullvad/translations/he.json new file mode 100644 index 00000000000..7f60f15d598 --- /dev/null +++ b/homeassistant/components/mullvad/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05d4\u05de\u05db\u05e9\u05d9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, + "error": { + "cannot_connect": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05ea\u05e7\u05d9\u05df", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/de.json b/homeassistant/components/mysensors/translations/de.json new file mode 100644 index 00000000000..189226f29d5 --- /dev/null +++ b/homeassistant/components/mysensors/translations/de.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "error": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/he.json b/homeassistant/components/netatmo/translations/he.json new file mode 100644 index 00000000000..54bef84c30a --- /dev/null +++ b/homeassistant/components/netatmo/translations/he.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "trigger_type": { + "animal": "\u05d6\u05d9\u05d4\u05d4 \u05d1\u05e2\u05dc-\u05d7\u05d9\u05d9\u05dd", + "human": "\u05d6\u05d9\u05d4\u05d4 \u05d0\u05d3\u05dd", + "movement": "\u05d6\u05d9\u05d4\u05d4 \u05ea\u05e0\u05d5\u05e2\u05d4", + "turned_off": "\u05db\u05d1\u05d4", + "turned_on": "\u05e0\u05d3\u05dc\u05e7" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/et.json b/homeassistant/components/nightscout/translations/et.json index 361b4789328..0d00cebb6a5 100644 --- a/homeassistant/components/nightscout/translations/et.json +++ b/homeassistant/components/nightscout/translations/et.json @@ -15,7 +15,7 @@ "api_key": "API v\u00f5ti", "url": "" }, - "description": "- URL: NightScout eksemplari aadress. St: https://myhomeassistant.duckdns.org:5423\n - API v\u00f5ti (valikuline): kasuta ainult siis kui teie eksemplar on kaitstud (auth_default_roles! = readable).", + "description": "- URL: NightScout eksemplari aadress. St: https://myhomeassistant.duckdns.org:5423\n - API v\u00f5ti (valikuline): kasuta ainult siis kui eksemplar on kaitstud (auth_default_roles! = readable).", "title": "Sisesta oma Nightscouti serveri teave." } } diff --git a/homeassistant/components/nuki/translations/de.json b/homeassistant/components/nuki/translations/de.json new file mode 100644 index 00000000000..30d7e6865cd --- /dev/null +++ b/homeassistant/components/nuki/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port", + "token": "Zugangstoken" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/zh-Hant.json b/homeassistant/components/nuki/translations/zh-Hant.json index 662d7ed6ed9..4bf21552952 100644 --- a/homeassistant/components/nuki/translations/zh-Hant.json +++ b/homeassistant/components/nuki/translations/zh-Hant.json @@ -10,7 +10,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0", - "token": "\u5b58\u53d6\u5bc6\u9470" + "token": "\u5b58\u53d6\u6b0a\u6756" } } } diff --git a/homeassistant/components/philips_js/translations/de.json b/homeassistant/components/philips_js/translations/de.json new file mode 100644 index 00000000000..f59a17bce49 --- /dev/null +++ b/homeassistant/components/philips_js/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_pin": "Ung\u00fcltige PIN", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "api_version": "API-Version", + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/he.json b/homeassistant/components/philips_js/translations/he.json new file mode 100644 index 00000000000..04648fe5845 --- /dev/null +++ b/homeassistant/components/philips_js/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "pairing_failure": "\u05e6\u05d9\u05de\u05d5\u05d3 \u05e0\u05db\u05e9\u05dc" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/zh-Hant.json b/homeassistant/components/philips_js/translations/zh-Hant.json index af161b6b16b..13bfd52e980 100644 --- a/homeassistant/components/philips_js/translations/zh-Hant.json +++ b/homeassistant/components/philips_js/translations/zh-Hant.json @@ -5,6 +5,8 @@ }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_pin": "PIN \u78bc\u7121\u6548", + "pairing_failure": "\u7121\u6cd5\u914d\u5c0d\uff1a{error_id}", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { diff --git a/homeassistant/components/plaato/translations/de.json b/homeassistant/components/plaato/translations/de.json index 5171baab654..eaf68b507f9 100644 --- a/homeassistant/components/plaato/translations/de.json +++ b/homeassistant/components/plaato/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Konto wurde bereits konfiguriert", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." }, diff --git a/homeassistant/components/plaato/translations/zh-Hant.json b/homeassistant/components/plaato/translations/zh-Hant.json index 2890c5c31c6..26d7b728771 100644 --- a/homeassistant/components/plaato/translations/zh-Hant.json +++ b/homeassistant/components/plaato/translations/zh-Hant.json @@ -10,16 +10,16 @@ }, "error": { "invalid_webhook_device": "\u6240\u9078\u64c7\u7684\u88dd\u7f6e\u4e0d\u652f\u63f4\u50b3\u9001\u8cc7\u6599\u81f3 Webhook\u3001AirLock \u50c5\u652f\u63f4\u6b64\u985e\u578b", - "no_api_method": "\u9700\u8981\u65b0\u589e\u6388\u6b0a\u5bc6\u9470\u6216\u9078\u64c7 Webhook", - "no_auth_token": "\u9700\u8981\u65b0\u589e\u6388\u6b0a\u5bc6\u9470" + "no_api_method": "\u9700\u8981\u65b0\u589e\u6388\u6b0a\u6b0a\u6756\u6216\u9078\u64c7 Webhook", + "no_auth_token": "\u9700\u8981\u65b0\u589e\u6388\u6b0a\u6b0a\u6756" }, "step": { "api_method": { "data": { - "token": "\u65bc\u6b64\u8cbc\u4e0a\u6388\u6b0a\u5bc6\u9470", + "token": "\u65bc\u6b64\u8cbc\u4e0a\u6388\u6b0a\u6b0a\u6756", "use_webhook": "\u4f7f\u7528 Webhook" }, - "description": "\u9700\u8981\u6388\u6b0a\u5bc6\u8981 `auth_token` \u65b9\u80fd\u67e5\u8a62 API\u3002\u7372\u5f97\u7684\u65b9\u6cd5\u8acb [\u53c3\u95b1](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) \u6559\u5b78\n\n\u9078\u64c7\u7684\u88dd\u7f6e\uff1a**{device_type}** \n\n\u5047\u5982\u9078\u64c7\u5167\u5efa Webhook \u65b9\u6cd5\uff08Airlock \u552f\u4e00\u652f\u63f4\uff09\uff0c\u8acb\u6aa2\u67e5\u4e0b\u65b9\u6838\u9078\u76d2\u4e26\u78ba\u5b9a\u4fdd\u6301\u6388\u6b0a\u5bc6\u9470\u6b04\u4f4d\u7a7a\u767d", + "description": "\u9700\u8981\u6388\u6b0a\u5bc6\u8981 `auth_token` \u65b9\u80fd\u67e5\u8a62 API\u3002\u7372\u5f97\u7684\u65b9\u6cd5\u8acb [\u53c3\u95b1](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) \u6559\u5b78\n\n\u9078\u64c7\u7684\u88dd\u7f6e\uff1a**{device_type}** \n\n\u5047\u5982\u9078\u64c7\u5167\u5efa Webhook \u65b9\u6cd5\uff08Airlock \u552f\u4e00\u652f\u63f4\uff09\uff0c\u8acb\u6aa2\u67e5\u4e0b\u65b9\u6838\u9078\u76d2\u4e26\u78ba\u5b9a\u4fdd\u6301\u6388\u6b0a\u6b0a\u6756\u6b04\u4f4d\u7a7a\u767d", "title": "\u9078\u64c7 API \u65b9\u5f0f" }, "user": { diff --git a/homeassistant/components/plex/translations/zh-Hant.json b/homeassistant/components/plex/translations/zh-Hant.json index 137b953a145..7f19fa0d035 100644 --- a/homeassistant/components/plex/translations/zh-Hant.json +++ b/homeassistant/components/plex/translations/zh-Hant.json @@ -5,12 +5,12 @@ "already_configured": "Plex \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", - "token_request_timeout": "\u53d6\u5f97\u5bc6\u9470\u903e\u6642", + "token_request_timeout": "\u53d6\u5f97\u6b0a\u6756\u903e\u6642", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { - "faulty_credentials": "\u9a57\u8b49\u5931\u6557\u3001\u78ba\u8a8d\u5bc6\u9470", - "host_or_token": "\u5fc5\u9808\u81f3\u5c11\u63d0\u4f9b\u4e3b\u6a5f\u7aef\u6216\u5bc6\u9470", + "faulty_credentials": "\u9a57\u8b49\u5931\u6557\u3001\u78ba\u8a8d\u6b0a\u6756", + "host_or_token": "\u5fc5\u9808\u81f3\u5c11\u63d0\u4f9b\u4e3b\u6a5f\u7aef\u6216\u6b0a\u6756", "no_servers": "Plex \u5e33\u865f\u672a\u7d81\u5b9a\u4efb\u4f55\u4f3a\u670d\u5668", "not_found": "\u627e\u4e0d\u5230 Plex \u4f3a\u670d\u5668", "ssl_error": "SSL \u8a8d\u8b49\u554f\u984c" @@ -22,7 +22,7 @@ "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0", "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49", - "token": "\u5bc6\u9470\uff08\u9078\u9805\uff09", + "token": "\u6b0a\u6756\uff08\u9078\u9805\uff09", "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" }, "title": "Plex \u624b\u52d5\u8a2d\u5b9a" diff --git a/homeassistant/components/point/translations/zh-Hant.json b/homeassistant/components/point/translations/zh-Hant.json index 710d363f771..2bb1a8fc239 100644 --- a/homeassistant/components/point/translations/zh-Hant.json +++ b/homeassistant/components/point/translations/zh-Hant.json @@ -13,7 +13,7 @@ }, "error": { "follow_link": "\u8acb\u65bc\u50b3\u9001\u524d\uff0c\u5148\u4f7f\u7528\u9023\u7d50\u4e26\u9032\u884c\u8a8d\u8b49\u3002", - "no_token": "\u5b58\u53d6\u5bc6\u9470\u7121\u6548" + "no_token": "\u5b58\u53d6\u6b0a\u6756\u7121\u6548" }, "step": { "auth": { diff --git a/homeassistant/components/powerwall/translations/de.json b/homeassistant/components/powerwall/translations/de.json index c30286d8744..0ccd42c812b 100644 --- a/homeassistant/components/powerwall/translations/de.json +++ b/homeassistant/components/powerwall/translations/de.json @@ -1,17 +1,20 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "flow_title": "Tesla Powerwall ({ip_address})", "step": { "user": { "data": { - "ip_address": "IP-Adresse" + "ip_address": "IP-Adresse", + "password": "Passwort" }, "title": "Stellen Sie eine Verbindung zur Powerwall her" } diff --git a/homeassistant/components/powerwall/translations/et.json b/homeassistant/components/powerwall/translations/et.json index 4a937029296..8811b870316 100644 --- a/homeassistant/components/powerwall/translations/et.json +++ b/homeassistant/components/powerwall/translations/et.json @@ -8,7 +8,7 @@ "cannot_connect": "\u00dchenduse loomine nurjus. Proovi uuesti", "invalid_auth": "Vigane autentimine", "unknown": "Ootamatu t\u00f5rge", - "wrong_version": "Teie Powerwall kasutab tarkvaraversiooni, mida ei toetata. Kaaluge tarkvara uuendamist v\u00f5i probleemist teavitamist, et see saaks lahendatud." + "wrong_version": "Powerwall kasutab tarkvaraversiooni, mida ei toetata. Kaaluge tarkvara uuendamist v\u00f5i probleemist teavitamist, et see saaks lahendatud." }, "flow_title": "Tesla Powerwall ( {ip_address} )", "step": { diff --git a/homeassistant/components/rituals_perfume_genie/translations/de.json b/homeassistant/components/rituals_perfume_genie/translations/de.json new file mode 100644 index 00000000000..67b8ed59e0b --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "email": "E-Mail", + "password": "Passwort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/de.json b/homeassistant/components/roku/translations/de.json index 4bfb3c7503d..152161cb27f 100644 --- a/homeassistant/components/roku/translations/de.json +++ b/homeassistant/components/roku/translations/de.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Das Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "unknown": "Unerwarteter Fehler" }, "error": { diff --git a/homeassistant/components/simplisafe/translations/zh-Hant.json b/homeassistant/components/simplisafe/translations/zh-Hant.json index ad5323d3957..27064ed1055 100644 --- a/homeassistant/components/simplisafe/translations/zh-Hant.json +++ b/homeassistant/components/simplisafe/translations/zh-Hant.json @@ -19,7 +19,7 @@ "data": { "password": "\u5bc6\u78bc" }, - "description": "\u5b58\u53d6\u5bc6\u9470\u5df2\u7d93\u904e\u671f\u6216\u53d6\u6d88\uff0c\u8acb\u8f38\u5165\u5bc6\u78bc\u4ee5\u91cd\u65b0\u9023\u7d50\u5e33\u865f\u3002", + "description": "\u5b58\u53d6\u6b0a\u6756\u5df2\u7d93\u904e\u671f\u6216\u53d6\u6d88\uff0c\u8acb\u8f38\u5165\u5bc6\u78bc\u4ee5\u91cd\u65b0\u9023\u7d50\u5e33\u865f\u3002", "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" }, "user": { diff --git a/homeassistant/components/smartthings/translations/et.json b/homeassistant/components/smartthings/translations/et.json index 04cd0d70218..18d6076898d 100644 --- a/homeassistant/components/smartthings/translations/et.json +++ b/homeassistant/components/smartthings/translations/et.json @@ -19,7 +19,7 @@ "data": { "access_token": "Juurdep\u00e4\u00e4sut\u00f5end" }, - "description": "Sisesta SmartThingsi [isiklik juurdep\u00e4\u00e4suluba] ( {token_url} ), mis on loodud vastavalt [juhistele] ( {component_url} ). Seda kasutatakse Home Assistanti sidumise loomiseks teie SmartThingsi kontol.", + "description": "Sisesta SmartThingsi [isiklik juurdep\u00e4\u00e4suluba] ( {token_url} ), mis on loodud vastavalt [juhistele] ( {component_url} ). Seda kasutatakse Home Assistanti sidumise loomiseks SmartThingsi kontol.", "title": "Sisesta isiklik juurdep\u00e4\u00e4suluba (PAT)" }, "select_location": { diff --git a/homeassistant/components/smartthings/translations/zh-Hant.json b/homeassistant/components/smartthings/translations/zh-Hant.json index d9a17e46058..88360c75678 100644 --- a/homeassistant/components/smartthings/translations/zh-Hant.json +++ b/homeassistant/components/smartthings/translations/zh-Hant.json @@ -6,9 +6,9 @@ }, "error": { "app_setup_error": "\u7121\u6cd5\u8a2d\u5b9a SmartApp\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", - "token_forbidden": "\u5bc6\u9470\u4e0d\u5177\u6240\u9700\u7684 OAuth \u7bc4\u570d\u3002", - "token_invalid_format": "\u5bc6\u9470\u5fc5\u9808\u70ba UID/GUID \u683c\u5f0f", - "token_unauthorized": "\u5bc6\u9470\u7121\u6548\u6216\u4e0d\u518d\u5177\u6709\u6388\u6b0a\u3002", + "token_forbidden": "\u6b0a\u6756\u4e0d\u5177\u6240\u9700\u7684 OAuth \u7bc4\u570d\u3002", + "token_invalid_format": "\u6b0a\u6756\u5fc5\u9808\u70ba UID/GUID \u683c\u5f0f", + "token_unauthorized": "\u6b0a\u6756\u7121\u6548\u6216\u4e0d\u518d\u5177\u6709\u6388\u6b0a\u3002", "webhook_error": "SmartThings \u7121\u6cd5\u8a8d\u8b49 Webhook URL\u3002\u8acb\u78ba\u8a8d Webhook URL \u53ef\u7531\u7db2\u8def\u5b58\u53d6\u5f8c\u518d\u8a66\u4e00\u6b21\u3002" }, "step": { @@ -17,10 +17,10 @@ }, "pat": { "data": { - "access_token": "\u5b58\u53d6\u5bc6\u9470" + "access_token": "\u5b58\u53d6\u6b0a\u6756" }, - "description": "\u8acb\u8f38\u5165\u8ddf\u96a8\u6b64[\u6559\u5b78]({component_url}) \u6240\u5efa\u7acb\u7684 SmartThings [\u500b\u4eba\u5b58\u53d6\u5bc6\u9470]({token_url})\u3002\u5c07\u4f7f\u7528 SmartThings \u5e33\u865f\u65b0\u589e Home Assistant \u6574\u5408\u3002", - "title": "\u8f38\u5165\u500b\u4eba\u5b58\u53d6\u5bc6\u9470" + "description": "\u8acb\u8f38\u5165\u8ddf\u96a8\u6b64[\u6559\u5b78]({component_url}) \u6240\u5efa\u7acb\u7684 SmartThings [\u500b\u4eba\u5b58\u53d6\u6b0a\u6756]({token_url})\u3002\u5c07\u4f7f\u7528 SmartThings \u5e33\u865f\u65b0\u589e Home Assistant \u6574\u5408\u3002", + "title": "\u8f38\u5165\u500b\u4eba\u5b58\u53d6\u6b0a\u6756" }, "select_location": { "data": { diff --git a/homeassistant/components/smarttub/translations/de.json b/homeassistant/components/smarttub/translations/de.json new file mode 100644 index 00000000000..fbb3411a6c5 --- /dev/null +++ b/homeassistant/components/smarttub/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "email": "E-Mail", + "password": "Passwort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/de.json b/homeassistant/components/subaru/translations/de.json new file mode 100644 index 00000000000..1c162d61e99 --- /dev/null +++ b/homeassistant/components/subaru/translations/de.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "error": { + "bad_pin_format": "Die PIN sollte 4-stellig sein", + "cannot_connect": "Verbindung fehlgeschlagen", + "incorrect_pin": "Falsche PIN", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "pin": { + "data": { + "pin": "PIN" + } + }, + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/translations/de.json b/homeassistant/components/tesla/translations/de.json index 558209af411..2fd964fe013 100644 --- a/homeassistant/components/tesla/translations/de.json +++ b/homeassistant/components/tesla/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, "error": { "already_configured": "Konto wurde bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/tibber/translations/zh-Hant.json b/homeassistant/components/tibber/translations/zh-Hant.json index ce10615a289..e4d0ec10e23 100644 --- a/homeassistant/components/tibber/translations/zh-Hant.json +++ b/homeassistant/components/tibber/translations/zh-Hant.json @@ -5,15 +5,15 @@ }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_access_token": "\u5b58\u53d6\u5bc6\u9470\u7121\u6548", + "invalid_access_token": "\u5b58\u53d6\u6b0a\u6756\u7121\u6548", "timeout": "\u9023\u7dda\u81f3 Tibber \u903e\u6642" }, "step": { "user": { "data": { - "access_token": "\u5b58\u53d6\u5bc6\u9470" + "access_token": "\u5b58\u53d6\u6b0a\u6756" }, - "description": "\u8f38\u5165\u7531 https://developer.tibber.com/settings/accesstoken \u6240\u7372\u5f97\u7684\u5b58\u53d6\u5bc6\u9470", + "description": "\u8f38\u5165\u7531 https://developer.tibber.com/settings/accesstoken \u6240\u7372\u5f97\u7684\u5b58\u53d6\u6b0a\u6756", "title": "Tibber" } } diff --git a/homeassistant/components/totalconnect/translations/de.json b/homeassistant/components/totalconnect/translations/de.json index 530fef95af2..3fb5bb8f3e1 100644 --- a/homeassistant/components/totalconnect/translations/de.json +++ b/homeassistant/components/totalconnect/translations/de.json @@ -1,12 +1,21 @@ { "config": { "abort": { - "already_configured": "Konto wurde bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { + "locations": { + "data": { + "location": "Standort" + } + }, + "reauth_confirm": { + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "password": "Passwort", diff --git a/homeassistant/components/tuya/translations/et.json b/homeassistant/components/tuya/translations/et.json index 0fc1297ce7c..48161f552b8 100644 --- a/homeassistant/components/tuya/translations/et.json +++ b/homeassistant/components/tuya/translations/et.json @@ -12,9 +12,9 @@ "step": { "user": { "data": { - "country_code": "Teie konto riigikood (nt 1 USA v\u00f5i 372 Eesti)", + "country_code": "Konto riigikood (nt 1 USA v\u00f5i 372 Eesti)", "password": "Salas\u00f5na", - "platform": "\u00c4pp kus teie konto registreeriti", + "platform": "\u00c4pp kus konto registreeriti", "username": "Kasutajanimi" }, "description": "Sisesta oma Tuya konto andmed.", diff --git a/homeassistant/components/unifi/translations/de.json b/homeassistant/components/unifi/translations/de.json index be38ddf1a4d..05dd66fe56c 100644 --- a/homeassistant/components/unifi/translations/de.json +++ b/homeassistant/components/unifi/translations/de.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Controller-Site ist bereits konfiguriert" + "already_configured": "Controller-Site ist bereits konfiguriert", + "configuration_updated": "Konfiguration aktualisiert.", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "faulty_credentials": "Ung\u00fcltige Authentifizierung", diff --git a/homeassistant/components/vilfo/translations/zh-Hant.json b/homeassistant/components/vilfo/translations/zh-Hant.json index b266e25b39c..88180f9bacf 100644 --- a/homeassistant/components/vilfo/translations/zh-Hant.json +++ b/homeassistant/components/vilfo/translations/zh-Hant.json @@ -11,10 +11,10 @@ "step": { "user": { "data": { - "access_token": "\u5b58\u53d6\u5bc6\u9470", + "access_token": "\u5b58\u53d6\u6b0a\u6756", "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u8a2d\u5b9a Vilfo \u8def\u7531\u5668\u6574\u5408\u3002\u9700\u8981\u8f38\u5165 Vilfo \u8def\u7531\u5668\u4e3b\u6a5f\u540d\u7a31/IP \u4f4d\u5740\u3001API \u5b58\u53d6\u5bc6\u9470\u3002\u5176\u4ed6\u6574\u5408\u76f8\u95dc\u8cc7\u8a0a\uff0c\u8acb\u53c3\u8003\uff1ahttps://www.home-assistant.io/integrations/vilfo", + "description": "\u8a2d\u5b9a Vilfo \u8def\u7531\u5668\u6574\u5408\u3002\u9700\u8981\u8f38\u5165 Vilfo \u8def\u7531\u5668\u4e3b\u6a5f\u540d\u7a31/IP \u4f4d\u5740\u3001API \u5b58\u53d6\u6b0a\u6756\u3002\u5176\u4ed6\u6574\u5408\u76f8\u95dc\u8cc7\u8a0a\uff0c\u8acb\u53c3\u8003\uff1ahttps://www.home-assistant.io/integrations/vilfo", "title": "\u9023\u7dda\u81f3 Vilfo \u8def\u7531\u5668" } } diff --git a/homeassistant/components/vizio/translations/zh-Hant.json b/homeassistant/components/vizio/translations/zh-Hant.json index 257ed829b6a..5f21dd0c2b6 100644 --- a/homeassistant/components/vizio/translations/zh-Hant.json +++ b/homeassistant/components/vizio/translations/zh-Hant.json @@ -23,17 +23,17 @@ "title": "\u914d\u5c0d\u5b8c\u6210" }, "pairing_complete_import": { - "description": "VIZIO SmartCast \u88dd\u7f6e \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\u6b0a\u6756\u70ba '**{access_token}**'\u3002", "title": "\u914d\u5c0d\u5b8c\u6210" }, "user": { "data": { - "access_token": "\u5b58\u53d6\u5bc6\u9470", + "access_token": "\u5b58\u53d6\u6b0a\u6756", "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", + "description": "\u6b64\u96fb\u8996\u50c5\u9700\u5b58\u53d6\u6b0a\u6756\u5047\u5982\u60a8\u6b63\u5728\u8a2d\u5b9a\u96fb\u8996\u3001\u5c1a\u672a\u53d6\u5f97\u5b58\u53d6\u6b0a\u6756 \uff0c\u4fdd\u6301\u7a7a\u767d\u4ee5\u9032\u884c\u914d\u5c0d\u904e\u7a0b\u3002", "title": "VIZIO SmartCast \u88dd\u7f6e" } } diff --git a/homeassistant/components/xiaomi_miio/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json index d56a81e14d4..7cf11a1085e 100644 --- a/homeassistant/components/xiaomi_miio/translations/de.json +++ b/homeassistant/components/xiaomi_miio/translations/de.json @@ -10,6 +10,13 @@ }, "flow_title": "Xiaomi Miio: {name}", "step": { + "device": { + "data": { + "host": "IP-Adresse", + "name": "Name des Ger\u00e4ts", + "token": "API-Token" + } + }, "gateway": { "data": { "host": "IP-Adresse", diff --git a/homeassistant/components/xiaomi_miio/translations/en.json b/homeassistant/components/xiaomi_miio/translations/en.json index 951ae546b56..3d893ade2f0 100644 --- a/homeassistant/components/xiaomi_miio/translations/en.json +++ b/homeassistant/components/xiaomi_miio/translations/en.json @@ -18,7 +18,7 @@ "name": "Name of the device", "token": "API Token" }, - "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.", + "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.", "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" }, "gateway": { diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json index dce2002faa9..3b0a89b7485 100644 --- a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json @@ -16,18 +16,18 @@ "host": "IP \u4f4d\u5740", "model": "\u88dd\u7f6e\u578b\u865f\uff08\u9078\u9805\uff09", "name": "\u88dd\u7f6e\u540d\u7a31", - "token": "API \u5bc6\u9470" + "token": "API \u6b0a\u6756" }, - "description": "\u5c07\u9700\u8981\u8f38\u5165 32 \u4f4d\u5b57\u5143 API \u5bc6\u9470\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u4ee5\u7372\u5f97\u7372\u53d6\u5bc6\u9470\u7684\u6559\u5b78\u3002\u8acb\u6ce8\u610f\uff1a\u6b64API \u5bc6\u9470\u8207 Xiaomi Aqara \u6574\u5408\u6240\u4f7f\u7528\u4e4b\u5bc6\u9470\u4e0d\u540c\u3002", + "description": "\u5c07\u9700\u8981\u8f38\u5165 32 \u4f4d\u5b57\u5143 API \u6b0a\u6756\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u4ee5\u7372\u5f97\u7372\u53d6\u6b0a\u6756\u7684\u6559\u5b78\u3002\u8acb\u6ce8\u610f\uff1a\u6b64API \u6b0a\u6756\u8207 Xiaomi Aqara \u6574\u5408\u6240\u4f7f\u7528\u4e4b\u6b0a\u6756\u4e0d\u540c\u3002", "title": "\u9023\u7dda\u81f3\u5c0f\u7c73 MIIO \u88dd\u7f6e\u6216\u5c0f\u7c73\u7db2\u95dc" }, "gateway": { "data": { "host": "IP \u4f4d\u5740", "name": "\u7db2\u95dc\u540d\u7a31", - "token": "API \u5bc6\u9470" + "token": "API \u6b0a\u6756" }, - "description": "\u5c07\u9700\u8981\u8f38\u5165 32 \u4f4d\u5b57\u5143 API \u5bc6\u9470\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u4ee5\u7372\u5f97\u7372\u53d6\u5bc6\u9470\u7684\u6559\u5b78\u3002\u8acb\u6ce8\u610f\uff1a\u6b64API \u5bc6\u9470\u8207 Xiaomi Aqara \u6574\u5408\u6240\u4f7f\u7528\u4e4b\u5bc6\u9470\u4e0d\u540c\u3002", + "description": "\u5c07\u9700\u8981\u8f38\u5165 32 \u4f4d\u5b57\u5143 API \u6b0a\u6756\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u4ee5\u7372\u5f97\u7372\u53d6\u6b0a\u6756\u7684\u6559\u5b78\u3002\u8acb\u6ce8\u610f\uff1a\u6b64API \u6b0a\u6756\u8207 Xiaomi Aqara \u6574\u5408\u6240\u4f7f\u7528\u4e4b\u6b0a\u6756\u4e0d\u540c\u3002", "title": "\u9023\u7dda\u81f3\u5c0f\u7c73\u7db2\u95dc" }, "user": { diff --git a/homeassistant/components/zwave_js/translations/de.json b/homeassistant/components/zwave_js/translations/de.json index d4903bc8c6d..9ff130605ef 100644 --- a/homeassistant/components/zwave_js/translations/de.json +++ b/homeassistant/components/zwave_js/translations/de.json @@ -1,13 +1,25 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, "step": { + "configure_addon": { + "data": { + "usb_path": "USB-Ger\u00e4te-Pfad" + } + }, + "manual": { + "data": { + "url": "URL" + } + }, "user": { "data": { "url": "URL" From dddf28b13899d29c70b9fa202a9e5c840621a2b1 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 2 Mar 2021 21:50:28 +0100 Subject: [PATCH 779/796] Limit log spam by ConfigEntryNotReady (#47201) --- homeassistant/config_entries.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b54300faaa7..b0ec71be9cf 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -257,12 +257,19 @@ class ConfigEntry: self.state = ENTRY_STATE_SETUP_RETRY wait_time = 2 ** min(tries, 4) * 5 tries += 1 - _LOGGER.warning( - "Config entry '%s' for %s integration not ready yet. Retrying in %d seconds", - self.title, - self.domain, - wait_time, - ) + if tries == 1: + _LOGGER.warning( + "Config entry '%s' for %s integration not ready yet. Retrying in background", + self.title, + self.domain, + ) + else: + _LOGGER.debug( + "Config entry '%s' for %s integration not ready yet. Retrying in %d seconds", + self.title, + self.domain, + wait_time, + ) async def setup_again(now: Any) -> None: """Run setup again.""" From d88ee3bf4a9019c44be2ec2538170528baf645ee Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Mar 2021 22:02:41 +0100 Subject: [PATCH 780/796] Upgrade pillow to 8.1.1 (#47223) --- homeassistant/components/doods/manifest.json | 2 +- homeassistant/components/image/manifest.json | 2 +- homeassistant/components/proxy/manifest.json | 2 +- homeassistant/components/qrcode/manifest.json | 2 +- homeassistant/components/seven_segments/manifest.json | 2 +- homeassistant/components/sighthound/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index ecbcd8563a7..f5d425cb9ef 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -2,6 +2,6 @@ "domain": "doods", "name": "DOODS - Dedicated Open Object Detection Service", "documentation": "https://www.home-assistant.io/integrations/doods", - "requirements": ["pydoods==1.0.2", "pillow==8.1.0"], + "requirements": ["pydoods==1.0.2", "pillow==8.1.1"], "codeowners": [] } diff --git a/homeassistant/components/image/manifest.json b/homeassistant/components/image/manifest.json index 6978f09ab68..c8029c2e313 100644 --- a/homeassistant/components/image/manifest.json +++ b/homeassistant/components/image/manifest.json @@ -3,7 +3,7 @@ "name": "Image", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/image", - "requirements": ["pillow==8.1.0"], + "requirements": ["pillow==8.1.1"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 65d8d21fc0c..c1a01004fe9 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -2,6 +2,6 @@ "domain": "proxy", "name": "Camera Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["pillow==8.1.0"], + "requirements": ["pillow==8.1.1"], "codeowners": [] } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index b16eace14fd..5867d0d6b51 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -2,6 +2,6 @@ "domain": "qrcode", "name": "QR Code", "documentation": "https://www.home-assistant.io/integrations/qrcode", - "requirements": ["pillow==8.1.0", "pyzbar==0.1.7"], + "requirements": ["pillow==8.1.1", "pyzbar==0.1.7"], "codeowners": [] } diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 01e0275feeb..4f9f6514531 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -2,6 +2,6 @@ "domain": "seven_segments", "name": "Seven Segments OCR", "documentation": "https://www.home-assistant.io/integrations/seven_segments", - "requirements": ["pillow==8.1.0"], + "requirements": ["pillow==8.1.1"], "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index 99902b8dd36..aa9519fd68b 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -2,6 +2,6 @@ "domain": "sighthound", "name": "Sighthound", "documentation": "https://www.home-assistant.io/integrations/sighthound", - "requirements": ["pillow==8.1.0", "simplehound==0.3"], + "requirements": ["pillow==8.1.1", "simplehound==0.3"], "codeowners": ["@robmarkcole"] } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index f039a14d5b3..300c3ddd1db 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -7,7 +7,7 @@ "tf-models-official==2.3.0", "pycocotools==2.0.1", "numpy==1.19.2", - "pillow==8.1.0" + "pillow==8.1.1" ], "codeowners": [] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cb211fb1962..5aaa5b1b469 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -20,7 +20,7 @@ httpx==0.16.1 jinja2>=2.11.3 netdisco==2.8.2 paho-mqtt==1.5.1 -pillow==8.1.0 +pillow==8.1.1 pip>=8.0.3,<20.3 python-slugify==4.0.1 pytz>=2021.1 diff --git a/requirements_all.txt b/requirements_all.txt index e27b43f94a8..d63ab3c5b83 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1129,7 +1129,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==8.1.0 +pillow==8.1.1 # homeassistant.components.dominos pizzapi==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 400cc372b2b..ef6c0706c87 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -578,7 +578,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==8.1.0 +pillow==8.1.1 # homeassistant.components.plex plexapi==4.4.0 From 23049955f8349f16d0b41ac81be22fb91a3d2d9b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 2 Mar 2021 23:22:42 +0100 Subject: [PATCH 781/796] Add zwave_js add-on manager (#47251) Co-authored-by: Paulus Schoutsen --- homeassistant/components/hassio/__init__.py | 27 ++ homeassistant/components/zwave_js/__init__.py | 101 +++++-- homeassistant/components/zwave_js/addon.py | 246 ++++++++++++++++ .../components/zwave_js/config_flow.py | 55 ++-- homeassistant/components/zwave_js/const.py | 8 + .../components/zwave_js/strings.json | 1 - .../components/zwave_js/translations/en.json | 6 - tests/components/zwave_js/conftest.py | 118 ++++++++ tests/components/zwave_js/test_config_flow.py | 127 +++------ tests/components/zwave_js/test_init.py | 263 ++++++++++++++++-- 10 files changed, 797 insertions(+), 155 deletions(-) create mode 100644 homeassistant/components/zwave_js/addon.py diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index fdeb10bcafe..5b40d7142f1 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -169,6 +169,18 @@ async def async_uninstall_addon(hass: HomeAssistantType, slug: str) -> dict: return await hassio.send_command(command, timeout=60) +@bind_hass +@api_data +async def async_update_addon(hass: HomeAssistantType, slug: str) -> dict: + """Update add-on. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = f"/addons/{slug}/update" + return await hassio.send_command(command, timeout=None) + + @bind_hass @api_data async def async_start_addon(hass: HomeAssistantType, slug: str) -> dict: @@ -218,6 +230,21 @@ async def async_get_addon_discovery_info( return next((addon for addon in discovered_addons if addon["addon"] == slug), None) +@bind_hass +@api_data +async def async_create_snapshot( + hass: HomeAssistantType, payload: dict, partial: bool = False +) -> dict: + """Create a full or partial snapshot. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + snapshot_type = "partial" if partial else "full" + command = f"/snapshots/new/{snapshot_type}" + return await hassio.send_command(command, payload=payload, timeout=None) + + @callback @bind_hass def get_info(hass): diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 798fd9fda2c..c19b1b355a4 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -1,16 +1,14 @@ """The Z-Wave JS integration.""" import asyncio -import logging from typing import Callable, List from async_timeout import timeout from zwave_js_server.client import Client as ZwaveClient -from zwave_js_server.exceptions import BaseZwaveJSServerError +from zwave_js_server.exceptions import BaseZwaveJSServerError, InvalidServerVersion from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.notification import Notification from zwave_js_server.model.value import ValueNotification -from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_DOMAIN, CONF_URL, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback @@ -19,9 +17,9 @@ from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send +from .addon import AddonError, AddonManager, get_addon_manager from .api import async_register_api from .const import ( - ADDON_SLUG, ATTR_COMMAND_CLASS, ATTR_COMMAND_CLASS_NAME, ATTR_DEVICE_ID, @@ -35,10 +33,14 @@ from .const import ( ATTR_TYPE, ATTR_VALUE, CONF_INTEGRATION_CREATED_ADDON, + CONF_NETWORK_KEY, + CONF_USB_PATH, + CONF_USE_ADDON, DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY, + LOGGER, PLATFORMS, ZWAVE_JS_EVENT, ) @@ -46,10 +48,11 @@ from .discovery import async_discover_values from .helpers import get_device_id, get_old_value_id, get_unique_id from .services import ZWaveServices -LOGGER = logging.getLogger(__package__) CONNECT_TIMEOUT = 10 DATA_CLIENT_LISTEN_TASK = "client_listen_task" DATA_START_PLATFORM_TASK = "start_platform_task" +DATA_CONNECT_FAILED_LOGGED = "connect_failed_logged" +DATA_INVALID_SERVER_VERSION_LOGGED = "invalid_server_version_logged" async def async_setup(hass: HomeAssistant, config: dict) -> bool: @@ -84,6 +87,10 @@ def register_node_in_dev_reg( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Z-Wave JS from a config entry.""" + use_addon = entry.data.get(CONF_USE_ADDON) + if use_addon: + await async_ensure_addon_running(hass, entry) + client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass)) dev_reg = await device_registry.async_get_registry(hass) ent_reg = entity_registry.async_get(hass) @@ -251,21 +258,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: }, ) + entry_hass_data: dict = hass.data[DOMAIN].setdefault(entry.entry_id, {}) # connect and throw error if connection failed try: async with timeout(CONNECT_TIMEOUT): await client.connect() + except InvalidServerVersion as err: + if not entry_hass_data.get(DATA_INVALID_SERVER_VERSION_LOGGED): + LOGGER.error("Invalid server version: %s", err) + entry_hass_data[DATA_INVALID_SERVER_VERSION_LOGGED] = True + if use_addon: + async_ensure_addon_updated(hass) + raise ConfigEntryNotReady from err except (asyncio.TimeoutError, BaseZwaveJSServerError) as err: - LOGGER.error("Failed to connect: %s", err) + if not entry_hass_data.get(DATA_CONNECT_FAILED_LOGGED): + LOGGER.error("Failed to connect: %s", err) + entry_hass_data[DATA_CONNECT_FAILED_LOGGED] = True raise ConfigEntryNotReady from err else: LOGGER.info("Connected to Zwave JS Server") + entry_hass_data[DATA_CONNECT_FAILED_LOGGED] = False + entry_hass_data[DATA_INVALID_SERVER_VERSION_LOGGED] = False unsubscribe_callbacks: List[Callable] = [] - hass.data[DOMAIN][entry.entry_id] = { - DATA_CLIENT: client, - DATA_UNSUBSCRIBE: unsubscribe_callbacks, - } + entry_hass_data[DATA_CLIENT] = client + entry_hass_data[DATA_UNSUBSCRIBE] = unsubscribe_callbacks services = ZWaveServices(hass, ent_reg) services.async_register() @@ -292,7 +309,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: listen_task = asyncio.create_task( client_listen(hass, entry, client, driver_ready) ) - hass.data[DOMAIN][entry.entry_id][DATA_CLIENT_LISTEN_TASK] = listen_task + entry_hass_data[DATA_CLIENT_LISTEN_TASK] = listen_task unsubscribe_callbacks.append( hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, handle_ha_shutdown) ) @@ -334,7 +351,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) platform_task = hass.async_create_task(start_platforms()) - hass.data[DOMAIN][entry.entry_id][DATA_START_PLATFORM_TASK] = platform_task + entry_hass_data[DATA_START_PLATFORM_TASK] = platform_task return True @@ -410,6 +427,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: platform_task=info[DATA_START_PLATFORM_TASK], ) + if entry.data.get(CONF_USE_ADDON) and entry.disabled_by: + addon_manager: AddonManager = get_addon_manager(hass) + LOGGER.debug("Stopping Z-Wave JS add-on") + try: + await addon_manager.async_stop_addon() + except AddonError as err: + LOGGER.error("Failed to stop the Z-Wave JS add-on: %s", err) + return False + return True @@ -418,12 +444,51 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: if not entry.data.get(CONF_INTEGRATION_CREATED_ADDON): return + addon_manager: AddonManager = get_addon_manager(hass) try: - await hass.components.hassio.async_stop_addon(ADDON_SLUG) - except HassioAPIError as err: - LOGGER.error("Failed to stop the Z-Wave JS add-on: %s", err) + await addon_manager.async_stop_addon() + except AddonError as err: + LOGGER.error(err) return try: - await hass.components.hassio.async_uninstall_addon(ADDON_SLUG) - except HassioAPIError as err: - LOGGER.error("Failed to uninstall the Z-Wave JS add-on: %s", err) + await addon_manager.async_create_snapshot() + except AddonError as err: + LOGGER.error(err) + return + try: + await addon_manager.async_uninstall_addon() + except AddonError as err: + LOGGER.error(err) + + +async def async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Ensure that Z-Wave JS add-on is installed and running.""" + addon_manager: AddonManager = get_addon_manager(hass) + if addon_manager.task_in_progress(): + raise ConfigEntryNotReady + try: + addon_is_installed = await addon_manager.async_is_addon_installed() + addon_is_running = await addon_manager.async_is_addon_running() + except AddonError as err: + LOGGER.error("Failed to get the Z-Wave JS add-on info") + raise ConfigEntryNotReady from err + + usb_path: str = entry.data[CONF_USB_PATH] + network_key: str = entry.data[CONF_NETWORK_KEY] + + if not addon_is_installed: + addon_manager.async_schedule_install_addon(usb_path, network_key) + raise ConfigEntryNotReady + + if not addon_is_running: + addon_manager.async_schedule_setup_addon(usb_path, network_key) + raise ConfigEntryNotReady + + +@callback +def async_ensure_addon_updated(hass: HomeAssistant) -> None: + """Ensure that Z-Wave JS add-on is updated and running.""" + addon_manager: AddonManager = get_addon_manager(hass) + if addon_manager.task_in_progress(): + raise ConfigEntryNotReady + addon_manager.async_schedule_update_addon() diff --git a/homeassistant/components/zwave_js/addon.py b/homeassistant/components/zwave_js/addon.py new file mode 100644 index 00000000000..54169dcaf94 --- /dev/null +++ b/homeassistant/components/zwave_js/addon.py @@ -0,0 +1,246 @@ +"""Provide add-on management.""" +from __future__ import annotations + +import asyncio +from functools import partial +from typing import Any, Callable, Optional, TypeVar, cast + +from homeassistant.components.hassio import ( + async_create_snapshot, + async_get_addon_discovery_info, + async_get_addon_info, + async_install_addon, + async_set_addon_options, + async_start_addon, + async_stop_addon, + async_uninstall_addon, + async_update_addon, +) +from homeassistant.components.hassio.handler import HassioAPIError +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.singleton import singleton + +from .const import ADDON_SLUG, CONF_ADDON_DEVICE, CONF_ADDON_NETWORK_KEY, DOMAIN, LOGGER + +F = TypeVar("F", bound=Callable[..., Any]) # pylint: disable=invalid-name + +DATA_ADDON_MANAGER = f"{DOMAIN}_addon_manager" + + +@singleton(DATA_ADDON_MANAGER) +@callback +def get_addon_manager(hass: HomeAssistant) -> AddonManager: + """Get the add-on manager.""" + return AddonManager(hass) + + +def api_error(error_message: str) -> Callable[[F], F]: + """Handle HassioAPIError and raise a specific AddonError.""" + + def handle_hassio_api_error(func: F) -> F: + """Handle a HassioAPIError.""" + + async def wrapper(*args, **kwargs): # type: ignore + """Wrap an add-on manager method.""" + try: + return_value = await func(*args, **kwargs) + except HassioAPIError as err: + raise AddonError(error_message) from err + + return return_value + + return cast(F, wrapper) + + return handle_hassio_api_error + + +class AddonManager: + """Manage the add-on. + + Methods may raise AddonError. + Only one instance of this class may exist + to keep track of running add-on tasks. + """ + + def __init__(self, hass: HomeAssistant) -> None: + """Set up the add-on manager.""" + self._hass = hass + self._install_task: Optional[asyncio.Task] = None + self._update_task: Optional[asyncio.Task] = None + self._setup_task: Optional[asyncio.Task] = None + + def task_in_progress(self) -> bool: + """Return True if any of the add-on tasks are in progress.""" + return any( + task and not task.done() + for task in ( + self._install_task, + self._setup_task, + self._update_task, + ) + ) + + @api_error("Failed to get Z-Wave JS add-on discovery info") + async def async_get_addon_discovery_info(self) -> dict: + """Return add-on discovery info.""" + discovery_info = await async_get_addon_discovery_info(self._hass, ADDON_SLUG) + + if not discovery_info: + raise AddonError("Failed to get Z-Wave JS add-on discovery info") + + discovery_info_config: dict = discovery_info["config"] + return discovery_info_config + + @api_error("Failed to get the Z-Wave JS add-on info") + async def async_get_addon_info(self) -> dict: + """Return and cache Z-Wave JS add-on info.""" + addon_info: dict = await async_get_addon_info(self._hass, ADDON_SLUG) + return addon_info + + async def async_is_addon_running(self) -> bool: + """Return True if Z-Wave JS add-on is running.""" + addon_info = await self.async_get_addon_info() + return bool(addon_info["state"] == "started") + + async def async_is_addon_installed(self) -> bool: + """Return True if Z-Wave JS add-on is installed.""" + addon_info = await self.async_get_addon_info() + return addon_info["version"] is not None + + async def async_get_addon_options(self) -> dict: + """Get Z-Wave JS add-on options.""" + addon_info = await self.async_get_addon_info() + return cast(dict, addon_info["options"]) + + @api_error("Failed to set the Z-Wave JS add-on options") + async def async_set_addon_options(self, config: dict) -> None: + """Set Z-Wave JS add-on options.""" + options = {"options": config} + await async_set_addon_options(self._hass, ADDON_SLUG, options) + + @api_error("Failed to install the Z-Wave JS add-on") + async def async_install_addon(self) -> None: + """Install the Z-Wave JS add-on.""" + await async_install_addon(self._hass, ADDON_SLUG) + + @callback + def async_schedule_install_addon( + self, usb_path: str, network_key: str + ) -> asyncio.Task: + """Schedule a task that installs and sets up the Z-Wave JS add-on. + + Only schedule a new install task if the there's no running task. + """ + if not self._install_task or self._install_task.done(): + LOGGER.info("Z-Wave JS add-on is not installed. Installing add-on") + self._install_task = self._async_schedule_addon_operation( + self.async_install_addon, + partial(self.async_setup_addon, usb_path, network_key), + ) + return self._install_task + + @api_error("Failed to uninstall the Z-Wave JS add-on") + async def async_uninstall_addon(self) -> None: + """Uninstall the Z-Wave JS add-on.""" + await async_uninstall_addon(self._hass, ADDON_SLUG) + + @api_error("Failed to update the Z-Wave JS add-on") + async def async_update_addon(self) -> None: + """Update the Z-Wave JS add-on if needed.""" + addon_info = await self.async_get_addon_info() + addon_version = addon_info["version"] + update_available = addon_info["update_available"] + + if addon_version is None: + raise AddonError("Z-Wave JS add-on is not installed") + + if not update_available: + return + + await async_update_addon(self._hass, ADDON_SLUG) + + @callback + def async_schedule_update_addon(self) -> asyncio.Task: + """Schedule a task that updates and sets up the Z-Wave JS add-on. + + Only schedule a new update task if the there's no running task. + """ + if not self._update_task or self._update_task.done(): + LOGGER.info("Trying to update the Z-Wave JS add-on") + self._update_task = self._async_schedule_addon_operation( + self.async_create_snapshot, self.async_update_addon + ) + return self._update_task + + @api_error("Failed to start the Z-Wave JS add-on") + async def async_start_addon(self) -> None: + """Start the Z-Wave JS add-on.""" + await async_start_addon(self._hass, ADDON_SLUG) + + @api_error("Failed to stop the Z-Wave JS add-on") + async def async_stop_addon(self) -> None: + """Stop the Z-Wave JS add-on.""" + await async_stop_addon(self._hass, ADDON_SLUG) + + async def async_setup_addon(self, usb_path: str, network_key: str) -> None: + """Configure and start Z-Wave JS add-on.""" + addon_options = await self.async_get_addon_options() + + new_addon_options = { + CONF_ADDON_DEVICE: usb_path, + CONF_ADDON_NETWORK_KEY: network_key, + } + + if new_addon_options != addon_options: + await self.async_set_addon_options(new_addon_options) + + await self.async_start_addon() + + @callback + def async_schedule_setup_addon( + self, usb_path: str, network_key: str + ) -> asyncio.Task: + """Schedule a task that configures and starts the Z-Wave JS add-on. + + Only schedule a new setup task if the there's no running task. + """ + if not self._setup_task or self._setup_task.done(): + LOGGER.info("Z-Wave JS add-on is not running. Starting add-on") + self._setup_task = self._async_schedule_addon_operation( + partial(self.async_setup_addon, usb_path, network_key) + ) + return self._setup_task + + @api_error("Failed to create a snapshot of the Z-Wave JS add-on.") + async def async_create_snapshot(self) -> None: + """Create a partial snapshot of the Z-Wave JS add-on.""" + addon_info = await self.async_get_addon_info() + addon_version = addon_info["version"] + name = f"addon_{ADDON_SLUG}_{addon_version}" + + LOGGER.debug("Creating snapshot: %s", name) + await async_create_snapshot( + self._hass, + {"name": name, "addons": [ADDON_SLUG]}, + partial=True, + ) + + @callback + def _async_schedule_addon_operation(self, *funcs: Callable) -> asyncio.Task: + """Schedule an add-on task.""" + + async def addon_operation() -> None: + """Do the add-on operation and catch AddonError.""" + for func in funcs: + try: + await func() + except AddonError as err: + LOGGER.error(err) + break + + return self._hass.async_create_task(addon_operation()) + + +class AddonError(HomeAssistantError): + """Represent an error with Z-Wave JS add-on.""" diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index cc19fb85d3a..37923c574b4 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -9,33 +9,25 @@ import voluptuous as vol from zwave_js_server.version import VersionInfo, get_server_version from homeassistant import config_entries, exceptions -from homeassistant.components.hassio import ( - async_get_addon_discovery_info, - async_get_addon_info, - async_install_addon, - async_set_addon_options, - async_start_addon, - is_hassio, -) -from homeassistant.components.hassio.handler import HassioAPIError +from homeassistant.components.hassio import is_hassio from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers.aiohttp_client import async_get_clientsession +from .addon import AddonError, AddonManager, get_addon_manager from .const import ( # pylint:disable=unused-import - ADDON_SLUG, + CONF_ADDON_DEVICE, + CONF_ADDON_NETWORK_KEY, CONF_INTEGRATION_CREATED_ADDON, + CONF_NETWORK_KEY, + CONF_USB_PATH, CONF_USE_ADDON, DOMAIN, ) _LOGGER = logging.getLogger(__name__) -CONF_ADDON_DEVICE = "device" -CONF_ADDON_NETWORK_KEY = "network_key" -CONF_NETWORK_KEY = "network_key" -CONF_USB_PATH = "usb_path" DEFAULT_URL = "ws://localhost:3000" TITLE = "Z-Wave JS" @@ -180,6 +172,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: """Handle logic when on Supervisor host.""" + # Only one entry with Supervisor add-on support is allowed. + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.data.get(CONF_USE_ADDON): + return await self.async_step_manual() + if user_input is None: return self.async_show_form( step_id="on_supervisor", data_schema=ON_SUPERVISOR_SCHEMA @@ -212,7 +209,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: await self.install_task - except HassioAPIError as err: + except AddonError as err: _LOGGER.error("Failed to install Z-Wave JS add-on: %s", err) return self.async_show_progress_done(next_step_id="install_failed") @@ -275,7 +272,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: await self.start_task - except (CannotConnect, HassioAPIError) as err: + except (CannotConnect, AddonError) as err: _LOGGER.error("Failed to start Z-Wave JS add-on: %s", err) return self.async_show_progress_done(next_step_id="start_failed") @@ -290,8 +287,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_start_addon(self) -> None: """Start the Z-Wave JS add-on.""" assert self.hass + addon_manager: AddonManager = get_addon_manager(self.hass) try: - await async_start_addon(self.hass, ADDON_SLUG) + await addon_manager.async_start_addon() # Sleep some seconds to let the add-on start properly before connecting. for _ in range(ADDON_SETUP_TIMEOUT_ROUNDS): await asyncio.sleep(ADDON_SETUP_TIMEOUT) @@ -345,9 +343,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_get_addon_info(self) -> dict: """Return and cache Z-Wave JS add-on info.""" + addon_manager: AddonManager = get_addon_manager(self.hass) try: - addon_info: dict = await async_get_addon_info(self.hass, ADDON_SLUG) - except HassioAPIError as err: + addon_info: dict = await addon_manager.async_get_addon_info() + except AddonError as err: _LOGGER.error("Failed to get Z-Wave JS add-on info: %s", err) raise AbortFlow("addon_info_failed") from err @@ -371,16 +370,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_set_addon_config(self, config: dict) -> None: """Set Z-Wave JS add-on config.""" options = {"options": config} + addon_manager: AddonManager = get_addon_manager(self.hass) try: - await async_set_addon_options(self.hass, ADDON_SLUG, options) - except HassioAPIError as err: + await addon_manager.async_set_addon_options(options) + except AddonError as err: _LOGGER.error("Failed to set Z-Wave JS add-on config: %s", err) raise AbortFlow("addon_set_config_failed") from err async def _async_install_addon(self) -> None: """Install the Z-Wave JS add-on.""" + addon_manager: AddonManager = get_addon_manager(self.hass) try: - await async_install_addon(self.hass, ADDON_SLUG) + await addon_manager.async_install_addon() finally: # Continue the flow after show progress when the task is done. self.hass.async_create_task( @@ -389,17 +390,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_get_addon_discovery_info(self) -> dict: """Return add-on discovery info.""" + addon_manager: AddonManager = get_addon_manager(self.hass) try: - discovery_info = await async_get_addon_discovery_info(self.hass, ADDON_SLUG) - except HassioAPIError as err: + discovery_info_config = await addon_manager.async_get_addon_discovery_info() + except AddonError as err: _LOGGER.error("Failed to get Z-Wave JS add-on discovery info: %s", err) raise AbortFlow("addon_get_discovery_info_failed") from err - if not discovery_info: - _LOGGER.error("Failed to get Z-Wave JS add-on discovery info") - raise AbortFlow("addon_missing_discovery_info") - - discovery_info_config: dict = discovery_info["config"] return discovery_info_config diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 19e6fc3db14..e3f20366ab0 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -1,5 +1,11 @@ """Constants for the Z-Wave JS integration.""" +import logging + +CONF_ADDON_DEVICE = "device" +CONF_ADDON_NETWORK_KEY = "network_key" CONF_INTEGRATION_CREATED_ADDON = "integration_created_addon" +CONF_NETWORK_KEY = "network_key" +CONF_USB_PATH = "usb_path" CONF_USE_ADDON = "use_addon" DOMAIN = "zwave_js" PLATFORMS = [ @@ -19,6 +25,8 @@ DATA_UNSUBSCRIBE = "unsubs" EVENT_DEVICE_ADDED_TO_REGISTRY = f"{DOMAIN}_device_added_to_registry" +LOGGER = logging.getLogger(__package__) + # constants for events ZWAVE_JS_EVENT = f"{DOMAIN}_event" ATTR_NODE_ID = "node_id" diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 5d3aa730a7c..eb13ad512e3 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -41,7 +41,6 @@ "addon_set_config_failed": "Failed to set Z-Wave JS configuration.", "addon_start_failed": "Failed to start the Z-Wave JS add-on.", "addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.", - "addon_missing_discovery_info": "Missing Z-Wave JS add-on discovery info.", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "progress": { diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json index 5be980d52cb..101942dc717 100644 --- a/homeassistant/components/zwave_js/translations/en.json +++ b/homeassistant/components/zwave_js/translations/en.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.", "addon_info_failed": "Failed to get Z-Wave JS add-on info.", "addon_install_failed": "Failed to install the Z-Wave JS add-on.", - "addon_missing_discovery_info": "Missing Z-Wave JS add-on discovery info.", "addon_set_config_failed": "Failed to set Z-Wave JS configuration.", "addon_start_failed": "Failed to start the Z-Wave JS add-on.", "already_configured": "Device is already configured", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "The Z-Wave JS add-on is starting." - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 72835fb17c1..50cacd97422 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -14,6 +14,124 @@ from homeassistant.helpers.device_registry import async_get as async_get_device_ from tests.common import MockConfigEntry, load_fixture +# Add-on fixtures + + +@pytest.fixture(name="addon_info_side_effect") +def addon_info_side_effect_fixture(): + """Return the add-on info side effect.""" + return None + + +@pytest.fixture(name="addon_info") +def mock_addon_info(addon_info_side_effect): + """Mock Supervisor add-on info.""" + with patch( + "homeassistant.components.zwave_js.addon.async_get_addon_info", + side_effect=addon_info_side_effect, + ) as addon_info: + addon_info.return_value = {} + yield addon_info + + +@pytest.fixture(name="addon_running") +def mock_addon_running(addon_info): + """Mock add-on already running.""" + addon_info.return_value["state"] = "started" + return addon_info + + +@pytest.fixture(name="addon_installed") +def mock_addon_installed(addon_info): + """Mock add-on already installed but not running.""" + addon_info.return_value["state"] = "stopped" + addon_info.return_value["version"] = "1.0" + return addon_info + + +@pytest.fixture(name="addon_options") +def mock_addon_options(addon_info): + """Mock add-on options.""" + addon_info.return_value["options"] = {} + return addon_info.return_value["options"] + + +@pytest.fixture(name="set_addon_options_side_effect") +def set_addon_options_side_effect_fixture(): + """Return the set add-on options side effect.""" + return None + + +@pytest.fixture(name="set_addon_options") +def mock_set_addon_options(set_addon_options_side_effect): + """Mock set add-on options.""" + with patch( + "homeassistant.components.zwave_js.addon.async_set_addon_options", + side_effect=set_addon_options_side_effect, + ) as set_options: + yield set_options + + +@pytest.fixture(name="install_addon") +def mock_install_addon(): + """Mock install add-on.""" + with patch( + "homeassistant.components.zwave_js.addon.async_install_addon" + ) as install_addon: + yield install_addon + + +@pytest.fixture(name="update_addon") +def mock_update_addon(): + """Mock update add-on.""" + with patch( + "homeassistant.components.zwave_js.addon.async_update_addon" + ) as update_addon: + yield update_addon + + +@pytest.fixture(name="start_addon_side_effect") +def start_addon_side_effect_fixture(): + """Return the set add-on options side effect.""" + return None + + +@pytest.fixture(name="start_addon") +def mock_start_addon(start_addon_side_effect): + """Mock start add-on.""" + with patch( + "homeassistant.components.zwave_js.addon.async_start_addon", + side_effect=start_addon_side_effect, + ) as start_addon: + yield start_addon + + +@pytest.fixture(name="stop_addon") +def stop_addon_fixture(): + """Mock stop add-on.""" + with patch( + "homeassistant.components.zwave_js.addon.async_stop_addon" + ) as stop_addon: + yield stop_addon + + +@pytest.fixture(name="uninstall_addon") +def uninstall_addon_fixture(): + """Mock uninstall add-on.""" + with patch( + "homeassistant.components.zwave_js.addon.async_uninstall_addon" + ) as uninstall_addon: + yield uninstall_addon + + +@pytest.fixture(name="create_shapshot") +def create_snapshot_fixture(): + """Mock create snapshot.""" + with patch( + "homeassistant.components.zwave_js.addon.async_create_snapshot" + ) as create_shapshot: + yield create_shapshot + @pytest.fixture(name="device_registry") async def device_registry_fixture(hass): diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 08b0ffe3080..fc97f7420cf 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -44,93 +44,13 @@ def discovery_info_side_effect_fixture(): def mock_get_addon_discovery_info(discovery_info, discovery_info_side_effect): """Mock get add-on discovery info.""" with patch( - "homeassistant.components.zwave_js.config_flow.async_get_addon_discovery_info", + "homeassistant.components.zwave_js.addon.async_get_addon_discovery_info", side_effect=discovery_info_side_effect, return_value=discovery_info, ) as get_addon_discovery_info: yield get_addon_discovery_info -@pytest.fixture(name="addon_info_side_effect") -def addon_info_side_effect_fixture(): - """Return the add-on info side effect.""" - return None - - -@pytest.fixture(name="addon_info") -def mock_addon_info(addon_info_side_effect): - """Mock Supervisor add-on info.""" - with patch( - "homeassistant.components.zwave_js.config_flow.async_get_addon_info", - side_effect=addon_info_side_effect, - ) as addon_info: - addon_info.return_value = {} - yield addon_info - - -@pytest.fixture(name="addon_running") -def mock_addon_running(addon_info): - """Mock add-on already running.""" - addon_info.return_value["state"] = "started" - return addon_info - - -@pytest.fixture(name="addon_installed") -def mock_addon_installed(addon_info): - """Mock add-on already installed but not running.""" - addon_info.return_value["state"] = "stopped" - addon_info.return_value["version"] = "1.0" - return addon_info - - -@pytest.fixture(name="addon_options") -def mock_addon_options(addon_info): - """Mock add-on options.""" - addon_info.return_value["options"] = {} - return addon_info.return_value["options"] - - -@pytest.fixture(name="set_addon_options_side_effect") -def set_addon_options_side_effect_fixture(): - """Return the set add-on options side effect.""" - return None - - -@pytest.fixture(name="set_addon_options") -def mock_set_addon_options(set_addon_options_side_effect): - """Mock set add-on options.""" - with patch( - "homeassistant.components.zwave_js.config_flow.async_set_addon_options", - side_effect=set_addon_options_side_effect, - ) as set_options: - yield set_options - - -@pytest.fixture(name="install_addon") -def mock_install_addon(): - """Mock install add-on.""" - with patch( - "homeassistant.components.zwave_js.config_flow.async_install_addon" - ) as install_addon: - yield install_addon - - -@pytest.fixture(name="start_addon_side_effect") -def start_addon_side_effect_fixture(): - """Return the set add-on options side effect.""" - return None - - -@pytest.fixture(name="start_addon") -def mock_start_addon(start_addon_side_effect): - """Mock start add-on.""" - with patch( - "homeassistant.components.zwave_js.config_flow.async_start_addon", - side_effect=start_addon_side_effect, - ) as start_addon: - yield start_addon - - @pytest.fixture(name="server_version_side_effect") def server_version_side_effect_fixture(): """Return the server version side effect.""" @@ -587,6 +507,49 @@ async def test_not_addon(hass, supervisor): assert len(mock_setup_entry.mock_calls) == 1 +async def test_addon_already_configured(hass, supervisor): + """Test add-on already configured leads to manual step.""" + entry = MockConfigEntry( + domain=DOMAIN, data={"use_addon": True}, title=TITLE, unique_id=5678 + ) + entry.add_to_hass(hass) + + 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["step_id"] == "manual" + + with patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "ws://localhost:3000", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == TITLE + assert result["data"] == { + "url": "ws://localhost:3000", + "usb_path": None, + "network_key": None, + "use_addon": False, + "integration_created_addon": False, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 2 + + @pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) async def test_addon_running( hass, @@ -654,7 +617,7 @@ async def test_addon_running( None, None, None, - "addon_missing_discovery_info", + "addon_get_discovery_info_failed", ), ( {"config": ADDON_DISCOVERY_INFO}, diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 2a2f249c361..6f60bbc0300 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -1,9 +1,9 @@ """Test the Z-Wave JS init module.""" from copy import deepcopy -from unittest.mock import patch +from unittest.mock import call, patch import pytest -from zwave_js_server.exceptions import BaseZwaveJSServerError +from zwave_js_server.exceptions import BaseZwaveJSServerError, InvalidServerVersion from zwave_js_server.model.node import Node from homeassistant.components.hassio.handler import HassioAPIError @@ -11,6 +11,7 @@ from homeassistant.components.zwave_js.const import DOMAIN from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.config_entries import ( CONN_CLASS_LOCAL_PUSH, + DISABLED_USER, ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED, ENTRY_STATE_SETUP_RETRY, @@ -34,22 +35,6 @@ def connect_timeout_fixture(): yield timeout -@pytest.fixture(name="stop_addon") -def stop_addon_fixture(): - """Mock stop add-on.""" - with patch("homeassistant.components.hassio.async_stop_addon") as stop_addon: - yield stop_addon - - -@pytest.fixture(name="uninstall_addon") -def uninstall_addon_fixture(): - """Mock uninstall add-on.""" - with patch( - "homeassistant.components.hassio.async_uninstall_addon" - ) as uninstall_addon: - yield uninstall_addon - - async def test_entry_setup_unload(hass, client, integration): """Test the integration set up and unload.""" entry = integration @@ -367,7 +352,203 @@ async def test_existing_node_not_ready(hass, client, multisensor_6, device_regis ) -async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog): +async def test_start_addon( + hass, addon_installed, install_addon, addon_options, set_addon_options, start_addon +): + """Test start the Z-Wave JS add-on during entry setup.""" + device = "/test" + network_key = "abc123" + addon_options = { + "device": device, + "network_key": network_key, + } + entry = MockConfigEntry( + domain=DOMAIN, + title="Z-Wave JS", + connection_class=CONN_CLASS_LOCAL_PUSH, + data={"use_addon": True, "usb_path": device, "network_key": network_key}, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ENTRY_STATE_SETUP_RETRY + assert install_addon.call_count == 0 + assert set_addon_options.call_count == 1 + assert set_addon_options.call_args == call( + hass, "core_zwave_js", {"options": addon_options} + ) + assert start_addon.call_count == 1 + assert start_addon.call_args == call(hass, "core_zwave_js") + + +async def test_install_addon( + hass, addon_installed, install_addon, addon_options, set_addon_options, start_addon +): + """Test install and start the Z-Wave JS add-on during entry setup.""" + addon_installed.return_value["version"] = None + device = "/test" + network_key = "abc123" + addon_options = { + "device": device, + "network_key": network_key, + } + entry = MockConfigEntry( + domain=DOMAIN, + title="Z-Wave JS", + connection_class=CONN_CLASS_LOCAL_PUSH, + data={"use_addon": True, "usb_path": device, "network_key": network_key}, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ENTRY_STATE_SETUP_RETRY + assert install_addon.call_count == 1 + assert install_addon.call_args == call(hass, "core_zwave_js") + assert set_addon_options.call_count == 1 + assert set_addon_options.call_args == call( + hass, "core_zwave_js", {"options": addon_options} + ) + assert start_addon.call_count == 1 + assert start_addon.call_args == call(hass, "core_zwave_js") + + +@pytest.mark.parametrize("addon_info_side_effect", [HassioAPIError("Boom")]) +async def test_addon_info_failure( + hass, + addon_installed, + install_addon, + addon_options, + set_addon_options, + start_addon, +): + """Test failure to get add-on info for Z-Wave JS add-on during entry setup.""" + device = "/test" + network_key = "abc123" + entry = MockConfigEntry( + domain=DOMAIN, + title="Z-Wave JS", + connection_class=CONN_CLASS_LOCAL_PUSH, + data={"use_addon": True, "usb_path": device, "network_key": network_key}, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ENTRY_STATE_SETUP_RETRY + assert install_addon.call_count == 0 + assert start_addon.call_count == 0 + + +@pytest.mark.parametrize( + "addon_version, update_available, update_calls, update_addon_side_effect", + [ + ("1.0", True, 1, None), + ("1.0", False, 0, None), + ("1.0", True, 1, HassioAPIError("Boom")), + ], +) +async def test_update_addon( + hass, + client, + addon_info, + addon_installed, + addon_running, + create_shapshot, + update_addon, + addon_options, + addon_version, + update_available, + update_calls, + update_addon_side_effect, +): + """Test update the Z-Wave JS add-on during entry setup.""" + addon_info.return_value["version"] = addon_version + addon_info.return_value["update_available"] = update_available + update_addon.side_effect = update_addon_side_effect + client.connect.side_effect = InvalidServerVersion("Invalid version") + device = "/test" + network_key = "abc123" + entry = MockConfigEntry( + domain=DOMAIN, + title="Z-Wave JS", + connection_class=CONN_CLASS_LOCAL_PUSH, + data={ + "url": "ws://host1:3001", + "use_addon": True, + "usb_path": device, + "network_key": network_key, + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ENTRY_STATE_SETUP_RETRY + assert create_shapshot.call_count == 1 + assert create_shapshot.call_args == call( + hass, + {"name": f"addon_core_zwave_js_{addon_version}", "addons": ["core_zwave_js"]}, + partial=True, + ) + assert update_addon.call_count == update_calls + + +@pytest.mark.parametrize( + "stop_addon_side_effect, entry_state", + [ + (None, ENTRY_STATE_NOT_LOADED), + (HassioAPIError("Boom"), ENTRY_STATE_LOADED), + ], +) +async def test_stop_addon( + hass, + client, + addon_installed, + addon_running, + addon_options, + stop_addon, + stop_addon_side_effect, + entry_state, +): + """Test stop the Z-Wave JS add-on on entry unload if entry is disabled.""" + stop_addon.side_effect = stop_addon_side_effect + device = "/test" + network_key = "abc123" + entry = MockConfigEntry( + domain=DOMAIN, + title="Z-Wave JS", + connection_class=CONN_CLASS_LOCAL_PUSH, + data={ + "url": "ws://host1:3001", + "use_addon": True, + "usb_path": device, + "network_key": network_key, + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ENTRY_STATE_LOADED + + await hass.config_entries.async_set_disabled_by(entry.entry_id, DISABLED_USER) + await hass.async_block_till_done() + + assert entry.state == entry_state + assert stop_addon.call_count == 1 + assert stop_addon.call_args == call(hass, "core_zwave_js") + + +async def test_remove_entry( + hass, addon_installed, stop_addon, create_shapshot, uninstall_addon, caplog +): """Test remove the config entry.""" # test successful remove without created add-on entry = MockConfigEntry( @@ -398,10 +579,19 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog): await hass.config_entries.async_remove(entry.entry_id) assert stop_addon.call_count == 1 + assert stop_addon.call_args == call(hass, "core_zwave_js") + assert create_shapshot.call_count == 1 + assert create_shapshot.call_args == call( + hass, + {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]}, + partial=True, + ) assert uninstall_addon.call_count == 1 + assert uninstall_addon.call_args == call(hass, "core_zwave_js") assert entry.state == ENTRY_STATE_NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 stop_addon.reset_mock() + create_shapshot.reset_mock() uninstall_addon.reset_mock() # test add-on stop failure @@ -412,12 +602,39 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog): await hass.config_entries.async_remove(entry.entry_id) assert stop_addon.call_count == 1 + assert stop_addon.call_args == call(hass, "core_zwave_js") + assert create_shapshot.call_count == 0 assert uninstall_addon.call_count == 0 assert entry.state == ENTRY_STATE_NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert "Failed to stop the Z-Wave JS add-on" in caplog.text stop_addon.side_effect = None stop_addon.reset_mock() + create_shapshot.reset_mock() + uninstall_addon.reset_mock() + + # test create snapshot failure + entry.add_to_hass(hass) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + create_shapshot.side_effect = HassioAPIError() + + await hass.config_entries.async_remove(entry.entry_id) + + assert stop_addon.call_count == 1 + assert stop_addon.call_args == call(hass, "core_zwave_js") + assert create_shapshot.call_count == 1 + assert create_shapshot.call_args == call( + hass, + {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]}, + partial=True, + ) + assert uninstall_addon.call_count == 0 + assert entry.state == ENTRY_STATE_NOT_LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + assert "Failed to create a snapshot of the Z-Wave JS add-on" in caplog.text + create_shapshot.side_effect = None + stop_addon.reset_mock() + create_shapshot.reset_mock() uninstall_addon.reset_mock() # test add-on uninstall failure @@ -428,7 +645,15 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog): await hass.config_entries.async_remove(entry.entry_id) assert stop_addon.call_count == 1 + assert stop_addon.call_args == call(hass, "core_zwave_js") + assert create_shapshot.call_count == 1 + assert create_shapshot.call_args == call( + hass, + {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]}, + partial=True, + ) assert uninstall_addon.call_count == 1 + assert uninstall_addon.call_args == call(hass, "core_zwave_js") assert entry.state == ENTRY_STATE_NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert "Failed to uninstall the Z-Wave JS add-on" in caplog.text From d7f4416421202ebe5ee533e1b714789da85d1808 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 2 Mar 2021 06:13:45 -0800 Subject: [PATCH 782/796] Fix Alexa doorbells (#47257) --- .../components/alexa/state_report.py | 30 +++++++------------ tests/components/alexa/test_state_report.py | 16 ++++++++++ 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index d66906810b2..c34dc34f0dd 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -73,10 +73,7 @@ async def async_enable_proactive_mode(hass, smart_home_config): if not should_report and interface.properties_proactively_reported(): should_report = True - if ( - interface.name() == "Alexa.DoorbellEventSource" - and new_state.state == STATE_ON - ): + if interface.name() == "Alexa.DoorbellEventSource": should_doorbell = True break @@ -84,27 +81,22 @@ async def async_enable_proactive_mode(hass, smart_home_config): return if should_doorbell: - should_report = False + if new_state.state == STATE_ON: + await async_send_doorbell_event_message( + hass, smart_home_config, alexa_changed_entity + ) + return - if should_report: - alexa_properties = list(alexa_changed_entity.serialize_properties()) - else: - alexa_properties = None + alexa_properties = list(alexa_changed_entity.serialize_properties()) if not checker.async_is_significant_change( new_state, extra_arg=alexa_properties ): return - if should_report: - await async_send_changereport_message( - hass, smart_home_config, alexa_changed_entity, alexa_properties - ) - - elif should_doorbell: - await async_send_doorbell_event_message( - hass, smart_home_config, alexa_changed_entity - ) + await async_send_changereport_message( + hass, smart_home_config, alexa_changed_entity, alexa_properties + ) return hass.helpers.event.async_track_state_change( MATCH_ALL, async_entity_state_listener @@ -246,7 +238,7 @@ async def async_send_delete_message(hass, config, entity_ids): async def async_send_doorbell_event_message(hass, config, alexa_entity): """Send a DoorbellPress event message for an Alexa entity. - https://developer.amazon.com/docs/smarthome/send-events-to-the-alexa-event-gateway.html + https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-doorbelleventsource.html """ token = await config.async_get_access_token() diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index a057eada531..2cbf8636d79 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -175,6 +175,22 @@ async def test_doorbell_event(hass, aioclient_mock): assert call_json["event"]["payload"]["cause"]["type"] == "PHYSICAL_INTERACTION" assert call_json["event"]["endpoint"]["endpointId"] == "binary_sensor#test_doorbell" + hass.states.async_set( + "binary_sensor.test_doorbell", + "off", + {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, + ) + + hass.states.async_set( + "binary_sensor.test_doorbell", + "on", + {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, + ) + + await hass.async_block_till_done() + + assert len(aioclient_mock.mock_calls) == 2 + async def test_proactive_mode_filter_states(hass, aioclient_mock): """Test all the cases that filter states.""" From 6c5c3233f1ca81c8d311b6c95354820b4b1d5c20 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 2 Mar 2021 16:10:30 -0500 Subject: [PATCH 783/796] Add raw values to zwave_js value notification event (#47258) * add value_raw to value notification event that always shows the untranslated state value * add property key and property to event params --- homeassistant/components/zwave_js/__init__.py | 8 +++++++- homeassistant/components/zwave_js/const.py | 3 +++ tests/components/zwave_js/test_events.py | 3 +++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index c19b1b355a4..d4e349645cf 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -28,10 +28,13 @@ from .const import ( ATTR_LABEL, ATTR_NODE_ID, ATTR_PARAMETERS, + ATTR_PROPERTY, + ATTR_PROPERTY_KEY, ATTR_PROPERTY_KEY_NAME, ATTR_PROPERTY_NAME, ATTR_TYPE, ATTR_VALUE, + ATTR_VALUE_RAW, CONF_INTEGRATION_CREATED_ADDON, CONF_NETWORK_KEY, CONF_USB_PATH, @@ -220,7 +223,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def async_on_value_notification(notification: ValueNotification) -> None: """Relay stateless value notification events from Z-Wave nodes to hass.""" device = dev_reg.async_get_device({get_device_id(client, notification.node)}) - value = notification.value + raw_value = value = notification.value if notification.metadata.states: value = notification.metadata.states.get(str(value), value) hass.bus.async_fire( @@ -235,9 +238,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ATTR_COMMAND_CLASS: notification.command_class, ATTR_COMMAND_CLASS_NAME: notification.command_class_name, ATTR_LABEL: notification.metadata.label, + ATTR_PROPERTY: notification.property_, ATTR_PROPERTY_NAME: notification.property_name, + ATTR_PROPERTY_KEY: notification.property_key, ATTR_PROPERTY_KEY_NAME: notification.property_key_name, ATTR_VALUE: value, + ATTR_VALUE_RAW: raw_value, }, ) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index e3f20366ab0..ffd6031349a 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -34,12 +34,15 @@ ATTR_HOME_ID = "home_id" ATTR_ENDPOINT = "endpoint" ATTR_LABEL = "label" ATTR_VALUE = "value" +ATTR_VALUE_RAW = "value_raw" ATTR_COMMAND_CLASS = "command_class" ATTR_COMMAND_CLASS_NAME = "command_class_name" ATTR_TYPE = "type" ATTR_DEVICE_ID = "device_id" ATTR_PROPERTY_NAME = "property_name" ATTR_PROPERTY_KEY_NAME = "property_key_name" +ATTR_PROPERTY = "property" +ATTR_PROPERTY_KEY = "property_key" ATTR_PARAMETERS = "parameters" # service constants diff --git a/tests/components/zwave_js/test_events.py b/tests/components/zwave_js/test_events.py index 2a347f6afea..e40782270a9 100644 --- a/tests/components/zwave_js/test_events.py +++ b/tests/components/zwave_js/test_events.py @@ -47,6 +47,7 @@ async def test_scenes(hass, hank_binary_switch, integration, client): assert events[0].data["command_class_name"] == "Basic" assert events[0].data["label"] == "Event value" assert events[0].data["value"] == 255 + assert events[0].data["value_raw"] == 255 # Publish fake Scene Activation value notification event = Event( @@ -82,6 +83,7 @@ async def test_scenes(hass, hank_binary_switch, integration, client): assert events[1].data["command_class_name"] == "Scene Activation" assert events[1].data["label"] == "Scene ID" assert events[1].data["value"] == 16 + assert events[1].data["value_raw"] == 16 # Publish fake Central Scene value notification event = Event( @@ -128,6 +130,7 @@ async def test_scenes(hass, hank_binary_switch, integration, client): assert events[2].data["command_class_name"] == "Central Scene" assert events[2].data["label"] == "Scene 001" assert events[2].data["value"] == "KeyPressed3x" + assert events[2].data["value_raw"] == 4 async def test_notifications(hass, hank_binary_switch, integration, client): From 7a6edf9725326764c26ba2c251e64602f27160b6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Mar 2021 14:28:31 +0100 Subject: [PATCH 784/796] Make MQTT number respect retain setting (#47270) --- homeassistant/components/mqtt/number.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 969eb254072..aa24f81eb69 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -28,6 +28,7 @@ from . import ( subscription, ) from .. import mqtt +from .const import CONF_RETAIN from .debug_info import log_messages from .mixins import ( MQTT_AVAILABILITY_SCHEMA, @@ -161,6 +162,7 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): self._config[CONF_COMMAND_TOPIC], current_number, self._config[CONF_QOS], + self._config[CONF_RETAIN], ) @property From b8bc0a7fe9b5b4577e167e2e01a5420a0f7ef645 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 2 Mar 2021 12:19:04 -0700 Subject: [PATCH 785/796] Bump simplisafe-python to 9.6.9 (#47273) --- 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 6122428ea98..45deb938b59 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.8"], + "requirements": ["simplisafe-python==9.6.9"], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index d63ab3c5b83..a2d1c502081 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2044,7 +2044,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.6.8 +simplisafe-python==9.6.9 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ef6c0706c87..5e65e950654 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1051,7 +1051,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.6.8 +simplisafe-python==9.6.9 # homeassistant.components.slack slackclient==2.5.0 From 4b9c1489893db6968b33ad5f141a5f72647b1636 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 2 Mar 2021 22:02:59 +0100 Subject: [PATCH 786/796] Fix issue when setting boost preset for a turned off Netatmo thermostat (#47275) --- homeassistant/components/netatmo/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 91026c40c2f..a53f7f9fb08 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -352,6 +352,9 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" + if self.hvac_mode == HVAC_MODE_OFF: + self.turn_on() + if self.target_temperature == 0: self._home_status.set_room_thermpoint( self._id, From ebb9008c270b51d3a56a42e2469c4ad0d34be00d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 2 Mar 2021 20:15:09 +0100 Subject: [PATCH 787/796] Update frontend to 20210302.0 (#47278) --- 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 e8f9ff2698d..e7d7723a510 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210301.0" + "home-assistant-frontend==20210302.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5aaa5b1b469..44e6195317d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.41.0 -home-assistant-frontend==20210301.0 +home-assistant-frontend==20210302.0 httpx==0.16.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index a2d1c502081..c4962d24400 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -763,7 +763,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210301.0 +home-assistant-frontend==20210302.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e65e950654..ed722a07256 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -412,7 +412,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210301.0 +home-assistant-frontend==20210302.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 39b9ad0ca0ed4f3e2ceba10b37724417deea3878 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 2 Mar 2021 15:12:30 -0500 Subject: [PATCH 788/796] Update ZHA dependencies (#47282) --- 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 d7bb0dbe5bc..7d367c3dc00 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.21.0", + "bellows==0.22.0", "pyserial==3.5", "pyserial-asyncio==0.5", "zha-quirks==0.0.54", diff --git a/requirements_all.txt b/requirements_all.txt index c4962d24400..e97062e4c11 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -340,7 +340,7 @@ beautifulsoup4==4.9.3 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.21.0 +bellows==0.22.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.7.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ed722a07256..2c9ac79095d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -193,7 +193,7 @@ azure-eventhub==5.1.0 base36==0.1.1 # homeassistant.components.zha -bellows==0.21.0 +bellows==0.22.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.7.15 From eb981fb007f93998a9c5da7c94d03ef41fad5a55 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 2 Mar 2021 16:25:09 -0500 Subject: [PATCH 789/796] Convert climacell forecast timestamp to isoformat so that UI shows the right times (#47286) --- homeassistant/components/climacell/weather.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index da3282108a5..c77bbfbd50a 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -1,4 +1,5 @@ """Weather component that handles meteorological data for your location.""" +from datetime import datetime import logging from typing import Any, Callable, Dict, List, Optional @@ -80,7 +81,7 @@ def _translate_condition( def _forecast_dict( hass: HomeAssistantType, - time: str, + forecast_dt: datetime, use_datetime: bool, condition: str, precipitation: Optional[float], @@ -92,10 +93,7 @@ def _forecast_dict( ) -> Dict[str, Any]: """Return formatted Forecast dict from ClimaCell forecast data.""" if use_datetime: - translated_condition = _translate_condition( - condition, - is_up(hass, dt_util.as_utc(dt_util.parse_datetime(time))), - ) + translated_condition = _translate_condition(condition, is_up(hass, forecast_dt)) else: translated_condition = _translate_condition(condition, True) @@ -112,7 +110,7 @@ def _forecast_dict( wind_speed = distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS) data = { - ATTR_FORECAST_TIME: time, + ATTR_FORECAST_TIME: forecast_dt.isoformat(), ATTR_FORECAST_CONDITION: translated_condition, ATTR_FORECAST_PRECIPITATION: precipitation, ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability, @@ -246,7 +244,9 @@ class ClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): # Set default values (in cases where keys don't exist), None will be # returned. Override properties per forecast type as needed for forecast in self.coordinator.data[FORECASTS][self.forecast_type]: - timestamp = self._get_cc_value(forecast, CC_ATTR_TIMESTAMP) + forecast_dt = dt_util.parse_datetime( + self._get_cc_value(forecast, CC_ATTR_TIMESTAMP) + ) use_datetime = True condition = self._get_cc_value(forecast, CC_ATTR_CONDITION) precipitation = self._get_cc_value(forecast, CC_ATTR_PRECIPITATION) @@ -290,7 +290,7 @@ class ClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): forecasts.append( _forecast_dict( self.hass, - timestamp, + forecast_dt, use_datetime, condition, precipitation, From f74b88a29cdc3bff10bba03bafe4b49328bc66b6 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 2 Mar 2021 17:09:50 -0500 Subject: [PATCH 790/796] Bump zwave-js-server-python to 0.20.1 (#47289) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 9e57a3b72e2..c812515a179 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.20.0"], + "requirements": ["zwave-js-server-python==0.20.1"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"] } diff --git a/requirements_all.txt b/requirements_all.txt index e97062e4c11..aa5c9d2bb85 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2397,4 +2397,4 @@ zigpy==0.32.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.20.0 +zwave-js-server-python==0.20.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2c9ac79095d..28b2d0ca08c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1234,4 +1234,4 @@ zigpy-znp==0.4.0 zigpy==0.32.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.20.0 +zwave-js-server-python==0.20.1 From da2c7dc743bfa25c9460604ae3b4fc13460988f5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 2 Mar 2021 22:37:27 +0000 Subject: [PATCH 791/796] Bumped version to 2021.3.0b7 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2edbfa33a10..47fb090305d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 3 -PATCH_VERSION = "0b6" +PATCH_VERSION = "0b7" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From 24919e99b81aa99bba0e5962f957af7a48583ccc Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 2 Mar 2021 19:55:10 -0500 Subject: [PATCH 792/796] Correct climacell device info (#47292) --- homeassistant/components/climacell/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index d6bf0ec4e12..b6e70ab56e8 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -256,7 +256,8 @@ class ClimaCellEntity(CoordinatorEntity): """Return device registry information.""" return { "identifiers": {(DOMAIN, self._config_entry.data[CONF_API_KEY])}, - "name": self.name, + "name": "ClimaCell", "manufacturer": "ClimaCell", + "sw_version": "v3", "entry_type": "service", } From 15c89ebada8a41e43891c885569ecde73d79778d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Mar 2021 14:35:58 +0100 Subject: [PATCH 793/796] Update frontend to 20210302.3 (#47310) --- 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 e7d7723a510..4d4127fc2f2 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210302.0" + "home-assistant-frontend==20210302.3" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 44e6195317d..752a3755169 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.41.0 -home-assistant-frontend==20210302.0 +home-assistant-frontend==20210302.3 httpx==0.16.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index aa5c9d2bb85..ffbd439ee2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -763,7 +763,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210302.0 +home-assistant-frontend==20210302.3 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 28b2d0ca08c..318b04e5e70 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -412,7 +412,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210302.0 +home-assistant-frontend==20210302.3 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From a89ba0ed8e0a86ef61a8fce3f839d2c834389c34 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 3 Mar 2021 19:12:37 +0100 Subject: [PATCH 794/796] Improve behaviour when disabling or enabling config entries (#47301) --- homeassistant/config_entries.py | 32 ++++++--- homeassistant/const.py | 1 - homeassistant/helpers/device_registry.py | 71 +++++++++---------- homeassistant/helpers/entity_registry.py | 87 +++++++++++------------- tests/helpers/test_entity_registry.py | 2 +- 5 files changed, 97 insertions(+), 96 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b0ec71be9cf..12a795d0a51 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -11,10 +11,9 @@ import weakref import attr from homeassistant import data_entry_flow, loader -from homeassistant.const import EVENT_CONFIG_ENTRY_DISABLED_BY_UPDATED from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError -from homeassistant.helpers import entity_registry +from homeassistant.helpers import device_registry, 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 @@ -807,12 +806,21 @@ class ConfigEntries: entry.disabled_by = disabled_by self._async_schedule_save() - # Unload the config entry, then fire an event + dev_reg = device_registry.async_get(self.hass) + ent_reg = entity_registry.async_get(self.hass) + + if not entry.disabled_by: + # The config entry will no longer be disabled, enable devices and entities + device_registry.async_config_entry_disabled_by_changed(dev_reg, entry) + entity_registry.async_config_entry_disabled_by_changed(ent_reg, entry) + + # Load or unload the config entry reload_result = await self.async_reload(entry_id) - self.hass.bus.async_fire( - EVENT_CONFIG_ENTRY_DISABLED_BY_UPDATED, {"config_entry_id": entry_id} - ) + if entry.disabled_by: + # The config entry has been disabled, disable devices and entities + device_registry.async_config_entry_disabled_by_changed(dev_reg, entry) + entity_registry.async_config_entry_disabled_by_changed(ent_reg, entry) return reload_result @@ -1251,8 +1259,16 @@ class EntityRegistryDisabledHandler: @callback def _handle_entry_updated_filter(event: Event) -> bool: - """Handle entity registry entry update filter.""" - if event.data["action"] != "update" or "disabled_by" not in event.data["changes"]: + """Handle entity registry entry update filter. + + Only handle changes to "disabled_by". + If "disabled_by" was DISABLED_CONFIG_ENTRY, reload is not needed. + """ + if ( + event.data["action"] != "update" + or "disabled_by" not in event.data["changes"] + or event.data["changes"]["disabled_by"] == entity_registry.DISABLED_CONFIG_ENTRY + ): return False return True diff --git a/homeassistant/const.py b/homeassistant/const.py index 47fb090305d..2a9bd69c761 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -202,7 +202,6 @@ CONF_ZONE = "zone" # #### EVENTS #### EVENT_CALL_SERVICE = "call_service" EVENT_COMPONENT_LOADED = "component_loaded" -EVENT_CONFIG_ENTRY_DISABLED_BY_UPDATED = "config_entry_disabled_by_updated" EVENT_CORE_CONFIG_UPDATE = "core_config_updated" EVENT_HOMEASSISTANT_CLOSE = "homeassistant_close" EVENT_HOMEASSISTANT_START = "homeassistant_start" diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 705f6cdd89a..d311538f27f 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -6,10 +6,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union, import attr -from homeassistant.const import ( - EVENT_CONFIG_ENTRY_DISABLED_BY_UPDATED, - EVENT_HOMEASSISTANT_STARTED, -) +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import Event, callback from homeassistant.loader import bind_hass import homeassistant.util.uuid as uuid_util @@ -20,6 +17,8 @@ from .typing import UNDEFINED, HomeAssistantType, UndefinedType # mypy: disallow_any_generics if TYPE_CHECKING: + from homeassistant.config_entries import ConfigEntry + from . import entity_registry _LOGGER = logging.getLogger(__name__) @@ -143,10 +142,6 @@ class DeviceRegistry: self.hass = hass self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) self._clear_index() - self.hass.bus.async_listen( - EVENT_CONFIG_ENTRY_DISABLED_BY_UPDATED, - self.async_config_entry_disabled_by_changed, - ) @callback def async_get(self, device_id: str) -> Optional[DeviceEntry]: @@ -618,38 +613,6 @@ class DeviceRegistry: if area_id == device.area_id: self._async_update_device(dev_id, area_id=None) - @callback - def async_config_entry_disabled_by_changed(self, event: Event) -> None: - """Handle a config entry being disabled or enabled. - - Disable devices in the registry that are associated to a config entry when - the config entry is disabled. - """ - config_entry = self.hass.config_entries.async_get_entry( - event.data["config_entry_id"] - ) - - # The config entry may be deleted already if the event handling is late - if not config_entry: - return - - if not config_entry.disabled_by: - devices = async_entries_for_config_entry( - self, event.data["config_entry_id"] - ) - for device in devices: - if device.disabled_by != DISABLED_CONFIG_ENTRY: - continue - self.async_update_device(device.id, disabled_by=None) - return - - devices = async_entries_for_config_entry(self, event.data["config_entry_id"]) - for device in devices: - if device.disabled: - # Entity already disabled, do not overwrite - continue - self.async_update_device(device.id, disabled_by=DISABLED_CONFIG_ENTRY) - @callback def async_get(hass: HomeAssistantType) -> DeviceRegistry: @@ -691,6 +654,34 @@ def async_entries_for_config_entry( ] +@callback +def async_config_entry_disabled_by_changed( + registry: DeviceRegistry, config_entry: "ConfigEntry" +) -> None: + """Handle a config entry being disabled or enabled. + + Disable devices in the registry that are associated with a config entry when + the config entry is disabled, enable devices in the registry that are associated + with a config entry when the config entry is enabled and the devices are marked + DISABLED_CONFIG_ENTRY. + """ + + devices = async_entries_for_config_entry(registry, config_entry.entry_id) + + if not config_entry.disabled_by: + for device in devices: + if device.disabled_by != DISABLED_CONFIG_ENTRY: + continue + registry.async_update_device(device.id, disabled_by=None) + return + + for device in devices: + if device.disabled: + # Device already disabled, do not overwrite + continue + registry.async_update_device(device.id, disabled_by=DISABLED_CONFIG_ENTRY) + + @callback def async_cleanup( hass: HomeAssistantType, diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index c86bd64d73e..8a7a4de970a 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -31,7 +31,6 @@ from homeassistant.const import ( ATTR_RESTORED, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - EVENT_CONFIG_ENTRY_DISABLED_BY_UPDATED, EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE, ) @@ -158,10 +157,6 @@ class EntityRegistry: self.hass.bus.async_listen( EVENT_DEVICE_REGISTRY_UPDATED, self.async_device_modified ) - self.hass.bus.async_listen( - EVENT_CONFIG_ENTRY_DISABLED_BY_UPDATED, - self.async_config_entry_disabled_by_changed, - ) @callback def async_get_device_class_lookup(self, domain_device_classes: set) -> dict: @@ -363,40 +358,6 @@ class EntityRegistry: for entity in entities: self.async_update_entity(entity.entity_id, disabled_by=DISABLED_DEVICE) - @callback - def async_config_entry_disabled_by_changed(self, event: Event) -> None: - """Handle a config entry being disabled or enabled. - - Disable entities in the registry that are associated to a config entry when - the config entry is disabled. - """ - config_entry = self.hass.config_entries.async_get_entry( - event.data["config_entry_id"] - ) - - # The config entry may be deleted already if the event handling is late - if not config_entry: - return - - if not config_entry.disabled_by: - entities = async_entries_for_config_entry( - self, event.data["config_entry_id"] - ) - for entity in entities: - if entity.disabled_by != DISABLED_CONFIG_ENTRY: - continue - self.async_update_entity(entity.entity_id, disabled_by=None) - return - - entities = async_entries_for_config_entry(self, event.data["config_entry_id"]) - for entity in entities: - if entity.disabled: - # Entity already disabled, do not overwrite - continue - self.async_update_entity( - entity.entity_id, disabled_by=DISABLED_CONFIG_ENTRY - ) - @callback def async_update_entity( self, @@ -443,7 +404,8 @@ class EntityRegistry: """Private facing update properties method.""" old = self.entities[entity_id] - changes = {} + new_values = {} # Dict with new key/value pairs + old_values = {} # Dict with old key/value pairs for attr_name, value in ( ("name", name), @@ -460,7 +422,8 @@ class EntityRegistry: ("original_icon", original_icon), ): if value is not UNDEFINED and value != getattr(old, attr_name): - changes[attr_name] = value + new_values[attr_name] = value + old_values[attr_name] = getattr(old, attr_name) if new_entity_id is not UNDEFINED and new_entity_id != old.entity_id: if self.async_is_registered(new_entity_id): @@ -473,7 +436,8 @@ class EntityRegistry: raise ValueError("New entity ID should be same domain") self.entities.pop(entity_id) - entity_id = changes["entity_id"] = new_entity_id + entity_id = new_values["entity_id"] = new_entity_id + old_values["entity_id"] = old.entity_id if new_unique_id is not UNDEFINED: conflict_entity_id = self.async_get_entity_id( @@ -484,18 +448,19 @@ class EntityRegistry: f"Unique id '{new_unique_id}' is already in use by " f"'{conflict_entity_id}'" ) - changes["unique_id"] = new_unique_id + new_values["unique_id"] = new_unique_id + old_values["unique_id"] = old.unique_id - if not changes: + if not new_values: return old self._remove_index(old) - new = attr.evolve(old, **changes) + new = attr.evolve(old, **new_values) self._register_entry(new) self.async_schedule_save() - data = {"action": "update", "entity_id": entity_id, "changes": list(changes)} + data = {"action": "update", "entity_id": entity_id, "changes": old_values} if old.entity_id != entity_id: data["old_entity_id"] = old.entity_id @@ -670,6 +635,36 @@ def async_entries_for_config_entry( ] +@callback +def async_config_entry_disabled_by_changed( + registry: EntityRegistry, config_entry: "ConfigEntry" +) -> None: + """Handle a config entry being disabled or enabled. + + Disable entities in the registry that are associated with a config entry when + the config entry is disabled, enable entities in the registry that are associated + with a config entry when the config entry is enabled and the entities are marked + DISABLED_CONFIG_ENTRY. + """ + + entities = async_entries_for_config_entry(registry, config_entry.entry_id) + + if not config_entry.disabled_by: + for entity in entities: + if entity.disabled_by != DISABLED_CONFIG_ENTRY: + continue + registry.async_update_entity(entity.entity_id, disabled_by=None) + return + + for entity in entities: + if entity.disabled: + # Entity already disabled, do not overwrite + continue + registry.async_update_entity( + entity.entity_id, disabled_by=DISABLED_CONFIG_ENTRY + ) + + async def _async_migrate(entities: Dict[str, Any]) -> Dict[str, List[Dict[str, Any]]]: """Migrate the YAML config file to storage helper format.""" return { diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 86cdab82238..0a1a27efef5 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -313,7 +313,7 @@ async def test_updating_config_entry_id(hass, registry, update_events): assert update_events[0]["entity_id"] == entry.entity_id assert update_events[1]["action"] == "update" assert update_events[1]["entity_id"] == entry.entity_id - assert update_events[1]["changes"] == ["config_entry_id"] + assert update_events[1]["changes"] == {"config_entry_id": "mock-id-1"} async def test_removing_config_entry_id(hass, registry, update_events): From 584ad0756770f08019f6972dcc20972a7f19253c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 3 Mar 2021 10:13:04 -0800 Subject: [PATCH 795/796] Simplify switch light (#47317) --- homeassistant/components/switch/light.py | 50 +++++++++--------------- 1 file changed, 19 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index 5128a49d8b7..2650bd61bfb 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -12,7 +12,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import CALLBACK_TYPE, callback +from homeassistant.core import State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change_event @@ -37,7 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, - async_add_entities: Callable[[Sequence[Entity], bool], None], + async_add_entities: Callable[[Sequence[Entity]], None], discovery_info: Optional[DiscoveryInfoType] = None, ) -> None: """Initialize Light Switch platform.""" @@ -53,8 +53,7 @@ async def async_setup_platform( config[CONF_ENTITY_ID], unique_id, ) - ], - True, + ] ) @@ -66,9 +65,7 @@ class LightSwitch(LightEntity): self._name = name self._switch_entity_id = switch_entity_id self._unique_id = unique_id - self._is_on = False - self._available = False - self._async_unsub_state_changed: Optional[CALLBACK_TYPE] = None + self._switch_state: Optional[State] = None @property def name(self) -> str: @@ -78,12 +75,16 @@ class LightSwitch(LightEntity): @property def is_on(self) -> bool: """Return true if light switch is on.""" - return self._is_on + assert self._switch_state is not None + return self._switch_state.state == STATE_ON @property def available(self) -> bool: """Return true if light switch is on.""" - return self._available + return ( + self._switch_state is not None + and self._switch_state.state != STATE_UNAVAILABLE + ) @property def should_poll(self) -> bool: @@ -117,33 +118,20 @@ class LightSwitch(LightEntity): context=self._context, ) - async def async_update(self): - """Query the switch in this light switch and determine the state.""" - switch_state = self.hass.states.get(self._switch_entity_id) - - if switch_state is None: - self._available = False - return - - self._is_on = switch_state.state == STATE_ON - self._available = switch_state.state != STATE_UNAVAILABLE - async def async_added_to_hass(self) -> None: """Register callbacks.""" + assert self.hass is not None + self._switch_state = self.hass.states.get(self._switch_entity_id) @callback def async_state_changed_listener(*_: Any) -> None: """Handle child updates.""" - self.async_schedule_update_ha_state(True) + assert self.hass is not None + self._switch_state = self.hass.states.get(self._switch_entity_id) + self.async_write_ha_state() - assert self.hass is not None - self._async_unsub_state_changed = async_track_state_change_event( - self.hass, [self._switch_entity_id], async_state_changed_listener + self.async_on_remove( + async_track_state_change_event( + self.hass, [self._switch_entity_id], async_state_changed_listener + ) ) - - async def async_will_remove_from_hass(self): - """Handle removal from Home Assistant.""" - if self._async_unsub_state_changed is not None: - self._async_unsub_state_changed() - self._async_unsub_state_changed = None - self._available = False From b711686e10791f92979d0e96b83d8b28b0e26fc1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Mar 2021 19:17:17 +0100 Subject: [PATCH 796/796] Bumped version to 2021.3.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2a9bd69c761..ec2ab3bff0c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 3 -PATCH_VERSION = "0b7" +PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0)